Browse Source

Merge pull request #14629 from thingsboard/feature/cfs-page

Calculated fields page
pull/14630/merge
Vladyslav Prykhodko 8 hours ago
committed by GitHub
parent
commit
7c6fbf2ec8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 22
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  2. 3
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  3. 2
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  4. 7
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java
  5. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java
  6. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java
  7. 12
      ui-ngx/src/app/core/services/menu.models.ts
  8. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  9. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  10. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  11. 8
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
  12. 108
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  13. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html
  14. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss
  15. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
  16. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  17. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  18. 61
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  19. 81
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  20. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
  21. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  22. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html
  23. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  25. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  26. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html
  27. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  28. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html
  29. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  30. 96
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html
  31. 56
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss
  32. 281
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts
  33. 20
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html
  34. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss
  35. 43
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts
  36. 15
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  37. 43
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-routing.module.ts
  38. 34
      ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields.module.ts
  39. 2
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  40. 8
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  41. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  42. 10
      ui-ngx/src/form.scss

22
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus;
@ -68,7 +69,7 @@ import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@ -198,12 +199,12 @@ public class CalculatedFieldController extends BaseController {
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Calculated field type filter.")
@RequestParam CalculatedFieldType type,
@Parameter(description = "Calculated field types filter.")
@RequestParam(required = false) Set<CalculatedFieldType> types,
@Parameter(description = "Entity type filter. If not specified, calculated fields for all supported entity types will be returned.")
@RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.")
@RequestParam(required = false) List<UUID> entities,
@RequestParam(required = false) Set<UUID> entities,
@Parameter(description = "Name filter. To specify multiple names, duplicate 'name' parameter for each name, for example '?name=name1&name=name2")
@RequestParam(required = false) String name, // for Swagger only, retrieved from MultiValueMap params (due to issues when name contains comma)
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@ -215,6 +216,12 @@ public class CalculatedFieldController extends BaseController {
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser();
if (CollectionUtils.isEmpty(types)) {
types = EnumSet.allOf(CalculatedFieldType.class);
types.remove(CalculatedFieldType.ALARM);
}
Set<EntityType> entityTypes;
if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.keySet();
@ -223,10 +230,10 @@ public class CalculatedFieldController extends BaseController {
}
CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.type(type)
.types(types)
.entityTypes(entityTypes)
.entityIds(entities)
.names(params.get("name"))
.names(Optional.ofNullable(params.get("name")).map(HashSet::new).orElse(null))
.build();
return calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink);
}
@ -361,8 +368,7 @@ public class CalculatedFieldController extends BaseController {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default ->
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
}
}
}

3
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -1441,7 +1441,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
EntityType entityType,
List<UUID> entities,
List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" +
return doGetTypedWithPageLink("/api/calculatedFields?" +
(type != null ? "types=" + type + "&" : "") +
(entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") +

2
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -259,6 +259,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
List<CalculatedFieldInfo> allCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
null, null, null);
assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
allCalculatedFields = getCalculatedFields(null, null, null, null);
assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
List<CalculatedFieldInfo> profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
EntityType.DEVICE_PROFILE, null, null);

7
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java

@ -21,7 +21,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.server.common.data.EntityType;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@ -30,12 +29,12 @@ import java.util.UUID;
public class CalculatedFieldFilter {
@NonNull
private final CalculatedFieldType type;
private final Set<CalculatedFieldType> types;
@NonNull
private final Set<EntityType> entityTypes;
@Nullable
private final List<UUID> entityIds;
private final Set<UUID> entityIds;
@Nullable
private final List<String> names;
private final Set<String> names;
}

9
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public interface CalculatedFieldRepository extends JpaRepository<CalculatedFieldEntity, UUID> {
@ -43,18 +44,18 @@ public interface CalculatedFieldRepository extends JpaRepository<CalculatedField
Page<CalculatedFieldEntity> findByTenantIdAndEntityIdAndTypes(UUID tenantId, UUID entityId, List<String> types, String textSearch, Pageable pageable);
@Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " +
"AND cf.type = :type " +
"AND cf.type IN :types " +
"AND cf.entityType IN :entityTypes " +
"AND (:entityIds IS NULL OR cf.entityId IN :entityIds) " +
"AND (:names IS NULL OR cf.name IN :names) " +
"AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)")
Page<CalculatedFieldEntity> findByTenantIdAndFilter(UUID tenantId, String type, List<String> entityTypes,
List<UUID> entityIds, List<String> names, String textSearch, Pageable pageable);
Page<CalculatedFieldEntity> findByTenantIdAndFilter(UUID tenantId, List<String> types, List<String> entityTypes,
Set<UUID> entityIds, Set<String> names, String textSearch, Pageable pageable);
@Query("SELECT DISTINCT cf.name FROM CalculatedFieldEntity cf " +
"WHERE cf.tenantId = :tenantId AND cf.type = :type AND " +
"(:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)")
Page<String> findNamesByTenantIdAndType(UUID tenantId, String type,String textSearch, Pageable pageable);
Page<String> findNamesByTenantIdAndType(UUID tenantId, String type, String textSearch, Pageable pageable);
List<CalculatedFieldEntity> findAllByTenantId(UUID tenantId);

3
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java

@ -107,7 +107,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
@Override
public PageData<CalculatedField> findByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink) {
return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndFilter(tenantId.getId(), filter.getType().name(),
return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndFilter(tenantId.getId(),
filter.getTypes().stream().map(Enum::name).toList(),
filter.getEntityTypes().stream().map(Enum::name).toList(),
CollectionUtils.isNotEmpty(filter.getEntityIds()) ? filter.getEntityIds() : null,
CollectionUtils.isNotEmpty(filter.getNames()) ? filter.getNames() : null,

12
ui-ngx/src/app/core/services/menu.models.ts

@ -98,6 +98,7 @@ export enum MenuId {
device_profiles = 'device_profiles',
asset_profiles = 'asset_profiles',
customers = 'customers',
calculated_fields = 'calculated_fields',
rule_chains = 'rule_chains',
edge_management = 'edge_management',
edges = 'edges',
@ -626,6 +627,16 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
icon: 'supervisor_account'
}
],
[
MenuId.calculated_fields,
{
id: MenuId.calculated_fields,
name: 'entity.type-calculated-fields',
type: 'link',
path: '/calculatedFields',
icon: 'mdi:function-variant',
}
],
[
MenuId.rule_chains,
{
@ -839,6 +850,7 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
]
},
{id: MenuId.customers},
{id: MenuId.calculated_fields},
{id: MenuId.rule_chains},
{
id: MenuId.edge_management,

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -56,7 +56,7 @@
/>
</div>
@if (!data.entityId) {
<div class="flex flex-row xs:flex-col gap-4" formGroupName="entityId">
<div class="flex flex-row gap-4 xs:flex-col" formGroupName="entityId">
<tb-entity-type-select #entityTypeSelect
appearance="outline"
subscriptSizing="dynamic"
@ -118,7 +118,7 @@
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate class="tb-prompt text-base flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
<span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span>
</div>
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"

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

@ -60,7 +60,7 @@ import { UtilsService } from "@core/services/utils.service";
import { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component';
import { DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models';
import { DebugEventType, EventType } from '@shared/models/event.models';
import { ActionNotificationShow } from "@core/notification/notification.actions";
import {
CalculatedFieldScriptTestDialogComponent,
@ -188,7 +188,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
return this.pageMode ?
this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, {type: CalculatedFieldType.ALARM, ...this.alarmRuleFilterConfig}) :
this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, {types: [CalculatedFieldType.ALARM], ...this.alarmRuleFilterConfig}) :
this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM);
}

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

@ -88,7 +88,7 @@ export class AlarmRulesTableComponent {
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
this.pageMode
this.pageMode,
);
this.cd.markForCheck();
}

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

@ -44,12 +44,20 @@ 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';
@NgModule({
declarations: [
CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CalculatedFieldsHeaderComponent,
CalculatedFieldsFilterConfigComponent
],
imports: [
CommonModule,

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

@ -16,6 +16,7 @@
import {
DateEntityTableColumn,
EntityLinkTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
@ -39,8 +40,10 @@ import {
ArgumentEntityType,
ArgumentType,
CalculatedField,
CalculatedFieldAlarmRule,
CalculatedFieldEventArguments,
CalculatedFieldScriptConfiguration,
CalculatedFieldsQuery,
CalculatedFieldType,
CalculatedFieldTypeTranslations,
getCalculatedFieldArgumentsEditorCompleter,
@ -54,22 +57,27 @@ 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 { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component';
import {
CalculatedFieldsHeaderComponent
} from '@home/components/calculated-fields/table-header/calculated-fields-header.component';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'),
action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField),
action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, null, calculatedField),
};
calculatedFieldFilterConfig: CalculatedFieldsQuery;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
@ -83,11 +91,20 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
public pageMode = false,
) {
super();
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
if (this.pageMode) {
this.headerComponent = CalculatedFieldsHeaderComponent;
this.handleRowClick = ($event, entity) => {
this.editCalculatedField($event, entity);
this.rowPointer = true;
return true;
};
}
this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields');
this.detailsPanelEnabled = false;
this.pageMode = false;
this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD);
@ -115,35 +132,34 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
const expressionColumn = new EntityTableColumn<CalculatedField>('expression', 'calculated-fields.expression', '250px');
expressionColumn.sortable = false;
expressionColumn.cellContentFunction = entity => {
const expressionLabel = this.getExpressionLabel(entity);
return expressionLabel?.length < 45 ? expressionLabel : `<span style="display: inline-block; width: 45ch">${expressionLabel.substring(0, 44)}…</span>`;
}
expressionColumn.cellTooltipFunction = entity => {
const expressionLabel = this.getExpressionLabel(entity);
return expressionLabel?.length < 45 ? null : expressionLabel
};
this.columns.push(new DateEntityTableColumn<CalculatedField>('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%',
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', this.pageMode ? '33%' : '60%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
this.columns.push(new EntityTableColumn<CalculatedField>('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 EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'calculated-fields.target-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<CalculatedField>('type', 'common.type', this.pageMode ? '33%' : '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.openDebugEventsDialog($event, entity),
},
{
name: '',
@ -157,34 +173,24 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
name: this.translate.instant('action.edit'),
icon: 'edit',
isEnabled: () => true,
onAction: (_, entity) => this.editCalculatedField(entity),
onAction: ($event, entity) => this.editCalculatedField($event, 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 ?? '';
}
return '';
}
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink);
return this.pageMode ?
this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, this.calculatedFieldFilterConfig):
this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink);
}
onOpenDebugConfig($event: Event, calculatedField: CalculatedField): 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 +208,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}, $event.target as Element);
}
private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
private editCalculatedField($event: Event, calculatedField: CalculatedField, isDirty = false): void {
$event?.stopPropagation();
this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
.subscribe((res) => {
if (res) {
@ -212,13 +219,14 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}
private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> {
const entityId = this.entityId || value?.entityId;
return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle,
entityId: this.entityId,
entityId,
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
@ -232,7 +240,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
.pipe(filter(Boolean));
}
private openDebugEventsDialog(calculatedField: CalculatedField): void {
private openDebugEventsDialog($event: Event, calculatedField: CalculatedField): void {
$event?.stopPropagation();
const debugActionEnabledFn = (event: DebugEvent) => {
return (calculatedField.type === CalculatedFieldType.SCRIPT ||
(calculatedField.type === CalculatedFieldType.PROPAGATION &&
@ -264,12 +273,25 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}
private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
if ($event) {
$event.stopPropagation();
}
$event?.stopPropagation();
this.importExportService.exportCalculatedField(calculatedField.id.id);
}
private copyCalculatedField($event: Event, calculatedField: CalculatedField): void {
$event?.stopPropagation();
const copyCalculatedAlarmRule = deepClone(calculatedField);
if (this.pageMode) {
copyCalculatedAlarmRule.entityId = null;
}
delete copyCalculatedAlarmRule.id;
this.getCalculatedFieldDialog(copyCalculatedAlarmRule, 'action.apply', false)
.subscribe((res) => {
if (res) {
this.updateData();
}
});
}
private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
@ -355,7 +377,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField({
this.editCalculatedField(null, {
entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any
}, true)

2
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html

@ -16,5 +16,5 @@
-->
@if (calculatedFieldsTableConfig) {
<tb-entities-table [entitiesTableConfig]="calculatedFieldsTableConfig"></tb-entities-table>
<tb-entities-table [class.white-background]="!pageMode" [entitiesTableConfig]="calculatedFieldsTableConfig"></tb-entities-table>
}

6
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss

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

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

@ -36,6 +36,7 @@ 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 } from '@angular/router';
@Component({
selector: 'tb-calculated-fields-table',
@ -55,6 +56,8 @@ export class CalculatedFieldsTableComponent {
calculatedFieldsTableConfig: CalculatedFieldsTableConfig;
pageMode: boolean = false;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
@ -65,10 +68,12 @@ export class CalculatedFieldsTableComponent {
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
private destroyRef: DestroyRef) {
private destroyRef: DestroyRef,
private route: ActivatedRoute,
) {
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 +88,7 @@ export class CalculatedFieldsTableComponent {
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
this.pageMode,
);
this.cd.markForCheck();
}

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

@ -111,6 +111,7 @@
</button>
<button type="button"
mat-icon-button
[disabled]="disable"
(click)="onDelete($event, argument)"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above">
@ -128,7 +129,7 @@
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
[class.required]="!disable"
class="tb-prompt flex flex-1 items-end justify-center text-base required">
class="tb-prompt flex flex-1 items-end justify-center text-base">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
<div class="flex h-9 justify-between">

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

@ -157,6 +157,10 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
return this.errorText ? { argumentsFormArray: false } : null;
}
setDisabledState(isDisabled: boolean): void {
this.disable = isDisabled;
}
onDelete($event: Event, argument: CalculatedFieldArgumentValue): void {
$event.stopPropagation();
const index = this.argumentsFormArray.controls.findIndex(control => isEqual(control.value, argument));

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

@ -21,6 +21,7 @@
<span class="flex-1"></span>
<div tb-help="calculatedField"></div>
<button mat-icon-button
[disabled]="isLoading"
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
@ -28,8 +29,8 @@
</mat-toolbar>
<div mat-dialog-content class="flex-1">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div>
<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>
@ -45,14 +46,40 @@
}
</mat-error>
}
<mat-hint></mat-hint>
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
@if (!data.entityId) {
<div class="flex flex-row gap-4 xs:flex-col" formGroupName="entityId">
<tb-entity-type-select #entityTypeSelect
appearance="outline"
class="flex-1"
showLabel
required
label="{{ 'alarm-rule.target-entity-type' | translate }}"
[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-label>{{ 'common.type' | translate }}</mat-label>
<mat-select formControlName="type">
@ -68,16 +95,18 @@
@switch (fieldFormGroup.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="data.ownerId"
[tenantId]="data.tenantId"
></tb-geofencing-configuration>
}
@case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="data.tenantId"
[ownerId]="data.ownerId"
[testScript]="onTestScript.bind(this)"
@ -85,23 +114,26 @@
}
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="data.tenantId"
[testScript]="onTestScript.bind(this)"
></tb-related-entities-aggregation-component>
}
@case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="data.tenantId"
></tb-entity-aggregation-component>
}
@default {
<tb-simple-configuration formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="data.ownerId"
[tenantId]="data.tenantId"
[isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT"
@ -115,12 +147,13 @@
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="isLoading"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty">
[disabled]="isLoading || fieldFormGroup.invalid || !fieldFormGroup.dirty">
{{ data.buttonTitle | translate }}
</button>
</div>

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -24,13 +24,15 @@ import { DialogComponent } from '@shared/components/dialog.component';
import {
CalculatedField,
CalculatedFieldConfiguration,
calculatedFieldsEntityTypeList,
CalculatedFieldTestScriptFn,
CalculatedFieldType,
calculatedFieldTypes,
CalculatedFieldTypeTranslations,
OutputStrategyType
} 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 { CalculatedFieldsService } from '@core/http/calculated-fields.service';
@ -38,6 +40,9 @@ import { Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { deepTrim, isDefined } 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';
export interface CalculatedFieldDialogData {
value?: CalculatedField;
@ -61,6 +66,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
fieldFormGroup = this.fb.group({
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),
@ -71,11 +80,20 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
} : null;
entityName = this.data.entityName;
disabledConfiguration = false;
isLoading = false;
readonly EntityType = EntityType;
readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList;
readonly CalculatedFieldType = CalculatedFieldType;
readonly fieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) as CalculatedFieldType[];
readonly fieldTypes = calculatedFieldTypes;
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
@ -84,9 +102,25 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private destroyRef: DestroyRef,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.observeIsLoading();
this.observeType();
this.applyDialogData();
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}
if (!this.data.entityId) {
this.fieldFormGroup.get('entityId.id').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((entityId) => {
this.disabledConfiguration = !entityId;
if (this.disabledConfiguration) {
this.fieldFormGroup.get('configuration').disable({emitEvent: false});
} else {
this.fieldFormGroup.get('configuration').enable({emitEvent: false});
}
});
}
}
get fromGroupValue(): CalculatedField {
@ -99,9 +133,17 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
add(): void {
if (this.fieldFormGroup.valid) {
this.isLoading = true;
this.calculatedFieldsService.saveCalculatedField({ entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(calculatedField => this.dialogRef.close(calculatedField));
.subscribe({
next: calculatedField => this.dialogRef.close(calculatedField),
error: () => this.isLoading = false
});
} else {
this.fieldFormGroup.get('name').markAsTouched();
this.entityTypeSelect?.markAsTouched();
this.entityAutocompleteComponent?.markAsTouched();
}
}
@ -120,28 +162,27 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression);
}
changeEntity(entity: BaseData<EntityId>): void {
this.entityName = entity.name;
}
get entityId(): EntityId {
return this.data.entityId || this.fieldFormGroup.get('entityId').value;
}
private applyDialogData(): void {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...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) {
if (isDefined(configuration?.output) && !configuration?.output?.strategy) {
configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN};
}
}
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false});
setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
}
private observeIsLoading(): void {
this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => {
if (loading) {
this.fieldFormGroup.disable({emitEvent: false});
} else {
this.fieldFormGroup.enable({emitEvent: false});
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}
}
});
if (!this.data.entityId) {
this.fieldFormGroup.get('configuration').disable({emitEvent: false});
this.disabledConfiguration = true;
}
}
private observeType(): void {

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

@ -101,6 +101,7 @@
#button
(click)="manageZone($event, button, geofenceZone)"
[matTooltip]="'action.edit' | translate"
[disabled]="disable"
matTooltipPosition="above">
<mat-icon [matBadgeHidden]="geofenceZone.refEntityId?.id !== NULL_UUID"
matBadgeColor="warn"
@ -112,6 +113,7 @@
<button type="button"
mat-icon-button
(click)="onDelete($event, geofenceZone)"
[disabled]="disable"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
@ -128,7 +130,7 @@
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center text-base required">
class="tb-prompt flex flex-1 items-end justify-center text-base" [class.required]="!disable">
{{ 'calculated-fields.no-zone-configured' | translate }}
</div>
<div class="flex h-9 justify-between">
@ -137,7 +139,7 @@
color="primary"
#button
(click)="manageZone($event, button)"
[disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF">
[disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF || disable">
{{ 'calculated-fields.add-zone-group' | translate }}
</button>
@if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) {

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

@ -90,6 +90,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
entityNameMap = new Map<string, string>();
sortOrder = { direction: 'asc', property: '' };
dataSource = new CalculatedFieldZoneDatasource();
disable = false;
readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations;
readonly entityTypeTranslations = entityTypeTranslations;
@ -135,6 +136,10 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
return this.errorText ? { zonesFormArray: false } : null;
}
setDisabledState(isDisabled: boolean): void {
this.disable = isDisabled;
}
onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void {
$event.stopPropagation();
const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone));

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

@ -97,6 +97,7 @@
mat-icon-button
#button
(click)="manageMetrics($event, button, metric)"
[disabled]="disable"
[matTooltip]="'action.edit' | translate"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
@ -104,6 +105,7 @@
<button type="button"
mat-icon-button
(click)="onDelete($event, metric)"
[disabled]="disable"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
@ -116,7 +118,7 @@
</table>
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center text-base required">
class="tb-prompt flex flex-1 items-end justify-center text-base" [class.required]="!disable">
{{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
</div>
<div class="flex h-9 justify-between">
@ -125,7 +127,7 @@
color="primary"
#button
(click)="manageMetrics($event, button)"
[disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF">
[disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF || disable">
{{ 'calculated-fields.metrics.add-metric' | translate }}
</button>
@if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) {

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

@ -89,6 +89,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
metricsFormArray = this.fb.array<CalculatedFieldAggMetricValue>([]);
sortOrder = { direction: 'asc' as SortDirection, property: '' };
dataSource = new CalculatedFieldMetricsDatasource();
disable = false;
displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions'];
@ -141,6 +142,10 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
return this.errorText ? { metricsFormArray: false } : null;
}
setDisabledState(isDisabled: boolean): void {
this.disable = isDisabled;
}
onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void {
$event.stopPropagation();
const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric));

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

@ -78,7 +78,7 @@
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<div class="tb-form-panel-title" style="color: var(--mat-expansion-header-text-color)" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
<div class="tb-form-panel-title" [style.color]="disabled ? 'var(--mdc-outlined-text-field-disabled-input-text-color)' : 'var(--mat-expansion-header-text-color)'" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
{{ 'calculated-fields.output-strategy.strategy' | translate }}
</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill" (click)="$event.stopPropagation()">

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

@ -78,6 +78,8 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
@Input({required: true})
entityId: EntityId;
disabled = false;
readonly outputTypes = Object.values(OutputType) as OutputType[];
readonly OutputType = OutputType;
readonly AttributeScope = AttributeScope;
@ -175,6 +177,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
registerOnTouched(_: any): void { }
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.outputForm.disable({emitEvent: false});
} else {

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html

@ -81,7 +81,7 @@
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="propagateConfiguration.get('arguments').invalid"
[disabled]="propagateConfiguration.get('arguments').invalid || disabled"
(click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
@ -90,7 +90,7 @@
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="propagateConfiguration.get('arguments').invalid">
[disabled]="propagateConfiguration.get('arguments').invalid || disabled">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>

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

@ -88,6 +88,8 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput),
});
disabled = false;
readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType;
@ -142,6 +144,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
registerOnTouched(_: any): void { }
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.propagateConfiguration.disable({emitEvent: false});
} else {

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html

@ -72,7 +72,7 @@
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="simpleConfiguration.get('arguments').invalid"
[disabled]="simpleConfiguration.get('arguments').invalid || disabled"
(click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
@ -81,7 +81,7 @@
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="simpleConfiguration.get('arguments').invalid">
[disabled]="simpleConfiguration.get('arguments').invalid || disabled">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>

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

@ -104,6 +104,8 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj))
);
disabled = false;
private propagateChange: (config: SimpeConfiguration) => void = () => { };
constructor(private fb: FormBuilder) {
@ -127,7 +129,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue) {
if (propName === 'isScript') {
if (propName === 'isScript' && !this.disabled) {
this.updatedFormWithScript();
if (!change.firstChange) {
this.simpleConfiguration.updateValueAndValidity();
@ -163,6 +165,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.simpleConfiguration.disable({emitEvent: false});
} else {

96
ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html

@ -0,0 +1,96 @@
<!--
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.
-->
<ng-container *ngIf="panelMode; else componentMode">
<ng-container *ngTemplateOutlet="calculatedFieldsFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else calculatedFieldsFilter">
<button color="primary"
matTooltip="{{ 'calculated-fields.calculated-fields-filter' | translate }}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleCfFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{ 'calculated-fields.calculated-fields-filter' | translate }}
</button>
</ng-container>
</ng-template>
<ng-template #calculatedFieldsFilterPanel>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="calculatedFieldsFilter"></ng-container>
<div class="tb-panel-actions flex flex-row items-center justify-end">
<button type="button"
mat-button
color="primary"
(click)="reset()">
{{ 'action.reset' | translate }}
</button>
<span class="flex-1"></span>
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="cfFilterForm.invalid || !cfFilterForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #calculatedFieldsFilter>
<div class="tb-form-panel tb-calculated-fields-config-component no-padding no-border" [formGroup]="cfFilterForm">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>calculated-fields.calculated-field-types</div>
<tb-string-items-list subscriptSizing="dynamic"
formControlName="types"
appearance="outline"
fieldClass="flex"
class="flex-1"
placeholder="{{ !cfFilterForm.get('types').value?.length ? ('calculated-fields.any-type' | translate) : '' }}"
[predefinedValues]="types">
</tb-string-items-list>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.target-entity-type</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
<mat-option *ngFor="let type of listEntityTypes" [value]="type">
{{ entityTypeTranslations.get(type)?.type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
@if (cfFilterForm.get('entityType').value) {
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.target-entities</div>
<tb-entity-list appearance="outline"
subscriptSizing="dynamic"
class="flex flex-1"
inlineField
syncIdsWithDB
labelText="{{'entity.entity-list' | translate}}"
[entityType]="cfFilterForm.get('entityType').value"
formControlName="entities">
</tb-entity-list>
</div>
}
</div>
</ng-template>

56
ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss

@ -0,0 +1,56 @@
/**
* 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 '../scss/constants';
:host {
display: flex;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
}
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
::ng-deep {
.tb-calculated-fields-config-component {
max-width: 100%;
width: 600px;
min-width: 100%;
flex: 1;
tb-entity-subtype-list {
flex: 1;
@media #{$mat-gt-xs} {
width: 180px;
}
.mdc-evolution-chip-set__chips {
width: 100%;
}
}
}
}

281
ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts

@ -0,0 +1,281 @@
///
/// 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,
DestroyRef,
ElementRef,
forwardRef,
Inject,
InjectionToken,
Input,
OnInit,
Optional,
TemplateRef,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { deepClone, isArraysEqualIgnoreUndefined, isDefinedAndNotNull, isEmpty, isUndefinedOrNull } from '@core/utils';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { fromEvent, Subscription } from 'rxjs';
import { POSITION_MAP } from '@shared/models/overlay.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
calculatedFieldsEntityTypeList,
CalculatedFieldsQuery,
calculatedFieldTypes,
CalculatedFieldTypeTranslations
} from '@shared/models/calculated-field.models';
import { StringItemsOption } from '@shared/components/string-items-list.component';
import { TranslateService } from '@ngx-translate/core';
export const CALCULATED_FIELDS_CONFIG_DATA = new InjectionToken<any>('CalculatedFieldsFilterConfigData');
export interface CalculatedFieldsFilterConfigData {
panelMode: boolean;
userMode: boolean;
filterConfig: CalculatedFieldsQuery;
initialFilterConfig?: CalculatedFieldsQuery;
}
@Component({
selector: 'tb-calculated-fields-filter-config',
templateUrl: './calculated-fields-filter-config.component.html',
styleUrls: ['./calculated-fields-filter-config.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CalculatedFieldsFilterConfigComponent),
multi: true
}
]
})
export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlValueAccessor {
@ViewChild('calculatedFieldsFilterPanel')
calculatedFieldsFilterPanel: TemplateRef<any>;
@Input()
disabled: boolean;
@coerceBoolean()
@Input()
buttonMode = true;
@coerceBoolean()
@Input()
userMode = false;
@coerceBoolean()
@Input()
propagatedFilter = true;
@Input()
initialCfFilterConfig: CalculatedFieldsQuery = {
types: [],
entityType: null,
entities: []
};
panelMode = false;
cfFilterForm: FormGroup;
panelResult: CalculatedFieldsQuery = null;
entityType = EntityType;
listEntityTypes = calculatedFieldsEntityTypeList;
entityTypeTranslations = entityTypeTranslations;
readonly types: StringItemsOption[] = calculatedFieldTypes.map(item => ({
name: this.translate.instant(CalculatedFieldTypeTranslations.get(item).name),
value: item
}));
private cfFilterOverlayRef: OverlayRef;
private cfFilterConfig: CalculatedFieldsQuery;
private resizeWindows: Subscription;
private propagateChange = (_: any) => {};
constructor(@Optional() @Inject(CALCULATED_FIELDS_CONFIG_DATA)
private data: CalculatedFieldsFilterConfigData | undefined,
@Optional() private overlayRef: OverlayRef,
private fb: FormBuilder,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef,
private destroyRef: DestroyRef,
private translate: TranslateService) {
}
ngOnInit(): void {
if (this.data) {
this.panelMode = this.data.panelMode;
this.userMode = this.data.userMode;
this.cfFilterConfig = this.data.filterConfig;
this.initialCfFilterConfig = this.data.initialFilterConfig;
if (this.panelMode && !this.initialCfFilterConfig) {
this.initialCfFilterConfig = deepClone(this.cfFilterConfig);
}
}
this.cfFilterForm = this.fb.group({
types: [null, []],
entityType: [null, []],
entities: [null, []]
});
this.cfFilterForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
if (!this.buttonMode) {
this.cfConfigUpdated(this.cfFilterForm.value);
}
}
);
if (this.panelMode) {
this.updateCfConfigForm(this.cfFilterConfig);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.cfFilterForm.disable({emitEvent: false});
} else {
this.cfFilterForm.enable({emitEvent: false});
}
}
writeValue(cfFilterConfig?: CalculatedFieldsQuery): void {
this.cfFilterConfig = cfFilterConfig;
if (!this.initialCfFilterConfig && cfFilterConfig) {
this.initialCfFilterConfig = deepClone(cfFilterConfig);
}
this.updateCfConfigForm(cfFilterConfig);
}
toggleCfFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',
height: 'min-content',
minWidth: ''
});
config.hasBackdrop = true;
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(this.nativeElement)
.withPositions([POSITION_MAP.bottomLeft]);
this.cfFilterOverlayRef = this.overlay.create(config);
this.cfFilterOverlayRef.backdropClick().subscribe(() => {
this.cfFilterOverlayRef.dispose();
});
this.cfFilterOverlayRef.attach(new TemplatePortal(this.calculatedFieldsFilterPanel,
this.viewContainerRef));
this.resizeWindows = fromEvent(window, 'resize').subscribe(() => {
this.cfFilterOverlayRef.updatePosition();
});
}
cancel() {
this.updateCfConfigForm(this.cfFilterConfig);
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.resizeWindows.unsubscribe();
this.cfFilterOverlayRef.dispose();
}
}
update() {
this.cfConfigUpdated(this.cfFilterForm.value);
this.cfFilterForm.markAsPristine();
if (this.panelMode) {
this.panelResult = this.cfFilterConfig;
}
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.resizeWindows.unsubscribe();
this.cfFilterOverlayRef.dispose();
}
}
reset() {
const cfFilterConfig = this.cfFilterFromFormValue(this.cfFilterForm.value);
if (!this.cfFilterConfigEquals(cfFilterConfig, this.initialCfFilterConfig)) {
this.updateCfConfigForm(this.initialCfFilterConfig);
this.cfFilterForm.markAsDirty();
}
}
private cfFilterConfigEquals = (filter1?: CalculatedFieldsQuery, filter2?: CalculatedFieldsQuery): boolean => {
if (filter1 === filter2) {
return true;
}
if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) {
return true;
} else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) {
if (!isArraysEqualIgnoreUndefined(filter1.types, filter2.types)) {
return false;
}
if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) {
return false;
}
return filter1.entityType !== filter2.entityType;
}
return false;
};
private updateCfConfigForm(cfFilterConfig?: CalculatedFieldsQuery) {
this.cfFilterForm.patchValue({
types: cfFilterConfig?.types ?? [],
entityType: cfFilterConfig?.entityType ?? null,
entities: cfFilterConfig?.entities ?? [],
}, {emitEvent: false});
}
private cfConfigUpdated(formValue: any) {
this.cfFilterConfig = this.cfFilterFromFormValue(formValue);
this.propagateChange(this.cfFilterConfig);
}
private cfFilterFromFormValue(formValue: any): CalculatedFieldsQuery {
return {
types: formValue?.types ?? [],
entityType: formValue?.entityType ?? null,
entities: formValue?.entities ?? [],
};
}
}

20
ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html

@ -0,0 +1,20 @@
<!--
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.
-->
<tb-calculated-fields-filter-config [ngModel]="calculatedFieldsTableConfig.calculatedFieldFilterConfig"
(ngModelChange)="calculatedFieldsFilterChanged($event)">
</tb-calculated-fields-filter-config>

21
ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss

@ -0,0 +1,21 @@
/**
* 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 {
padding-right: 8px;
overflow: hidden;
max-width: 100%;
}

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

@ -0,0 +1,43 @@
///
/// 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { CalculatedField, CalculatedFieldsQuery } from "@shared/models/calculated-field.models";
import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config';
@Component({
selector: 'tb-calculated-fields-table-header',
templateUrl: './calculated-fields-header.component.html',
styleUrls: ['./calculated-fields-header.component.scss']
})
export class CalculatedFieldsHeaderComponent extends EntityTableHeaderComponent<CalculatedField> {
get calculatedFieldsTableConfig(): CalculatedFieldsTableConfig {
return this.entitiesTableConfig as CalculatedFieldsTableConfig;
}
constructor(protected store: Store<AppState>) {
super(store);
}
calculatedFieldsFilterChanged(calculatedFieldFilterConfig: CalculatedFieldsQuery) {
this.calculatedFieldsTableConfig.calculatedFieldFilterConfig = calculatedFieldFilterConfig;
this.calculatedFieldsTableConfig.getTable().resetSortAndFilter(true, true);
}
}

15
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -14,28 +14,15 @@
/// limitations under the License.
///
import { Injectable, NgModule } from '@angular/core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { Observable } from 'rxjs';
import { OAuth2Service } from '@core/http/oauth2.service';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models';
import { RouterTabsComponent } from "@home/components/router-tabs.component";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component";
@Injectable()
export class OAuth2LoginProcessingUrlResolver {
constructor(private oauth2Service: OAuth2Service) {
}
resolve(): Observable<string> {
return this.oauth2Service.getLoginProcessingUrl();
}
}
const routes: Routes = [
{
path: 'alarms',

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

@ -0,0 +1,43 @@
///
/// 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 { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { MenuId } from '@core/services/menu.models';
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
const routes: Routes = [
{
path: 'calculatedFields',
component: CalculatedFieldsTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'entity.type-calculated-fields',
breadcrumb: {
menuId: MenuId.calculated_fields
},
isPage: true,
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class CalculatedFieldsRoutingModule { }

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

@ -0,0 +1,34 @@
///
/// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { HomeDialogsModule } from '../../dialogs/home-dialogs.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { CalculatedFieldsRoutingModule } from '@home/pages/calculated-fields/calculated-fields-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
HomeDialogsModule,
CalculatedFieldsRoutingModule
]
})
export class CalculatedFieldsModule { }

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

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

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

@ -119,6 +119,8 @@ export const CalculatedFieldTypeTranslations = new Map<CalculatedFieldType, Calc
]
)
export const calculatedFieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM)
export type CalculatedFieldConfiguration =
| CalculatedFieldSimpleConfiguration
| CalculatedFieldScriptConfiguration
@ -567,6 +569,8 @@ export type CalculatedFieldArgumentEventValue<ValueType = unknown> = CalculatedF
export type CalculatedFieldEventArguments<ValueType = unknown> = Record<string, CalculatedFieldArgumentEventValue<ValueType>>;
export const calculatedFieldsEntityTypeList = [EntityType.DEVICE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE];
export const defaultCalculatedFieldOutput: CalculatedFieldOutputTimeSeries = {
type: OutputType.Timeseries,
strategy: {
@ -1062,8 +1066,8 @@ export function uniqueNameValidator(existingNames: string[]): ValidatorFn {
}
export interface CalculatedFieldsQuery {
type: CalculatedFieldType;
types: Array<CalculatedFieldType>;
entityType?: EntityType;
entities?: Array<string>;
name?: string;
name?: Array<string>;
}

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

@ -1219,6 +1219,10 @@
"deduplication-interval": "Deduplication interval",
"deduplication-interval-min": "Deduplication interval should be at least {{ sec }} seconds.",
"deduplication-interval-required": "Deduplication interval is required.",
"calculated-fields-filter": "Calculated fields filter",
"calculated-field-types": "Calculated field types",
"any-type": "Any type",
"target-entity": "Target entity",
"metrics": {
"metrics": "Metrics",
"metrics-empty": "At least one metric must be configured.",

10
ui-ngx/src/form.scss

@ -107,6 +107,9 @@
}
}
}
&.disabled {
color: var(--mdc-outlined-text-field-disabled-input-text-color);
}
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
@ -160,6 +163,13 @@
}
}
.tb-form-panel-disabled {
color: var(--mdc-outlined-text-field-disabled-input-text-color);
.tb-form-panel {
color: var(--mdc-outlined-text-field-disabled-input-text-color);
}
}
.tb-form-panel-title {
font-weight: 500;
font-size: 16px;

Loading…
Cancel
Save