From bd34ed5011ac6d854bd06dcd92095662bc1a16b0 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 24 Jan 2025 15:34:18 +0200 Subject: [PATCH 01/19] Implemented calculated fields table --- .../core/http/calculated-fields.service.ts | 78 +++++++++ .../calculated-fields-table-config.ts | 157 ++++++++++++++++++ .../calculated-fields-table.component.html | 1 + .../calculated-fields-table.component.ts | 104 ++++++++++++ .../entity/entities-table.component.ts | 8 +- .../home/components/home-components.module.ts | 9 +- .../entity/entity-table-component.models.ts | 4 +- .../pages/device/device-tabs.component.html | 4 + .../app/shared/models/entity-type.models.ts | 15 +- .../assets/locale/locale.constant-en_US.json | 9 + 10 files changed, 383 insertions(+), 6 deletions(-) create mode 100644 ui-ngx/src/app/core/http/calculated-fields.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts new file mode 100644 index 0000000000..92eae2c25a --- /dev/null +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -0,0 +1,78 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageData } from '@shared/models/page/page-data'; + +@Injectable({ + providedIn: 'root' +}) +// [TODO]: [Calculated fields] - implement when BE ready +export class CalculatedFieldsService { + + fieldsMock = [ + { + name: 'Calculated Field 1', + type: 'Simple', + expression: '1 + 2', + id: { + id: '1', + } + }, + { + name: 'Calculated Field 2', + type: 'Script', + expression: '${power}', + id: { + id: '2', + } + } + ]; + + constructor( + private http: HttpClient + ) { } + + public getCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { + return of(this.fieldsMock[0]); + // return this.http.get(`/api/calculated-field/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveCalculatedField(calculatedField: any, config?: RequestConfig): Observable { + return of(this.fieldsMock[1]); + // return this.http.post('/api/calculated-field', calculatedField, defaultHttpOptionsFromConfig(config)); + } + + public deleteCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { + return of(true); + // return this.http.delete(`/api/calculated-field/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); + } + + public getCalculatedFields(query: any, + config?: RequestConfig): Observable> { + return of({ + data: this.fieldsMock, + totalPages: 1, + totalElements: 2, + hasNext: false, + }); + // return this.http.get>(`/api/calculated-field${query.toQuery()}`, + // defaultHttpOptionsFromConfig(config)); + } +} 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 new file mode 100644 index 0000000000..f2e2a64b49 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -0,0 +1,157 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Direction } from '@shared/models/page/sort-order'; +import { MatDialog } from '@angular/material/dialog'; +import { TimePageLink } from '@shared/models/page/page-link'; +import { Observable, of } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { DialogService } from '@core/services/dialog.service'; +import { MINUTE } from '@shared/models/time/time.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { ChangeDetectorRef, DestroyRef, ViewContainerRef } from '@angular/core'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { EntityDebugSettings } from '@shared/models/entity.models'; +import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { catchError, switchMap } from 'rxjs/operators'; + +export class CalculatedFieldsTableConfig extends EntityTableConfig { + + readonly calculatedFieldsDebugPerTenantLimitsConfiguration = + getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1'; + readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; + + constructor(private calculatedFieldsService: CalculatedFieldsService, + private entityService: EntityService, + private dialogService: DialogService, + private translate: TranslateService, + private dialog: MatDialog, + public entityId: EntityId = null, + private store: Store, + private viewContainerRef: ViewContainerRef, + private overlay: Overlay, + private cd: ChangeDetectorRef, + private utilsService: UtilsService, + private durationLeft: DurationLeftPipe, + private popoverService: TbPopoverService, + private destroyRef: DestroyRef, + ) { + super(); + this.tableTitle = this.translate.instant('calculated-fields.label'); + this.detailsPanelEnabled = false; + this.selectionEnabled = true; + this.searchEnabled = true; + this.addEnabled = true; + this.entitiesDeleteEnabled = true; + this.actionsColumnTitle = ''; + this.entityType = EntityType.CALCULATED_FIELDS; + this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELDS); + + this.entitiesFetchFunction = pageLink => this.fetchCalculatedFields(pageLink); + + this.defaultSortOrder = {property: 'name', direction: Direction.DESC}; + + this.columns.push( + new EntityTableColumn('name', 'common.name', '33%')); + this.columns.push( + new EntityTableColumn('type', 'common.type', '50px')); + this.columns.push( + new EntityTableColumn('expression', 'calculated-fields.expression', '50%')); + + this.cellActionDescriptors.push( + { + name: '', + nameFunction: (entity) => this.getDebugConfigLabel(entity?.debugSettings), + icon: 'mdi:bug', + isEnabled: () => true, + iconFunction: ({ debugSettings }) => this.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', + onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), + }, + { + name: this.translate.instant('action.edit'), + icon: 'edit', + isEnabled: () => true, + // // [TODO]: [Calculated fields] - implement edit + onAction: (_, entity) => {} + } + ); + } + + fetchCalculatedFields(pageLink: TimePageLink): Observable> { + return this.calculatedFieldsService.getCalculatedFields(pageLink); + } + + onOpenDebugConfig($event: Event, { debugSettings = {}, id }: any): void { + const { renderer, viewContainerRef } = this.getTable(); + if ($event) { + $event.stopPropagation(); + } + const trigger = $event.target as Element; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const debugStrategyPopover = this.popoverService.displayPopover(trigger, renderer, + viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null, + { + debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration, + maxDebugModeDuration: this.maxDebugModeDuration, + entityLabel: this.translate.instant('debug-settings.integration'), + ...debugSettings + }, + {}, + {}, {}, true); + debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover; + debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((settings: EntityDebugSettings) => { + this.onDebugConfigChanged(id.id, settings); + debugStrategyPopover.hide(); + }); + } + } + + private getDebugConfigLabel(debugSettings: EntityDebugSettings): string { + const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil); + + if (!isDebugActive) { + return debugSettings?.failuresEnabled ? this.translate.instant('debug-settings.failures') : this.translate.instant('common.disabled'); + } else { + return this.durationLeft.transform(debugSettings?.allEnabledUntil) + } + } + + private isDebugActive(allEnabledUntil: number): boolean { + return allEnabledUntil > new Date().getTime(); + } + + private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { + this.calculatedFieldsService.getCalculatedField(id).pipe( + switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), + catchError(() => of(null)), + takeUntilDestroyed(this.destroyRef), + ).subscribe(() => this.updateData()); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html new file mode 100644 index 0000000000..38aa1487af --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html @@ -0,0 +1 @@ + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts new file mode 100644 index 0000000000..a5b1ab34c1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -0,0 +1,104 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + Input, + OnInit, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { EntityService } from '@core/http/entity.service'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; +import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; + +@Component({ + selector: 'tb-calculated-fields-table', + templateUrl: './calculated-fields-table.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CalculatedFieldsTableComponent implements OnInit { + + @Input() entityId: EntityId; + + @Input() + set active(active: boolean) { + if (this.activeValue !== active) { + this.activeValue = active; + if (this.activeValue && this.dirtyValue) { + this.dirtyValue = false; + this.entitiesTable.updateData(); + } + } + } + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + calculatedFieldsTableConfig: CalculatedFieldsTableConfig; + + private activeValue = false; + private dirtyValue = false; + + constructor(private calculatedFieldsService: CalculatedFieldsService, + private entityService: EntityService, + private dialogService: DialogService, + private translate: TranslateService, + private dialog: MatDialog, + private store: Store, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private durationLeft: DurationLeftPipe, + private popoverService: TbPopoverService, + private destroyRef: DestroyRef, + private utilsService: UtilsService) { + } + + ngOnInit() { + this.dirtyValue = !this.activeValue; + + this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( + this.calculatedFieldsService, + this.entityService, + this.dialogService, + this.translate, + this.dialog, + this.entityId, + this.store, + this.viewContainerRef, + this.overlay, + this.cd, + this.utilsService, + this.durationLeft, + this.popoverService, + this.destroyRef + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 9bb5fe4d8a..3c723f8080 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -25,8 +25,10 @@ import { OnChanges, OnDestroy, OnInit, + Renderer2, SimpleChanges, - ViewChild + ViewChild, + ViewContainerRef, } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; @@ -141,7 +143,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa private router: Router, private elementRef: ElementRef, private fb: FormBuilder, - private zone: NgZone) { + private zone: NgZone, + public viewContainerRef: ViewContainerRef, + public renderer: Renderer2) { super(store); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 32e509e842..1a6b9c08e0 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -183,6 +183,8 @@ import { } from '@home/components/dashboard-page/layout/select-dashboard-breakpoint.component'; import { EntityChipsComponent } from '@home/components/entity/entity-chips.component'; import { DashboardViewComponent } from '@home/components/dashboard-view/dashboard-view.component'; +import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; +import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; @NgModule({ declarations: @@ -326,7 +328,8 @@ import { DashboardViewComponent } from '@home/components/dashboard-view/dashboar RateLimitsDetailsDialogComponent, SendNotificationButtonComponent, EntityChipsComponent, - DashboardViewComponent + DashboardViewComponent, + CalculatedFieldsTableComponent, ], imports: [ CommonModule, @@ -463,11 +466,13 @@ import { DashboardViewComponent } from '@home/components/dashboard-view/dashboar RateLimitsDetailsDialogComponent, SendNotificationButtonComponent, EntityChipsComponent, - DashboardViewComponent + DashboardViewComponent, + CalculatedFieldsTableComponent, ], providers: [ WidgetComponentService, CustomDialogService, + DurationLeftPipe, {provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent}, {provide: COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN, useValue: ComplexFilterPredicateDialogComponent}, {provide: DASHBOARD_PAGE_COMPONENT_TOKEN, useValue: DashboardPageComponent}, diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index b6f634195e..8c4f856609 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -20,7 +20,7 @@ import { SafeHtml } from '@angular/platform-browser'; import { PageLink } from '@shared/models/page/page-link'; import { Timewindow } from '@shared/models/time/time.models'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; -import { ElementRef, EventEmitter } from '@angular/core'; +import { ElementRef, EventEmitter, Renderer2, ViewContainerRef } from '@angular/core'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; @@ -64,6 +64,8 @@ export interface IEntitiesTableComponent { paginator: MatPaginator; sort: MatSort; route: ActivatedRoute; + viewContainerRef: ViewContainerRef; + renderer: Renderer2; addEnabled(): boolean; clearSelection(): void; 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 5e30694719..357bb587cc 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 @@ -32,6 +32,10 @@ [entityName]="entity.name"> + + + diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index e1b59a9243..5b540a6c54 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -49,7 +49,8 @@ export enum EntityType { OAUTH2_CLIENT = 'OAUTH2_CLIENT', DOMAIN = 'DOMAIN', MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', - MOBILE_APP = 'MOBILE_APP' + MOBILE_APP = 'MOBILE_APP', + CALCULATED_FIELDS = 'CALCULATED_FIELDS', } export enum AliasEntityType { @@ -478,6 +479,18 @@ export const entityTypeTranslations = new MapAre you sure you want to leave this page?", @@ -1027,6 +1034,8 @@ "city-max-length": "Specified city should be less than 256" }, "common": { + "name": "Name", + "type": "Type", "username": "Username", "password": "Password", "enter-username": "Enter username", From b169dfab2793ef1b0721f1b4b4ab7545823bdf81 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 24 Jan 2025 15:46:48 +0200 Subject: [PATCH 02/19] added license headers --- .../calculated-fields-table.component.html | 17 ++++++++++++++ .../calculated-fields-table.component.scss | 22 +++++++++++++++++++ .../calculated-fields-table.component.ts | 1 + 3 files changed, 40 insertions(+) create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html index 38aa1487af..d627e16ce9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html @@ -1 +1,18 @@ + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss new file mode 100644 index 0000000000..ea3f7d90b7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + tb-entities-table { + .mat-drawer-container { + background-color: white; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index a5b1ab34c1..5449bce5a7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -42,6 +42,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; @Component({ selector: 'tb-calculated-fields-table', templateUrl: './calculated-fields-table.component.html', + styleUrls: ['./calculated-fields-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class CalculatedFieldsTableComponent implements OnInit { From be4ea19b91636d2e9c1e927a4776c954972ebc8c Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 24 Jan 2025 17:20:55 +0200 Subject: [PATCH 03/19] added calculated fields typing --- .../core/http/calculated-fields.service.ts | 21 ++++++--- .../calculated-fields-table-config.ts | 21 ++++----- .../pages/device/device-tabs.component.html | 2 +- .../shared/models/calculated-field.models.ts | 44 +++++++++++++++++++ .../app/shared/models/entity-type.models.ts | 8 ++-- .../shared/models/id/calculated-field-id.ts | 26 +++++++++++ .../assets/locale/locale.constant-en_US.json | 3 +- 7 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 ui-ngx/src/app/shared/models/calculated-field.models.ts create mode 100644 ui-ngx/src/app/shared/models/id/calculated-field-id.ts diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 92eae2c25a..66e0833128 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -19,6 +19,7 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; +import { CalculatedField } from '@shared/models/calculated-field.models'; @Injectable({ providedIn: 'root' @@ -30,7 +31,11 @@ export class CalculatedFieldsService { { name: 'Calculated Field 1', type: 'Simple', - expression: '1 + 2', + configuration: { + expression: '1 + 2', + type: 'SIMPLE', + }, + entityId: '1', id: { id: '1', } @@ -38,23 +43,27 @@ export class CalculatedFieldsService { { name: 'Calculated Field 2', type: 'Script', - expression: '${power}', + entityId: '2', + configuration: { + expression: '${power}', + type: 'SIMPLE', + }, id: { id: '2', } } - ]; + ] as any[]; constructor( private http: HttpClient ) { } - public getCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { + public getCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { return of(this.fieldsMock[0]); // return this.http.get(`/api/calculated-field/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public saveCalculatedField(calculatedField: any, config?: RequestConfig): Observable { + public saveCalculatedField(calculatedField: any, config?: RequestConfig): Observable { return of(this.fieldsMock[1]); // return this.http.post('/api/calculated-field', calculatedField, defaultHttpOptionsFromConfig(config)); } @@ -65,7 +74,7 @@ export class CalculatedFieldsService { } public getCalculatedFields(query: any, - config?: RequestConfig): Observable> { + config?: RequestConfig): Observable> { return of({ data: this.fieldsMock, totalPages: 1, 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 f2e2a64b49..22f742b9d8 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 @@ -39,8 +39,9 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { catchError, switchMap } from 'rxjs/operators'; +import { CalculatedField } from '@shared/models/calculated-field.models'; -export class CalculatedFieldsTableConfig extends EntityTableConfig { +export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly calculatedFieldsDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1'; @@ -62,31 +63,31 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); this.defaultSortOrder = {property: 'name', direction: Direction.DESC}; this.columns.push( - new EntityTableColumn('name', 'common.name', '33%')); + new EntityTableColumn('name', 'common.name', '33%')); this.columns.push( - new EntityTableColumn('type', 'common.type', '50px')); + new EntityTableColumn('type', 'common.type', '50px')); this.columns.push( - new EntityTableColumn('expression', 'calculated-fields.expression', '50%')); + new EntityTableColumn('expression', 'calculated-fields.expression', '50%', entity => entity.configuration.expression)); this.cellActionDescriptors.push( { name: '', - nameFunction: (entity) => this.getDebugConfigLabel(entity?.debugSettings), + nameFunction: entity => this.getDebugConfigLabel(entity?.debugSettings), icon: 'mdi:bug', isEnabled: () => true, iconFunction: ({ debugSettings }) => this.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', @@ -102,11 +103,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig> { + fetchCalculatedFields(pageLink: TimePageLink): Observable> { return this.calculatedFieldsService.getCalculatedFields(pageLink); } - onOpenDebugConfig($event: Event, { debugSettings = {}, id }: any): void { + onOpenDebugConfig($event: Event, { debugSettings = {}, id }: CalculatedField): void { const { renderer, viewContainerRef } = this.getTable(); if ($event) { $event.stopPropagation(); 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 357bb587cc..ab59f412cd 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,7 +33,7 @@ + label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab"> , HasVersion, HasTenantId { + entityId: string; + type: CalculatedFieldType; + name: string; + debugSettings?: EntityDebugSettings; + externalId?: string; + createdTime?: number; + configuration: CalculatedFieldConfiguration; +} + +export enum CalculatedFieldType { + SIMPLE = 'SIMPLE', + COMPLEX = 'COMPLEX', +} + +export interface CalculatedFieldConfiguration { + type: CalculatedFieldConfigType; + expression: string; + arguments: Record; +} + +export enum CalculatedFieldConfigType { + SIMPLE = 'SIMPLE', + SCRIPT = 'SCRIPT', +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 5b540a6c54..ee39be0f27 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -50,7 +50,7 @@ export enum EntityType { DOMAIN = 'DOMAIN', MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', MOBILE_APP = 'MOBILE_APP', - CALCULATED_FIELDS = 'CALCULATED_FIELDS', + CALCULATED_FIELD = 'CALCULATED_FIELD', } export enum AliasEntityType { @@ -481,10 +481,10 @@ export const entityTypeTranslations = new Map Date: Fri, 24 Jan 2025 17:24:56 +0200 Subject: [PATCH 04/19] adjusted typing --- ui-ngx/src/app/core/http/calculated-fields.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 66e0833128..72c4f6c12c 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -20,6 +20,7 @@ import { Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; import { CalculatedField } from '@shared/models/calculated-field.models'; +import { PageLink } from '@shared/models/page/page-link'; @Injectable({ providedIn: 'root' @@ -73,7 +74,7 @@ export class CalculatedFieldsService { // return this.http.delete(`/api/calculated-field/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public getCalculatedFields(query: any, + public getCalculatedFields(pageLink: PageLink, config?: RequestConfig): Observable> { return of({ data: this.fieldsMock, @@ -81,7 +82,7 @@ export class CalculatedFieldsService { totalElements: 2, hasNext: false, }); - // return this.http.get>(`/api/calculated-field${query.toQuery()}`, + // return this.http.get>(`/api/calculated-field${pageLink.toQuery()}`, // defaultHttpOptionsFromConfig(config)); } } From 41b0963884edc21bb28569701d66fd269e7daa9e Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 24 Jan 2025 17:34:25 +0200 Subject: [PATCH 05/19] updated endpoint --- ui-ngx/src/app/core/http/calculated-fields.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 72c4f6c12c..5df4a84949 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -61,17 +61,17 @@ export class CalculatedFieldsService { public getCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { return of(this.fieldsMock[0]); - // return this.http.get(`/api/calculated-field/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); + // return this.http.get(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } public saveCalculatedField(calculatedField: any, config?: RequestConfig): Observable { return of(this.fieldsMock[1]); - // return this.http.post('/api/calculated-field', calculatedField, defaultHttpOptionsFromConfig(config)); + // return this.http.post('/api/calculatedField', calculatedField, defaultHttpOptionsFromConfig(config)); } public deleteCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { return of(true); - // return this.http.delete(`/api/calculated-field/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); + // return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } public getCalculatedFields(pageLink: PageLink, @@ -82,7 +82,7 @@ export class CalculatedFieldsService { totalElements: 2, hasNext: false, }); - // return this.http.get>(`/api/calculated-field${pageLink.toQuery()}`, + // return this.http.get>(`/api/calculatedField${pageLink.toQuery()}`, // defaultHttpOptionsFromConfig(config)); } } From ea7e6797edba6e113c581d5650870d7029d0af25 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 24 Jan 2025 18:11:18 +0200 Subject: [PATCH 06/19] Implemented set entityId --- .../calculated-fields-table.component.ts | 22 ++++++++++++++----- .../shared/models/calculated-field.models.ts | 5 +---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 5449bce5a7..f98d24bd52 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -47,14 +47,23 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; }) export class CalculatedFieldsTableComponent implements OnInit { - @Input() entityId: EntityId; + @Input() + set entityId(entityId: EntityId) { + if (this.entityIdValue !== entityId) { + this.entityIdValue = entityId; + this.entitiesTable.resetSortAndFilter(this.activeValue); + if (!this.activeValue) { + this.hasInitialized = true; + } + } + } @Input() set active(active: boolean) { if (this.activeValue !== active) { this.activeValue = active; - if (this.activeValue && this.dirtyValue) { - this.dirtyValue = false; + if (this.activeValue && this.hasInitialized) { + this.hasInitialized = false; this.entitiesTable.updateData(); } } @@ -65,7 +74,8 @@ export class CalculatedFieldsTableComponent implements OnInit { calculatedFieldsTableConfig: CalculatedFieldsTableConfig; private activeValue = false; - private dirtyValue = false; + private hasInitialized = false; + private entityIdValue: EntityId; constructor(private calculatedFieldsService: CalculatedFieldsService, private entityService: EntityService, @@ -83,7 +93,7 @@ export class CalculatedFieldsTableComponent implements OnInit { } ngOnInit() { - this.dirtyValue = !this.activeValue; + this.hasInitialized = !this.activeValue; this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( this.calculatedFieldsService, @@ -91,7 +101,7 @@ export class CalculatedFieldsTableComponent implements OnInit { this.dialogService, this.translate, this.dialog, - this.entityId, + this.entityIdValue, this.store, this.viewContainerRef, this.overlay, 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 714303e286..253bc58f39 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -17,13 +17,10 @@ import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/ent import { BaseData } from '@shared/models/base-data'; import { CalculatedFieldId } from '@shared/models/id/calculated-field-id'; -export interface CalculatedField extends BaseData, HasVersion, HasTenantId { - entityId: string; +export interface CalculatedField extends Omit, 'label'>, HasVersion, HasTenantId { type: CalculatedFieldType; - name: string; debugSettings?: EntityDebugSettings; externalId?: string; - createdTime?: number; configuration: CalculatedFieldConfiguration; } From fd42c51df1699c371350c264f5e7116df0ebda94 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 11:25:22 +0200 Subject: [PATCH 07/19] Calculated field add/edit basic implementation --- .../core/http/calculated-fields.service.ts | 56 +---- .../calculated-fields-table-config.ts | 74 ++++-- .../calculated-fields-table.component.ts | 23 +- ...lated-field-arguments-table.component.html | 106 +++++++++ ...lated-field-arguments-table.component.scss | 32 +++ ...culated-field-arguments-table.component.ts | 213 ++++++++++++++++++ .../calculated-field-dialog.component.html | 169 ++++++++++++++ .../calculated-field-dialog.component.ts | 122 ++++++++++ ...ulated-field-argument-panel.component.html | 198 ++++++++++++++++ ...ulated-field-argument-panel.component.scss | 29 +++ ...lculated-field-argument-panel.component.ts | 201 +++++++++++++++++ .../components/public-api.ts | 18 ++ .../home/components/home-components.module.ts | 19 +- .../pages/device/device-tabs.component.ts | 4 +- .../entity/entity-autocomplete.component.html | 13 +- .../entity/entity-autocomplete.component.ts | 7 + .../entity-key-autocomplete.component.html | 39 ++++ .../entity-key-autocomplete.component.ts | 134 +++++++++++ .../shared/components/js-func.component.ts | 46 ++-- .../shared/models/calculated-field.models.ts | 94 +++++++- ui-ngx/src/app/shared/models/public-api.ts | 1 + .../src/app/shared/models/regex.constants.ts | 17 ++ ui-ngx/src/app/shared/shared.module.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 67 +++++- 24 files changed, 1572 insertions(+), 117 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts create mode 100644 ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html create mode 100644 ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts create mode 100644 ui-ngx/src/app/shared/models/regex.constants.ts diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 5df4a84949..dca0c9a3c7 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; import { CalculatedField } from '@shared/models/calculated-field.models'; @@ -25,64 +25,26 @@ import { PageLink } from '@shared/models/page/page-link'; @Injectable({ providedIn: 'root' }) -// [TODO]: [Calculated fields] - implement when BE ready export class CalculatedFieldsService { - fieldsMock = [ - { - name: 'Calculated Field 1', - type: 'Simple', - configuration: { - expression: '1 + 2', - type: 'SIMPLE', - }, - entityId: '1', - id: { - id: '1', - } - }, - { - name: 'Calculated Field 2', - type: 'Script', - entityId: '2', - configuration: { - expression: '${power}', - type: 'SIMPLE', - }, - id: { - id: '2', - } - } - ] as any[]; - constructor( private http: HttpClient ) { } - public getCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { - return of(this.fieldsMock[0]); - // return this.http.get(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); + public getCalculatedFieldById(calculatedFieldId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public saveCalculatedField(calculatedField: any, config?: RequestConfig): Observable { - return of(this.fieldsMock[1]); - // return this.http.post('/api/calculatedField', calculatedField, defaultHttpOptionsFromConfig(config)); + public saveCalculatedField(calculatedField: CalculatedField, config?: RequestConfig): Observable { + return this.http.post('/api/calculatedField', calculatedField, defaultHttpOptionsFromConfig(config)); } public deleteCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable { - return of(true); - // return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); + return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public getCalculatedFields(pageLink: PageLink, - config?: RequestConfig): Observable> { - return of({ - data: this.fieldsMock, - totalPages: 1, - totalElements: 2, - hasNext: false, - }); - // return this.http.get>(`/api/calculatedField${pageLink.toQuery()}`, - // defaultHttpOptionsFromConfig(config)); + public getCalculatedFields(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config)); } } 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 22f742b9d8..2500c92336 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 @@ -23,41 +23,33 @@ import { TimePageLink } from '@shared/models/page/page-link'; import { Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; -import { DialogService } from '@core/services/dialog.service'; import { MINUTE } from '@shared/models/time/time.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { getCurrentAuthState } from '@core/auth/auth.selectors'; -import { ChangeDetectorRef, DestroyRef, ViewContainerRef } from '@angular/core'; -import { Overlay } from '@angular/cdk/overlay'; -import { UtilsService } from '@core/services/utils.service'; -import { EntityService } from '@core/http/entity.service'; +import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { DestroyRef } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TbPopoverService } from '@shared/components/popover.service'; import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; -import { catchError, switchMap } from 'rxjs/operators'; +import { catchError, filter, switchMap } from 'rxjs/operators'; import { CalculatedField } from '@shared/models/calculated-field.models'; +import { CalculatedFieldDialogComponent } from './components/public-api'; export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly calculatedFieldsDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1'; readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; + readonly tenantId = getCurrentAuthUser(this.store).tenantId; constructor(private calculatedFieldsService: CalculatedFieldsService, - private entityService: EntityService, - private dialogService: DialogService, private translate: TranslateService, private dialog: MatDialog, public entityId: EntityId = null, private store: Store, - private viewContainerRef: ViewContainerRef, - private overlay: Overlay, - private cd: ChangeDetectorRef, - private utilsService: UtilsService, private durationLeft: DurationLeftPipe, private popoverService: TbPopoverService, private destroyRef: DestroyRef, @@ -67,6 +59,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); + this.addEntity = this.addCalculatedField.bind(this); + this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); + this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); + this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); + this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); + this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); this.defaultSortOrder = {property: 'name', direction: Direction.DESC}; @@ -97,8 +96,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, - // // [TODO]: [Calculated fields] - implement edit - onAction: (_, entity) => {} + onAction: (_, entity) => this.editCalculatedField(entity) } ); } @@ -121,7 +119,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField} as any)), + ) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + + private editCalculatedField(calculatedField: CalculatedField): void { + this.getCalculatedFieldDialog(calculatedField, 'action.apply') + .afterClosed() + .pipe( + filter(Boolean), + switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField} as any)), + ) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + + private getCalculatedFieldDialog(value = {}, buttonTitle = 'action.add') { + return this.dialog.open(CalculatedFieldDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + value, + buttonTitle, + entityId: this.entityId, + debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration, + tenantId: this.tenantId, + } + }) + } + private getDebugConfigLabel(debugSettings: EntityDebugSettings): string { const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil); @@ -149,7 +189,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), catchError(() => of(null)), takeUntilDestroyed(this.destroyRef), diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index f98d24bd52..853870982a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -16,24 +16,18 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, Component, DestroyRef, Input, OnInit, ViewChild, - ViewContainerRef, } from '@angular/core'; import { EntityId } from '@shared/models/id/entity-id'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; -import { EntityService } from '@core/http/entity.service'; -import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { Overlay } from '@angular/cdk/overlay'; -import { UtilsService } from '@core/services/utils.service'; import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { TbPopoverService } from '@shared/components/popover.service'; @@ -51,7 +45,6 @@ export class CalculatedFieldsTableComponent implements OnInit { set entityId(entityId: EntityId) { if (this.entityIdValue !== entityId) { this.entityIdValue = entityId; - this.entitiesTable.resetSortAndFilter(this.activeValue); if (!this.activeValue) { this.hasInitialized = true; } @@ -78,18 +71,12 @@ export class CalculatedFieldsTableComponent implements OnInit { private entityIdValue: EntityId; constructor(private calculatedFieldsService: CalculatedFieldsService, - private entityService: EntityService, - private dialogService: DialogService, private translate: TranslateService, private dialog: MatDialog, private store: Store, - private overlay: Overlay, - private viewContainerRef: ViewContainerRef, - private cd: ChangeDetectorRef, private durationLeft: DurationLeftPipe, private popoverService: TbPopoverService, - private destroyRef: DestroyRef, - private utilsService: UtilsService) { + private destroyRef: DestroyRef) { } ngOnInit() { @@ -97,19 +84,13 @@ export class CalculatedFieldsTableComponent implements OnInit { this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( this.calculatedFieldsService, - this.entityService, - this.dialogService, this.translate, this.dialog, this.entityIdValue, this.store, - this.viewContainerRef, - this.overlay, - this.cd, - this.utilsService, this.durationLeft, this.popoverService, - this.destroyRef + this.destroyRef, ); } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html new file mode 100644 index 0000000000..99e68cf059 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -0,0 +1,106 @@ + +
+
+
{{ 'calculated-fields.argument-name' | translate }}
+
{{ 'calculated-fields.datasource' | translate }}
+
{{ 'common.type' | translate }}
+
{{ 'entity.key' | translate }}
+
+
+ @for (group of argumentsFormArray.controls; track group) { +
+ + + + @if (group.get('refEntityId')?.get('id').value) { + + + + + {{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }} + + + + + + } @else { + + + + {{ (group.get('refEntityId')?.get('entityType').value === ArgumentEntityType.Tenant + ? 'calculated-fields.argument-current-tenant' + : 'calculated-fields.argument-current') | translate }} + + + + } + + + + + {{ ArgumentTypeTranslations.get(group.get('refEntityKey').get('type').value) | translate }} + + + + + +
+ {{group.get('refEntityKey').get('key').value}} +
+
+
+
+
+ + +
+
+ } @empty { + {{ 'calculated-fields.no-arguments' | translate }} + } +
+ @if (errorText) { + + } +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss new file mode 100644 index 0000000000..9507d9f012 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .inline-entity-autocomplete { + .mat-mdc-form-field-infix { + padding-top: 8px; + padding-bottom: 8px; + min-height: 40px; + width: auto; + .mdc-text-field__input, .mat-mdc-select { + font-weight: 400; + line-height: 20px; + } + } + a { + font-size: 14px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts new file mode 100644 index 0000000000..0ff6ecdf7f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -0,0 +1,213 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + DestroyRef, + effect, + forwardRef, + input, + Input, + Renderer2, + ViewContainerRef, +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + ArgumentEntityType, + ArgumentType, + ArgumentTypeTranslations, + CalculatedFieldArgument, + CalculatedFieldArgumentValue, + CalculatedFieldType, +} from '@shared/models/calculated-field.models'; +import { + CalculatedFieldArgumentPanelComponent +} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; + +@Component({ + selector: 'tb-calculated-field-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent), + multi: true + } + ], +}) +export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator { + + @Input() entityId: EntityId; + @Input() tenantId: string; + + calculatedFieldType = input() + + errorText = ''; + argumentsFormArray = this.fb.array([]); + keysPopupClosed = true; + + readonly entityTypeTranslations = entityTypeTranslations; + readonly ArgumentTypeTranslations = ArgumentTypeTranslations; + readonly EntityType = EntityType; + readonly ArgumentEntityType = ArgumentEntityType; + + private onChange: (argumentsObj: Record) => void = () => {}; + + constructor( + private fb: FormBuilder, + private popoverService: TbPopoverService, + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef, + private cd: ChangeDetectorRef, + private renderer: Renderer2 + ) { + this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => { + this.onChange(this.getArgumentsObject()); + }); + effect(() => this.calculatedFieldType() && this.argumentsFormArray.updateValueAndValidity()); + } + + registerOnChange(fn: (argumentsObj: Record) => void): void { + this.onChange = fn; + } + + registerOnTouched(_): void {} + + validate(): ValidationErrors | null { + if (this.calculatedFieldType() === CalculatedFieldType.SIMPLE + && this.argumentsFormArray.controls.some(control => control.get('refEntityKey').get('type').value === ArgumentType.Rolling)) { + this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; + } else if (!this.argumentsFormArray.controls.length) { + this.errorText = 'calculated-fields.hint.arguments-empty'; + } else { + this.errorText = ''; + } + return this.errorText ? { argumentsFormArray: false } : null; + } + + private getArgumentsObject(): Record { + return this.argumentsFormArray.controls.reduce((acc, control) => { + const rawValue = control.getRawValue(); + const { argumentName, ...argument } = rawValue as CalculatedFieldArgumentValue; + acc[argumentName] = argument; + return acc; + }, {} as Record); + } + + writeValue(argumentsObj: Record): void { + this.argumentsFormArray.clear(); + Object.keys(argumentsObj).forEach(key => { + this.argumentsFormArray.push(this.fb.group({ + argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]], + ...argumentsObj[key], + ...(argumentsObj[key].refEntityId ? { + refEntityId: this.fb.group({ + entityType: [{ value: argumentsObj[key].refEntityId.entityType, disabled: true }], + id: [{ value: argumentsObj[key].refEntityId.id , disabled: true }], + }), + } : {}), + refEntityKey: this.fb.group({ + type: [{ value: argumentsObj[key].refEntityKey.type, disabled: true }], + key: [{ value: argumentsObj[key].refEntityKey.key, disabled: true }], + }), + }) as AbstractControl); + }); + } + + + manageArgument($event: Event, matButton: MatButton, index?: number): void { + $event?.stopPropagation(); + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx = { + index, + argument: this.argumentsFormArray.at(index)?.getRawValue() ?? {}, + entityId: this.entityId, + calculatedFieldType: this.calculatedFieldType(), + buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add', + tenantId: this.tenantId, + }; + this.keysPopupClosed = false; + const argumentsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'leftBottom', false, null, + ctx, + {}, + {}, {}, true); + argumentsPanelPopover.tbComponentRef.instance.popover = argumentsPanelPopover; + argumentsPanelPopover.tbComponentRef.instance.argumentsDataApplied.subscribe(({ value, index }) => { + argumentsPanelPopover.hide(); + const formGroup = this.getArgumentFormGroup(value); + if (isDefinedAndNotNull(index)) { + this.argumentsFormArray.setControl(index, formGroup); + } else { + this.argumentsFormArray.push(formGroup); + } + this.argumentsFormArray.markAsDirty(); + this.cd.markForCheck(); + }); + argumentsPanelPopover.tbHideStart.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.keysPopupClosed = true; + }); + } + } + + getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl { + return this.fb.group({ + ...value, + argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]], + ...(value.refEntityId ? { + refEntityId: this.fb.group({ + entityType: [{ value: value.refEntityId.entityType, disabled: true }], + id: [{ value: value.refEntityId.id , disabled: true }], + }), + } : {}), + refEntityKey: this.fb.group({ + type: [{ value: value.refEntityKey.type, disabled: true }], + key: [{ value: value.refEntityKey.key, disabled: true }], + }), + }) + } + + onDelete(index: number): void { + this.argumentsFormArray.removeAt(index); + this.argumentsFormArray.markAsDirty(); + } +} 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 new file mode 100644 index 0000000000..7d1373bebb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -0,0 +1,169 @@ + +
+ +

{{ 'entity.type-calculated-field' | translate}}

+ +
+ +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) { + + @if (fieldFormGroup.get('name').hasError('required')) { + {{ 'common.hint.name-required' | translate }} + } @else if (fieldFormGroup.get('name').hasError('pattern')) { + {{ 'common.hint.name-pattern' | translate }} + } @else if (fieldFormGroup.get('name').hasError('maxlength')) { + {{ 'common.hint.name-max-length' | translate }} + } + + } + + +
+ + {{ 'common.type' | translate }} + + @for (type of fieldTypes; track type) { + {{ CalculatedFieldTypeTranslations.get(type) | translate}} + } + + +
+ +
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
{{ 'calculated-fields.expression' | translate }}*
+ @if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { + + + @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { + + @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { + {{ 'calculated-fields.hint.expression-required' | translate }} + } @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) { + {{ 'calculated-fields.hint.expression-invalid' | translate }} + } @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) { + {{ 'calculated-fields.hint.expression-max-length' | translate }} + } + + } + + } @else { + + } +
+
+
{{ 'calculated-fields.output' | translate }}
+
+ + {{ 'calculated-fields.output-type' | translate }} + + @for (type of outputTypes; track type) { + {{ OutputTypeTranslations.get(type) | translate}} + } + + + @if (outputFormGroup.get('type').value === OutputType.Attribute) { + + {{ 'calculated-fields.output-type' | translate }} + + + {{ 'calculated-fields.server-attributes' | translate }} + + @if (data.entityId.entityType === EntityType.DEVICE) { + + {{ 'calculated-fields.shared-attributes' | translate }} + + } + + + } +
+ @if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { + + + {{ (outputFormGroup.get('type').value === OutputType.Timeseries + ? 'calculated-fields.timeseries-key' + : 'calculated-fields.attribute-key') + | translate }} + + + @if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) { + + @if (outputFormGroup.get('name').hasError('required')) { + {{ 'common.hint.key-required' | translate }} + } @else if (outputFormGroup.get('name').hasError('pattern')) { + {{ 'common.hint.key-pattern' | translate }} + } @else if (outputFormGroup.get('name').hasError('maxlength')) { + {{ 'common.hint.key-max-length' | translate }} + } + + } + + } +
+
+
+
+
+ + +
+
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 new file mode 100644 index 0000000000..99bf62ad46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +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 { FormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { helpBaseUrl } from '@shared/models/constants'; +import { + CalculatedField, + CalculatedFieldConfiguration, + CalculatedFieldDialogData, + CalculatedFieldType, + CalculatedFieldTypeTranslations, + OutputType, + OutputTypeTranslations +} from '@shared/models/calculated-field.models'; +import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { map } from 'rxjs/operators'; +import { isObject } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; + +@Component({ + selector: 'tb-calculated-field-dialog', + templateUrl: './calculated-field-dialog.component.html', +}) +export class CalculatedFieldDialogComponent extends DialogComponent { + + fieldFormGroup = this.fb.group({ + name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], + type: [CalculatedFieldType.SIMPLE, [Validators.required]], + debugSettings: [], + configuration: this.fb.group({ + arguments: [{}], + expressionSIMPLE: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], + expressionSCRIPT: [], + output: this.fb.group({ + name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], + scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }], + type: [OutputType.Timeseries] + }), + }), + }); + + functionArgs$ = this.fieldFormGroup.get('configuration').valueChanges + .pipe( + map(configuration => isObject(configuration?.arguments) ? Object.keys(configuration.arguments) : []) + ); + + readonly OutputTypeTranslations = OutputTypeTranslations; + readonly OutputType = OutputType; + readonly AttributeScope = AttributeScope; + readonly EntityType = EntityType; + readonly CalculatedFieldType = CalculatedFieldType; + readonly ScriptLanguage = ScriptLanguage; + readonly helpLink = `${helpBaseUrl}/[TODO: ADD VALID LINK!!!]`; + readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[]; + readonly outputTypes = Object.values(OutputType) as OutputType[]; + readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + this.applyDialogData(); + this.outputFormGroup.get('type').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(type => this.toggleScopeByOutputType(type)); + this.toggleScopeByOutputType(this.outputFormGroup.get('type').value); + } + + get configFormGroup(): FormGroup { + return this.fieldFormGroup.get('configuration') as FormGroup; + } + + get outputFormGroup(): FormGroup { + return this.fieldFormGroup.get('configuration').get('output') as FormGroup; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + if (this.fieldFormGroup.valid) { + const { configuration, type, ...rest } = this.fieldFormGroup.value; + const { expressionSIMPLE, expressionSCRIPT, ...restConfig } = configuration; + this.dialogRef.close({ configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type }); + } + } + + private applyDialogData(): void { + const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value; + const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration; + this.fieldFormGroup.patchValue({ configuration: { ...restConfig, ['expression'+type]: expression }, ...value }); + } + + private toggleScopeByOutputType(type: OutputType): void { + this.outputFormGroup.get('scope')[type === OutputType.Attribute? 'enable' : 'disable']({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html new file mode 100644 index 0000000000..26a347aaa2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -0,0 +1,198 @@ + +
+
+
{{ 'calculated-fields.argument-settings' | translate }}
+
+
+
{{ 'calculated-fields.argument-name' | translate }}
+
+ + + @if (argumentFormGroup.get('argumentName').hasError('required') && argumentFormGroup.get('argumentName').touched) { + + warning + + } @else if (argumentFormGroup.get('argumentName').hasError('pattern') && argumentFormGroup.get('argumentName').touched) { + + warning + + } @else if (argumentFormGroup.get('argumentName').hasError('maxlength') && argumentFormGroup.get('argumentName').touched) { + + warning + + } + +
+
+ +
+
{{ 'entity.entity-type' | translate }}
+ + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + +
+ @if (entityType === ArgumentEntityType.Device || entityType === ArgumentEntityType.Asset) { +
+
{{ 'calculated-fields.device-name' | translate }}
+ +
+ } @else if (entityType === ArgumentEntityType.Customer) { +
+
{{ 'calculated-fields.customer-name' | translate }}
+ +
+ } +
+ +
+
{{ 'calculated-fields.argument-type' | translate }}
+ + + @for (type of argumentTypes; track type) { + {{ ArgumentTypeTranslations.get(type) | translate }} + } + + @if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) { + + warning + + } + +
+ @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { +
+
{{ 'calculated-fields.timeseries-key' | translate }}
+ +
+ } @else { +
+
{{ 'calculated-fields.attribute-scope' | translate }}
+ + + + {{ 'calculated-fields.server-attributes' | translate }} + + @if ((keyEntityType$ | async) === EntityType.DEVICE) { + + {{ 'calculated-fields.client-attributes' | translate }} + + + {{ 'calculated-fields.shared-attributes' | translate }} + + } + + +
+
+
{{ 'calculated-fields.attribute-key' | translate }}
+ +
+ } +
+ @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { +
+
{{ 'calculated-fields.default-value' | translate }}
+
+ + + +
+
+ } @else { +
+
{{ 'calculated-fields.time-window' | translate }}
+
+ + + {{ 'common.suffix.ms' | translate }} + +
+
+
+
{{ 'calculated-fields.limit' | translate }}
+
+ + + +
+
+ } +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss new file mode 100644 index 0000000000..a784909b92 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-mdc-form-field { + width: 100%; + } +} + +:host ::ng-deep { + .entity-autocomplete { + .mat-mdc-form-field { + width: 100%; + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts new file mode 100644 index 0000000000..2ba38beee5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Input, OnInit, output, ViewChild } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; +import { + ArgumentEntityType, + ArgumentEntityTypeTranslations, + ArgumentType, + ArgumentTypeTranslations, + CalculatedFieldArgumentValue, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; +import { debounceTime, delay, distinctUntilChanged, filter, map, startWith } from 'rxjs/operators'; +import { EntityType } from '@shared/models/entity-type.models'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DatasourceType } from '@shared/models/widget.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { merge } from 'rxjs'; + +@Component({ + selector: 'tb-calculated-field-argument-panel', + templateUrl: './calculated-field-argument-panel.component.html', + styleUrls: ['./calculated-field-argument-panel.component.scss'] +}) +export class CalculatedFieldArgumentPanelComponent extends PageComponent implements OnInit { + + @Input() popover: TbPopoverComponent; + @Input() buttonTitle: string; + @Input() index: number; + @Input() argument: CalculatedFieldArgumentValue; + @Input() entityId: EntityId; + @Input() tenantId: string; + @Input() calculatedFieldType: CalculatedFieldType; + + @ViewChild('timeseriesInput') timeseriesInput: ElementRef; + + argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); + + argumentFormGroup = this.fb.group({ + argumentName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], + refEntityId: this.fb.group({ + entityType: [ArgumentEntityType.Current], + id: [''] + }), + refEntityKey: this.fb.group({ + type: [ArgumentType.LatestTelemetry, [Validators.required]], + key: [''], + scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }], + }), + defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + limit: [null], + timeWindow: [null], + }); + + argumentTypes: ArgumentType[]; + entityFilter: EntityFilter; + keyEntityType$ = this.refEntityIdFormGroup.get('entityType').valueChanges + .pipe( + startWith(this.refEntityIdFormGroup.get('entityType').value), + map(type => type === ArgumentEntityType.Current ? this.entityId.entityType : type) + ); + + readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; + readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; + readonly ArgumentType = ArgumentType; + readonly DataKeyType = DataKeyType; + readonly EntityType = EntityType; + readonly datasourceType = DatasourceType; + readonly ArgumentTypeTranslations = ArgumentTypeTranslations; + readonly AttributeScope = AttributeScope; + + constructor( + private fb: FormBuilder, + ) { + super(); + + this.observeEntityFilterChanges(); + this.observeEntityTypeChanges() + this.observeEntityKeyChanges(); + } + + get entityType(): ArgumentEntityType { + return this.argumentFormGroup.get('refEntityId').get('entityType').value; + } + + get refEntityIdFormGroup(): FormGroup { + return this.argumentFormGroup.get('refEntityId') as FormGroup; + } + + get refEntityKeyFormGroup(): FormGroup { + return this.argumentFormGroup.get('refEntityKey') as FormGroup; + } + + ngOnInit(): void { + this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); + this.updateEntityFilter(this.argument.refEntityId?.entityType, true); + this.toggleByEntityKeyType(this.argument.refEntityKey?.type); + this.setInitialEntityKeyType(); + + this.argumentTypes = Object.values(ArgumentType) + .filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT); + } + + saveArgument(): void { + this.argumentsDataApplied.emit({ value: this.argumentFormGroup.value as CalculatedFieldArgumentValue, index: this.index }); + } + + cancel(): void { + this.popover.hide(); + } + + private toggleByEntityKeyType(type: ArgumentType): void { + const isAttribute = type === ArgumentType.Attribute; + const isRolling = type === ArgumentType.Rolling; + this.argumentFormGroup.get('refEntityKey').get('scope')[isAttribute? 'enable' : 'disable']({ emitEvent: false }); + this.argumentFormGroup.get('limit')[isRolling? 'enable' : 'disable']({ emitEvent: false }); + this.argumentFormGroup.get('timeWindow')[isRolling? 'enable' : 'disable']({ emitEvent: false }); + this.argumentFormGroup.get('defaultValue')[isRolling? 'disable' : 'enable']({ emitEvent: false }); + } + + private updateEntityFilter(entityType: ArgumentEntityType, onInit = false): void { + let entityId: EntityId; + switch (entityType) { + case ArgumentEntityType.Current: + entityId = this.entityId + break; + case ArgumentEntityType.Tenant: + entityId = { + id: this.tenantId, + entityType: EntityType.TENANT + }; + break; + default: + entityId = this.argumentFormGroup.get('refEntityId').value as any; + } + if (onInit) { + this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); + } + this.entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: entityId, + }; + } + + private observeEntityFilterChanges(): void { + merge( + this.argumentFormGroup.get('refEntityId').get('entityType').valueChanges, + this.argumentFormGroup.get('refEntityId').get('id').valueChanges.pipe(filter(Boolean)), + this.argumentFormGroup.get('refEntityKey').get('scope').valueChanges, + ) + .pipe(debounceTime(300), delay(50), takeUntilDestroyed()) + .subscribe(() => this.updateEntityFilter(this.entityType)); + } + + private observeEntityTypeChanges(): void { + this.argumentFormGroup.get('refEntityId').get('entityType').valueChanges + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(type => { + this.argumentFormGroup.get('refEntityId').get('id').setValue(''); + this.argumentFormGroup.get('refEntityId') + .get('id')[type === ArgumentEntityType.Tenant || type === ArgumentEntityType.Current ? 'disable' : 'enable'](); + }); + } + + private observeEntityKeyChanges(): void { + this.argumentFormGroup.get('refEntityKey').get('type').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(type => this.toggleByEntityKeyType(type)); + } + + private setInitialEntityKeyType(): void { + if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argument.refEntityKey?.type === ArgumentType.Rolling) { + const typeControl = this.argumentFormGroup.get('refEntityKey').get('type'); + typeControl.setValue(null); + typeControl.markAsTouched(); + typeControl.updateValueAndValidity(); + } + } + + protected readonly ArgumentEntityType = ArgumentEntityType; +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts new file mode 100644 index 0000000000..c3d1ede02e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -0,0 +1,18 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export * from './dialog/calculated-field-dialog.component'; +export * from './arguments-table/calculated-field-arguments-table.component'; diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 1a6b9c08e0..f60ea15407 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -185,6 +185,16 @@ import { EntityChipsComponent } from '@home/components/entity/entity-chips.compo import { DashboardViewComponent } from '@home/components/dashboard-view/dashboard-view.component'; import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; +import { CalculatedFieldDialogComponent } from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; +import { + EntityDebugSettingsButtonComponent +} from '@home/components/entity/debug/entity-debug-settings-button.component'; +import { + CalculatedFieldArgumentsTableComponent +} from '@home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component'; +import { + CalculatedFieldArgumentPanelComponent +} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component'; @NgModule({ declarations: @@ -330,6 +340,9 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; EntityChipsComponent, DashboardViewComponent, CalculatedFieldsTableComponent, + CalculatedFieldDialogComponent, + CalculatedFieldArgumentsTableComponent, + CalculatedFieldArgumentPanelComponent, ], imports: [ CommonModule, @@ -341,7 +354,8 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; SnmpDeviceProfileTransportModule, StatesControllerModule, DeviceCredentialsModule, - DeviceProfileCommonModule + DeviceProfileCommonModule, + EntityDebugSettingsButtonComponent ], exports: [ RouterTabsComponent, @@ -468,6 +482,9 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; EntityChipsComponent, DashboardViewComponent, CalculatedFieldsTableComponent, + CalculatedFieldDialogComponent, + CalculatedFieldArgumentsTableComponent, + CalculatedFieldArgumentPanelComponent, ], providers: [ WidgetComponentService, 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 618650ac32..fbbf124629 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 { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-device-tabs', @@ -27,6 +28,8 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone }) export class DeviceTabsComponent extends EntityTabsComponent { + readonly EntityType = EntityType; + constructor(protected store: Store) { super(store); } @@ -34,5 +37,4 @@ export class DeviceTabsComponent extends EntityTabsComponent { ngOnInit() { super.ngOnInit(); } - } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index c194e04290..44a218b455 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -17,10 +17,11 @@ --> - {{ label | translate }} + {{ label | translate }} {{ displayEntityFn(selectEntityFormGroup.get('entity').value) }} + + warning + + } + + @for (key of filteredKeys$ | async; track key) { + + } + + 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 new file mode 100644 index 0000000000..2a33a74786 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts @@ -0,0 +1,134 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, effect, ElementRef, forwardRef, input, ViewChild, } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { map, startWith, switchMap } from 'rxjs/operators'; +import { combineLatest, of, Subject } from 'rxjs'; +import { EntityService } from '@core/http/entity.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntitiesKeysByQuery } from '@shared/models/entity.models'; +import { EntityFilter } from '@shared/models/query/query.models'; + +@Component({ + selector: 'tb-entity-key-autocomplete', + templateUrl: './entity-key-autocomplete.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityKeyAutocompleteComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityKeyAutocompleteComponent), + multi: true + } + ], + host: { + class: 'w-full' + } +}) +export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Validator { + + @ViewChild('keyInput') keyInput: ElementRef; + + entityFilter = input.required(); + dataKeyType = input.required(); + keyScopeType = input(); + + keyControl = this.fb.control('', [Validators.required]); + searchText = ''; + keyInputSubject = new Subject(); + + private onChange: (value: string) => void; + private cachedResult: EntitiesKeysByQuery; + + keys$ = this.keyInputSubject.asObservable() + .pipe( + switchMap(() => { + 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()); + }), + map(result => { + this.cachedResult = result; + switch (this.dataKeyType()) { + case DataKeyType.attribute: + return result.attribute; + case DataKeyType.timeseries: + return result.timeseries; + default: + return []; + } + }), + ); + + filteredKeys$ = combineLatest([this.keys$, this.keyControl.valueChanges.pipe(startWith(''))]) + .pipe( + map(([keys, searchText = '']) => { + this.searchText = searchText; + return searchText ? keys.filter(item => item.toLowerCase().includes(searchText.toLowerCase())) : keys; + }) + ); + + constructor( + private fb: FormBuilder, + private entityService: EntityService, + ) { + this.keyControl.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.onChange(value)); + effect(() => { + if (this.keyScopeType() || this.entityFilter() && this.dataKeyType()) { + this.cachedResult = null; + this.searchText = ''; + } + }); + } + + clear(): void { + this.keyControl.patchValue('', {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + registerOnChange(onChange: (value: string) => void): void { + this.onChange = onChange; + } + + registerOnTouched(_): void {} + + validate(): ValidationErrors | null { + return this.keyControl.valid ? null : { keyControl: false }; + } + + writeValue(value: string): void { + this.keyControl.patchValue(value, {emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 8e500bdbe9..57c15a5484 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -20,9 +20,11 @@ import { ElementRef, forwardRef, Input, + OnChanges, OnDestroy, OnInit, Renderer2, + SimpleChanges, ViewChild, ViewContainerRef, ViewEncapsulation @@ -67,7 +69,7 @@ import { catchError } from 'rxjs/operators'; ], encapsulation: ViewEncapsulation.None }) -export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { +export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlValueAccessor, Validator { @ViewChild('javascriptEditor', {static: true}) javascriptEditorElmRef: ElementRef; @@ -177,6 +179,13 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, private http: HttpClient) { } + ngOnChanges(changes: SimpleChanges): void { + if (changes.functionArgs) { + this.updateFunctionArgsString(); + this.updateFunctionLabel(); + } + } + ngOnInit(): void { if (this.functionTitle || this.label) { this.hideBrackets = true; @@ -184,22 +193,6 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, if (!this.resultType || this.resultType.length === 0) { this.resultType = 'nocheck'; } - if (this.functionArgs) { - this.functionArgs.forEach((functionArg) => { - if (this.functionArgsString.length > 0) { - this.functionArgsString += ', '; - } - this.functionArgsString += functionArg; - }); - } - if (this.functionTitle) { - this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`; - } else if (this.label) { - this.functionLabel = this.label; - } else { - this.functionLabel = - `function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`; - } const editorElement = this.javascriptEditorElmRef.nativeElement; let editorOptions: Partial = { mode: 'ace/mode/javascript', @@ -329,6 +322,25 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, ); } + private updateFunctionArgsString(): void { + this.functionArgsString = ''; + if (this.functionArgs) { + this.functionArgsString = this.functionArgs.join(', '); + } + } + + private updateFunctionLabel(): void { + if (this.functionTitle) { + this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`; + } else if (this.label) { + this.functionLabel = this.label; + } else { + this.functionLabel = + `function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`; + } + this.cd.markForCheck(); + } + validateOnSubmit(): Observable { if (!this.disabled) { this.cleanupJsErrors(); 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 253bc58f39..73b3ecd861 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -13,29 +13,109 @@ /// See the License for the specific language governing permissions and /// limitations under the License. /// + import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models'; import { BaseData } from '@shared/models/base-data'; import { CalculatedFieldId } from '@shared/models/id/calculated-field-id'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; export interface CalculatedField extends Omit, 'label'>, HasVersion, HasTenantId { - type: CalculatedFieldType; debugSettings?: EntityDebugSettings; externalId?: string; configuration: CalculatedFieldConfiguration; + type: CalculatedFieldType; } export enum CalculatedFieldType { SIMPLE = 'SIMPLE', - COMPLEX = 'COMPLEX', + SCRIPT = 'SCRIPT', } +export const CalculatedFieldTypeTranslations = new Map( + [ + [CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'], + [CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'], + ] +) + export interface CalculatedFieldConfiguration { - type: CalculatedFieldConfigType; + type: CalculatedFieldType; expression: string; - arguments: Record; + arguments: Record; } -export enum CalculatedFieldConfigType { - SIMPLE = 'SIMPLE', - SCRIPT = 'SCRIPT', +export enum ArgumentEntityType { + Current = 'CURRENT', + Device = 'DEVICE', + Asset = 'ASSET', + Customer = 'CUSTOMER', + Tenant = 'TENANT', +} + +export const ArgumentEntityTypeTranslations = new Map( + [ + [ArgumentEntityType.Current, 'calculated-fields.argument-current'], + [ArgumentEntityType.Device, 'calculated-fields.argument-device'], + [ArgumentEntityType.Asset, 'calculated-fields.argument-asset'], + [ArgumentEntityType.Customer, 'calculated-fields.argument-customer'], + [ArgumentEntityType.Tenant, 'calculated-fields.argument-tenant'], + ] +) + +export enum ArgumentType { + Attribute = 'ATTRIBUTE', + LatestTelemetry = 'TS_LATEST', + Rolling = 'TS_ROLLING', +} + +export enum OutputType { + Attribute = 'ATTRIBUTES', + Timeseries = 'TIME_SERIES', +} + +export const OutputTypeTranslations = new Map( + [ + [OutputType.Attribute, 'calculated-fields.attribute'], + [OutputType.Timeseries, 'calculated-fields.timeseries'], + ] +) + +export const ArgumentTypeTranslations = new Map( + [ + [ArgumentType.Attribute, 'calculated-fields.attribute'], + [ArgumentType.LatestTelemetry, 'calculated-fields.latest-telemetry'], + [ArgumentType.Rolling, 'calculated-fields.rolling'], + ] +) + +export interface CalculatedFieldArgument { + refEntityKey: RefEntityKey; + defaultValue?: string; + refEntityId?: RefEntityKey; + limit?: number; + timeWindow?: number; +} + +export interface RefEntityKey { + key: string; + type: ArgumentType; + scope?: AttributeScope; +} + +export interface RefEntityKey { + entityType: ArgumentEntityType; + id: string; +} + +export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument { + argumentName: string; +} + +export interface CalculatedFieldDialogData { + value: CalculatedField; + buttonTitle: string; + entityId: EntityId; + debugLimitsConfiguration: string; + tenantId: string; } diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index 736d885810..c48a3286cd 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -61,3 +61,4 @@ export * from './widgets-bundle.model'; export * from './window-message.model'; export * from './usage.models'; export * from './query/query.models'; +export * from './regex.constants'; diff --git a/ui-ngx/src/app/shared/models/regex.constants.ts b/ui-ngx/src/app/shared/models/regex.constants.ts new file mode 100644 index 0000000000..55742cefd4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/regex.constants.ts @@ -0,0 +1,17 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export const noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 5b40dadf5f..5825998434 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -224,6 +224,7 @@ import { IntervalOptionsConfigPanelComponent } from '@shared/components/time/int import { GroupingIntervalOptionsComponent } from '@shared/components/time/aggregation/grouping-interval-options.component'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.component'; +import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -432,7 +433,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ImageGalleryDialogComponent, WidgetButtonComponent, HexInputComponent, - ScadaSymbolInputComponent + ScadaSymbolInputComponent, + EntityKeyAutocompleteComponent, ], imports: [ CommonModule, @@ -694,7 +696,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EmbedImageDialogComponent, ImageGalleryDialogComponent, WidgetButtonComponent, - ScadaSymbolInputComponent + ScadaSymbolInputComponent, + EntityKeyAutocompleteComponent, ] }) export class SharedModule { } 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 beb6734451..be31b4f70b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -996,6 +996,7 @@ "failures": "Failures", "entity": "entity", "rule-node": "rule node", + "calculated-field": "calculated field", "hint": { "main": "All node debug messages rate limited with:", "main-limited": "All {{entity}} debug messages will be rate-limited, with a maximum of {{msg}} messages allowed per {{time}}.", @@ -1007,7 +1008,56 @@ "expression": "Expression", "no-found": "No calculated fields found", "list": "{ count, plural, =1 {One calculated field} other {List of # calculated fields} }", - "selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected" + "selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected", + "type": { + "simple": "Simple", + "script": "Script" + }, + "arguments": "Arguments", + "argument-name": "Argument name", + "datasource": "Datasource", + "add-argument": "Add argument", + "no-arguments": "No arguments configured", + "argument-settings": "Argument settings", + "argument-current": "Current entity", + "argument-current-tenant": "Current tenant", + "argument-device": "Device", + "argument-asset": "Asset", + "argument-customer": "Customer", + "argument-tenant": "Current tenant", + "argument-type": "Argument type", + "attribute": "Attribute", + "timeseries-key": "Time series key", + "device-name": "Device name", + "latest-telemetry": "Latest telemetry", + "rolling": "Rolling", + "attribute-scope": "Attribute scope", + "server-attributes": "Server attributes", + "client-attributes": "Client attributes", + "shared-attributes": "Shared attributes", + "attribute-key": "Attribute key", + "default-value": "Default value", + "limit": "Limit", + "time-window": "Time window", + "customer-name": "Customer name", + "timeseries": "Time series", + "output": "Output", + "output-type": "Output type", + "delete-title": "Are you sure you want to delete the calculated field '{{title}}'?", + "delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.", + "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 calculated field} other {# calculated fields} }?", + "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", + "hint": { + "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with rolling type.", + "arguments-empty": "Arguments should not be empty.", + "expression-required": "Expression is required.", + "expression-invalid": "Expression is invalid", + "expression-max-length": "Expression length should be less than 255 characters.", + "argument-name-required": "Argument name is required.", + "argument-name-pattern": "Argument name is invalid.", + "argument-name-max-length": "Argument name should be less than 256 characters.", + "argument-type-required": "Argument type is required." + } }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", @@ -1035,6 +1085,7 @@ "common": { "name": "Name", "type": "Type", + "general": "General", "username": "Username", "password": "Password", "enter-username": "Enter username", @@ -1047,7 +1098,19 @@ "open-details-page": "Open details page", "not-found": "Not found", "documentation": "Documentation", - "time-left": "{{time}} left" + "time-left": "{{time}} left", + "suffix": { + "s": "s", + "ms": "ms" + }, + "hint": { + "name-required": "Name is required.", + "name-pattern": "Name is invalid.", + "name-max-length": "Name should be less than 256 characters.", + "key-required": "Key is required.", + "key-pattern": "Key is invalid.", + "key-max-length": "Key should be less than 256 characters." + } }, "content-type": { "json": "Json", From a82d3690f3e282bb3ef18aec90bda15f69a387ff Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 12:30:36 +0200 Subject: [PATCH 08/19] Changed getAll endpoint and refactoring --- .../core/http/calculated-fields.service.ts | 5 +- .../calculated-fields-table-config.ts | 16 +- ...lated-field-arguments-table.component.html | 138 +++++++++--------- ...lated-field-arguments-table.component.scss | 1 + ...culated-field-arguments-table.component.ts | 101 +++++++------ .../calculated-field-dialog.component.html | 4 +- .../calculated-field-dialog.component.ts | 4 +- ...ulated-field-argument-panel.component.html | 53 +++---- ...lculated-field-argument-panel.component.ts | 9 +- .../components/public-api.ts | 1 + .../pages/device/device-tabs.component.html | 3 +- .../pages/device/device-tabs.component.ts | 4 +- .../entity-key-autocomplete.component.ts | 9 +- .../shared/models/calculated-field.models.ts | 1 + 14 files changed, 179 insertions(+), 170 deletions(-) diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index dca0c9a3c7..10ad366e5b 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -21,6 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; import { CalculatedField } from '@shared/models/calculated-field.models'; import { PageLink } from '@shared/models/page/page-link'; +import { EntityId } from '@shared/models/id/entity-id'; @Injectable({ providedIn: 'root' @@ -43,8 +44,8 @@ export class CalculatedFieldsService { return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public getCalculatedFields(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/calculatedFields${pageLink.toQuery()}`, + public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } } 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 2500c92336..ae025a13f6 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 @@ -40,6 +40,7 @@ import { CalculatedFieldDialogComponent } from './components/public-api'; export class CalculatedFieldsTableConfig extends EntityTableConfig { + // TODO: [Calculated Fields] remove hardcode when BE variable implemented readonly calculatedFieldsDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1'; readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; @@ -66,9 +67,9 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); + this.entitiesFetchFunction = (pageLink: TimePageLink) => this.fetchCalculatedFields(pageLink); this.addEntity = this.addCalculatedField.bind(this); - this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); + this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); @@ -102,7 +103,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig> { - return this.calculatedFieldsService.getCalculatedFields(pageLink); + return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); } onOpenDebugConfig($event: Event, { debugSettings = {}, id }: CalculatedField): void { @@ -134,10 +135,9 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField} as any)), + switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField })), ) .subscribe((res) => { if (res) { @@ -148,10 +148,9 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField} as any)), + switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField })), ) .subscribe((res) => { if (res) { @@ -160,7 +159,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { return this.dialog.open(CalculatedFieldDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], @@ -172,6 +171,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig{{ 'common.type' | translate }}
{{ 'entity.key' | translate }}
-
- @for (group of argumentsFormArray.controls; track group) { -
- - - - @if (group.get('refEntityId')?.get('id').value) { - - - - - {{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }} - - - - - - } @else { - - - - {{ (group.get('refEntityId')?.get('entityType').value === ArgumentEntityType.Tenant - ? 'calculated-fields.argument-current-tenant' - : 'calculated-fields.argument-current') | translate }} - - - - } - +
+ @for (group of argumentsFormArray.controls; track group) { +
+ + + + @if (group.get('refEntityId')?.get('id').value) { + - - - {{ ArgumentTypeTranslations.get(group.get('refEntityKey').get('type').value) | translate }} + + + {{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }} - - -
- {{group.get('refEntityKey').get('key').value}} -
-
-
+
-
- - -
+ } @else { + + + + {{ + (group.get('refEntityId')?.get('entityType').value === ArgumentEntityType.Tenant + ? 'calculated-fields.argument-current-tenant' + : 'calculated-fields.argument-current') | translate + }} + + + + } + + + @if (group.get('refEntityKey').get('type').value; as type) { + + + {{ ArgumentTypeTranslations.get(type) | translate }} + + + } + + + +
+ {{ group.get('refEntityKey').get('key').value }} +
+
+
+
+
+ +
- } @empty { - {{ 'calculated-fields.no-arguments' | translate }} - } -
+
+ } @empty { + {{ 'calculated-fields.no-arguments' | translate }} + } +
@if (errorText) { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss index 9507d9f012..8695ee4068 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss @@ -25,6 +25,7 @@ line-height: 20px; } } + a { font-size: 14px; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index 0ff6ecdf7f..1dedc5e80e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -43,9 +43,7 @@ import { CalculatedFieldArgumentValue, CalculatedFieldType, } from '@shared/models/calculated-field.models'; -import { - CalculatedFieldArgumentPanelComponent -} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component'; +import { CalculatedFieldArgumentPanelComponent } from '@home/components/calculated-fields/components/public-api'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -87,7 +85,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces readonly EntityType = EntityType; readonly ArgumentEntityType = ArgumentEntityType; - private onChange: (argumentsObj: Record) => void = () => {}; + private propagateChange: (argumentsObj: Record) => void = () => {}; constructor( private fb: FormBuilder, @@ -98,59 +96,27 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private renderer: Renderer2 ) { this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => { - this.onChange(this.getArgumentsObject()); + this.propagateChange(this.getArgumentsObject()); }); effect(() => this.calculatedFieldType() && this.argumentsFormArray.updateValueAndValidity()); } registerOnChange(fn: (argumentsObj: Record) => void): void { - this.onChange = fn; + this.propagateChange = fn; } registerOnTouched(_): void {} validate(): ValidationErrors | null { - if (this.calculatedFieldType() === CalculatedFieldType.SIMPLE - && this.argumentsFormArray.controls.some(control => control.get('refEntityKey').get('type').value === ArgumentType.Rolling)) { - this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; - } else if (!this.argumentsFormArray.controls.length) { - this.errorText = 'calculated-fields.hint.arguments-empty'; - } else { - this.errorText = ''; - } + this.updateErrorText(); return this.errorText ? { argumentsFormArray: false } : null; } - private getArgumentsObject(): Record { - return this.argumentsFormArray.controls.reduce((acc, control) => { - const rawValue = control.getRawValue(); - const { argumentName, ...argument } = rawValue as CalculatedFieldArgumentValue; - acc[argumentName] = argument; - return acc; - }, {} as Record); - } - - writeValue(argumentsObj: Record): void { - this.argumentsFormArray.clear(); - Object.keys(argumentsObj).forEach(key => { - this.argumentsFormArray.push(this.fb.group({ - argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]], - ...argumentsObj[key], - ...(argumentsObj[key].refEntityId ? { - refEntityId: this.fb.group({ - entityType: [{ value: argumentsObj[key].refEntityId.entityType, disabled: true }], - id: [{ value: argumentsObj[key].refEntityId.id , disabled: true }], - }), - } : {}), - refEntityKey: this.fb.group({ - type: [{ value: argumentsObj[key].refEntityKey.type, disabled: true }], - key: [{ value: argumentsObj[key].refEntityKey.key, disabled: true }], - }), - }) as AbstractControl); - }); + onDelete(index: number): void { + this.argumentsFormArray.removeAt(index); + this.argumentsFormArray.markAsDirty(); } - manageArgument($event: Event, matButton: MatButton, index?: number): void { $event?.stopPropagation(); const trigger = matButton._elementRef.nativeElement; @@ -189,7 +155,51 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } } - getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl { + private updateErrorText(): void { + if (this.calculatedFieldType() === CalculatedFieldType.SIMPLE + && this.argumentsFormArray.controls.some(control => control.get('refEntityKey').get('type').value === ArgumentType.Rolling)) { + this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; + } else if (!this.argumentsFormArray.controls.length) { + this.errorText = 'calculated-fields.hint.arguments-empty'; + } else { + this.errorText = ''; + } + } + + private getArgumentsObject(): Record { + return this.argumentsFormArray.controls.reduce((acc, control) => { + const rawValue = control.getRawValue(); + const { argumentName, ...argument } = rawValue as CalculatedFieldArgumentValue; + acc[argumentName] = argument; + return acc; + }, {} as Record); + } + + writeValue(argumentsObj: Record): void { + this.argumentsFormArray.clear(); + this.populateArgumentsFormArray(argumentsObj) + } + + private populateArgumentsFormArray(argumentsObj: Record): void { + Object.keys(argumentsObj).forEach(key => { + this.argumentsFormArray.push(this.fb.group({ + argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]], + ...argumentsObj[key], + ...(argumentsObj[key].refEntityId ? { + refEntityId: this.fb.group({ + entityType: [{ value: argumentsObj[key].refEntityId.entityType, disabled: true }], + id: [{ value: argumentsObj[key].refEntityId.id , disabled: true }], + }), + } : {}), + refEntityKey: this.fb.group({ + type: [{ value: argumentsObj[key].refEntityKey.type, disabled: true }], + key: [{ value: argumentsObj[key].refEntityKey.key, disabled: true }], + }), + }) as AbstractControl); + }); + } + + private getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl { return this.fb.group({ ...value, argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]], @@ -205,9 +215,4 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces }), }) } - - onDelete(index: number): void { - this.argumentsFormArray.removeAt(index); - this.argumentsFormArray.markAsDirty(); - } } 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 7d1373bebb..e6adc1b4d8 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 @@ -68,7 +68,7 @@ formControlName="arguments" [entityId]="data.entityId" [tenantId]="data.tenantId" - [calculatedFieldType]="fieldFormGroup.get('type').valueChanges | async" + [calculatedFieldType]="fieldFormGroup.get('type').value" />
@@ -96,7 +96,7 @@ [functionArgs]="functionArgs$ | async" [disableUndefinedCheck]="true" [scriptLanguage]="ScriptLanguage.TBEL" - helpId="[TODO]: ADD VALID LINK HERE!!!" + helpId="[TODO]: [Calculated Fields] add valid link" /> }
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 99bf62ad46..7f9af5cead 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 @@ -47,7 +47,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent - @if (argumentFormGroup.get('argumentName').hasError('required') && argumentFormGroup.get('argumentName').touched) { - - warning - - } @else if (argumentFormGroup.get('argumentName').hasError('pattern') && argumentFormGroup.get('argumentName').touched) { - - warning - - } @else if (argumentFormGroup.get('argumentName').hasError('maxlength') && argumentFormGroup.get('argumentName').touched) { - - warning - + @if (argumentFormGroup.get('argumentName').touched) { + @if (argumentFormGroup.get('argumentName').hasError('required')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').hasError('pattern')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').hasError('maxlength')) { + + warning + + } } @@ -118,7 +120,7 @@ @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
{{ 'calculated-fields.timeseries-key' | translate }}
- +
} @else {
@@ -143,6 +145,7 @@
{{ 'calculated-fields.attribute-key' | translate }}
this.updateEntityFilter(this.entityType)); @@ -196,6 +197,4 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme typeControl.updateValueAndValidity(); } } - - protected readonly ArgumentEntityType = ArgumentEntityType; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts index c3d1ede02e..bc89e4dc6f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -16,3 +16,4 @@ export * from './dialog/calculated-field-dialog.component'; export * from './arguments-table/calculated-field-arguments-table.component'; +export * from './panel/calculated-field-argument-panel.component'; 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 ab59f412cd..6a4f87ca6b 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 @@ -32,8 +32,7 @@ [entityName]="entity.name"> - + { - readonly EntityType = EntityType; - constructor(protected store: Store) { super(store); } @@ -37,4 +34,5 @@ export class DeviceTabsComponent extends EntityTabsComponent { ngOnInit() { super.ngOnInit(); } + } 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 2a33a74786..fc13238db0 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 @@ -47,9 +47,6 @@ import { EntityFilter } from '@shared/models/query/query.models'; multi: true } ], - host: { - class: 'w-full' - } }) export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Validator { @@ -63,7 +60,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val searchText = ''; keyInputSubject = new Subject(); - private onChange: (value: string) => void; + private propagateChange: (value: string) => void; private cachedResult: EntitiesKeysByQuery; keys$ = this.keyInputSubject.asObservable() @@ -101,7 +98,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val ) { this.keyControl.valueChanges .pipe(takeUntilDestroyed()) - .subscribe(value => this.onChange(value)); + .subscribe(value => this.propagateChange(value)); effect(() => { if (this.keyScopeType() || this.entityFilter() && this.dataKeyType()) { this.cachedResult = null; @@ -119,7 +116,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val } registerOnChange(onChange: (value: string) => void): void { - this.onChange = onChange; + this.propagateChange = onChange; } registerOnTouched(_): void {} 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 73b3ecd861..54798a23e7 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -25,6 +25,7 @@ export interface CalculatedField extends Omit, 'labe externalId?: string; configuration: CalculatedFieldConfiguration; type: CalculatedFieldType; + entityId: EntityId; } export enum CalculatedFieldType { From d3216f3ee78ce154ea3a4c8a224003728497c84f Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 13:11:12 +0200 Subject: [PATCH 09/19] Changed table load --- .../calculated-fields-table.component.ts | 62 ++++++------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 853870982a..4c22ad2896 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -18,8 +18,8 @@ import { ChangeDetectionStrategy, Component, DestroyRef, - Input, - OnInit, + effect, + input, ViewChild, } from '@angular/core'; import { EntityId } from '@shared/models/id/entity-id'; @@ -39,36 +39,14 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; styleUrls: ['./calculated-fields-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CalculatedFieldsTableComponent implements OnInit { - - @Input() - set entityId(entityId: EntityId) { - if (this.entityIdValue !== entityId) { - this.entityIdValue = entityId; - if (!this.activeValue) { - this.hasInitialized = true; - } - } - } - - @Input() - set active(active: boolean) { - if (this.activeValue !== active) { - this.activeValue = active; - if (this.activeValue && this.hasInitialized) { - this.hasInitialized = false; - this.entitiesTable.updateData(); - } - } - } +export class CalculatedFieldsTableComponent { @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; - calculatedFieldsTableConfig: CalculatedFieldsTableConfig; + active = input(); + entityId = input(); - private activeValue = false; - private hasInitialized = false; - private entityIdValue: EntityId; + calculatedFieldsTableConfig: CalculatedFieldsTableConfig; constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, @@ -77,20 +55,20 @@ export class CalculatedFieldsTableComponent implements OnInit { private durationLeft: DurationLeftPipe, private popoverService: TbPopoverService, private destroyRef: DestroyRef) { - } - ngOnInit() { - this.hasInitialized = !this.activeValue; - - this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( - this.calculatedFieldsService, - this.translate, - this.dialog, - this.entityIdValue, - this.store, - this.durationLeft, - this.popoverService, - this.destroyRef, - ); + effect(() => { + if (this.active()) { + this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( + this.calculatedFieldsService, + this.translate, + this.dialog, + this.entityId(), + this.store, + this.durationLeft, + this.popoverService, + this.destroyRef, + ); + } + }); } } From bc835eb9d467f3bcff9777e86944171cdf64a852 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 15:33:30 +0200 Subject: [PATCH 10/19] Fixes --- .../calculated-fields-table-config.ts | 12 +- ...culated-field-arguments-table.component.ts | 2 +- .../calculated-field-dialog.component.ts | 23 +++- ...ulated-field-argument-panel.component.html | 122 +++++++++--------- ...ulated-field-argument-panel.component.scss | 4 + ...lculated-field-argument-panel.component.ts | 11 +- .../entity/entity-autocomplete.component.html | 2 +- .../entity-key-autocomplete.component.html | 12 +- 8 files changed, 111 insertions(+), 77 deletions(-) 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 ae025a13f6..fcef11799d 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 @@ -77,12 +77,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('name', 'common.name', '33%')); - this.columns.push( - new EntityTableColumn('type', 'common.type', '50px')); - this.columns.push( - new EntityTableColumn('expression', 'calculated-fields.expression', '50%', entity => entity.configuration.expression)); + const expressionColumn = new EntityTableColumn('expression', 'calculated-fields.expression', '33%', entity => entity.configuration?.expression); + expressionColumn.sortable = false; + + this.columns.push(new EntityTableColumn('name', 'common.name', '33%')); + this.columns.push(new EntityTableColumn('type', 'common.type', '50px')); + this.columns.push(expressionColumn); this.cellActionDescriptors.push( { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index 1dedc5e80e..657b20abb4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -133,7 +133,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces }; this.keysPopupClosed = false; const argumentsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'leftBottom', false, null, + this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null, ctx, {}, {}, {}, true); 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 7f9af5cead..71fea36959 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 @@ -84,10 +84,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.toggleScopeByOutputType(type)); - this.toggleScopeByOutputType(this.outputFormGroup.get('type').value); + this.observeTypeChanges(); } get configFormGroup(): FormGroup { @@ -113,10 +110,26 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.toggleScopeByOutputType(type)); + this.fieldFormGroup.get('type').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(type => this.toggleKeyByCalculatedFieldType(type)); } private toggleScopeByOutputType(type: OutputType): void { this.outputFormGroup.get('scope')[type === OutputType.Attribute? 'enable' : 'disable']({emitEvent: false}); } + + private toggleKeyByCalculatedFieldType(type: CalculatedFieldType): void { + this.outputFormGroup.get('name')[type === CalculatedFieldType.SIMPLE? 'enable' : 'disable']({emitEvent: false}); + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 865b71406d..58923e0029 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -24,33 +24,35 @@
- @if (argumentFormGroup.get('argumentName').touched) { - @if (argumentFormGroup.get('argumentName').hasError('required')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').hasError('pattern')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').hasError('maxlength')) { - - warning - +
+ @if (argumentFormGroup.get('argumentName').touched) { + @if (argumentFormGroup.get('argumentName').hasError('required')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').hasError('pattern')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').hasError('maxlength')) { + + warning + + } } - } +
@@ -117,40 +119,42 @@ } - @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { -
-
{{ 'calculated-fields.timeseries-key' | translate }}
- -
- } @else { -
-
{{ 'calculated-fields.attribute-scope' | translate }}
- - - - {{ 'calculated-fields.server-attributes' | translate }} - - @if ((keyEntityType$ | async) === EntityType.DEVICE) { - - {{ 'calculated-fields.client-attributes' | translate }} - - - {{ 'calculated-fields.shared-attributes' | translate }} + @if (entityFilter.singleEntity.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) { + @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { +
+
{{ 'calculated-fields.timeseries-key' | translate }}
+ +
+ } @else { +
+
{{ 'calculated-fields.attribute-scope' | translate }}
+ + + + {{ 'calculated-fields.server-attributes' | translate }} - } - - -
-
-
{{ 'calculated-fields.attribute-key' | translate }}
- -
+ @if ((keyEntityType$ | async) === EntityType.DEVICE) { + + {{ 'calculated-fields.client-attributes' | translate }} + + + {{ 'calculated-fields.shared-attributes' | translate }} + + } +
+
+
+
+
{{ 'calculated-fields.attribute-key' | translate }}
+ +
+ } } @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss index a784909b92..520f2d3ec0 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss @@ -25,5 +25,9 @@ width: 100%; } } + + .mat-mdc-form-field-infix { + display: flex; + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index b476bc0fee..79f34d7c20 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, ElementRef, Input, OnInit, output, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, output, ViewChild } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @@ -27,7 +27,7 @@ import { CalculatedFieldArgumentValue, CalculatedFieldType } from '@shared/models/calculated-field.models'; -import { debounceTime, delay, distinctUntilChanged, filter, map, startWith } from 'rxjs/operators'; +import { delay, distinctUntilChanged, filter, map, startWith, throttleTime } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DatasourceType } from '@shared/models/widget.models'; @@ -92,6 +92,7 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme constructor( private fb: FormBuilder, + private cd: ChangeDetectorRef ) { super(); @@ -154,22 +155,24 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme default: entityId = this.argumentFormGroup.get('refEntityId').value as any; } - if (onInit) { + if (!onInit) { this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); } this.entityFilter = { type: AliasFilterType.singleEntity, singleEntity: entityId, }; + this.cd.markForCheck(); } private observeEntityFilterChanges(): void { merge( this.refEntityIdFormGroup.get('entityType').valueChanges, + this.refEntityKeyFormGroup.get('type').valueChanges, this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), this.refEntityKeyFormGroup.get('scope').valueChanges, ) - .pipe(debounceTime(300), delay(50), takeUntilDestroyed()) + .pipe(throttleTime(100), delay(50), takeUntilDestroyed()) .subscribe(() => this.updateEntityFilter(this.entityType)); } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 44a218b455..cbec73e64d 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -29,7 +29,7 @@ {{ displayEntityFn(selectEntityFormGroup.get('entity').value) }} - - close + } @else if (keyControl.hasError('required') && keyControl.touched) { + + warning + } @for (key of filteredKeys$ | async; track key) { + } @empty { + {{ 'entity.no-keys-found' | translate }} } From 75369604a1cc1410b9af6249a3eb448d162a416a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 16:46:28 +0200 Subject: [PATCH 11/19] Fixes --- .../calculated-field-arguments-table.component.html | 2 +- .../calculated-field-arguments-table.component.ts | 12 ++++++++---- .../dialog/calculated-field-dialog.component.ts | 4 +++- .../calculated-field-argument-panel.component.html | 3 ++- .../calculated-field-argument-panel.component.ts | 13 ++++--------- ui-ngx/src/app/shared/models/regex.constants.ts | 2 ++ ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 021bd15c58..f6b82dd954 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -99,7 +99,7 @@ {{ 'calculated-fields.no-arguments' | translate }} } - @if (errorText) { + @if (errorText && this.argumentsFormArray.dirty) { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index 657b20abb4..7357912bdc 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -50,7 +50,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { isDefinedAndNotNull } from '@core/utils'; -import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; +import { charNumRegex } from '@shared/models/regex.constants'; @Component({ selector: 'tb-calculated-field-arguments-table', @@ -98,7 +98,11 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => { this.propagateChange(this.getArgumentsObject()); }); - effect(() => this.calculatedFieldType() && this.argumentsFormArray.updateValueAndValidity()); + effect(() => { + if (this.calculatedFieldType() && this.argumentsFormArray.dirty) { + this.argumentsFormArray.updateValueAndValidity(); + } + }); } registerOnChange(fn: (argumentsObj: Record) => void): void { @@ -183,7 +187,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private populateArgumentsFormArray(argumentsObj: Record): void { Object.keys(argumentsObj).forEach(key => { this.argumentsFormArray.push(this.fb.group({ - argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]], + argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(charNumRegex)]], ...argumentsObj[key], ...(argumentsObj[key].refEntityId ? { refEntityId: this.fb.group({ @@ -202,7 +206,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl { return this.fb.group({ ...value, - argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]], + argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(charNumRegex)]], ...(value.refEntityId ? { refEntityId: this.fb.group({ entityType: [{ value: value.refEntityId.entityType, disabled: true }], 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 71fea36959..d30a90e954 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 @@ -110,7 +110,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent {{ 'calculated-fields.server-attributes' | translate }} - @if ((keyEntityType$ | async) === EntityType.DEVICE) { + @if (entityType === ArgumentEntityType.Device + || entityType === ArgumentEntityType.Current && entityId.entityType === EntityType.DEVICE) { {{ 'calculated-fields.client-attributes' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 79f34d7c20..2949ffdaee 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -18,7 +18,7 @@ import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, output, ViewCh import { TbPopoverComponent } from '@shared/components/popover.component'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; +import { charNumRegex, noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; import { ArgumentEntityType, ArgumentEntityTypeTranslations, @@ -27,7 +27,7 @@ import { CalculatedFieldArgumentValue, CalculatedFieldType } from '@shared/models/calculated-field.models'; -import { delay, distinctUntilChanged, filter, map, startWith, throttleTime } from 'rxjs/operators'; +import { delay, distinctUntilChanged, filter, throttleTime } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DatasourceType } from '@shared/models/widget.models'; @@ -57,7 +57,7 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], + argumentName: ['', [Validators.required, Validators.pattern(charNumRegex), Validators.maxLength(255)]], refEntityId: this.fb.group({ entityType: [ArgumentEntityType.Current], id: [''] @@ -74,11 +74,6 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme argumentTypes: ArgumentType[]; entityFilter: EntityFilter; - keyEntityType$ = this.refEntityIdFormGroup.get('entityType').valueChanges - .pipe( - startWith(this.refEntityIdFormGroup.get('entityType').value), - map(type => type === ArgumentEntityType.Current ? this.entityId.entityType : type) - ); readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; @@ -140,7 +135,7 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme this.argumentFormGroup.get('defaultValue')[isRolling? 'disable' : 'enable']({ emitEvent: false }); } - private updateEntityFilter(entityType: ArgumentEntityType, onInit = false): void { + private updateEntityFilter(entityType: ArgumentEntityType = ArgumentEntityType.Current, onInit = false): void { let entityId: EntityId; switch (entityType) { case ArgumentEntityType.Current: diff --git a/ui-ngx/src/app/shared/models/regex.constants.ts b/ui-ngx/src/app/shared/models/regex.constants.ts index 55742cefd4..60bf154423 100644 --- a/ui-ngx/src/app/shared/models/regex.constants.ts +++ b/ui-ngx/src/app/shared/models/regex.constants.ts @@ -15,3 +15,5 @@ /// export const noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/; + +export const charNumRegex = /^[a-zA-Z0-9]+$/; 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 be31b4f70b..ab696e226c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1030,7 +1030,7 @@ "timeseries-key": "Time series key", "device-name": "Device name", "latest-telemetry": "Latest telemetry", - "rolling": "Rolling", + "rolling": "Time series rolling", "attribute-scope": "Attribute scope", "server-attributes": "Server attributes", "client-attributes": "Client attributes", From ff8a9309f4a0cba877aa5d401a325021ed1d2cf6 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 16:57:46 +0200 Subject: [PATCH 12/19] Fixes --- .../calculated-field-arguments-table.component.html | 4 ++-- .../panel/calculated-field-argument-panel.component.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index f6b82dd954..4f1d1d3980 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -28,7 +28,7 @@ - @if (group.get('refEntityId')?.get('id').value) { + @if (group.get('refEntityId')?.get('id')?.value) { @@ -51,7 +51,7 @@ {{ - (group.get('refEntityId')?.get('entityType').value === ArgumentEntityType.Tenant + (group.get('refEntityId')?.get('entityType')?.value === ArgumentEntityType.Tenant ? 'calculated-fields.argument-current-tenant' : 'calculated-fields.argument-current') | translate }} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 2949ffdaee..6e232bdb3d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -119,7 +119,9 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme } saveArgument(): void { - this.argumentsDataApplied.emit({ value: this.argumentFormGroup.value as CalculatedFieldArgumentValue, index: this.index }); + const { refEntityId, ...restConfig } = this.argumentFormGroup.value; + const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue; + this.argumentsDataApplied.emit({ value, index: this.index }); } cancel(): void { @@ -148,7 +150,7 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme }; break; default: - entityId = this.argumentFormGroup.get('refEntityId').value as any; + entityId = this.argumentFormGroup.get('refEntityId').value as unknown as EntityId; } if (!onInit) { this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); From 14feedbfa05b11f1d20dfa0f6fe9c17a7ea03f57 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 18:01:28 +0200 Subject: [PATCH 13/19] Changed UI of timeWindow --- ...ulated-field-argument-panel.component.html | 22 +++++++++---------- ...lculated-field-argument-panel.component.ts | 5 +++-- .../assets/locale/locale.constant-en_US.json | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 7c33cc2eb3..b1d463a2d3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -168,22 +168,20 @@ } @else { -
-
{{ 'calculated-fields.time-window' | translate }}
-
- - - {{ 'common.suffix.ms' | translate }} - +
+
{{ 'calculated-fields.time-window' | translate }}
+
+
{{ 'calculated-fields.limit' | translate }}
-
- - - -
+
}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 6e232bdb3d..354b6b9f58 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -36,6 +36,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityFilter } from '@shared/models/query/query.models'; import { AliasFilterType } from '@shared/models/alias.models'; import { merge } from 'rxjs'; +import { MINUTE } from '@shared/models/time/time.models'; @Component({ selector: 'tb-calculated-field-argument-panel', @@ -68,8 +69,8 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }], }), defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - limit: [null], - timeWindow: [null], + limit: [10], + timeWindow: [MINUTE * 15], }); argumentTypes: ArgumentType[]; 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 ab696e226c..f0f0671c5c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1037,7 +1037,7 @@ "shared-attributes": "Shared attributes", "attribute-key": "Attribute key", "default-value": "Default value", - "limit": "Limit", + "limit": "Max values", "time-window": "Time window", "customer-name": "Customer name", "timeseries": "Time series", From 8dca91c909138deb18a5b8d1b6aed332bcad2e2e Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 19:08:56 +0200 Subject: [PATCH 14/19] Fixed device filter bug --- .../calculated-fields/calculated-fields-table-config.ts | 6 +++--- .../calculated-fields-table.component.html | 4 +++- .../calculated-fields/calculated-fields-table.component.ts | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) 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 fcef11799d..58caa759da 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 @@ -97,7 +97,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, - onAction: (_, entity) => this.editCalculatedField(entity) + onAction: (_, entity) => this.editCalculatedField(entity), } ); } @@ -171,7 +171,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig - +@if (calculatedFieldsTableConfig) { + +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 4c22ad2896..4fda1cc075 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -16,6 +16,7 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, DestroyRef, effect, @@ -43,7 +44,7 @@ export class CalculatedFieldsTableComponent { @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; - active = input(); + active = input(); entityId = input(); calculatedFieldsTableConfig: CalculatedFieldsTableConfig; @@ -54,6 +55,7 @@ export class CalculatedFieldsTableComponent { private store: Store, private durationLeft: DurationLeftPipe, private popoverService: TbPopoverService, + private cd: ChangeDetectorRef, private destroyRef: DestroyRef) { effect(() => { @@ -68,6 +70,7 @@ export class CalculatedFieldsTableComponent { this.popoverService, this.destroyRef, ); + this.cd.markForCheck(); } }); } From dd8ce35b86f2da4102bb1bfebceada167bc72a99 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 19:18:57 +0200 Subject: [PATCH 15/19] Fixes --- .../calculated-field-arguments-table.component.ts | 6 +++--- .../dialog/calculated-field-dialog.component.ts | 2 +- .../panel/calculated-field-argument-panel.component.html | 2 +- .../panel/calculated-field-argument-panel.component.ts | 8 ++++---- .../entity/entity-key-autocomplete.component.html | 4 +++- ui-ngx/src/app/shared/models/regex.constants.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index 7357912bdc..c8dae67aec 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -50,7 +50,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { isDefinedAndNotNull } from '@core/utils'; -import { charNumRegex } from '@shared/models/regex.constants'; +import { charsWithNumRegex } from '@shared/models/regex.constants'; @Component({ selector: 'tb-calculated-field-arguments-table', @@ -187,7 +187,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private populateArgumentsFormArray(argumentsObj: Record): void { Object.keys(argumentsObj).forEach(key => { this.argumentsFormArray.push(this.fb.group({ - argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(charNumRegex)]], + argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(charsWithNumRegex)]], ...argumentsObj[key], ...(argumentsObj[key].refEntityId ? { refEntityId: this.fb.group({ @@ -206,7 +206,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl { return this.fb.group({ ...value, - argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(charNumRegex)]], + argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(charsWithNumRegex)]], ...(value.refEntityId ? { refEntityId: this.fb.group({ entityType: [{ value: value.refEntityId.entityType, disabled: true }], 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 d30a90e954..6d0687fe9a 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 @@ -61,7 +61,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent isObject(configuration?.arguments) ? Object.keys(configuration.arguments) : []) ); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index b1d463a2d3..020fad4fd2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -23,7 +23,7 @@
{{ 'calculated-fields.argument-name' | translate }}
- +
@if (argumentFormGroup.get('argumentName').touched) { @if (argumentFormGroup.get('argumentName').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 354b6b9f58..792742e5d0 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -18,7 +18,7 @@ import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, output, ViewCh import { TbPopoverComponent } from '@shared/components/popover.component'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { charNumRegex, noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; +import { charsWithNumRegex, noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; import { ArgumentEntityType, ArgumentEntityTypeTranslations, @@ -58,7 +58,7 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, Validators.pattern(charNumRegex), Validators.maxLength(255)]], + argumentName: ['', [Validators.required, Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], refEntityId: this.fb.group({ entityType: [ArgumentEntityType.Current], id: [''] @@ -66,10 +66,10 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme refEntityKey: this.fb.group({ type: [ArgumentType.LatestTelemetry, [Validators.required]], key: [''], - scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }], + scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]], }), defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - limit: [10], + limit: [1000], timeWindow: [MINUTE * 15], }); 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 3e4ff6e90d..f1073738f9 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,9 @@ @for (key of filteredKeys$ | async; track key) { } @empty { - {{ 'entity.no-keys-found' | translate }} + @if (!this.keyControl.value) { + {{ 'entity.no-keys-found' | translate }} + } } diff --git a/ui-ngx/src/app/shared/models/regex.constants.ts b/ui-ngx/src/app/shared/models/regex.constants.ts index 60bf154423..c6b231ef6e 100644 --- a/ui-ngx/src/app/shared/models/regex.constants.ts +++ b/ui-ngx/src/app/shared/models/regex.constants.ts @@ -16,4 +16,4 @@ export const noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/; -export const charNumRegex = /^[a-zA-Z0-9]+$/; +export const charsWithNumRegex = /^[a-zA-Z]+[a-zA-Z0-9]*$/; 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 f0f0671c5c..ee348cb6c3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1048,7 +1048,7 @@ "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 calculated field} other {# calculated fields} }?", "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", "hint": { - "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with rolling type.", + "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-empty": "Arguments should not be empty.", "expression-required": "Expression is required.", "expression-invalid": "Expression is invalid", From c82b6c8c45291d35bb15b128d7313167dd8a9dda Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 31 Jan 2025 19:20:26 +0200 Subject: [PATCH 16/19] Empty arguments fix --- .../components/dialog/calculated-field-dialog.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 6d0687fe9a..c9f1a22157 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 @@ -34,7 +34,7 @@ import { import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { EntityType } from '@shared/models/entity-type.models'; -import { map } from 'rxjs/operators'; +import { map, startWith } from 'rxjs/operators'; import { isObject } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; @@ -63,6 +63,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent isObject(configuration?.arguments) ? Object.keys(configuration.arguments) : []) ); From 0590b5b271c0f464f8ce7ec75f22c669b2ebb979 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 3 Feb 2025 17:54:11 +0200 Subject: [PATCH 17/19] Improvements, refactoring, review comments resolve --- .../calculated-fields-table-config.ts | 36 ++-- .../calculated-fields-table.component.ts | 3 + ...lated-field-arguments-table.component.html | 184 +++++++++--------- ...lated-field-arguments-table.component.scss | 13 +- ...culated-field-arguments-table.component.ts | 44 ++--- .../calculated-field-dialog.component.html | 15 +- .../calculated-field-dialog.component.ts | 32 +-- ...ulated-field-argument-panel.component.html | 131 +++++-------- ...ulated-field-argument-panel.component.scss | 33 ---- ...lculated-field-argument-panel.component.ts | 22 +-- .../entity-debug-settings-button.component.ts | 1 - .../entity-debug-settings-panel.component.ts | 6 +- .../entity/entities-table.component.ts | 4 +- .../entity/entity-table-component.models.ts | 1 - .../pages/device/device-tabs.component.html | 3 +- .../entity/entity-autocomplete.component.html | 12 +- .../entity/entity-autocomplete.component.ts | 4 +- .../entity-key-autocomplete.component.ts | 22 ++- .../shared/models/calculated-field.models.ts | 14 +- ui-ngx/src/app/shared/models/constants.ts | 1 + .../assets/locale/locale.constant-en_US.json | 4 + 21 files changed, 265 insertions(+), 320 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss 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 58caa759da..d8c02558f8 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 @@ -19,7 +19,7 @@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.m import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; import { MatDialog } from '@angular/material/dialog'; -import { TimePageLink } from '@shared/models/page/page-link'; +import { PageLink } from '@shared/models/page/page-link'; import { Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; @@ -27,7 +27,7 @@ import { MINUTE } from '@shared/models/time/time.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; -import { DestroyRef } from '@angular/core'; +import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -35,10 +35,10 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { catchError, filter, switchMap } from 'rxjs/operators'; -import { CalculatedField } from '@shared/models/calculated-field.models'; +import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models'; import { CalculatedFieldDialogComponent } from './components/public-api'; -export class CalculatedFieldsTableConfig extends EntityTableConfig { +export class CalculatedFieldsTableConfig extends EntityTableConfig { // TODO: [Calculated Fields] remove hardcode when BE variable implemented readonly calculatedFieldsDebugPerTenantLimitsConfiguration = @@ -54,20 +54,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); + this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.addEntity = this.addCalculatedField.bind(this); this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); @@ -102,12 +98,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig> { + fetchCalculatedFields(pageLink: PageLink): Observable> { return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); } onOpenDebugConfig($event: Event, { debugSettings = {}, id }: CalculatedField): void { - const { renderer, viewContainerRef } = this.getTable(); + const { viewContainerRef } = this.getTable(); if ($event) { $event.stopPropagation(); } @@ -115,7 +111,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { this.onDebugConfigChanged(id.id, settings); debugStrategyPopover.hide(); @@ -133,17 +128,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + return this.getCalculatedFieldDialog() .pipe( filter(Boolean), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField })), ) - .subscribe((res) => { - if (res) { - this.updateData(); - } - }); } private editCalculatedField(calculatedField: CalculatedField): void { @@ -159,8 +149,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - return this.dialog.open(CalculatedFieldDialogComponent, { + private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add'): Observable { + return this.dialog.open(CalculatedFieldDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 4fda1cc075..bc979a5f0d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -21,6 +21,7 @@ import { DestroyRef, effect, input, + Renderer2, ViewChild, } from '@angular/core'; import { EntityId } from '@shared/models/id/entity-id'; @@ -56,6 +57,7 @@ export class CalculatedFieldsTableComponent { private durationLeft: DurationLeftPipe, private popoverService: TbPopoverService, private cd: ChangeDetectorRef, + private renderer: Renderer2, private destroyRef: DestroyRef) { effect(() => { @@ -69,6 +71,7 @@ export class CalculatedFieldsTableComponent { this.durationLeft, this.popoverService, this.destroyRef, + this.renderer ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 4f1d1d3980..d1d9998e5a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -15,96 +15,100 @@ limitations under the License. --> -
-
-
{{ 'calculated-fields.argument-name' | translate }}
-
{{ 'calculated-fields.datasource' | translate }}
-
{{ 'common.type' | translate }}
-
{{ 'entity.key' | translate }}
-
-
- @for (group of argumentsFormArray.controls; track group) { -
- - - - @if (group.get('refEntityId')?.get('id')?.value) { - - - - - {{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }} - - - - - - } @else { - - - - {{ - (group.get('refEntityId')?.get('entityType')?.value === ArgumentEntityType.Tenant - ? 'calculated-fields.argument-current-tenant' - : 'calculated-fields.argument-current') | translate - }} - - - - } - - - @if (group.get('refEntityKey').get('type').value; as type) { - - - {{ ArgumentTypeTranslations.get(type) | translate }} - - +
+
+
+
{{ 'calculated-fields.argument-name' | translate }}
+
{{ 'calculated-fields.datasource' | translate }}
+
{{ 'common.type' | translate }}
+
{{ 'entity.key' | translate }}
+
+
+
+ @for (group of argumentsFormArray.controls; track group) { +
+ + + +
+ @if (group.get('refEntityId')?.get('id')?.value) { + + + + + {{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }} + + + + + + } @else { + + + + {{ + (group.get('refEntityId')?.get('entityType')?.value === ArgumentEntityType.Tenant + ? 'calculated-fields.argument-current-tenant' + : 'calculated-fields.argument-current') | translate + }} + + + + } +
+ + + @if (group.get('refEntityKey').get('type').value; as type) { + + + {{ ArgumentTypeTranslations.get(type) | translate }} + + + } + + + +
+ {{ group.get('refEntityKey').get('key').value }} +
+
+
+
+
+ + +
+
+ } @empty { + {{ 'calculated-fields.no-arguments' | translate }} } - - - -
- {{ group.get('refEntityKey').get('key').value }} -
-
-
- -
- -
-
- } @empty { - {{ 'calculated-fields.no-arguments' | translate }} - } -
- @if (errorText && this.argumentsFormArray.dirty) { - - } -
-
- + @if (errorText && this.argumentsFormArray.dirty) { + + } +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss index 8695ee4068..73f03dc497 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss @@ -14,18 +14,7 @@ * limitations under the License. */ :host ::ng-deep { - .inline-entity-autocomplete { - .mat-mdc-form-field-infix { - padding-top: 8px; - padding-bottom: 8px; - min-height: 40px; - width: auto; - .mdc-text-field__input, .mat-mdc-select { - font-weight: 400; - line-height: 20px; - } - } - + .tb-inline-field { a { font-size: 14px; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index c8dae67aec..328a82184b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -17,7 +17,6 @@ import { ChangeDetectorRef, Component, - DestroyRef, effect, forwardRef, input, @@ -29,6 +28,7 @@ import { AbstractControl, ControlValueAccessor, FormBuilder, + FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, @@ -51,6 +51,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { isDefinedAndNotNull } from '@core/utils'; import { charsWithNumRegex } from '@shared/models/regex.constants'; +import { TbPopoverComponent } from '@shared/components/popover.component'; @Component({ selector: 'tb-calculated-field-arguments-table', @@ -78,20 +79,19 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces errorText = ''; argumentsFormArray = this.fb.array([]); - keysPopupClosed = true; readonly entityTypeTranslations = entityTypeTranslations; readonly ArgumentTypeTranslations = ArgumentTypeTranslations; readonly EntityType = EntityType; readonly ArgumentEntityType = ArgumentEntityType; + private popoverComponent: TbPopoverComponent; private propagateChange: (argumentsObj: Record) => void = () => {}; constructor( private fb: FormBuilder, private popoverService: TbPopoverService, private viewContainerRef: ViewContainerRef, - private destroyRef: DestroyRef, private cd: ChangeDetectorRef, private renderer: Renderer2 ) { @@ -123,6 +123,9 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces manageArgument($event: Event, matButton: MatButton, index?: number): void { $event?.stopPropagation(); + if (this.popoverComponent && !this.popoverComponent.tbHidden) { + this.popoverComponent.hide(); + } const trigger = matButton._elementRef.nativeElement; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); @@ -135,27 +138,22 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add', tenantId: this.tenantId, }; - this.keysPopupClosed = false; - const argumentsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer, this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null, ctx, {}, {}, {}, true); - argumentsPanelPopover.tbComponentRef.instance.popover = argumentsPanelPopover; - argumentsPanelPopover.tbComponentRef.instance.argumentsDataApplied.subscribe(({ value, index }) => { - argumentsPanelPopover.hide(); + this.popoverComponent.tbComponentRef.instance.argumentsDataApplied.subscribe(({ value, index }) => { + this.popoverComponent.hide(); const formGroup = this.getArgumentFormGroup(value); if (isDefinedAndNotNull(index)) { this.argumentsFormArray.setControl(index, formGroup); } else { this.argumentsFormArray.push(formGroup); } - this.argumentsFormArray.markAsDirty(); + formGroup.markAsDirty(); this.cd.markForCheck(); }); - argumentsPanelPopover.tbHideStart.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { - this.keysPopupClosed = true; - }); } } @@ -171,8 +169,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces } private getArgumentsObject(): Record { - return this.argumentsFormArray.controls.reduce((acc, control) => { - const rawValue = control.getRawValue(); + return this.argumentsFormArray.getRawValue().reduce((acc, rawValue) => { const { argumentName, ...argument } = rawValue as CalculatedFieldArgumentValue; acc[argumentName] = argument; return acc; @@ -186,24 +183,15 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private populateArgumentsFormArray(argumentsObj: Record): void { Object.keys(argumentsObj).forEach(key => { - this.argumentsFormArray.push(this.fb.group({ - argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(charsWithNumRegex)]], + const value: CalculatedFieldArgumentValue = { ...argumentsObj[key], - ...(argumentsObj[key].refEntityId ? { - refEntityId: this.fb.group({ - entityType: [{ value: argumentsObj[key].refEntityId.entityType, disabled: true }], - id: [{ value: argumentsObj[key].refEntityId.id , disabled: true }], - }), - } : {}), - refEntityKey: this.fb.group({ - type: [{ value: argumentsObj[key].refEntityKey.type, disabled: true }], - key: [{ value: argumentsObj[key].refEntityKey.key, disabled: true }], - }), - }) as AbstractControl); + argumentName: key + }; + this.argumentsFormArray.push(this.getArgumentFormGroup(value), {emitEvent: false}); }); } - private getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl { + private getArgumentFormGroup(value: CalculatedFieldArgumentValue): FormGroup { return this.fb.group({ ...value, argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(charsWithNumRegex)]], 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 e6adc1b4d8..ca27ac6fd1 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 @@ -19,7 +19,7 @@

{{ 'entity.type-calculated-field' | translate}}

-
+
{{ 'common.type' | translate }} - + @for (type of fieldTypes; track type) { {{ CalculatedFieldTypeTranslations.get(type) | translate}} } @@ -102,10 +103,10 @@
{{ 'calculated-fields.output' | translate }}
-
+
{{ 'calculated-fields.output-type' | translate }} - + @for (type of outputTypes; track type) { {{ OutputTypeTranslations.get(type) | translate}} } 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 c9f1a22157..c8b2073309 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 @@ -18,10 +18,9 @@ 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 { FormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { helpBaseUrl } from '@shared/models/constants'; import { CalculatedField, CalculatedFieldConfiguration, @@ -35,7 +34,6 @@ import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { EntityType } from '@shared/models/entity-type.models'; import { map, startWith } from 'rxjs/operators'; -import { isObject } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; @@ -50,7 +48,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent isObject(configuration?.arguments) ? Object.keys(configuration.arguments) : []) + map(configuration => Object.keys(configuration.arguments)) ); readonly OutputTypeTranslations = OutputTypeTranslations; @@ -73,7 +71,6 @@ export class CalculatedFieldDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData, - public dialogRef: MatDialogRef, - public fb: UntypedFormBuilder) { + protected dialogRef: MatDialogRef, + private fb: FormBuilder) { super(store, router, dialogRef); this.applyDialogData(); this.observeTypeChanges(); @@ -104,14 +101,15 @@ export class CalculatedFieldDialogComponent extends DialogComponent
{{ 'calculated-fields.argument-name' | translate }}
-
- - -
- @if (argumentFormGroup.get('argumentName').touched) { - @if (argumentFormGroup.get('argumentName').hasError('required')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').hasError('pattern')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').hasError('maxlength')) { - - warning - - } - } -
-
-
+ + + @if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) { + + warning + + } +
-
{{ 'entity.entity-type' | translate }}
+
{{ 'entity.entity-type' | translate }}
@for (type of argumentEntityTypes; track type) { @@ -67,34 +61,17 @@
- @if (entityType === ArgumentEntityType.Device || entityType === ArgumentEntityType.Asset) { -
-
{{ 'calculated-fields.device-name' | translate }}
- -
- } @else if (entityType === ArgumentEntityType.Customer) { + @if (ArgumentEntityTypeParamsMap.has(entityType)) {
-
{{ 'calculated-fields.customer-name' | translate }}
+
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
} @@ -123,13 +100,13 @@ @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
{{ 'calculated-fields.timeseries-key' | translate }}
- +
} @else {
{{ 'calculated-fields.attribute-scope' | translate }}
- - + + {{ 'calculated-fields.server-attributes' | translate }} @@ -149,7 +126,7 @@
{{ 'calculated-fields.attribute-key' | translate }}
{{ 'calculated-fields.default-value' | translate }}
-
- - - -
+ + +
} @else { -
-
{{ 'calculated-fields.time-window' | translate }}
-
- -
+
+
{{ 'calculated-fields.time-window' | translate }}
+
{{ 'calculated-fields.limit' | translate }}
- +
}
-
+