audit-log.filter-types
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss
index 77bd5aea60..501a5e2419 100644
--- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss
+++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss
@@ -1,5 +1,5 @@
/**
- * Copyright © 2016-2025 The Thingsboard Authors
+ * Copyright © 2016-2026 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.
@@ -33,3 +33,12 @@
}
}
}
+
+::ng-deep {
+ .tb-audit-log-config-component {
+ max-width: 100%;
+ width: 600px;
+ min-width: 100%;
+ flex: 1;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts
index e78b38965d..631a795f7d 100644
--- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts
+++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts
@@ -1,5 +1,5 @@
///
-/// Copyright © 2016-2025 The Thingsboard Authors
+/// Copyright © 2016-2026 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.
@@ -16,32 +16,44 @@
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
-import { ChangeDetectorRef, Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
+import {
+ ChangeDetectorRef,
+ Component,
+ DestroyRef,
+ ElementRef,
+ forwardRef,
+ Input,
+ OnInit,
+ TemplateRef,
+ ViewChild,
+ ViewContainerRef
+} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
+import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { isDefined, isDefinedAndNotNull } from '@app/core/utils';
import { StringItemsOption } from '@app/shared/components/string-items-list.component';
import {
ActionType,
actionTypeTranslations,
- auditLogFilterEquals,
AuditLogFilter,
+ auditLogFilterEquals,
POSITION_MAP,
} from '@app/shared/public-api';
import { TranslateService } from '@ngx-translate/core';
// @dynamic
@Component({
- selector: 'tb-audit-log-filter',
- templateUrl: './audit-log-filter.component.html',
- styleUrls: ['./audit-log-filter.component.scss'],
- providers: [
- {
- provide: NG_VALUE_ACCESSOR,
- useExisting: forwardRef(() => AuditLogFilterComponent),
- multi: true
- }
- ]
+ selector: 'tb-audit-log-filter',
+ templateUrl: './audit-log-filter.component.html',
+ styleUrls: ['./audit-log-filter.component.scss'],
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => AuditLogFilterComponent),
+ multi: true
+ }
+ ],
+ standalone: false
})
export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
@@ -56,9 +68,9 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
actionTypeTranslations = actionTypeTranslations;
- buttonDisplayValue = this.translate.instant('audit-log.filter');
+ buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title');
- auditLogFilterForm: UntypedFormGroup;
+ auditLogFilterForm: FormGroup;
auditLogOverlayRef: OverlayRef;
@@ -68,7 +80,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
private propagateChange = (_: any) => {};
- constructor(private fb: UntypedFormBuilder,
+ constructor(private fb: FormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
@@ -93,7 +105,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
this.propagateChange = fn;
}
- registerOnTouched(fn: any): void {
+ registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
@@ -152,11 +164,13 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
cancel() {
this.updateAuditLogFilterForm(this.auditLogFilter);
+ this.auditLogFilterForm.markAsPristine();
this.auditLogOverlayRef.dispose();
}
update() {
this.auditLogFilterUpdated(this.auditLogFilterForm.value);
+ this.auditLogFilterForm.markAsPristine();
this.auditLogOverlayRef.dispose();
}
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html
index 3d982197c8..c82257803d 100644
--- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html
+++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html
@@ -1,6 +1,6 @@
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss
new file mode 100644
index 0000000000..695df5063a
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2026 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.
+ */
+::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;
+ }
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts
new file mode 100644
index 0000000000..4790789080
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts
@@ -0,0 +1,169 @@
+///
+/// Copyright © 2016-2026 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 '@core/services/calculated-field-form.service';
+import { AssetInfo } from '@shared/models/asset.models';
+import { DeviceInfo } from '@shared/models/device.models';
+import { NULL_UUID } from '@shared/models/id/has-uuid';
+import { EntityService } from '@core/http/entity.service';
+
+@Component({
+ selector: 'tb-calculated-field',
+ templateUrl: './calculated-field.component.html',
+ styleUrls: ['./calculated-field.component.scss'],
+ standalone: false
+})
+export class CalculatedFieldComponent extends EntityComponent
{
+
+ @Input()
+ standalone = false;
+
+ @Input()
+ entityName: string;
+
+ disabledConfiguration = false;
+
+ ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId);
+ readonly 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,
+ protected translate: TranslateService,
+ @Inject('entity') protected entityValue: CalculatedFieldInfo,
+ @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig,
+ protected fb: FormBuilder,
+ protected cd: ChangeDetectorRef,
+ private entityService: EntityService) {
+ 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): 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});
+ this.entityForm.get('type').updateValueAndValidity();
+ });
+ }
+
+ onTestScript(expression?: string): Observable {
+ return this.cfFormService.testScript(
+ this.entity?.id?.id,
+ this.entityValue,
+ 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});
+ this.getOwnerId(this.entityId);
+ } else {
+ this.entityForm.disable({emitEvent: false});
+ }
+ }
+ }
+
+ getOwnerId(entityId: EntityId) {
+ if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) {
+ this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe(
+ (entity: AssetInfo | DeviceInfo) => {
+ if (this.isAssignedToCustomer(entity)) {
+ this.ownerId = entity.customerId;
+ }
+ }
+ );
+ }
+ }
+
+ private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean {
+ return entity && entity.customerId && entity.customerId.id !== NULL_UUID;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
index e9bf146bfd..c58b595ba1 100644
--- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
+++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
@@ -1,5 +1,5 @@
///
-/// Copyright © 2016-2025 The Thingsboard Authors
+/// Copyright © 2016-2026 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.
@@ -44,12 +44,22 @@ import {
import {
EntityAggregationComponentModule
} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module';
+import {
+ CalculatedFieldsHeaderComponent
+} from '@home/components/calculated-fields/table-header/calculated-fields-header.component';
+import {
+ CalculatedFieldsFilterConfigComponent
+} from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component';
+import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component';
@NgModule({
declarations: [
CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
+ CalculatedFieldsHeaderComponent,
+ CalculatedFieldsFilterConfigComponent,
+ CalculatedFieldComponent,
],
imports: [
CommonModule,
@@ -64,6 +74,7 @@ import {
exports: [
CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent,
+ CalculatedFieldComponent,
]
})
export class CalculatedFieldsModule {}
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 60c2878802..aac49a3f37 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
@@ -1,5 +1,5 @@
///
-/// Copyright © 2016-2025 The Thingsboard Authors
+/// Copyright © 2016-2026 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.
@@ -16,10 +16,11 @@
import {
DateEntityTableColumn,
+ EntityLinkTableColumn,
EntityTableColumn,
EntityTableConfig
} 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 { Direction } from '@shared/models/page/sort-order';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
@@ -34,15 +35,18 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
-import { catchError, filter, switchMap, tap } from 'rxjs/operators';
+import { catchError, filter, first, switchMap, tap } from 'rxjs/operators';
import {
ArgumentEntityType,
ArgumentType,
CalculatedField,
CalculatedFieldEventArguments,
+ CalculatedFieldInfo,
CalculatedFieldScriptConfiguration,
+ CalculatedFieldsQuery,
CalculatedFieldType,
CalculatedFieldTypeTranslations,
+ debugCfActionEnabled,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
PropagationWithExpression,
@@ -54,50 +58,79 @@ import {
CalculatedFieldTestScriptDialogData
} from './components/public-api';
import { ImportExportService } from '@shared/import-export/import-export.service';
-import { isObject } from '@core/utils';
+import { deepClone, getEntityDetailsPageURL, isObject } from '@core/utils';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions";
-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 {
+ CalculatedFieldsHeaderComponent
+} 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';
+
+export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo;
-export class CalculatedFieldsTableConfig extends EntityTableConfig {
+export class CalculatedFieldsTableConfig extends EntityTableConfig {
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
- title: this.translate.instant('calculated-fields.see-debug-events'),
- action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField),
+ title: this.translate.instant('action.see-debug-events'),
+ action: (calculatedField: CalculatedFieldsTableEntity,
+ openCalculatedFieldEdit = true,
+ afterCloseCallback?: (expression: string) => void) => this.openDebugEventsDialog.call(this, null, calculatedField, openCalculatedFieldEdit, afterCloseCallback),
};
+ calculatedFieldFilterConfig: CalculatedFieldsQuery;
+
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
private datePipe: DatePipe,
- public entityId: EntityId = null,
+ private entityId: EntityId = null,
private store: Store,
private destroyRef: DestroyRef,
private renderer: Renderer2,
- public entityName: string,
+ private entityName: string,
private ownerId: EntityId = null,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
+ private router: Router,
+ public pageMode = false,
) {
super();
+ if (this.pageMode) {
+ this.headerComponent = CalculatedFieldsHeaderComponent;
+
+ this.entityComponent = CalculatedFieldComponent;
+ this.entityTabsComponent = CalculatedFieldsTabsComponent;
+ this.rowPointer = true;
+ } else {
+ this.addAsTextButton = false;
+ }
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
- this.detailsPanelEnabled = false;
- this.pageMode = false;
+ this.detailsPanelEnabled = this.pageMode;
this.entityType = 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.addEntity = this.getCalculatedFieldDialog.bind(this);
- this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name});
+ 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.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.onEntityAction = action => this.onCFAction(action);
+
this.addActionDescriptors = [
{
name: this.translate.instant('calculated-fields.create'),
@@ -115,35 +148,37 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('expression', 'calculated-fields.expression', '250px');
- expressionColumn.sortable = false;
- expressionColumn.cellContentFunction = entity => {
- const expressionLabel = this.getExpressionLabel(entity);
- return expressionLabel?.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`;
- }
- expressionColumn.cellTooltipFunction = entity => {
- const expressionLabel = this.getExpressionLabel(entity);
- return expressionLabel?.length < 45 ? null : expressionLabel
- };
-
this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'));
- this.columns.push(new EntityTableColumn('name', 'common.name', '33%',
+ this.columns.push(new EntityTableColumn('name', 'common.name', this.pageMode ? '33%' : '60%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
- this.columns.push(new EntityTableColumn('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' })));
- this.columns.push(expressionColumn);
+ if (this.pageMode) {
+ this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%',
+ entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)));
+ this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%',
+ entity => this.utilsService.customTranslation(entity.entityName, entity.entityName),
+ entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false));
+ }
+ this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '23%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' })));
this.cellActionDescriptors.push(
+ {
+ name: this.translate.instant('action.copy'),
+ icon: 'content_copy',
+ isEnabled: () => true,
+ onAction: ($event, entity) => this.copyCalculatedField($event, entity),
+ },
{
name: this.translate.instant('action.export'),
icon: 'file_download',
isEnabled: () => true,
- onAction: (event$, entity) => this.exportCalculatedField(event$, entity),
+ onAction: ($event, entity) => this.exportCalculatedField($event, entity),
},
{
name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock',
isEnabled: () => true,
- onAction: (_, entity) => this.openDebugEventsDialog(entity),
+ onAction: ($event, entity) =>
+ this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity),
},
{
name: '',
@@ -152,39 +187,31 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
- },
- {
+ }
+ );
+ if (!this.pageMode) {
+ this.cellActionDescriptors.push({
name: this.translate.instant('action.edit'),
icon: 'edit',
isEnabled: () => true,
- onAction: (_, entity) => this.editCalculatedField(entity),
- }
- );
- }
-
- private getExpressionLabel(entity: CalculatedField): string {
- if (entity.type === CalculatedFieldType.SCRIPT ||
- entity.type === CalculatedFieldType.PROPAGATION && entity.configuration.applyExpressionToResolvedArguments === true) {
- return 'function calculate(ctx, ' + Object.keys(entity.configuration.arguments).join(', ') + ')';
- } else if (entity.type === CalculatedFieldType.SIMPLE) {
- return entity.configuration.expression ?? '';
+ onAction: ($event, entity) => this.editCalculatedField($event, entity),
+ })
}
- return '';
}
- fetchCalculatedFields(pageLink: PageLink): Observable> {
- return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink);
+ fetchCalculatedFields(pageLink: PageLink): Observable> {
+ return this.pageMode ?
+ this.calculatedFieldsService.getCalculatedFields(pageLink, this.calculatedFieldFilterConfig):
+ this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink);
}
- onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
+ private onOpenDebugConfig($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
+ $event?.stopPropagation();
const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = {
...this.additionalDebugActionConfig,
- action: () => this.openDebugEventsDialog(calculatedField)
+ action: () => this.openDebugEventsDialog($event, calculatedField)
};
- if ($event) {
- $event.stopPropagation();
- }
const { viewContainerRef, renderer } = this.entityDebugSettingsService;
if (!viewContainerRef || !renderer) {
@@ -202,7 +229,24 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig 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 {
+ $event?.stopPropagation();
this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
.subscribe((res) => {
if (res) {
@@ -211,20 +255,23 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig {
+ private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false, disabledSelectType = false): Observable {
+ const entityId = this.entityId || value?.entityId;
+ const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName;
return this.dialog.open(CalculatedFieldDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle,
- entityId: this.entityId,
+ entityId,
+ entityName,
tenantId: this.tenantId,
- entityName: this.entityName,
- ownerId: this.ownerId,
+ ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty,
+ disabledSelectType
},
enterAnimationDuration: isDirty ? 0 : null,
})
@@ -232,16 +279,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig {
- return (calculatedField.type === CalculatedFieldType.SCRIPT ||
- (calculatedField.type === CalculatedFieldType.PROPAGATION &&
- calculatedField.configuration.applyExpressionToResolvedArguments)
- ) && !!(event as DebugEvent).body.arguments;
- };
+ private openDebugEventsDialog($event: Event, calculatedField: CalculatedFieldsTableEntity, openCalculatedFieldEdit = true, afterCloseCallback?: (expression: string) => void ): void {
+ $event?.stopPropagation();
const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef) => {
- this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments))
+ this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit)
.subscribe(expression => dialogRef.close(expression));
};
@@ -256,26 +298,44 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig {
+ if (afterCloseCallback) {
+ afterCloseCallback(value)
+ }
+ });
}
- private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
- if ($event) {
- $event.stopPropagation();
- }
+ private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void {
+ $event?.stopPropagation();
this.importExportService.exportCalculatedField(calculatedField.id.id);
}
+ private copyCalculatedField($event: Event, calculatedField: CalculatedField): void {
+ $event?.stopPropagation();
+ const copyCalculatedField = deepClone(calculatedField);
+ if (this.pageMode) {
+ copyCalculatedField.entityId = null;
+ delete (copyCalculatedField as CalculatedFieldInfo).entityName;
+ }
+ delete copyCalculatedField.id;
+ this.getCalculatedFieldDialog(copyCalculatedField, 'action.apply', false)
+ .subscribe((res) => {
+ if (res) {
+ this.updateData();
+ }
+ });
+ }
+
private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
filter(Boolean),
switchMap(calculatedField => {
- if (calculatedField.type === CalculatedFieldType.ALARM) {
+ if (calculatedField.type === CalculatedFieldType.ALARM || !CalculatedFieldType[calculatedField.type]) {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('calculated-fields.hint.import-invalid-calculated-field-type'),
type: 'error',
@@ -287,7 +347,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
+ switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true, true)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
filter(Boolean),
@@ -326,7 +386,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData());
}
- private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable {
+ getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable {
if (
calculatedField.type === CalculatedFieldType.SCRIPT ||
calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION ||
@@ -355,7 +415,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig {
if (openCalculatedFieldEdit) {
- this.editCalculatedField({
+ this.editCalculatedField(null, {
entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any
}, true)
@@ -366,4 +426,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig): 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;
+ }
}
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 df433bc70e..102a334402 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,6 +1,6 @@
@if (calculatedFieldsTableConfig) {
-
+
}
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
index 3feb1e7429..175df5b779 100644
--- 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
@@ -1,5 +1,5 @@
/**
- * Copyright © 2016-2025 The Thingsboard Authors
+ * Copyright © 2016-2026 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.
@@ -14,9 +14,7 @@
* limitations under the License.
*/
:host ::ng-deep {
- tb-entities-table {
- .mat-drawer-container {
- background-color: white;
- }
+ .white-background {
+ --mat-sidenav-content-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 ee3d013907..feba407f52 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
@@ -1,5 +1,5 @@
///
-/// Copyright © 2016-2025 The Thingsboard Authors
+/// Copyright © 2016-2026 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.
@@ -36,13 +36,15 @@ import { ImportExportService } from '@shared/import-export/import-export.service
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
+import { ActivatedRoute, Router } from '@angular/router';
@Component({
- selector: 'tb-calculated-fields-table',
- templateUrl: './calculated-fields-table.component.html',
- styleUrls: ['./calculated-fields-table.component.scss'],
- changeDetection: ChangeDetectionStrategy.OnPush,
- providers: [EntityDebugSettingsService]
+ selector: 'tb-calculated-fields-table',
+ templateUrl: './calculated-fields-table.component.html',
+ styleUrls: ['./calculated-fields-table.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [EntityDebugSettingsService],
+ standalone: false
})
export class CalculatedFieldsTableComponent {
@@ -55,6 +57,8 @@ export class CalculatedFieldsTableComponent {
calculatedFieldsTableConfig: CalculatedFieldsTableConfig;
+ pageMode: boolean = false;
+
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
@@ -65,10 +69,13 @@ export class CalculatedFieldsTableComponent {
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
- private destroyRef: DestroyRef) {
-
+ private destroyRef: DestroyRef,
+ private route: ActivatedRoute,
+ private router: Router
+ ) {
+ this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => {
- if (this.active()) {
+ if (this.active() || this.pageMode) {
this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig(
this.calculatedFieldsService,
this.translate,
@@ -83,6 +90,8 @@ export class CalculatedFieldsTableComponent {
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
+ this.router,
+ this.pageMode,
);
this.cd.markForCheck();
}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
index 30533dc714..56f6cb0331 100644
--- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
+++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
@@ -1,6 +1,6 @@