Browse Source

Merge pull request #14691 from vvlladd28/improvement/calculated-fields/details-page

Add calculated fields details page
pull/14692/head
Vladyslav Prykhodko 9 months ago
committed by GitHub
parent
commit
9409eff1e9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  2. 94
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field-form.service.ts
  3. 144
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html
  4. 52
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss
  5. 145
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts
  6. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
  7. 105
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  8. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
  9. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  10. 30
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  11. 78
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  12. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  13. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  14. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  15. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  16. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  17. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  18. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  19. 4
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts
  20. 10
      ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts
  21. 16
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  22. 24
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  23. 1
      ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html
  24. 6
      ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts
  25. 9
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  26. 10
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-field-page.module.ts
  27. 84
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts
  28. 30
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.html
  29. 57
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts
  30. 4
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  31. 2
      ui-ngx/src/app/shared/components/entity/entity-select.component.html
  32. 41
      ui-ngx/src/app/shared/components/entity/entity-select.component.ts
  33. 6
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  34. 8
      ui-ngx/src/app/shared/models/entity-type.models.ts
  35. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -288,7 +288,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD], debugEventTypes:[DebugEventType.DEBUG_CALCULATED_FIELD],
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
debugActionEnabledFn: () => false debugActionDisabled: true
} }
}) })
.afterClosed() .afterClosed()

94
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field-form.service.ts

@ -0,0 +1,94 @@
///
/// Copyright © 2016-2025 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 { DestroyRef, inject, Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { pairwise, switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import {
CalculatedField,
CalculatedFieldConfiguration,
CalculatedFieldEventArguments,
CalculatedFieldType,
OutputStrategyType
} from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { isDefined } from '@core/utils';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { CalculatedFieldsTableEntity } from '@home/components/calculated-fields/calculated-fields-table-config';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityType } from '@shared/models/entity-type.models';
@Injectable({ providedIn: 'root' })
export class CalculatedFieldFormService {
private fb = inject(FormBuilder);
private calculatedFieldsService = inject(CalculatedFieldsService);
buildForm(): FormGroup {
return this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
entityId: [{type: EntityType.DEVICE_PROFILE, id: null}, Validators.required],
type: [CalculatedFieldType.SIMPLE],
debugSettings: [],
configuration: this.fb.control<CalculatedFieldConfiguration>({} as CalculatedFieldConfiguration),
});
}
setupTypeChange(form: FormGroup, destroyRef: DestroyRef, isEditActive?: () => boolean): void {
form.get('type').valueChanges.pipe(
pairwise(),
takeUntilDestroyed(destroyRef)
).subscribe(([prevType, nextType]) => {
const shouldCheck = isEditActive ? isEditActive() : true;
if (shouldCheck) {
if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) ||
![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) {
form.get('configuration').setValue({} as CalculatedFieldConfiguration, { emitEvent: false });
}
}
});
}
prepareConfig(configuration: CalculatedFieldConfiguration): CalculatedFieldConfiguration {
const config = configuration || ({} as CalculatedFieldConfiguration);
if (config.type !== CalculatedFieldType.ALARM) {
if (isDefined(config?.output) && !config?.output?.strategy) {
config.output.strategy = { type: OutputStrategyType.RULE_CHAIN };
}
}
return config;
}
testScript(
calculatedFieldId: string,
formValue: CalculatedField,
testDialogFn: (calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit?: boolean, expression?: string) => Observable<string>,
destroyRef: DestroyRef,
expression?: string,
): Observable<string> {
if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return testDialogFn(formValue, args, false, expression);
}),
takeUntilDestroyed(destroyRef)
);
}
return testDialogFn(formValue, null, false, expression);
}
}

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

@ -0,0 +1,144 @@
<!--
Copyright © 2016-2025 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.
-->
<div class="tb-details-buttons xs:flex xs:flex-col" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'action.delete' | translate }}
</button>
<!-- <div class="flex flex-row xs:flex-col">-->
<!-- <button mat-raised-button-->
<!-- ngxClipboard-->
<!-- (cbOnSuccess)="onAssetProfileIdCopied($event)"-->
<!-- [cbContent]="entity?.id?.id"-->
<!-- [class.!hidden]="isEdit">-->
<!-- <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>-->
<!-- <span translate>asset-profile.copyId</span>-->
<!-- </button>-->
<!-- </div>-->
</div>
<div class="mat-padding flex flex-1 flex-col" [formGroup]="entityForm">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title mb-4">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'entity-field.title' | translate }}</mat-label>
<input matInput maxlength="255" formControlName="name" required>
@if (entityForm.get('name').errors && entityForm.get('name').touched) {
<mat-error>
@if (entityForm.get('name').hasError('required')) {
{{ 'common.hint.title-required' | translate }}
} @else if (entityForm.get('name').hasError('pattern')) {
{{ 'common.hint.title-pattern' | translate }}
} @else if (entityForm.get('name').hasError('maxlength')) {
{{ 'common.hint.title-max-length' | translate }}
}
</mat-error>
}
<mat-hint></mat-hint>
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
<tb-entity-select formControlName="entityId"
appearance="outline"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="calculatedFieldsEntityTypeList"
(entityChanged)="changeEntity($event)">
</tb-entity-select>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'common.type' | translate }}</mat-label>
<mat-select formControlName="type">
@for (type of fieldTypes; track type) {
<mat-option [value]="type">{{ CalculatedFieldTypeTranslations.get(type).name | translate }}</mat-option>
}
</mat-select>
@if (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).hint) {
<mat-hint>{{ CalculatedFieldTypeTranslations.get(entityForm.get('type').value).hint | translate }}</mat-hint>
}
</mat-form-field>
</div>
@switch (entityForm.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
></tb-geofencing-configuration>
}
@case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[ownerId]="ownerId"
[testScript]="onTestScript.bind(this)"
></tb-propagation-configuration>
}
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[testScript]="onTestScript.bind(this)"
></tb-related-entities-aggregation-component>
}
@case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
></tb-entity-aggregation-component>
}
@default {
<tb-simple-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
[isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)"
></tb-simple-configuration>
}
}
</div>
</div>

52
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2025 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 {
.tbel-script-lang-chip {
line-height: 20px;
font-size: 14px;
font-weight: 500;
color: white;
border-radius: 100px;
width: 70px;
min-width: 70px;
display: flex;
justify-content: center;
margin-top: 2px;
margin-right: 4px;
}
.tb-js-func {
.ace_tb {
&.ace_calculated-field {
&-ctx {
color: #C52F00;
}
&-args {
color: #185F2A;
}
&-key {
color: #c24c1a;
}
&-time-window, &-values, &-func, &-value, &-ts, &-latestTs {
color: #7214D0;
}
&-start-ts, &-end-ts {
color: #2CAA00;
}
}
}
}
}

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

@ -0,0 +1,145 @@
///
/// Copyright © 2016-2025 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, inject, Inject, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import {
CalculatedFieldConfiguration,
CalculatedFieldInfo,
calculatedFieldsEntityTypeList,
CalculatedFieldType,
calculatedFieldTypes,
CalculatedFieldTypeTranslations
} from '@shared/models/calculated-field.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData } from '@shared/models/base-data';
import { Observable } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
import { TenantId } from '@shared/models/id/tenant-id';
import { CalculatedFieldFormService } from '@home/components/calculated-fields/calculated-field-form.service';
@Component({
selector: 'tb-calculated-field',
templateUrl: './calculated-field.component.html',
styleUrls: ['./calculated-field.component.scss']
})
export class CalculatedFieldComponent extends EntityComponent<CalculatedFieldsTableEntity> {
@Input()
standalone = false;
@Input()
entityName: string;
disabledConfiguration = false;
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
readonly ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId);
readonly EntityType = EntityType;
readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList;
readonly CalculatedFieldType = CalculatedFieldType;
readonly fieldTypes = calculatedFieldTypes;
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
private cfFormService = inject(CalculatedFieldFormService);
private destroyRef = inject(DestroyRef);
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Inject('entity') protected entityValue: CalculatedFieldInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig,
protected fb: FormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
additionalDebugActionConfig = {
...this.entitiesTableConfig.additionalDebugActionConfig,
action: () => this.entitiesTableConfig.additionalDebugActionConfig.action(
{ id: this.entity.id, ...this.entityFormValue() }, false,
(expression) => {
if (expression) {
this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression});
this.entityForm.get('configuration').markAsDirty();
}
}),
};
get entityId(): EntityId {
return this.entityForm.get('entityId').value;
}
get entitiesTableConfig(): CalculatedFieldsTableConfig {
return this.entitiesTableConfigValue;
}
changeEntity(entity: BaseData<EntityId>): void {
this.entityName = entity?.name;
}
buildForm(_entity?: CalculatedFieldInfo): FormGroup {
const form = inject(CalculatedFieldFormService).buildForm();
inject(CalculatedFieldFormService).setupTypeChange(form, inject(DestroyRef), () => this.isEditValue);
return form;
}
updateForm(entity: CalculatedFieldInfo) {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {};
const preparedConfig = this.cfFormService.prepareConfig(configuration);
this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true});
setTimeout(() => {
this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false});
});
}
onTestScript(expression?: string): Observable<string> {
return this.cfFormService.testScript(
this.entity?.id?.id,
this.entityFormValue(),
this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig),
this.destroyRef,
expression
);
}
updateFormState() {
if (this.entityForm) {
if (this.isEditValue) {
this.entityForm.enable({emitEvent: false});
this.entityForm.get('entityId').disable({emitEvent: false});
} else {
this.entityForm.disable({emitEvent: false});
}
}
}
}

5
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts

@ -50,6 +50,7 @@ import {
import { import {
CalculatedFieldsFilterConfigComponent CalculatedFieldsFilterConfigComponent
} from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component'; } from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component';
import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -57,7 +58,8 @@ import {
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent, CalculatedFieldTestArgumentsComponent,
CalculatedFieldsHeaderComponent, CalculatedFieldsHeaderComponent,
CalculatedFieldsFilterConfigComponent CalculatedFieldsFilterConfigComponent,
CalculatedFieldComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -72,6 +74,7 @@ import {
exports: [ exports: [
CalculatedFieldDialogComponent, CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
CalculatedFieldComponent,
] ]
}) })
export class CalculatedFieldsModule {} export class CalculatedFieldsModule {}

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

@ -20,7 +20,7 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
@ -35,7 +35,7 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap, tap } from 'rxjs/operators'; import { catchError, filter, first, switchMap, tap } from 'rxjs/operators';
import { import {
ArgumentEntityType, ArgumentEntityType,
ArgumentType, ArgumentType,
@ -46,6 +46,7 @@ import {
CalculatedFieldsQuery, CalculatedFieldsQuery,
CalculatedFieldType, CalculatedFieldType,
CalculatedFieldTypeTranslations, CalculatedFieldTypeTranslations,
debugCfActionEnabled,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
PropagationWithExpression, PropagationWithExpression,
@ -62,20 +63,26 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions"; import { ActionNotificationShow } from "@core/notification/notification.actions";
import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component';
import { import {
CalculatedFieldsHeaderComponent CalculatedFieldsHeaderComponent
} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; } from '@home/components/calculated-fields/table-header/calculated-fields-header.component';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component';
import { Router } from '@angular/router';
import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component';
type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo;
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFieldsTableEntity> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFieldsTableEntity> {
readonly tenantId = getCurrentAuthUser(this.store).tenantId; readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = { additionalDebugActionConfig = {
title: this.translate.instant('action.see-debug-events'), title: this.translate.instant('action.see-debug-events'),
action: (calculatedField: CalculatedFieldsTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField), action: (calculatedField: CalculatedFieldsTableEntity,
openCalculatedFieldEdit = true,
afterCloseCallback?: (expression: string) => void) => this.openDebugEventsDialog.call(this, null, calculatedField, openCalculatedFieldEdit, afterCloseCallback),
}; };
calculatedFieldFilterConfig: CalculatedFieldsQuery; calculatedFieldFilterConfig: CalculatedFieldsQuery;
@ -93,30 +100,35 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private importExportService: ImportExportService, private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService, private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService, private utilsService: UtilsService,
private router: Router,
public pageMode = false, public pageMode = false,
) { ) {
super(); super();
if (this.pageMode) { if (this.pageMode) {
this.headerComponent = CalculatedFieldsHeaderComponent; this.headerComponent = CalculatedFieldsHeaderComponent;
this.handleRowClick = ($event, entity) => { this.entityComponent = CalculatedFieldComponent;
this.editCalculatedField($event, entity); this.entityTabsComponent = CalculatedFieldsTabsComponent;
this.rowPointer = true; this.rowPointer = true;
return true;
};
} }
this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields'); this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields');
this.detailsPanelEnabled = false; this.detailsPanelEnabled = this.pageMode;
this.entityType = EntityType.CALCULATED_FIELD; this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD); this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD);
this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD);
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
this.addEntity = this.getCalculatedFieldDialog.bind(this); this.addEntity = this.getCalculatedFieldDialog.bind(this);
this.saveEntity = (cf) => this.calculatedFieldsService.saveCalculatedField(cf);
this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id);
this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name});
this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.onEntityAction = action => this.onCFAction(action);
this.addActionDescriptors = [ this.addActionDescriptors = [
{ {
name: this.translate.instant('calculated-fields.create'), name: this.translate.instant('calculated-fields.create'),
@ -163,7 +175,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
name: this.translate.instant('entity-view.events'), name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock', icon: 'mdi:clipboard-text-clock',
isEnabled: () => true, isEnabled: () => true,
onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), onAction: ($event, entity) =>
this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity),
}, },
{ {
name: '', name: '',
@ -172,14 +185,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
isEnabled: () => true, isEnabled: () => true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
}, }
{ );
if (!this.pageMode) {
this.cellActionDescriptors.push({
name: this.translate.instant('action.edit'), name: this.translate.instant('action.edit'),
icon: 'edit', icon: 'edit',
isEnabled: () => true, isEnabled: () => true,
onAction: ($event, entity) => this.editCalculatedField($event, entity), onAction: ($event, entity) => this.editCalculatedField($event, entity),
} })
); }
} }
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedFieldsTableEntity>> { fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedFieldsTableEntity>> {
@ -188,7 +203,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink); this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink);
} }
onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void { private onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
$event?.stopPropagation(); $event?.stopPropagation();
const { debugSettings = {}, id } = calculatedField; const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = { const additionalActionConfig = {
@ -212,6 +227,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}, $event.target as Element); }, $event.target as Element);
} }
private openDebugTab($event: Event, calculatedField: CalculatedFieldsTableEntity) {
const table = this.getTable();
if (!table.isDetailsOpen) {
table.toggleEntityDetails($event, calculatedField);
if (table.entityDetailsPanel.matTabGroup._tabs.length > 1) {
table.entityDetailsPanel.matTabGroup.selectedIndex = 1;
} else {
table.entityDetailsPanel.matTabGroup._tabs.changes.pipe(
first()
).subscribe(() => {
table.entityDetailsPanel.matTabGroup.selectedIndex = 1;
})
}
}
}
private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void { private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void {
$event?.stopPropagation(); $event?.stopPropagation();
this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty) this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
@ -234,7 +265,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
entityId, entityId,
entityName, entityName,
tenantId: this.tenantId, tenantId: this.tenantId,
ownerId: this.ownerId, ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig, additionalDebugActionConfig: this.additionalDebugActionConfig,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this), getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty, isDirty,
@ -245,17 +276,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
.pipe(filter(Boolean)); .pipe(filter(Boolean));
} }
private openDebugEventsDialog($event: Event, calculatedField: CalculatedFieldsTableEntity): void { private openDebugEventsDialog($event: Event, calculatedField: CalculatedFieldsTableEntity, openCalculatedFieldEdit = true, afterCloseCallback?: (expression: string) => void ): void {
$event?.stopPropagation(); $event?.stopPropagation();
const debugActionEnabledFn = (event: DebugEvent) => {
return (calculatedField.type === CalculatedFieldType.SCRIPT ||
(calculatedField.type === CalculatedFieldType.PROPAGATION &&
calculatedField.configuration.applyExpressionToResolvedArguments)
) && !!(event as DebugEvent).body.arguments;
};
const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef<EventsDialogComponent, string>) => { const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef<EventsDialogComponent, string>) => {
this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments)) this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit)
.subscribe(expression => dialogRef.close(expression)); .subscribe(expression => dialogRef.close(expression));
}; };
@ -270,11 +295,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS], disabledEventTypes:[EventType.LC_EVENT, EventType.ERROR, EventType.STATS],
defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD, defaultEventType: DebugEventType.DEBUG_CALCULATED_FIELD,
onDebugEventSelected, onDebugEventSelected,
debugActionEnabledFn debugActionDisabled: !debugCfActionEnabled(calculatedField)
} }
}) })
.afterClosed() .afterClosed()
.subscribe(); .subscribe(value => {
if (afterCloseCallback) {
afterCloseCallback(value)
}
});
} }
private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void { private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
@ -354,7 +383,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
).subscribe(() => this.updateData()); ).subscribe(() => this.updateData());
} }
private getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> { getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> {
if ( if (
calculatedField.type === CalculatedFieldType.SCRIPT || calculatedField.type === CalculatedFieldType.SCRIPT ||
calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION || calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION ||
@ -394,4 +423,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
return of(null); return of(null);
} }
} }
private openCalculatedField($event: Event, entity: CalculatedFieldsTableEntity) {
$event?.stopPropagation();
const url = this.router.createUrlTree(['calculatedFields', entity.id.id]);
this.router.navigateByUrl(url);
}
private onCFAction(action: EntityAction<CalculatedFieldsTableEntity>): boolean {
switch (action.action) {
case 'open':
this.openCalculatedField(action.event, action.entity);
return true;
case 'export':
this.exportCalculatedField(action.event, action.entity);
return true;
}
return false;
}
} }

4
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts

@ -36,7 +36,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@Component({ @Component({
selector: 'tb-calculated-fields-table', selector: 'tb-calculated-fields-table',
@ -70,6 +70,7 @@ export class CalculatedFieldsTableComponent {
private utilsService: UtilsService, private utilsService: UtilsService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router
) { ) {
this.pageMode = !!this.route.snapshot.data.isPage; this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => { effect(() => {
@ -88,6 +89,7 @@ export class CalculatedFieldsTableComponent {
this.importExportService, this.importExportService,
this.entityDebugSettingsService, this.entityDebugSettingsService,
this.utilsService, this.utilsService,
this.router,
this.pageMode, this.pageMode,
); );
this.cd.markForCheck(); this.cd.markForCheck();

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

@ -239,7 +239,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
} }
writeValue(argumentsObj: Record<string, CalculatedFieldArgument>): void { writeValue(argumentsObj: Record<string, CalculatedFieldArgument>): void {
this.argumentsFormArray.clear(); this.argumentsFormArray.clear({emitEvent: false});
this.populateArgumentsFormArray(argumentsObj); this.populateArgumentsFormArray(argumentsObj);
this.updateEntityNameMap(this.argumentsFormArray.value); this.updateEntityNameMap(this.argumentsFormArray.value);
} }
@ -249,7 +249,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
} }
protected changeIsScriptMode(): void { protected changeIsScriptMode(): void {
this.argumentsFormArray.updateValueAndValidity(); this.argumentsFormArray.updateValueAndValidity({emitEvent: !this.disable});
} }
protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean { protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean {
@ -264,7 +264,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}; };
this.argumentsFormArray.push(this.fb.control(value), { emitEvent: false }); this.argumentsFormArray.push(this.fb.control(value), { emitEvent: false });
}); });
this.argumentsFormArray.updateValueAndValidity(); this.updateDataSource(this.argumentsFormArray.value);
} }
private updateEntityNameMap(values: CalculatedFieldArgumentValue[]): void { private updateEntityNameMap(values: CalculatedFieldArgumentValue[]): void {

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

@ -56,29 +56,13 @@
/> />
</div> </div>
@if (!data.entityId) { @if (!data.entityId) {
<div class="flex flex-row gap-4 xs:flex-col" formGroupName="entityId"> <tb-entity-select formControlName="entityId"
<tb-entity-type-select #entityTypeSelect appearance="outline"
appearance="outline" [filterAllowedEntityTypes]="false"
class="flex-1" [allowedEntityTypes]="calculatedFieldsEntityTypeList"
showLabel [defaultEntityType]="EntityType.DEVICE_PROFILE"
required (entityChanged)="changeEntity($event)">
label="{{ 'entity.entity-type' | translate }}" </tb-entity-select>
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="calculatedFieldsEntityTypeList"
formControlName="entityType">
</tb-entity-type-select>
@if (fieldFormGroup.get('entityId.entityType').value) {
<tb-entity-autocomplete #entityAutocompleteComponent
formControlName="id"
appearance="outline"
class="flex-1"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
(entityChanged)="changeEntity($event)"
/>
}
</div>
} }
<mat-form-field appearance="outline" subscriptSizing="dynamic"> <mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'common.type' | translate }}</mat-label> <mat-label>{{ 'common.type' | translate }}</mat-label>

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

@ -14,11 +14,10 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { import {
@ -28,21 +27,18 @@ import {
CalculatedFieldTestScriptFn, CalculatedFieldTestScriptFn,
CalculatedFieldType, CalculatedFieldType,
calculatedFieldTypes, calculatedFieldTypes,
CalculatedFieldTypeTranslations, CalculatedFieldTypeTranslations
OutputStrategyType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { pairwise, switchMap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { deepTrim, isDefined } from '@core/utils'; import { deepTrim } from '@core/utils';
import { EntityTypeSelectComponent } from '@shared/components/entity/entity-type-select.component';
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
import { BaseData } from '@shared/models/base-data'; import { BaseData } from '@shared/models/base-data';
import { CalculatedFieldFormService } from '@home/components/calculated-fields/calculated-field-form.service';
import { FormGroup } from '@angular/forms';
export interface CalculatedFieldDialogData { export interface CalculatedFieldDialogData {
value?: CalculatedField; value?: CalculatedField;
@ -64,16 +60,7 @@ export interface CalculatedFieldDialogData {
}) })
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> { export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> {
fieldFormGroup = this.fb.group({ fieldFormGroup: FormGroup;
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(EntityType.DEVICE_PROFILE, Validators.required),
id: [null as null | string, Validators.required],
}),
type: [CalculatedFieldType.SIMPLE],
debugSettings: [],
configuration: this.fb.control<CalculatedFieldConfiguration>({} as CalculatedFieldConfiguration),
});
additionalDebugActionConfig = this.data.value?.id ? { additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig, ...this.data.additionalDebugActionConfig,
@ -91,18 +78,16 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
readonly fieldTypes = calculatedFieldTypes; readonly fieldTypes = calculatedFieldTypes;
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>, protected dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService, private calculatedFieldsService: CalculatedFieldsService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: FormBuilder) { private cfFormService: CalculatedFieldFormService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.observeType(); this.fieldFormGroup = this.cfFormService.buildForm();
this.cfFormService.setupTypeChange(this.fieldFormGroup, this.destroyRef);
this.applyDialogData(); this.applyDialogData();
if (this.data.isDirty) { if (this.data.isDirty) {
@ -110,7 +95,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
} }
if (!this.data.entityId) { if (!this.data.entityId) {
this.fieldFormGroup.get('entityId.id').valueChanges.pipe( this.fieldFormGroup.get('entityId').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((entityId) => { ).subscribe((entityId) => {
this.disabledConfiguration = !entityId; this.disabledConfiguration = !entityId;
@ -142,24 +127,17 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}); });
} else { } else {
this.fieldFormGroup.get('name').markAsTouched(); this.fieldFormGroup.get('name').markAsTouched();
this.entityTypeSelect?.markAsTouched();
this.entityAutocompleteComponent?.markAsTouched();
} }
} }
onTestScript(expression?: string): Observable<string> { onTestScript(expression?: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id; return this.cfFormService.testScript(
if (calculatedFieldId) { this.data.value?.id?.id,
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) this.fromGroupValue,
.pipe( this.data.getTestScriptDialogFn,
switchMap(event => { this.destroyRef,
const args = event?.arguments ? JSON.parse(event.arguments) : null; expression
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression); );
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression);
} }
changeEntity(entity: BaseData<EntityId>): void { changeEntity(entity: BaseData<EntityId>): void {
@ -172,28 +150,12 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private applyDialogData(): void { private applyDialogData(): void {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {}; const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {};
if (configuration.type !== CalculatedFieldType.ALARM) { const preparedConfig = this.cfFormService.prepareConfig(configuration);
if (isDefined(configuration?.output) && !configuration?.output?.strategy) { this.fieldFormGroup.patchValue({ configuration: preparedConfig, type, debugSettings, entityId, ...value }, {emitEvent: false});
configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN};
}
}
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false});
setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
if (!this.data.entityId) { if (!this.data.entityId) {
this.fieldFormGroup.get('configuration').disable({emitEvent: false}); this.fieldFormGroup.get('configuration').disable({emitEvent: false});
this.disabledConfiguration = true; this.disabledConfiguration = true;
} }
} }
private observeType(): void {
this.fieldFormGroup.get('type').valueChanges.pipe(
pairwise(),
takeUntilDestroyed(this.destroyRef)
).subscribe(([prevType, nextType]) => {
if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) ||
![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) {
this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false});
}
});
}
} }

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

@ -182,10 +182,12 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
interval: {...value.interval, allowOffsetSec: isDefinedAndNotNull(value?.interval?.offsetSec)} interval: {...value.interval, allowOffsetSec: isDefinedAndNotNull(value?.interval?.offsetSec)}
} }
this.entityAggregationConfiguration.patchValue(data, {emitEvent: false}); this.entityAggregationConfiguration.patchValue(data, {emitEvent: false});
this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); if (this.entityAggregationConfiguration.enabled) {
this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value);
this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value);
this.checkProduceIntermediate(); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value);
this.checkProduceIntermediate();
}
this.updatedOffsetHint(); this.updatedOffsetHint();
setTimeout(() => { setTimeout(() => {
this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});

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

@ -214,7 +214,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
} }
writeValue(zonesObj: Record<string, CalculatedFieldGeofencing>): void { writeValue(zonesObj: Record<string, CalculatedFieldGeofencing>): void {
this.zoneGroupsFormArray.clear(); this.zoneGroupsFormArray.clear({emitEvent: false});
this.populateZonesFormArray(zonesObj); this.populateZonesFormArray(zonesObj);
this.updateEntityNameMap(this.zoneGroupsFormArray.value); this.updateEntityNameMap(this.zoneGroupsFormArray.value);
} }
@ -231,7 +231,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
}; };
this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false }); this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false });
}); });
this.zoneGroupsFormArray.updateValueAndValidity(); this.updateDataSource(this.zoneGroupsFormArray.value);
} }
private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void { private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void {

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

@ -124,7 +124,9 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V
writeValue(config: CalculatedFieldGeofencingConfiguration): void { writeValue(config: CalculatedFieldGeofencingConfiguration): void {
this.geofencingConfiguration.patchValue(config, {emitEvent: false}); this.geofencingConfiguration.patchValue(config, {emitEvent: false});
this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value); this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value);
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); if (this.geofencingConfiguration.enabled) {
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value);
}
} }
registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void { registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void {

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

@ -218,7 +218,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
} }
writeValue(metrics: Record<string, CalculatedFieldAggMetric>): void { writeValue(metrics: Record<string, CalculatedFieldAggMetric>): void {
this.metricsFormArray.clear(); this.metricsFormArray.clear({emitEvent: false});
this.populateZonesFormArray(metrics); this.populateZonesFormArray(metrics);
} }
@ -230,7 +230,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
}; };
this.metricsFormArray.push(this.fb.control(value), { emitEvent: false }); this.metricsFormArray.push(this.fb.control(value), { emitEvent: false });
}); });
this.metricsFormArray.updateValueAndValidity(); this.updateDataSource(this.metricsFormArray.value);
} }
private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string { private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string {

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

@ -148,7 +148,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
for (const propName of Object.keys(changes)) { for (const propName of Object.keys(changes)) {
const change = changes[propName]; const change = changes[propName];
if (change.currentValue !== change.previousValue) { if (change.currentValue !== change.previousValue) {
if (propName === 'simpleMode') { if (propName === 'simpleMode' && !this.disabled) {
this.updatedFormWithMode(); this.updatedFormWithMode();
if (!change.firstChange) { if (!change.firstChange) {
this.outputForm.updateValueAndValidity(); this.outputForm.updateValueAndValidity();
@ -167,7 +167,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) { if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) {
this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false}); this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false});
} }
this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); this.outputForm.get('type').updateValueAndValidity({onlySelf: true, emitEvent: false});
} }
registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void {
@ -253,7 +253,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
private updateTimeSeriesTtl(value: boolean) { private updateTimeSeriesTtl(value: boolean) {
if (value) { if (value) {
this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: true}); this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false});
} else { } else {
this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false}); this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false});
this.outputForm.get('strategy.ttl').disable({emitEvent: false}); this.outputForm.get('strategy.ttl').disable({emitEvent: false});

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts

@ -131,9 +131,11 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
writeValue(value: PropagationWithExpression): void { writeValue(value: PropagationWithExpression): void {
value.expression = value.expression ?? calculatedFieldDefaultScript; value.expression = value.expression ?? calculatedFieldDefaultScript;
this.propagateConfiguration.patchValue(value, {emitEvent: false}); this.propagateConfiguration.patchValue(value, {emitEvent: false});
this.updatedFormWithScript(); if (!this.disabled) {
this.updatedFormWithScript();
}
setTimeout(() => { setTimeout(() => {
this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true, emitEvent: false});
}); });
} }

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -151,7 +151,6 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
formValue.expressionSIMPLE = formValue.expression; formValue.expressionSIMPLE = formValue.expression;
} }
this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); this.simpleConfiguration.patchValue(formValue, {emitEvent: false});
this.updatedFormWithScript();
setTimeout(() => { setTimeout(() => {
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
}); });

4
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.service.ts

@ -22,7 +22,9 @@ import { TranslateService } from '@ngx-translate/core';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { EntityDebugSettingPanelConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { EntityDebugSettingPanelConfig } from '@home/components/entity/debug/entity-debug-settings.model';
@Injectable() @Injectable({
providedIn: 'root'
})
export class EntityDebugSettingsService { export class EntityDebugSettingsService {
constructor( constructor(

10
ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts

@ -16,7 +16,7 @@
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, Directive, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; import { AfterViewInit, Directive, inject, Input, OnInit, QueryList, ViewChildren } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
@ -100,9 +100,11 @@ export abstract class EntityTabsComponent<T extends BaseData<HasId>,
entityTabsChanged = this.entityTabsSubject.asObservable(); entityTabsChanged = this.entityTabsSubject.asObservable();
protected constructor(protected store: Store<AppState>) { protected store: Store<AppState> = inject(Store<AppState>);
super(store);
this.authUser = getCurrentAuthUser(store); protected constructor(...args: unknown[]) {
super();
this.authUser = getCurrentAuthUser(this.store);
} }
ngOnInit() { ngOnInit() {

16
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -22,7 +22,14 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { DebugEventType, Event, EventBody, EventType, FilterEventBody } from '@shared/models/event.models'; import {
DebugEventType,
Event as DebugEvent,
Event,
EventBody,
EventType,
FilterEventBody
} from '@shared/models/event.models';
import { TimePageLink } from '@shared/models/page/page-link'; import { TimePageLink } from '@shared/models/page/page-link';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -95,7 +102,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private store: Store<AppState>, private store: Store<AppState>,
public testButtonLabel?: string, public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>, private debugEventSelected?: EventEmitter<EventBody>,
public hideClearEventAction = false) { public hideClearEventAction = false,
public disableDebugEventAction = false) {
super(); super();
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
@ -453,7 +461,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.cellActionDescriptors.push({ this.cellActionDescriptors.push({
name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}), name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}),
icon: 'bug_report', icon: 'bug_report',
isEnabled: (entity) => entity.body.type === 'IN' || entity.body.error !== undefined, isEnabled: (entity) => (entity.body.type === 'IN' || entity.body.error !== undefined) && !this.disableDebugEventAction,
onAction: ($event, entity) => { onAction: ($event, entity) => {
this.debugEventSelected.next(entity.body); this.debugEventSelected.next(entity.body);
} }
@ -464,7 +472,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.cellActionDescriptors.push({ this.cellActionDescriptors.push({
name: this.translate.instant('calculated-fields.test-with-this-message'), name: this.translate.instant('calculated-fields.test-with-this-message'),
icon: 'bug_report', icon: 'bug_report',
isEnabled: () => true, isEnabled: (event) => !this.disableDebugEventAction && !!(event as DebugEvent).body.arguments,
onAction: (_, entity) => this.debugEventSelected.next(entity.body) onAction: (_, entity) => this.debugEventSelected.next(entity.body)
}); });
break; break;

24
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -17,10 +17,12 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, EventEmitter, Component,
EventEmitter,
Input, Input,
OnDestroy, OnDestroy,
OnInit, Output, OnInit,
Output,
ViewChild, ViewChild,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
@ -61,6 +63,21 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() @Input()
hideClearEventAction: boolean = false; hideClearEventAction: boolean = false;
private disableDebugEventActionValue = false;
get disableDebugEventAction() {
return this.disableDebugEventActionValue;
}
@Input()
set disableDebugEventAction(value) {
this.disableDebugEventActionValue = value;
if (this.eventTableConfig) {
this.eventTableConfig.disableDebugEventAction = this.disableDebugEventAction;
this.eventTableConfig.updateCellAction();
}
};
activeValue = false; activeValue = false;
dirtyValue = false; dirtyValue = false;
entityIdValue: EntityId; entityIdValue: EntityId;
@ -151,7 +168,8 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.store, this.store,
this.functionTestButtonLabel, this.functionTestButtonLabel,
this.debugEventSelected, this.debugEventSelected,
this.hideClearEventAction this.hideClearEventAction,
this.disableDebugEventAction
); );
} }

1
ui-ngx/src/app/modules/home/dialogs/events-dialog.component.html

@ -34,6 +34,7 @@
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[entityId]="data.entityId" [entityId]="data.entityId"
[functionTestButtonLabel]="data.functionTestButtonLabel ?? ''" [functionTestButtonLabel]="data.functionTestButtonLabel ?? ''"
[disableDebugEventAction]="data.debugActionDisabled"
(debugEventSelected)="this.onDebugEventSelected($event)" (debugEventSelected)="this.onDebugEventSelected($event)"
/> />
</div> </div>

6
ui-ngx/src/app/modules/home/dialogs/events-dialog.component.ts

@ -23,7 +23,6 @@ import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { DebugEventType, EventType } from '@shared/models/event.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { BaseData, HasId } from '@shared/models/base-data';
export interface EventsDialogData { export interface EventsDialogData {
title: string; title: string;
@ -34,7 +33,7 @@ export interface EventsDialogData {
disabledEventTypes?: Array<EventType | DebugEventType>; disabledEventTypes?: Array<EventType | DebugEventType>;
functionTestButtonLabel?: string; functionTestButtonLabel?: string;
onDebugEventSelected?: (event: any, dialogRef: MatDialogRef<EventsDialogComponent, string>) => void; onDebugEventSelected?: (event: any, dialogRef: MatDialogRef<EventsDialogComponent, string>) => void;
debugActionEnabledFn?: (event: BaseData<HasId>) => boolean; debugActionDisabled?: boolean;
} }
@Component({ @Component({
@ -54,9 +53,6 @@ export class EventsDialogComponent extends DialogComponent<EventsDialogComponent
} }
ngAfterViewInit() { ngAfterViewInit() {
if (this.data.debugActionEnabledFn && this.eventsTable.entitiesTable.cellActionDescriptors?.length > 0) {
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = this.data.debugActionEnabledFn;
}
this.eventsTable.entitiesTable.updateData(); this.eventsTable.entitiesTable.updateData();
} }

9
ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts

@ -20,18 +20,22 @@ import { SafeHtml } from '@angular/platform-browser';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource';
import { ElementRef, EventEmitter, Renderer2, ViewContainerRef } from '@angular/core'; import { ElementRef, EventEmitter, ViewContainerRef } from '@angular/core';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { import {
CellActionDescriptor, EntityActionTableColumn, EntityColumn, EntityTableColumn, CellActionDescriptor,
EntityActionTableColumn,
EntityColumn,
EntityTableColumn,
EntityTableConfig, EntityTableConfig,
GroupActionDescriptor, GroupActionDescriptor,
HeaderActionDescriptor HeaderActionDescriptor
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import type { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component';
export type EntitiesTableAction = 'add'; export type EntitiesTableAction = 'add';
@ -64,6 +68,7 @@ export interface IEntitiesTableComponent {
paginator: MatPaginator; paginator: MatPaginator;
sort: MatSort; sort: MatSort;
route: ActivatedRoute; route: ActivatedRoute;
entityDetailsPanel: EntityDetailsPanelComponent;
viewContainerRef: ViewContainerRef; viewContainerRef: ViewContainerRef;
addEnabled(): boolean; addEnabled(): boolean;

10
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields.module.ts → ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-field-page.module.ts

@ -20,15 +20,19 @@ import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { CalculatedFieldsRoutingModule } from '@home/pages/calculated-fields/calculated-fields-routing.module'; import { CalculatedFieldsRoutingModule } from '@home/pages/calculated-fields/calculated-fields-routing.module';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component';
@NgModule({ @NgModule({
declarations: [], declarations: [
CalculatedFieldsTabsComponent
],
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
HomeComponentsModule, HomeComponentsModule,
HomeDialogsModule, HomeDialogsModule,
CalculatedFieldsRoutingModule CalculatedFieldsRoutingModule,
] ]
}) })
export class CalculatedFieldsModule { } export class CalculatedFieldPageModule { }

84
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts

@ -14,24 +14,94 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { NgModule } from '@angular/core'; import { DestroyRef, inject, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { MenuId } from '@core/services/menu.models'; import { MenuId } from '@core/services/menu.models';
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component'; import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config';
import { CalculatedFieldsService } from '@core/http/calculated-fields.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 { DatePipe } from '@angular/common';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { UtilsService } from '@core/services/utils.service';
export const CalculatedFieldsTableConfigResolver: ResolveFn<CalculatedFieldsTableConfig> =
(_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot,
calculatedFieldsService = inject(CalculatedFieldsService),
translate = inject(TranslateService),
dialog = inject(MatDialog),
store = inject(Store<AppState>),
datePipe = inject(DatePipe),
destroyRef = inject(DestroyRef),
importExportService = inject(ImportExportService),
entityDebugSettingsService = inject(EntityDebugSettingsService),
utilsService = inject(UtilsService),
router = inject(Router),
) => {
return new CalculatedFieldsTableConfig(
calculatedFieldsService,
translate,
dialog,
datePipe,
null,
store,
destroyRef,
null,
null,
null,
importExportService,
entityDebugSettingsService,
utilsService,
router,
true,
);
};
const routes: Routes = [ const routes: Routes = [
{ {
path: 'calculatedFields', path: 'calculatedFields',
component: CalculatedFieldsTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
breadcrumb: { breadcrumb: {
menuId: MenuId.calculated_fields menuId: MenuId.calculated_fields
}
},
children: [
{
path: '',
component: CalculatedFieldsTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
isPage: true,
}
}, },
isPage: true, {
} path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'mdi:function-variant'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
},
resolve: {
entitiesTableConfig: CalculatedFieldsTableConfigResolver
}
}
]
} }
]; ];

30
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2025 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.
-->
@if (entity) {
<mat-tab label="{{ 'calculated-fields.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[debugEventTypes]="[DebugEventType.DEBUG_CALCULATED_FIELD]"
[active]="eventsTab.isActive"
[tenantId]="entity.tenantId.id"
[entityId]="entity.id"
[disableDebugEventAction]="debugActionDisabled"
(debugEventSelected)="onDebugEventSelected($event)"
></tb-event-table>
</mat-tab>
}

57
ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts

@ -0,0 +1,57 @@
///
/// Copyright © 2016-2025 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 } from '@angular/core';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import type {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
import { debugCfActionEnabled } from '@shared/models/calculated-field.models';
@Component({
selector: 'tb-calculated-fields-tabs',
templateUrl: './calculated-fields-tabs.component.html',
styleUrls: []
})
export class CalculatedFieldsTabsComponent extends EntityTabsComponent<CalculatedFieldsTableEntity> {
readonly DebugEventType = DebugEventType;
readonly EventType = EventType;
constructor() {
super();
}
get debugActionDisabled(): boolean {
return !debugCfActionEnabled(this.entity);
};
onDebugEventSelected(event: CalculatedFieldEventBody) {
(this.entitiesTableConfig as CalculatedFieldsTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments), false)
.subscribe((expression) => {
(this.entitiesTableConfig as CalculatedFieldsTableConfig).getTable();
const entityDetailsPanel = this.entitiesTableConfig.getTable().entityDetailsPanel;
entityDetailsPanel.onToggleEditMode(true);
entityDetailsPanel.selectedTab = 0;
setTimeout(() => {
entityDetailsPanel.detailsForm.get('configuration').setValue({...this.entity.configuration, expression});
entityDetailsPanel.detailsForm.get('configuration').markAsDirty();
});
});
};
}

4
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -27,7 +27,7 @@ import { UserModule } from '@modules/home/pages/user/user.module';
import { DeviceModule } from '@modules/home/pages/device/device.module'; import { DeviceModule } from '@modules/home/pages/device/device.module';
import { AssetModule } from '@modules/home/pages/asset/asset.module'; import { AssetModule } from '@modules/home/pages/asset/asset.module';
import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module'; import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module';
import { CalculatedFieldsModule } from '@home/pages/calculated-fields/calculated-fields.module'; import { CalculatedFieldPageModule } from '@home/pages/calculated-fields/calculated-field-page.module';
import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module'; import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module';
import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module'; import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module';
import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'; import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module';
@ -70,7 +70,7 @@ import { AiModelModule } from '@home/pages/ai-model/ai-model.module';
EdgeModule, EdgeModule,
EntityViewModule, EntityViewModule,
CustomerModule, CustomerModule,
CalculatedFieldsModule, CalculatedFieldPageModule,
RuleChainModule, RuleChainModule,
WidgetLibraryModule, WidgetLibraryModule,
DashboardModule, DashboardModule,

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

@ -25,6 +25,7 @@
[useAliasEntityTypes]="useAliasEntityTypes" [useAliasEntityTypes]="useAliasEntityTypes"
[allowedEntityTypes]="allowedEntityTypes" [allowedEntityTypes]="allowedEntityTypes"
[additionEntityTypes]="additionEntityTypes" [additionEntityTypes]="additionEntityTypes"
[filterAllowedEntityTypes]="filterAllowedEntityTypes"
formControlName="entityType"> formControlName="entityType">
</tb-entity-type-select> </tb-entity-type-select>
<tb-entity-autocomplete <tb-entity-autocomplete
@ -34,6 +35,7 @@
[required]="required" [required]="required"
[entityType]="modelValue.entityType" [entityType]="modelValue.entityType"
[useEntityDisplayName]="useEntityDisplayName" [useEntityDisplayName]="useEntityDisplayName"
(entityChanged)="changeEntity($event)"
formControlName="entityId"> formControlName="entityId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
</div> </div>

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { Component, DestroyRef, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -26,6 +26,7 @@ import { NULL_UUID } from '@shared/models/id/has-uuid';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatFormFieldAppearance } from '@angular/material/form-field'; import { MatFormFieldAppearance } from '@angular/material/form-field';
import { BaseData } from '@shared/models/base-data';
@Component({ @Component({
selector: 'tb-entity-select', selector: 'tb-entity-select',
@ -37,7 +38,7 @@ import { MatFormFieldAppearance } from '@angular/material/form-field';
multi: true multi: true
}] }]
}) })
export class EntitySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { export class EntitySelectComponent implements ControlValueAccessor, OnInit {
entitySelectFormGroup: UntypedFormGroup; entitySelectFormGroup: UntypedFormGroup;
@ -66,6 +67,15 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
@coerceBoolean() @coerceBoolean()
useEntityDisplayName = false; useEntityDisplayName = false;
@Input()
filterAllowedEntityTypes: boolean;
@Input()
defaultEntityType: AliasEntityType | EntityType;
@Output()
entityChanged = new EventEmitter<BaseData<EntityId>>();
displayEntityTypeSelect: boolean; displayEntityTypeSelect: boolean;
AliasEntityType = AliasEntityType; AliasEntityType = AliasEntityType;
@ -74,8 +84,6 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER
]); ]);
private readonly defaultEntityType: EntityType | AliasEntityType = null;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
@ -86,15 +94,18 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes,
this.useAliasEntityTypes); this.useAliasEntityTypes);
let defaultEntityType: EntityType | AliasEntityType = null
if (entityTypes.length === 1) { if (entityTypes.length === 1) {
this.displayEntityTypeSelect = false; this.displayEntityTypeSelect = false;
this.defaultEntityType = entityTypes[0]; defaultEntityType = entityTypes[0];
} else { } else {
this.displayEntityTypeSelect = true; this.displayEntityTypeSelect = true;
} }
this.entitySelectFormGroup = this.fb.group({ this.entitySelectFormGroup = this.fb.group({
entityType: [this.defaultEntityType], entityType: [defaultEntityType],
entityId: [null] entityId: [null]
}); });
} }
@ -126,9 +137,19 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
if (additionNullUIIDEntityTypes.length > 0) { if (additionNullUIIDEntityTypes.length > 0) {
additionNullUIIDEntityTypes.forEach((entityType) => this.entityTypeNullUUID.add(entityType)); additionNullUIIDEntityTypes.forEach((entityType) => this.entityTypeNullUUID.add(entityType));
} }
}
ngAfterViewInit(): void { if (this.filterAllowedEntityTypes === false) {
if (this.allowedEntityTypes?.length === 1) {
this.displayEntityTypeSelect = false;
this.entitySelectFormGroup.get('entityType').setValue(this.allowedEntityTypes[0])
} else {
this.displayEntityTypeSelect = true;
}
}
if (this.defaultEntityType) {
this.entitySelectFormGroup.get('entityType').setValue(this.defaultEntityType);
}
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
@ -176,4 +197,8 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte
} }
} }
} }
changeEntity(entity: BaseData<EntityId>): void {
this.entityChanged.emit(entity);
}
} }

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

@ -540,6 +540,12 @@ export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entity
} }
} }
export const debugCfActionEnabled = (cf: CalculatedField) => {
return (cf.type === CalculatedFieldType.SCRIPT ||
(cf.type === CalculatedFieldType.PROPAGATION && cf.configuration.applyExpressionToResolvedArguments)
);
}
export interface CalculatedFieldArgumentValueBase { export interface CalculatedFieldArgumentValueBase {
argumentName: string; argumentName: string;
type: ArgumentType; type: ArgumentType;

8
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -490,7 +490,7 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
type: 'entity.type-calculated-field', type: 'entity.type-calculated-field',
typePlural: 'entity.type-calculated-fields', typePlural: 'entity.type-calculated-fields',
list: 'calculated-fields.list', list: 'calculated-fields.list',
add: 'action.add', details: 'calculated-fields.calculated-field-details',
noEntities: 'calculated-fields.no-found', noEntities: 'calculated-fields.no-found',
search: 'action.search', search: 'action.search',
selectedEntities: 'calculated-fields.selected-fields' selectedEntities: 'calculated-fields.selected-fields'
@ -658,6 +658,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
helpLinkId: 'aiModels' helpLinkId: 'aiModels'
} }
], ],
[
EntityType.CALCULATED_FIELD,
{
helpLinkId: 'calculatedField'
}
],
[ [
EntityType.API_KEY, EntityType.API_KEY,
{ {

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

@ -1086,7 +1086,7 @@
"filter-types": "Audit log types" "filter-types": "Audit log types"
}, },
"debug-settings": { "debug-settings": {
"label": "Debug Configuration", "label": "Debug configuration",
"on-failure": "Failures only (24/7)", "on-failure": "Failures only (24/7)",
"all-messages": "All messages ({{time}})", "all-messages": "All messages ({{time}})",
"failures": "Failures", "failures": "Failures",
@ -1115,6 +1115,7 @@
"arguments": "Arguments", "arguments": "Arguments",
"decimals-by-default": "Decimals by default", "decimals-by-default": "Decimals by default",
"debugging": "Calculated field debugging", "debugging": "Calculated field debugging",
"calculated-field-details": "Calculated field details",
"argument-name": "Argument name", "argument-name": "Argument name",
"name": "Name", "name": "Name",
"datasource": "Datasource", "datasource": "Datasource",
@ -1220,6 +1221,7 @@
"calculated-field-filter-title": "Calculated field filter", "calculated-field-filter-title": "Calculated field filter",
"filter-title": "Filter", "filter-title": "Filter",
"calculated-field-types": "Calculated field types", "calculated-field-types": "Calculated field types",
"events": "Events",
"any-type": "Any type", "any-type": "Any type",
"metrics": { "metrics": {
"metrics": "Metrics", "metrics": "Metrics",

Loading…
Cancel
Save