From 1e01b326e42a79eb450a8786ff9ce6d3cd9f67c8 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 24 Nov 2025 14:52:37 +0200 Subject: [PATCH 01/13] moved to new ui and fixed data bug --- .../system/widget_types/timeseries_table.json | 2 +- ...eries-table-widget-settings.component.html | 25 ++++-- ...eseries-table-widget-settings.component.ts | 38 ++++++-- .../lib/timeseries-table-widget.component.ts | 88 +++++++++++++++---- .../assets/locale/locale.constant-en_US.json | 6 +- ui-ngx/src/form.scss | 6 ++ 6 files changed, 130 insertions(+), 35 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json index 625f7a1e7d..0283899134 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_table.json +++ b/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\":\"ASC\"}},\"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": [ { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index f236dd1acd..ae07674162 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -113,17 +113,24 @@
widgets.table.table-tabs
- {{ 'widgets.table.use-entity-label-tab-name' | translate }} + {{ 'widgets.table.use-entity-label-header-title' | translate }} -
+
widgets.table.sort-by
- - - {{ 'widgets.table.sort-timestamp-option' | translate }} - {{ 'widgets.table.sort-asc' | translate }} - {{ 'widgets.table.sort-desc' | translate }} - - +
+ + + {{ entityFields.createdTime.name | translate }} + {{ entityFields.name.name | translate }} + + + + + {{ 'widgets.table.sort-asc' | translate }} + {{ 'widgets.table.sort-desc' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index ccbb582679..95a9eb8014 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/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 = []; @@ -44,6 +46,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon } protected defaultSettings(): WidgetSettings { + console.log("default") return { enableSearch: true, enableSelectColumnDisplay: true, @@ -62,14 +65,21 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon disableStickyHeader: false, useRowStyleFunction: false, rowStyleFunction: '', - tabSortKey: TabSortKey.TIMESTAMP + sortOrder: { + property: this.entityFields.name.keyName, + direction: Direction.ASC + } }; } 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.name.keyName, + direction: settings.sortOrder?.direction || Direction.ASC + }; this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement); + console.log("input",settings) return settings; } @@ -99,7 +109,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 +126,15 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon return ['useRowStyleFunction', 'displayPagination', 'pageStepCount', 'pageStepIncrement']; } + protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + settings.sortOrder = { + property: settings.sortOrder?.property || this.entityFields.name.keyName, + direction: settings.sortOrder?.direction || Direction.ASC + }; + console.log("output",settings) + return settings; + } + protected updateValidators(emitEvent: boolean, trigger: string) { if (trigger === 'pageStepCount' || trigger === 'pageStepIncrement') { this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').reset(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index dbd4905299..d6c73d7f82 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/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 { @@ -405,7 +400,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI return entityLabelCache.get(source.entityId); } - const value = this.useEntityLabel + const value = this.useEntityLabel ? (source.entityLabel || source.entityName) : source.entityName; @@ -414,19 +409,78 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI return translated; } - private sortDatasources(source: Datasource[], entityLabelCache:Map): Datasource[] { + private sortDatasources(source: Datasource[], entityLabelCache: Map): Datasource[] { + const property = this.settings?.sortOrder?.property; + const direction = this.settings?.sortOrder?.direction; + const isAsc = direction === Direction.ASC; + let sortedSource = [...source]; + source.forEach(ds => this.getTabLabel(ds, entityLabelCache)); - if (this.settings.tabSortKey === TabSortKey.TIMESTAMP) { - return source; + if (property === entityFields.name.keyName) { + const collator = new Intl.Collator(undefined, { + sensitivity: "variant", + numeric: true, + ignorePunctuation: false + }); + + sortedSource.sort((a, b) => { + const valueA = entityLabelCache.get(a.entityId) || ''; + const valueB = entityLabelCache.get(b.entityId) || ''; + + return isAsc + ? collator.compare(valueA, valueB) + : collator.compare(valueB, valueA); + }); + } else if (property === entityFields.createdTime.keyName) { + if (!isAsc) { + sortedSource.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); + this.reorderDataArrays(source, sortedSource); + return sortedSource; + } + + private reorderDataArrays(originalOrder: Datasource[], newOrder: Datasource[]): void { + const indexMap = new Map(); + originalOrder.forEach((ds, oldIndex) => { + const newIndex = newOrder.findIndex(newDs => newDs.entityId === ds.entityId); + indexMap.set(oldIndex, newIndex); }); + + const newData: Array = []; + originalOrder.forEach((ds, oldIndex) => { + const dataKeys = ds.dataKeys; + const startIdx = oldIndex * dataKeys.length; + const endIdx = startIdx + dataKeys.length; + const datasourceData = this.data.slice(startIdx, endIdx); + + const newIndex = indexMap.get(oldIndex); + const newStartIdx = newIndex * dataKeys.length; + + datasourceData.forEach((data, i) => { + newData[newStartIdx + i] = data; + }); + }); + this.data = newData; + + if (this.latestData && this.latestData.length > 0) { + const newLatestData: Array = []; + originalOrder.forEach((ds, oldIndex) => { + const latestDataKeys = ds.latestDataKeys || []; + const startIdx = oldIndex * latestDataKeys.length; + const endIdx = startIdx + latestDataKeys.length; + const datasourceLatestData = this.latestData.slice(startIdx, endIdx); + + const newIndex = indexMap.get(oldIndex); + const newStartIdx = newIndex * latestDataKeys.length; + + datasourceLatestData.forEach((data, i) => { + newLatestData[newStartIdx + i] = data; + }); + }); + this.latestData = newLatestData; + } } private updateDatasources() { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 538b3a8352..9850ec73d7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -9482,7 +9482,7 @@ "page-step-increment": "Step increment", "page-step-count-format-message": "Should be an integer value, in the range from 1 to 100.", "page-step-increment-format-message": "Should be an integer value, greater or equal to 1.", - "use-entity-label-tab-name": "Use entity label in tab name", + "use-entity-label-header-title": "Use entity label in header title", "hide-empty-lines": "Hide empty lines", "row-style": "Row style", "use-row-style-function": "Use row style function", @@ -9538,8 +9538,8 @@ "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-asc": "Ascending", + "sort-desc": "Descending", "sort-timestamp-option": "Created time" }, "latest-chart": { diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index af166b406d..b8e269c14e 100644 --- a/ui-ngx/src/form.scss +++ b/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; From e1c169786b178a0301b7eae33715ba52ba6c2a8c Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 25 Nov 2025 15:04:37 +0200 Subject: [PATCH 02/13] UI: Improved CF strategy settings style --- .../calculated-field-output.component.html | 158 +++++++++--------- .../calculated-field-output.component.ts | 27 ++- .../assets/locale/locale.constant-en_US.json | 8 +- 3 files changed, 103 insertions(+), 90 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index a4cd8e3a3c..7e23acceae 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -46,7 +46,7 @@ @if (hiddenName) {
- +
} @else {
@@ -74,93 +74,94 @@
- + } }
-
-
- {{ 'calculated-fields.output-strategy.strategy' | translate }} -
- - @for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) { - {{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }} + + + +
+
+ {{ 'calculated-fields.output-strategy.strategy' | translate }} +
+ + @for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) { + {{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }} + } + +
+
+
+ @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { + @if (outputForm.get('type').value === OutputType.Timeseries) { +
+ +
+
calculated-fields.output-strategy.save-time-series
+
+
+
+
+ +
+
calculated-fields.output-strategy.save-latest-values
+
+
+
+ } @else { +
+ +
+
calculated-fields.output-strategy.save-database
+
+
+
} -
-
- @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { -
-
- {{ 'calculated-fields.output-strategy.processing-options' | translate }} +
+ +
+
calculated-fields.output-strategy.send-web-sockets
+
+
- - @if (outputForm.get('type').value === OutputType.Timeseries) { - - {{ 'calculated-fields.output-strategy.save-time-series' | translate }} - - - {{ 'calculated-fields.output-strategy.save-latest-values' | translate }} - - } @else { - - {{ 'calculated-fields.output-strategy.save-database' | translate }} - - } - - {{ 'calculated-fields.output-strategy.send-web-sockets' | translate }} - - - {{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }} - - -
- @if (outputForm.get('type').value === OutputType.Attribute) {
- -
-
calculated-fields.output-strategy.update-attributes-only-on-value-change
+ +
+
calculated-fields.output-strategy.save-calculated-fields
- } @else { - - - help_outline - - + @if (outputForm.get('type').value === OutputType.Attribute) { +
+ +
+
calculated-fields.output-strategy.update-attributes-only-on-value-change
+
+
+
+ } @else { +
+ +
+
calculated-fields.output-strategy.ttl
+
+
+ + +
+ } } - } +
@@ -172,3 +173,6 @@ } + + + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 4ed8c4ffba..4a975e59b9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/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}); + } } } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c0623c49fc..09dfb9cb2c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1248,15 +1248,15 @@ "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", "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." + "ttl": "Uncheck to use the default system settings" } }, "aggregate-interval-type": "Aggregate interval type", From 0e8b99c067a5a8d2c833ab2717a6c77be00ddc86 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 25 Nov 2025 16:00:37 +0200 Subject: [PATCH 03/13] moved sorting of datasources --- .../system/widget_types/timeseries_table.json | 2 +- ...eries-table-widget-settings.component.html | 6 +- ...eseries-table-widget-settings.component.ts | 13 ++-- .../lib/timeseries-table-widget.component.ts | 64 +++---------------- .../assets/locale/locale.constant-en_US.json | 6 +- 5 files changed, 21 insertions(+), 70 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json index 0283899134..543d189f70 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_table.json +++ b/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\":\"\",\"sortOrder\":{\"property\":\"createdTime\",\"direction\":\"ASC\"}},\"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\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"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\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"advanced\"}" }, "resources": [ { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index ae07674162..bd223238a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -113,7 +113,7 @@
widgets.table.table-tabs
- {{ 'widgets.table.use-entity-label-header-title' | translate }} + {{ 'widgets.table.use-entity-label-tab-name' | translate }}
widgets.table.sort-by
@@ -126,8 +126,8 @@ - {{ 'widgets.table.sort-asc' | translate }} - {{ 'widgets.table.sort-desc' | translate }} + {{ 'common.sort-asc' | translate }} + {{ 'common.sort-desc' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index 95a9eb8014..d501d581c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -46,7 +46,6 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon } protected defaultSettings(): WidgetSettings { - console.log("default") return { enableSearch: true, enableSelectColumnDisplay: true, @@ -67,7 +66,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon rowStyleFunction: '', sortOrder: { property: this.entityFields.name.keyName, - direction: Direction.ASC + direction: Direction.DESC } }; } @@ -75,11 +74,10 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize; settings.sortOrder = { - property: settings.sortOrder?.property || this.entityFields.name.keyName, - direction: settings.sortOrder?.direction || Direction.ASC + property: settings.sortOrder?.property || this.entityFields.createdTime.keyName, + direction: settings.sortOrder?.direction || Direction.DESC }; this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement); - console.log("input",settings) return settings; } @@ -128,10 +126,9 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { settings.sortOrder = { - property: settings.sortOrder?.property || this.entityFields.name.keyName, - direction: settings.sortOrder?.direction || Direction.ASC + property: settings.sortOrder?.property || this.entityFields.createdTime.keyName, + direction: settings.sortOrder?.direction || Direction.DESC }; - console.log("output",settings) return settings; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index d6c73d7f82..5b18fac9c0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -409,13 +409,10 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI return translated; } - private sortDatasources(source: Datasource[], entityLabelCache: Map): Datasource[] { + private sortDatasources(source: TimeseriesTableSource[], entityLabelCache: Map) { const property = this.settings?.sortOrder?.property; const direction = this.settings?.sortOrder?.direction; const isAsc = direction === Direction.ASC; - let sortedSource = [...source]; - - source.forEach(ds => this.getTabLabel(ds, entityLabelCache)); if (property === entityFields.name.keyName) { const collator = new Intl.Collator(undefined, { @@ -424,63 +421,19 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI ignorePunctuation: false }); - sortedSource.sort((a, b) => { - const valueA = entityLabelCache.get(a.entityId) || ''; - const valueB = entityLabelCache.get(b.entityId) || ''; + source.sort((a, b) => { + const valueA = entityLabelCache.get(a.datasource.entityId) || ''; + const valueB = entityLabelCache.get(a.datasource.entityId) || ''; return isAsc ? collator.compare(valueA, valueB) : collator.compare(valueB, valueA); }); } else if (property === entityFields.createdTime.keyName) { - if (!isAsc) { - sortedSource.reverse(); + if (isAsc) { + source.reverse(); } } - this.reorderDataArrays(source, sortedSource); - return sortedSource; - } - - private reorderDataArrays(originalOrder: Datasource[], newOrder: Datasource[]): void { - const indexMap = new Map(); - originalOrder.forEach((ds, oldIndex) => { - const newIndex = newOrder.findIndex(newDs => newDs.entityId === ds.entityId); - indexMap.set(oldIndex, newIndex); - }); - - const newData: Array = []; - originalOrder.forEach((ds, oldIndex) => { - const dataKeys = ds.dataKeys; - const startIdx = oldIndex * dataKeys.length; - const endIdx = startIdx + dataKeys.length; - const datasourceData = this.data.slice(startIdx, endIdx); - - const newIndex = indexMap.get(oldIndex); - const newStartIdx = newIndex * dataKeys.length; - - datasourceData.forEach((data, i) => { - newData[newStartIdx + i] = data; - }); - }); - this.data = newData; - - if (this.latestData && this.latestData.length > 0) { - const newLatestData: Array = []; - originalOrder.forEach((ds, oldIndex) => { - const latestDataKeys = ds.latestDataKeys || []; - const startIdx = oldIndex * latestDataKeys.length; - const endIdx = startIdx + latestDataKeys.length; - const datasourceLatestData = this.latestData.slice(startIdx, endIdx); - - const newIndex = indexMap.get(oldIndex); - const newStartIdx = newIndex * latestDataKeys.length; - - datasourceLatestData.forEach((data, i) => { - newLatestData[newStartIdx + i] = data; - }); - }); - this.latestData = newLatestData; - } } private updateDatasources() { @@ -491,8 +444,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const entityLabelCache = new Map(); const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; if (this.datasources) { - const sortedDatasources = this.sortDatasources(this.datasources, entityLabelCache); - for (const datasource of sortedDatasources) { + this.datasources.forEach(ds => this.getTabLabel(ds, entityLabelCache)); + for (const datasource of this.datasources) { const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); const source = {} as TimeseriesTableSource; source.header = this.prepareHeader(datasource); @@ -529,6 +482,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } } if (this.sources.length) { + this.sortDatasources(this.sources, entityLabelCache); this.sources.forEach((source, index) => { this.prepareDisplayedColumn(index); source.displayedColumns = this.displayedColumns[index].filter(value => value.display).map(value => value.def); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 9850ec73d7..5a0be16baf 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1639,6 +1639,8 @@ "documentation": "Documentation", "time-left": "{{time}} left", "output": "Output", + "sort-asc": "Ascending", + "sort-desc": "Descending", "suffix": { "s": "s", "ms": "ms" @@ -9482,7 +9484,7 @@ "page-step-increment": "Step increment", "page-step-count-format-message": "Should be an integer value, in the range from 1 to 100.", "page-step-increment-format-message": "Should be an integer value, greater or equal to 1.", - "use-entity-label-header-title": "Use entity label in header title", + "use-entity-label-tab-name": "Use entity label in tab name", "hide-empty-lines": "Hide empty lines", "row-style": "Row style", "use-row-style-function": "Use row style function", @@ -9538,8 +9540,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": "Ascending", - "sort-desc": "Descending", "sort-timestamp-option": "Created time" }, "latest-chart": { From 8fd3986ee9c846caf7035ed017ee652af8e147f4 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 25 Nov 2025 16:08:30 +0200 Subject: [PATCH 04/13] Update timeseries_table.json --- .../main/data/json/system/widget_types/timeseries_table.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json index 543d189f70..24443cfecf 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_table.json +++ b/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\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"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\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"advanced\"}" + "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": [ { From c4f7b385db756aa6c1c7e8b5b26f84e99599e114 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 25 Nov 2025 16:17:10 +0200 Subject: [PATCH 05/13] fixed typo in getting translation for sort value --- .../components/widget/lib/timeseries-table-widget.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 5b18fac9c0..47322aaed1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -423,7 +423,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI source.sort((a, b) => { const valueA = entityLabelCache.get(a.datasource.entityId) || ''; - const valueB = entityLabelCache.get(a.datasource.entityId) || ''; + const valueB = entityLabelCache.get(b.datasource.entityId) || ''; return isAsc ? collator.compare(valueA, valueB) From c74f0b42f5ba01fa320eb505d167b305bef7c10e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 25 Nov 2025 16:17:27 +0200 Subject: [PATCH 06/13] UI: Fixed incorrect preview for microsoft team notification --- .../sent-notification-dialog.component.html | 4 +- ...action-button-configuration.component.html | 46 +++++++++---------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 35095a16e3..39c94d1ffb 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -233,7 +233,9 @@
{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }} - + @if (preview.processedTemplates.MICROSOFT_TEAMS.button?.enabled) { + + }
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html index 3eb330bb50..cba2c54a7b 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html @@ -44,8 +44,8 @@
-
- +
+ notification.action-type - - notification.link - - - {{ 'notification.link-required' | translate }} - - - {{ 'notification.link-max-length' | translate : - {length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength} - }} - - - - + @if(actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK) { + + notification.link + + + {{ 'notification.link-required' | translate }} + + + {{ 'notification.link-max-length' | translate : + {length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength} + }} + + + } @else { + - - + }
From 53598ac49c1c1a4a3a0eb3cabf676a68307f2ebc Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 25 Nov 2025 16:43:37 +0200 Subject: [PATCH 07/13] updated api usage dashboard --- ...eseries-table-widget-settings.component.ts | 2 +- .../lib/timeseries-table-widget.component.ts | 22 ++++++------------- ui-ngx/src/assets/dashboard/api_usage.json | 5 ++++- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index d501d581c9..3911506be7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -65,7 +65,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon useRowStyleFunction: false, rowStyleFunction: '', sortOrder: { - property: this.entityFields.name.keyName, + property: this.entityFields.createdTime.keyName, direction: Direction.DESC } }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 47322aaed1..282fab9a3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -395,21 +395,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.updateDatasources(); } - private getTabLabel(source: Datasource, entityLabelCache:Map):string { - if (entityLabelCache.has(source.entityId)) { - return entityLabelCache.get(source.entityId); - } - + 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: TimeseriesTableSource[], entityLabelCache: Map) { + private sortDatasources(source: TimeseriesTableSource[]) { const property = this.settings?.sortOrder?.property; const direction = this.settings?.sortOrder?.direction; const isAsc = direction === Direction.ASC; @@ -422,8 +416,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI }); source.sort((a, b) => { - const valueA = entityLabelCache.get(a.datasource.entityId) || ''; - const valueB = entityLabelCache.get(b.datasource.entityId) || ''; + const valueA = a.displayName || ''; + const valueB = b.displayName || ''; return isAsc ? collator.compare(valueA, valueB) @@ -441,10 +435,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.sourceIndex = 0; let keyOffset = 0; let latestKeyOffset = 0; - const entityLabelCache = new Map(); const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; if (this.datasources) { - this.datasources.forEach(ds => this.getTabLabel(ds, entityLabelCache)); for (const datasource of this.datasources) { const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); const source = {} as TimeseriesTableSource; @@ -462,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'); } @@ -482,7 +474,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } } if (this.sources.length) { - this.sortDatasources(this.sources, entityLabelCache); + 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); diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 994b431ade..e45e9a59fa 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -99,7 +99,10 @@ "showTimestamp": true, "displayPagination": true, "defaultPageSize": 10, - "tabSortKey": "NAME_ASC" + "sortOrder": { + "property": "createdTime", + "direction": "DESC" + } }, "title": "{i18n:api-usage.exceptions}", "dropShadow": true, From dcf46a1cec14a150d63eddde046570cc71f7d9c7 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 25 Nov 2025 16:54:20 +0200 Subject: [PATCH 08/13] UI: Updated cf output strategy hint --- .../output/calculated-field-output.component.html | 10 +++++----- ui-ngx/src/assets/locale/locale.constant-en_US.json | 10 +++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index 7e23acceae..747faac88f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -98,14 +98,14 @@ @if (outputForm.get('type').value === OutputType.Timeseries) {
-
+
calculated-fields.output-strategy.save-time-series
-
+
calculated-fields.output-strategy.save-latest-values
@@ -113,7 +113,7 @@ } @else {
-
+
calculated-fields.output-strategy.save-database
@@ -121,14 +121,14 @@ }
-
+
calculated-fields.output-strategy.send-web-sockets
-
+
calculated-fields.output-strategy.save-calculated-fields
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 09dfb9cb2c..d208ef3fd4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1241,7 +1241,6 @@ "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", @@ -1252,11 +1251,16 @@ "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": "Uncheck to use the default system settings" + "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", From fb1e4632495d006eb64c510ccd25dc8034729acf Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 25 Nov 2025 17:20:57 +0200 Subject: [PATCH 09/13] UI: Bug fixes --- .../alarm-rule-dialog.component.html | 9 ++- .../alarm-rule-filter-config.component.ts | 5 +- .../alarm-rule-table-header.component.ts | 1 - .../alarm-rules/alarm-rules-table-config.ts | 31 +++++--- ...alarm-rule-condition-dialog.component.html | 19 ++++- ...f-alarm-rule-condition-dialog.component.ts | 71 ++++++++++++++++- .../cf-alarm-rule-condition.component.html | 2 +- .../cf-alarm-rule-condition.component.ts | 21 +++-- .../alarm-rules/cf-alarm-rule.component.html | 2 +- .../alarm-rules/cf-alarm-rule.component.scss | 1 + .../alarm-rules/cf-alarm-rule.component.ts | 9 ++- .../create-cf-alarm-rules.component.html | 3 +- .../create-cf-alarm-rules.component.ts | 4 +- .../alarm-rule-filter-dialog.component.html | 5 +- .../alarm-rule-filter-list.component.html | 1 - ...-rule-filter-predicate-list.component.html | 2 - .../calculated-fields-table-config.ts | 16 +++- ...ulated-field-argument-panel.component.html | 25 ++---- ...lculated-field-argument-panel.component.ts | 45 ++++++++--- ...lated-field-arguments-table.component.html | 6 +- ...culated-field-arguments-table.component.ts | 3 + .../propagate-arguments-table.component.ts | 1 + .../calculated-field-dialog.component.html | 1 + ...eofencing-zone-groups-panel.component.html | 3 +- ...-geofencing-zone-groups-panel.component.ts | 13 ++-- ...culated-field-metrics-panel.component.html | 48 ++++++++++-- ...alculated-field-metrics-panel.component.ts | 76 +++++++++++++++++-- ...alculated-field-metrics-table.component.ts | 5 +- ...ities-aggregation-component.component.html | 1 + ...ntities-aggregation-component.component.ts | 4 +- .../pages/asset/asset-tabs.component.html | 4 +- .../home/pages/asset/asset-tabs.component.ts | 8 ++ .../pages/device/device-tabs.component.html | 4 +- .../pages/device/device-tabs.component.ts | 8 ++ .../entity-key-autocomplete.component.html | 2 +- .../entity-key-autocomplete.component.ts | 7 ++ .../entity/entity-subtype-list.component.ts | 2 +- .../app/shared/models/alarm-rule.models.ts | 4 + .../shared/models/calculated-field.models.ts | 7 ++ .../assets/locale/locale.constant-en_US.json | 38 ++++++---- 40 files changed, 404 insertions(+), 113 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index d4a93d461f..eefa6ff209 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -58,6 +58,7 @@
{{ 'alarm-rule.create-conditions' | translate }}
- +
@@ -97,7 +100,7 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts index c961cebe33..a19fc688a8 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts +++ b/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 { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts index cf5462d1a2..8f2501d8f1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts +++ b/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"; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 1dbb7fdc83..c581db2b2b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -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'; @@ -59,6 +59,7 @@ import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.m import { UtilsService } from "@core/services/utils.service"; import { deepClone, getEntityDetailsPageURL } from "@core/utils"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; +import { ActionNotificationShow } from "@core/notification/notification.actions"; export class AlarmRulesTableConfig extends EntityTableConfig { @@ -223,7 +224,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig { 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) { @@ -277,6 +281,19 @@ export class AlarmRulesTableConfig extends EntityTableConfig { 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 +304,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } 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 diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index feafcaa0a4..34f165420a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/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">
{{ 'alarm-rule.expression-type.script' | translate }} + class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.tbel' | translate }}
+ +
+ +
}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index 48b290b32c..dc8b23c707 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Component, DestroyRef, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, Validators } from '@angular/forms'; @@ -28,21 +28,34 @@ import { AlarmRuleCondition, AlarmRuleConditionType, AlarmRuleConditionTypeTranslationMap, + alarmRuleDefaultScript, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models"; import { + ArgumentType, + CalculatedField, CalculatedFieldArgument, + CalculatedFieldEventArguments, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights } from "@shared/models/calculated-field.models"; 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"; +import { filter, switchMap, tap } from "rxjs/operators"; +import { isObject } from "@core/utils"; +import { + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestScriptDialogData +} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; +import { CalculatedFieldsService } from "@core/http/calculated-fields.service"; export interface CfAlarmRuleConditionDialogData { readonly: boolean; condition: AlarmRuleCondition; arguments?: Record; + value?: CalculatedField; } @Component({ @@ -106,7 +119,10 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent, - private fb: FormBuilder) { + private fb: FormBuilder, + private calculatedFieldsService: CalculatedFieldsService, + private destroyRef: DestroyRef, + private dialog: MatDialog) { super(store, router, dialogRef); this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; @@ -117,7 +133,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { + this.conditionFormGroup.get('expression.expression').setValue(expression); + this.conditionFormGroup.get('expression.expression').markAsDirty(); + }) + } + + testScript(): Observable { + if (this.data.value?.id?.id) { + return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(this.data.value?.id?.id, {ignoreLoading: true}) + .pipe( + switchMap(event => { + const args = event?.arguments ? JSON.parse(event.arguments) : null; + return this.getTestScriptDialog(this.arguments, this.conditionFormGroup.get('expression.expression').value, args); + }), + takeUntilDestroyed(this.destroyRef) + ) + } + return this.getTestScriptDialog(this.arguments, this.conditionFormGroup.get('expression.expression').value, null); + } + + getTestScriptDialog(argumentsList: Record, expression: string, argumentsObj?: CalculatedFieldEventArguments): Observable { + const resultArguments = Object.keys(argumentsList).reduce((acc, key) => { + const type = argumentsList[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, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], + data: { + arguments: resultArguments, + expression: expression, + argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(argumentsList), + argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(argumentsList) + } + }).afterClosed() + .pipe( + filter(Boolean), + tap(expression => expression) + ); + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html index e17d8c8261..0b722b4ec1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html +++ b/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 }}"> {{ conditionSet() ? 'edit' : 'add' }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 2e8d116ad2..0ae0c4a067 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/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'; @@ -44,7 +43,7 @@ import { AlarmRuleSchedule, AlarmRuleScheduleType } from "@shared/models/alarm-rule.models"; -import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { AlarmRuleScheduleDialogData, CfAlarmScheduleDialogComponent @@ -81,6 +80,13 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali @Input() arguments: Record; + @Input() + @coerceBoolean() + isClearCondition = false; + + @Input() + value: CalculatedField; + alarmRuleConditionFormGroup = this.fb.group({ type: ['SIMPLE'], expression: [{type: AlarmRuleExpressionType.SIMPLE}], @@ -118,10 +124,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 +151,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, + value: this.value, } }).afterClosed().subscribe((result) => { if (result) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index 14a88b2bf1..661573fc51 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -16,7 +16,7 @@ -->
- + @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss index 21b8a67c24..cea9c68955 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss +++ b/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 { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index f5e57310a2..d0947c651f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -28,7 +28,7 @@ import { isDefinedAndNotNull } from '@core/utils'; import { DashboardId } from '@shared/models/id/dashboard-id'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AlarmRule, AlarmRuleCondition } from "@shared/models/alarm-rule.models"; -import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { AlarmRuleDetailsDialogComponent, AlarmRuleDetailsDialogData @@ -65,6 +65,13 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid @Input() arguments: Record; + @Input() + @coerceBoolean() + isClearCondition = false; + + @Input() + value: CalculatedField; + private modelValue: AlarmRule; alarmRuleFormGroup = this.fb.group({ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html index b0e6164d30..6f6784b8d0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -34,7 +34,7 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts index 94eeb755a9..7fde250074 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -29,7 +29,7 @@ import { } from '@angular/forms'; import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; import { AlarmRule } from "@shared/models/alarm-rule.models"; -import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { CalculatedField, 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"; @@ -60,6 +60,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida @Input() arguments: Record; + @Input() + value: CalculatedField; alarmSeverities = Object.keys(AlarmSeverity); alarmSeverityEnum = AlarmSeverity; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html index abc1dd463d..8abb4ea45a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html @@ -67,7 +67,10 @@
-
{{ 'alarm-rule.filter' | translate }}
+
+ {{ 'alarm-rule.filter' | translate }} +
{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html index 3f588a3d70..3295bdc85e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html @@ -72,7 +72,6 @@ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html index b7450262f6..4882d37f0e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html @@ -70,7 +70,6 @@ [class.!hidden]="disabled" (click)="addPredicate(false)" type="button" - matTooltip="{{ 'filter.add-filter' | translate }}" matTooltipPosition="above"> {{ 'action.add' | translate }} @@ -78,7 +77,6 @@ [class.!hidden]="disabled" (click)="addPredicate(true)" type="button" - matTooltip="{{ 'filter.add-complex-filter' | translate }}" matTooltipPosition="above"> {{ 'filter.add-complex' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 0ac64b428f..6ac87e21ce 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/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 { @@ -256,6 +257,19 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + 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)), diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html index 9277bae767..30533dc714 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -24,15 +24,8 @@
}
- @if (!isOutputKey) { - + @if (!watchKeyChange) { + } @if (!hiddenEntityTypes) { @@ -105,7 +98,7 @@ } - +
} @else { @@ -132,6 +125,7 @@ - @if (isOutputKey) { - + @if (watchKeyChange) { + } @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { @if (!hiddenDefaultValue) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index ad97e7b06e..ac74815704 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -72,6 +72,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() isScript: boolean; @Input() usedArgumentNames: string[]; @Input() isOutputKey = false; + @Input() watchKeyChange = false; @Input() hiddenEntityTypes = false; @Input() hiddenEntityKeyTypes = false; @Input() hiddenDefaultValue = false; @@ -107,6 +108,17 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI entityFilter: EntityFilter; entityNameSubject = new BehaviorSubject(null); + enableAutocomplete = false; + + argumentNameContext = { + 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' + } + readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentType = ArgumentType; readonly DataKeyType = DataKeyType; @@ -155,7 +167,20 @@ 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.isOutputKey) { + this.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' + } + } if (this.defaultValueRequired) { this.argumentFormGroup.get('defaultValue').addValidators(Validators.required); @@ -202,6 +227,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 +296,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 +326,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 { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index 15d2f4b09b..6b8f4fda79 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -120,10 +120,10 @@
+ class="tb-prompt flex flex-1 items-end justify-center min-h-14"> {{ 'calculated-fields.no-arguments' | translate }}
- @if (errorText) { + @if (errorText || (dataSource.isEmpty() | async)) { }
@@ -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 }} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index f3d8117d3e..bca335211e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -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({ diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts index 94d94c2d76..2e4dd9a04d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts @@ -94,6 +94,7 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa this.panelAdditionalCtx = { argumentEntityTypes: [ArgumentEntityType.Current], isOutputKey: true, + watchKeyChange: true, forbiddenNames: [...FORBIDDEN_NAMES, 'propagationCtx'], }; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 7184a5f098..49f4664f13 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/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" + [calculatedFieldId]="data.value?.id?.id" > } @case (CalculatedFieldType.ENTITY_AGGREGATION) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html index 1f5444f8b3..9f4e331cc1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -176,7 +176,7 @@
- @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current) { + @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Owner) {
{{ 'calculated-fields.perimeter-attribute-key' | translate }} @@ -204,6 +204,7 @@ } @else { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts index 5f3e00a0cf..0bbb72efb5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts +++ b/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(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); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html index eda8a779ad..0a2358d544 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html @@ -27,7 +27,7 @@ warning @@ -35,7 +35,7 @@ warning @@ -43,7 +43,7 @@ warning @@ -51,7 +51,7 @@ warning @@ -59,7 +59,7 @@ warning @@ -105,7 +105,24 @@
{{ 'api-usage.tbel' | translate }}
+ +
+ +
@@ -128,7 +145,7 @@
{{ 'calculated-fields.argument-name' | translate }}
- @for (argument of arguments; track argument) { + @for (argument of argumentsList; track argument) { {{ argument }} } @@ -146,7 +163,7 @@ } @else { {{ 'api-usage.tbel' | translate }}
+ +
+ +
}
@if (simpleMode) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts index 4a843208c5..1c1c0da7d7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input, OnInit, output } from '@angular/core'; +import { Component, DestroyRef, Input, OnInit, output } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { FormBuilder, Validators } from '@angular/forms'; import { charsWithNumRegex } from '@shared/models/regex.constants'; @@ -23,9 +23,14 @@ import { AggFunctionTranslations, AggInputType, AggInputTypeTranslations, + ArgumentType, CalculatedFieldAggMetricValue, + CalculatedFieldArgument, + CalculatedFieldEventArguments, FORBIDDEN_NAMES, forbiddenNamesValidator, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, uniqueNameValidator } from '@shared/models/calculated-field.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -33,6 +38,15 @@ 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 { MatDialog } from "@angular/material/dialog"; +import { Observable } from "rxjs"; +import { isObject } from "@core/utils"; +import { + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestScriptDialogData +} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; +import { filter, switchMap, tap } from "rxjs/operators"; +import { CalculatedFieldsService } from "@core/http/calculated-fields.service"; interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue { allowFilter: boolean; @@ -48,13 +62,15 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { @Input() buttonTitle: string; @Input() metric: CalculatedFieldAggMetricValue; @Input() usedNames: string[]; - @Input() arguments: Array; + @Input() arguments: Record; @Input() simpleMode: boolean; @Input() editorCompleter: TbEditorCompleter; @Input() highlightRules: AceHighlightRules; + @Input() calculatedFieldId: string; metricDataApplied = output(); filterExpanded = false; + argumentsList: Array functionArgs: Array metricForm = this.fb.group({ @@ -81,7 +97,10 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { constructor( private fb: FormBuilder, - private popover: TbPopoverComponent + private popover: TbPopoverComponent, + private dialog: MatDialog, + private calculatedFieldsService: CalculatedFieldsService, + private destroyRef: DestroyRef ) { this.observeFilterAllowChange(); this.observeInputTypeChange(); @@ -100,7 +119,8 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { this.validateInputTypeFilter(data.input?.type ?? AggInputType.key); this.validateInputKey(); - this.functionArgs = ['ctx', ...this.arguments]; + this.argumentsList = Object.keys(this.arguments); + this.functionArgs = ['ctx', ...this.argumentsList]; } saveMetric(): void { @@ -158,9 +178,55 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { } private validateInputKey() { - if (this.metric.input?.type === AggInputType.key && !this.arguments.includes(this.metric.input.key)) { + if (this.metric.input?.type === AggInputType.key && !Object.keys(this.arguments).includes(this.metric.input.key)) { this.metricForm.get('input.key').setValue(null); this.metricForm.get('input.key').markAsTouched(); } } + + onTestScript(scriptFunc: 'filter' | 'input.function') { + this.testScript(scriptFunc).subscribe(expression => { + this.metricForm.get(scriptFunc).setValue(expression); + this.metricForm.get(scriptFunc).markAsDirty(); + }); + } + + testScript(scriptFunc: 'filter' | 'input.function'): Observable { + if (this.calculatedFieldId) { + return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(this.calculatedFieldId, {ignoreLoading: true}) + .pipe( + switchMap(event => { + const args = event?.arguments ? JSON.parse(event.arguments) : null; + return this.getTestScriptDialog(this.arguments, this.metricForm.get(scriptFunc).value, args); + }), + takeUntilDestroyed(this.destroyRef) + ) + } + return this.getTestScriptDialog(this.arguments, this.metricForm.get(scriptFunc).value, null); + } + + getTestScriptDialog(argumentsList: Record, expression: string, argumentsObj?: CalculatedFieldEventArguments): Observable { + const resultArguments = Object.keys(argumentsList).reduce((acc, key) => { + const type = argumentsList[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, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], + data: { + arguments: resultArguments, + expression: expression, + argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(argumentsList), + argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(argumentsList) + } + }).afterClosed() + .pipe( + filter(Boolean), + tap(expression => expression) + ); + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts index 22adf9a801..1a8821a15c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -40,6 +40,7 @@ import { AggInputTypeTranslations, CalculatedFieldAggMetric, CalculatedFieldAggMetricValue, + CalculatedFieldArgument, } from '@shared/models/calculated-field.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; @@ -76,10 +77,11 @@ import { AceHighlightRules } from '@shared/models/ace/ace.models'; }) export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit { - @Input() arguments: Array; + @Input() arguments: Record; @Input() editorCompleter: TbEditorCompleter; @Input() highlightRules: AceHighlightRules; @Input({transform: booleanAttribute}) simpleMode: boolean = false; + @Input() calculatedFieldId: 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, + calculatedFieldId: this.calculatedFieldId }; this.popoverComponent = this.popoverService.displayPopover({ trigger, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html index 8a0361ee58..d6b02b3bbe 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html +++ b/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 }}
Object.keys(argumentsObj)) + map(argumentsObj => argumentsObj) ); argumentsEditorCompleter$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html index 9cb82c07fb..d84ad03aca 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html @@ -33,11 +33,11 @@ @if (authUser.authority === authorities.TENANT_ADMIN) { - + - + } diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts index b5eecccb1a..ffde1502c8 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts +++ b/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 { + ownerId: EntityId; + constructor(protected store: Store) { super(store); } @@ -35,4 +38,9 @@ export class AssetTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + protected setEntity(entity: AssetInfo) { + this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId; + super.setEntity(entity); + } + } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html index 119da03175..cfd2d0d29c 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html @@ -33,10 +33,10 @@ @if (authUser.authority === authorities.TENANT_ADMIN) { - + - + } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts index 7bd86922bf..95f2fbfccf 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts +++ b/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 { + ownerId: EntityId; + constructor(protected store: Store) { super(store); } @@ -35,4 +38,9 @@ export class DeviceTabsComponent extends EntityTabsComponent { super.ngOnInit(); } + protected setEntity(entity: DeviceInfo) { + this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId; + super.setEntity(entity); + } + } diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html index c08355462f..06448d3d69 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html @@ -43,7 +43,7 @@ @for (key of filteredKeys$ | async; track key) { } @empty { - @if (!this.keyControl.value) { + @if (!this.keyControl.value && enableAutocomplete) { {{ 'entity.no-keys-found' | translate }} } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts index 8727c17f77..623f1d9ead 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts +++ b/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(); dataKeyType = input.required(); @@ -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: diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts index bb44f0ea53..dc89f05fde 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/models/alarm-rule.models.ts b/ui-ngx/src/app/shared/models/alarm-rule.models.ts index c9b549d78a..35b6690644 100644 --- a/ui-ngx/src/app/shared/models/alarm-rule.models.ts +++ b/ui-ngx/src/app/shared/models/alarm-rule.models.ts @@ -157,3 +157,7 @@ export interface AlarmRuleFilterConfig { entityType?: EntityType; entities?: Array; } + +export const alarmRuleDefaultScript = + '// Sample expression for an alarm rule: triggers when temperature is above 20 degree\n' + + 'return temperature > 20;' diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 8140f67e8f..5727bfe063 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/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 { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 538b3a8352..3fc9741952 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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": { @@ -1305,7 +1310,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 +1350,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 +1411,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 +1448,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 +1471,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 +1490,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 +1506,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", @@ -1708,7 +1718,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", From 352236e003d17e6c686caf8da6f0440c034ae5d9 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 25 Nov 2025 18:03:38 +0200 Subject: [PATCH 10/13] UI: Fixed validation --- .../components/alarm-rules/create-cf-alarm-rules.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts index 7fde250074..5f36f76dae 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -159,7 +159,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, }, From 7b1bee4380a55ed2ad634c9e7ef58f26a04dc169 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Tue, 25 Nov 2025 18:26:17 +0200 Subject: [PATCH 11/13] Update api_usage.json --- ui-ngx/src/assets/dashboard/api_usage.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index e45e9a59fa..bf3fd220be 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -100,8 +100,8 @@ "displayPagination": true, "defaultPageSize": 10, "sortOrder": { - "property": "createdTime", - "direction": "DESC" + "property": "name", + "direction": "ASC" } }, "title": "{i18n:api-usage.exceptions}", From 361c9c8593b8d21a3a582bd1d9f260c4e702530d Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 26 Nov 2025 13:25:49 +0200 Subject: [PATCH 12/13] UI: Fixed debug event function dialog --- .../alarm-rule-dialog.component.html | 7 +- .../alarm-rule-dialog.component.ts | 25 ++++++- .../alarm-rules/alarm-rules-table-config.ts | 50 +++++++++++++- ...f-alarm-rule-condition-dialog.component.ts | 63 ++--------------- .../cf-alarm-rule-condition.component.ts | 9 +-- .../alarm-rules/cf-alarm-rule.component.html | 2 +- .../alarm-rules/cf-alarm-rule.component.ts | 7 +- .../create-cf-alarm-rules.component.html | 5 +- .../create-cf-alarm-rules.component.ts | 7 +- .../alarm-rule-filter-list.component.html | 3 +- ...-rule-filter-predicate-list.component.html | 6 +- .../calculated-fields-table-config.ts | 5 +- ...lculated-field-argument-panel.component.ts | 29 +++----- .../propagate-arguments-table.component.ts | 9 ++- .../calculated-field-dialog.component.html | 2 +- .../calculated-field-dialog.component.ts | 6 +- ...ntity-aggregation-component.component.html | 1 + .../entity-aggregation-component.component.ts | 4 +- ...culated-field-metrics-panel.component.html | 10 +-- ...alculated-field-metrics-panel.component.ts | 69 ++----------------- ...alculated-field-metrics-table.component.ts | 8 +-- ...ities-aggregation-component.component.html | 2 +- ...ntities-aggregation-component.component.ts | 5 +- .../app/shared/models/alarm-rule.models.ts | 4 ++ .../shared/models/calculated-field.models.ts | 2 +- 25 files changed, 150 insertions(+), 190 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index eefa6ff209..b734757144 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -91,7 +91,7 @@
{{ 'alarm-rule.create-conditions' | translate }}
- +
@@ -100,7 +100,7 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index b3176414ab..2209cbafba 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -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 { + 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); + } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index c581db2b2b..6fa6515663 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -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,9 +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 { @@ -249,6 +257,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, additionalDebugActionConfig: this.additionalDebugActionConfig, isDirty, + getTestScriptDialogFn: this.getTestScriptDialog.bind(this), }, enterAnimationDuration: isDirty ? 0 : null, }) @@ -324,4 +333,41 @@ export class AlarmRulesTableConfig extends EntityTableConfig { takeUntilDestroyed(this.destroyRef), ).subscribe(() => this.updateData()); } + + private getTestScriptDialog(calculatedField: CalculatedField, expression: string, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable { + 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, + { + 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); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index dc8b23c707..2b0f25c2fe 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, Validators } from '@angular/forms'; @@ -32,10 +32,7 @@ import { AlarmRuleExpressionType } from "@shared/models/alarm-rule.models"; import { - ArgumentType, - CalculatedField, CalculatedFieldArgument, - CalculatedFieldEventArguments, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights } from "@shared/models/calculated-field.models"; @@ -43,19 +40,12 @@ 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"; -import { filter, switchMap, tap } from "rxjs/operators"; -import { isObject } from "@core/utils"; -import { - CalculatedFieldScriptTestDialogComponent, - CalculatedFieldTestScriptDialogData -} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; -import { CalculatedFieldsService } from "@core/http/calculated-fields.service"; export interface CfAlarmRuleConditionDialogData { readonly: boolean; condition: AlarmRuleCondition; arguments?: Record; - value?: CalculatedField; + testScript: (expression: string) => Observable; } @Component({ @@ -119,10 +109,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent, - private fb: FormBuilder, - private calculatedFieldsService: CalculatedFieldsService, - private destroyRef: DestroyRef, - private dialog: MatDialog) { + private fb: FormBuilder) { super(store, router, dialogRef); this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; @@ -240,50 +227,10 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { this.conditionFormGroup.get('expression.expression').setValue(expression); this.conditionFormGroup.get('expression.expression').markAsDirty(); }) } - - testScript(): Observable { - if (this.data.value?.id?.id) { - return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(this.data.value?.id?.id, {ignoreLoading: true}) - .pipe( - switchMap(event => { - const args = event?.arguments ? JSON.parse(event.arguments) : null; - return this.getTestScriptDialog(this.arguments, this.conditionFormGroup.get('expression.expression').value, args); - }), - takeUntilDestroyed(this.destroyRef) - ) - } - return this.getTestScriptDialog(this.arguments, this.conditionFormGroup.get('expression.expression').value, null); - } - - getTestScriptDialog(argumentsList: Record, expression: string, argumentsObj?: CalculatedFieldEventArguments): Observable { - const resultArguments = Object.keys(argumentsList).reduce((acc, key) => { - const type = argumentsList[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, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], - data: { - arguments: resultArguments, - expression: expression, - argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(argumentsList), - argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(argumentsList) - } - }).afterClosed() - .pipe( - filter(Boolean), - tap(expression => expression) - ); - } - } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 0ae0c4a067..2a0b7a7810 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -43,12 +43,13 @@ import { AlarmRuleSchedule, AlarmRuleScheduleType } from "@shared/models/alarm-rule.models"; -import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { AlarmRuleScheduleDialogData, 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', @@ -84,8 +85,8 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali @coerceBoolean() isClearCondition = false; - @Input() - value: CalculatedField; + @Input({required: true}) + testScript: (expression: string) => Observable; alarmRuleConditionFormGroup = this.fb.group({ type: ['SIMPLE'], @@ -152,7 +153,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali readonly: this.disabled, condition: this.disabled ? this.modelValue : deepClone(this.modelValue), arguments: this.arguments, - value: this.value, + testScript: this.testScript } }).afterClosed().subscribe((result) => { if (result) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index 661573fc51..38e0bd8023 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -16,7 +16,7 @@ -->
- + @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index d0947c651f..5db525074d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -28,12 +28,13 @@ import { isDefinedAndNotNull } from '@core/utils'; import { DashboardId } from '@shared/models/id/dashboard-id'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AlarmRule, AlarmRuleCondition } from "@shared/models/alarm-rule.models"; -import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { AlarmRuleDetailsDialogComponent, 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', @@ -69,8 +70,8 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid @coerceBoolean() isClearCondition = false; - @Input() - value: CalculatedField; + @Input({required: true}) + testScript: (expression: string) => Observable; private modelValue: AlarmRule; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html index 6f6784b8d0..359ad104f1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -34,7 +34,7 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts index 5f36f76dae..2197d6d14c 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -29,10 +29,11 @@ import { } from '@angular/forms'; import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; import { AlarmRule } from "@shared/models/alarm-rule.models"; -import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +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,8 +61,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida @Input() arguments: Record; - @Input() - value: CalculatedField; + @Input({required: true}) + testScript: (expression: string) => Observable; alarmSeverities = Object.keys(AlarmSeverity); alarmSeverityEnum = AlarmSeverity; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html index 3295bdc85e..19ccff4a52 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html @@ -71,8 +71,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html index 4882d37f0e..c094949fb6 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html @@ -69,15 +69,13 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 6ac87e21ce..98e4a29549 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -309,9 +309,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable { + private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { 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) => { @@ -327,7 +328,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig 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; @@ -110,15 +117,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI enableAutocomplete = false; - argumentNameContext = { - 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' - } - readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentType = ArgumentType; readonly DataKeyType = DataKeyType; @@ -171,17 +169,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.setWatchKeyChange(); } - if (this.isOutputKey) { - this.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' - } - } - if (this.defaultValueRequired) { this.argumentFormGroup.get('defaultValue').addValidators(Validators.required); this.argumentFormGroup.get('defaultValue').updateValueAndValidity({onlySelf: true}); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts index 2e4dd9a04d..7c667999d0 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts @@ -93,7 +93,14 @@ 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'], }; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 49f4664f13..3070a6cbb2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -88,7 +88,7 @@ [entityId]="data.entityId" [entityName]="data.entityName" [tenantId]="data.tenantId" - [calculatedFieldId]="data.value?.id?.id" + [testScript]="onTestScript.bind(this)" > } @case (CalculatedFieldType.ENTITY_AGGREGATION) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 7174ec9dd9..ee7bafee83 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/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 { + onTestScript(expression?: string): Observable { 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 { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html index e4a40c3cdd..4dcbf5b38d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -34,6 +34,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts index c0afae1c30..a09dd87cfa 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts +++ b/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; + readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; readonly DayInSec = DAY / SECOND; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html index 0a2358d544..f86c15ba73 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html @@ -110,7 +110,7 @@ matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" matTooltipPosition="above" class="tb-mat-32" - [disabled]="!argumentsList.length" + [disabled]="!arguments.length" (click)="onTestScript('filter')"> bug_report @@ -119,7 +119,7 @@ @@ -145,7 +145,7 @@
{{ 'calculated-fields.argument-name' | translate }}
- @for (argument of argumentsList; track argument) { + @for (argument of arguments; track argument) { {{ argument }} } @@ -179,7 +179,7 @@ matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" matTooltipPosition="above" class="tb-mat-32" - [disabled]="!argumentsList.length" + [disabled]="!arguments.length" (click)="onTestScript('input.function')"> bug_report @@ -188,7 +188,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts index 1c1c0da7d7..3d5a53ea4e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, Input, OnInit, output } from '@angular/core'; +import { Component, Input, OnInit, output } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { FormBuilder, Validators } from '@angular/forms'; import { charsWithNumRegex } from '@shared/models/regex.constants'; @@ -23,14 +23,9 @@ import { AggFunctionTranslations, AggInputType, AggInputTypeTranslations, - ArgumentType, CalculatedFieldAggMetricValue, - CalculatedFieldArgument, - CalculatedFieldEventArguments, FORBIDDEN_NAMES, forbiddenNamesValidator, - getCalculatedFieldArgumentsEditorCompleter, - getCalculatedFieldArgumentsHighlights, uniqueNameValidator } from '@shared/models/calculated-field.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -38,15 +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 { MatDialog } from "@angular/material/dialog"; import { Observable } from "rxjs"; -import { isObject } from "@core/utils"; -import { - CalculatedFieldScriptTestDialogComponent, - CalculatedFieldTestScriptDialogData -} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; -import { filter, switchMap, tap } from "rxjs/operators"; -import { CalculatedFieldsService } from "@core/http/calculated-fields.service"; interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue { allowFilter: boolean; @@ -62,15 +49,14 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { @Input() buttonTitle: string; @Input() metric: CalculatedFieldAggMetricValue; @Input() usedNames: string[]; - @Input() arguments: Record; + @Input() arguments: Array; @Input() simpleMode: boolean; @Input() editorCompleter: TbEditorCompleter; @Input() highlightRules: AceHighlightRules; - @Input() calculatedFieldId: string; + @Input({required: true}) testScript: (expression?: string) => Observable; metricDataApplied = output(); filterExpanded = false; - argumentsList: Array functionArgs: Array metricForm = this.fb.group({ @@ -98,9 +84,6 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { constructor( private fb: FormBuilder, private popover: TbPopoverComponent, - private dialog: MatDialog, - private calculatedFieldsService: CalculatedFieldsService, - private destroyRef: DestroyRef ) { this.observeFilterAllowChange(); this.observeInputTypeChange(); @@ -119,8 +102,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { this.validateInputTypeFilter(data.input?.type ?? AggInputType.key); this.validateInputKey(); - this.argumentsList = Object.keys(this.arguments); - this.functionArgs = ['ctx', ...this.argumentsList]; + this.functionArgs = ['ctx', ...this.arguments]; } saveMetric(): void { @@ -178,55 +160,16 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { } private validateInputKey() { - if (this.metric.input?.type === AggInputType.key && !Object.keys(this.arguments).includes(this.metric.input.key)) { + if (this.metric.input?.type === AggInputType.key && !this.arguments.includes(this.metric.input.key)) { this.metricForm.get('input.key').setValue(null); this.metricForm.get('input.key').markAsTouched(); } } onTestScript(scriptFunc: 'filter' | 'input.function') { - this.testScript(scriptFunc).subscribe(expression => { + this.testScript(this.metricForm.get(scriptFunc).value).subscribe(expression => { this.metricForm.get(scriptFunc).setValue(expression); this.metricForm.get(scriptFunc).markAsDirty(); }); } - - testScript(scriptFunc: 'filter' | 'input.function'): Observable { - if (this.calculatedFieldId) { - return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(this.calculatedFieldId, {ignoreLoading: true}) - .pipe( - switchMap(event => { - const args = event?.arguments ? JSON.parse(event.arguments) : null; - return this.getTestScriptDialog(this.arguments, this.metricForm.get(scriptFunc).value, args); - }), - takeUntilDestroyed(this.destroyRef) - ) - } - return this.getTestScriptDialog(this.arguments, this.metricForm.get(scriptFunc).value, null); - } - - getTestScriptDialog(argumentsList: Record, expression: string, argumentsObj?: CalculatedFieldEventArguments): Observable { - const resultArguments = Object.keys(argumentsList).reduce((acc, key) => { - const type = argumentsList[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, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], - data: { - arguments: resultArguments, - expression: expression, - argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(argumentsList), - argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(argumentsList) - } - }).afterClosed() - .pipe( - filter(Boolean), - tap(expression => expression) - ); - } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts index 1a8821a15c..ec916213e7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -40,7 +40,6 @@ import { AggInputTypeTranslations, CalculatedFieldAggMetric, CalculatedFieldAggMetricValue, - CalculatedFieldArgument, } from '@shared/models/calculated-field.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; @@ -57,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', @@ -77,11 +77,11 @@ import { AceHighlightRules } from '@shared/models/ace/ace.models'; }) export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit { - @Input() arguments: Record; + @Input() arguments: Array; @Input() editorCompleter: TbEditorCompleter; @Input() highlightRules: AceHighlightRules; @Input({transform: booleanAttribute}) simpleMode: boolean = false; - @Input() calculatedFieldId: string; + @Input({required: true}) testScript: (expression?: string) => Observable; @ViewChild(MatSort, { static: true }) sort: MatSort; @@ -168,7 +168,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu editorCompleter: this.editorCompleter, highlightRules: this.highlightRules, simpleMode: this.simpleMode, - calculatedFieldId: this.calculatedFieldId + testScript: this.testScript }; this.popoverComponent = this.popoverService.displayPopover({ trigger, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html index d6b02b3bbe..af3139801b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html @@ -55,7 +55,7 @@ {{ 'calculated-fields.metrics.metrics' | translate }} Observable; readonly ScriptLanguage = ScriptLanguage; readonly CalculatedFieldType = CalculatedFieldType; @@ -95,7 +96,7 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu }); arguments$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( - map(argumentsObj => argumentsObj) + map(argumentsObj => Object.keys(argumentsObj)) ); argumentsEditorCompleter$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( diff --git a/ui-ngx/src/app/shared/models/alarm-rule.models.ts b/ui-ngx/src/app/shared/models/alarm-rule.models.ts index 35b6690644..5170c916ff 100644 --- a/ui-ngx/src/app/shared/models/alarm-rule.models.ts +++ b/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', @@ -161,3 +163,5 @@ export interface AlarmRuleFilterConfig { 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, closeAllOnSave?: boolean) => Observable; diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 5727bfe063..a070fe23c2 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -496,7 +496,7 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument { entityName?: string; } -export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record, closeAllOnSave?: boolean) => Observable; +export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record, closeAllOnSave?: boolean, expression?: string) => Observable; export interface CalculatedFieldTestScriptInputParams { arguments: CalculatedFieldEventArguments; From 2fe7d34331b2b8594c325657e053d2f6d620674b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 26 Nov 2025 15:58:51 +0200 Subject: [PATCH 13/13] UI: Fixed progress filling and add label for entity aliase --- .../widget/lib/cards/api-usage-widget.component.ts | 10 +++++++--- .../cards/api-usage-widget-settings.component.html | 1 + .../common/alias/entity-alias-select.component.ts | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index b7f48d41f6..89937cda65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/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) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html index e96a654c44..f4e366cc6a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html @@ -20,6 +20,7 @@
widget-config.datasource