Browse Source

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

Calculated fields page
pull/14632/head
Vladyslav Prykhodko 6 months 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -68,7 +69,7 @@ import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -198,12 +199,12 @@ public class CalculatedFieldController extends BaseController {
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@Parameter(description = "Calculated field type filter.") @Parameter(description = "Calculated field types filter.")
@RequestParam CalculatedFieldType type, @RequestParam(required = false) Set<CalculatedFieldType> types,
@Parameter(description = "Entity type filter. If not specified, calculated fields for all supported entity types will be returned.") @Parameter(description = "Entity type filter. If not specified, calculated fields for all supported entity types will be returned.")
@RequestParam(required = false) EntityType entityType, @RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.") @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") @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) @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) @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@ -215,6 +216,12 @@ public class CalculatedFieldController extends BaseController {
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException { @RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
if (CollectionUtils.isEmpty(types)) {
types = EnumSet.allOf(CalculatedFieldType.class);
types.remove(CalculatedFieldType.ALARM);
}
Set<EntityType> entityTypes; Set<EntityType> entityTypes;
if (entityType == null) { if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.keySet(); entityTypes = CalculatedField.SUPPORTED_ENTITIES.keySet();
@ -223,10 +230,10 @@ public class CalculatedFieldController extends BaseController {
} }
CalculatedFieldFilter filter = CalculatedFieldFilter.builder() CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.type(type) .types(types)
.entityTypes(entityTypes) .entityTypes(entityTypes)
.entityIds(entities) .entityIds(entities)
.names(params.get("name")) .names(Optional.ofNullable(params.get("name")).map(HashSet::new).orElse(null))
.build(); .build();
return calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); return calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink);
} }
@ -361,8 +368,7 @@ public class CalculatedFieldController extends BaseController {
return; return;
} }
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
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, EntityType entityType,
List<UUID> entities, List<UUID> entities,
List<String> names) throws Exception { List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" + return doGetTypedWithPageLink("/api/calculatedFields?" +
(type != null ? "types=" + type + "&" : "") +
(entityType != null ? "entityType=" + entityType + "&" : "") + (entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",", (entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") + 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, List<CalculatedFieldInfo> allCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
null, null, null); null, null, null);
assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
allCalculatedFields = getCalculatedFields(null, null, null, null);
assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
List<CalculatedFieldInfo> profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, List<CalculatedFieldInfo> profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
EntityType.DEVICE_PROFILE, null, null); 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.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -30,12 +29,12 @@ import java.util.UUID;
public class CalculatedFieldFilter { public class CalculatedFieldFilter {
@NonNull @NonNull
private final CalculatedFieldType type; private final Set<CalculatedFieldType> types;
@NonNull @NonNull
private final Set<EntityType> entityTypes; private final Set<EntityType> entityTypes;
@Nullable @Nullable
private final List<UUID> entityIds; private final Set<UUID> entityIds;
@Nullable @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 org.thingsboard.server.dao.model.sql.CalculatedFieldEntity;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
public interface CalculatedFieldRepository extends JpaRepository<CalculatedFieldEntity, 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); Page<CalculatedFieldEntity> findByTenantIdAndEntityIdAndTypes(UUID tenantId, UUID entityId, List<String> types, String textSearch, Pageable pageable);
@Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + @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 cf.entityType IN :entityTypes " +
"AND (:entityIds IS NULL OR cf.entityId IN :entityIds) " + "AND (:entityIds IS NULL OR cf.entityId IN :entityIds) " +
"AND (:names IS NULL OR cf.name IN :names) " + "AND (:names IS NULL OR cf.name IN :names) " +
"AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)")
Page<CalculatedFieldEntity> findByTenantIdAndFilter(UUID tenantId, String type, List<String> entityTypes, Page<CalculatedFieldEntity> findByTenantIdAndFilter(UUID tenantId, List<String> types, List<String> entityTypes,
List<UUID> entityIds, List<String> names, String textSearch, Pageable pageable); Set<UUID> entityIds, Set<String> names, String textSearch, Pageable pageable);
@Query("SELECT DISTINCT cf.name FROM CalculatedFieldEntity cf " + @Query("SELECT DISTINCT cf.name FROM CalculatedFieldEntity cf " +
"WHERE cf.tenantId = :tenantId AND cf.type = :type AND " + "WHERE cf.tenantId = :tenantId AND cf.type = :type AND " +
"(:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") "(: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); 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 @Override
public PageData<CalculatedField> findByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink) { 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(), filter.getEntityTypes().stream().map(Enum::name).toList(),
CollectionUtils.isNotEmpty(filter.getEntityIds()) ? filter.getEntityIds() : null, CollectionUtils.isNotEmpty(filter.getEntityIds()) ? filter.getEntityIds() : null,
CollectionUtils.isNotEmpty(filter.getNames()) ? filter.getNames() : 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', device_profiles = 'device_profiles',
asset_profiles = 'asset_profiles', asset_profiles = 'asset_profiles',
customers = 'customers', customers = 'customers',
calculated_fields = 'calculated_fields',
rule_chains = 'rule_chains', rule_chains = 'rule_chains',
edge_management = 'edge_management', edge_management = 'edge_management',
edges = 'edges', edges = 'edges',
@ -626,6 +627,16 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
icon: 'supervisor_account' 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, MenuId.rule_chains,
{ {
@ -839,6 +850,7 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
] ]
}, },
{id: MenuId.customers}, {id: MenuId.customers},
{id: MenuId.calculated_fields},
{id: MenuId.rule_chains}, {id: MenuId.rule_chains},
{ {
id: MenuId.edge_management, id: MenuId.edge_management,

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

@ -56,7 +56,7 @@
/> />
</div> </div>
@if (!data.entityId) { @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 <tb-entity-type-select #entityTypeSelect
appearance="outline" appearance="outline"
subscriptSizing="dynamic" subscriptSizing="dynamic"
@ -118,7 +118,7 @@
</button> </button>
</div> </div>
<div *ngIf="!configFormGroup.get('clearRule').value"> <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>
<div [class.!hidden]="configFormGroup.get('clearRule').value"> <div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary" <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 { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.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 { ActionNotificationShow } from "@core/notification/notification.actions";
import { import {
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
@ -188,7 +188,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> { fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
return this.pageMode ? 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); 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.importExportService,
this.entityDebugSettingsService, this.entityDebugSettingsService,
this.utilsService, this.utilsService,
this.pageMode this.pageMode,
); );
this.cd.markForCheck(); this.cd.markForCheck();
} }

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

@ -44,12 +44,20 @@ import {
import { import {
EntityAggregationComponentModule EntityAggregationComponentModule
} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module'; } 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({ @NgModule({
declarations: [ declarations: [
CalculatedFieldDialogComponent, CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent, CalculatedFieldTestArgumentsComponent,
CalculatedFieldsHeaderComponent,
CalculatedFieldsFilterConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

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

@ -16,6 +16,7 @@
import { import {
DateEntityTableColumn, DateEntityTableColumn,
EntityLinkTableColumn,
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
@ -39,8 +40,10 @@ import {
ArgumentEntityType, ArgumentEntityType,
ArgumentType, ArgumentType,
CalculatedField, CalculatedField,
CalculatedFieldAlarmRule,
CalculatedFieldEventArguments, CalculatedFieldEventArguments,
CalculatedFieldScriptConfiguration, CalculatedFieldScriptConfiguration,
CalculatedFieldsQuery,
CalculatedFieldType, CalculatedFieldType,
CalculatedFieldTypeTranslations, CalculatedFieldTypeTranslations,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
@ -54,22 +57,27 @@ import {
CalculatedFieldTestScriptDialogData CalculatedFieldTestScriptDialogData
} from './components/public-api'; } from './components/public-api';
import { ImportExportService } from '@shared/import-export/import-export.service'; 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 { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions"; import { ActionNotificationShow } from "@core/notification/notification.actions";
import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models';
import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component';
import {
CalculatedFieldsHeaderComponent
} from '@home/components/calculated-fields/table-header/calculated-fields-header.component';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
readonly tenantId = getCurrentAuthUser(this.store).tenantId; readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = { additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'), 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, constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService, private translate: TranslateService,
private dialog: MatDialog, private dialog: MatDialog,
@ -83,11 +91,20 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private importExportService: ImportExportService, private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService, private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService, private utilsService: UtilsService,
public pageMode = false,
) { ) {
super(); 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.detailsPanelEnabled = false;
this.pageMode = false;
this.entityType = EntityType.CALCULATED_FIELD; this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = entityTypeTranslations.get(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}; 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 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))); 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' }))); if (this.pageMode) {
this.columns.push(expressionColumn); 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( 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'), name: this.translate.instant('action.export'),
icon: 'file_download', icon: 'file_download',
isEnabled: () => true, isEnabled: () => true,
onAction: (event$, entity) => this.exportCalculatedField(event$, entity), onAction: ($event, entity) => this.exportCalculatedField($event, entity),
}, },
{ {
name: this.translate.instant('entity-view.events'), name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock', icon: 'mdi:clipboard-text-clock',
isEnabled: () => true, isEnabled: () => true,
onAction: (_, entity) => this.openDebugEventsDialog(entity), onAction: ($event, entity) => this.openDebugEventsDialog($event, entity),
}, },
{ {
name: '', name: '',
@ -157,34 +173,24 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
name: this.translate.instant('action.edit'), name: this.translate.instant('action.edit'),
icon: 'edit', icon: 'edit',
isEnabled: () => true, 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>> { 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 { onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
$event?.stopPropagation();
const { debugSettings = {}, id } = calculatedField; const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = { const additionalActionConfig = {
...this.additionalDebugActionConfig, ...this.additionalDebugActionConfig,
action: () => this.openDebugEventsDialog(calculatedField) action: () => this.openDebugEventsDialog($event, calculatedField)
}; };
if ($event) {
$event.stopPropagation();
}
const { viewContainerRef, renderer } = this.entityDebugSettingsService; const { viewContainerRef, renderer } = this.entityDebugSettingsService;
if (!viewContainerRef || !renderer) { if (!viewContainerRef || !renderer) {
@ -202,7 +208,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}, $event.target as Element); }, $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) this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
.subscribe((res) => { .subscribe((res) => {
if (res) { if (res) {
@ -212,13 +219,14 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
} }
private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> { 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, { return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
value, value,
buttonTitle, buttonTitle,
entityId: this.entityId, entityId,
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName, entityName: this.entityName,
ownerId: this.ownerId, ownerId: this.ownerId,
@ -232,7 +240,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
.pipe(filter(Boolean)); .pipe(filter(Boolean));
} }
private openDebugEventsDialog(calculatedField: CalculatedField): void { private openDebugEventsDialog($event: Event, calculatedField: CalculatedField): void {
$event?.stopPropagation();
const debugActionEnabledFn = (event: DebugEvent) => { const debugActionEnabledFn = (event: DebugEvent) => {
return (calculatedField.type === CalculatedFieldType.SCRIPT || return (calculatedField.type === CalculatedFieldType.SCRIPT ||
(calculatedField.type === CalculatedFieldType.PROPAGATION && (calculatedField.type === CalculatedFieldType.PROPAGATION &&
@ -264,12 +273,25 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
} }
private exportCalculatedField($event: Event, calculatedField: CalculatedField): void { private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
if ($event) { $event?.stopPropagation();
$event.stopPropagation();
}
this.importExportService.exportCalculatedField(calculatedField.id.id); 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 { private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog() this.importExportService.openCalculatedFieldImportDialog()
.pipe( .pipe(
@ -355,7 +377,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
filter(Boolean), filter(Boolean),
tap(expression => { tap(expression => {
if (openCalculatedFieldEdit) { if (openCalculatedFieldEdit) {
this.editCalculatedField({ this.editCalculatedField(null, {
entityId: this.entityId, ...calculatedField, entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any configuration: {...calculatedField.configuration, expression} as any
}, true) }, true)

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

@ -16,5 +16,5 @@
--> -->
@if (calculatedFieldsTableConfig) { @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. * limitations under the License.
*/ */
:host ::ng-deep { :host ::ng-deep {
tb-entities-table { .white-background {
.mat-drawer-container { --mat-sidenav-content-background-color: white;
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 { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service"; import { UtilsService } from "@core/services/utils.service";
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'tb-calculated-fields-table', selector: 'tb-calculated-fields-table',
@ -55,6 +56,8 @@ export class CalculatedFieldsTableComponent {
calculatedFieldsTableConfig: CalculatedFieldsTableConfig; calculatedFieldsTableConfig: CalculatedFieldsTableConfig;
pageMode: boolean = false;
constructor(private calculatedFieldsService: CalculatedFieldsService, constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService, private translate: TranslateService,
private dialog: MatDialog, private dialog: MatDialog,
@ -65,10 +68,12 @@ export class CalculatedFieldsTableComponent {
private importExportService: ImportExportService, private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService, private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService, private utilsService: UtilsService,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef,
private route: ActivatedRoute,
) {
this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => { effect(() => {
if (this.active()) { if (this.active() || this.pageMode) {
this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig(
this.calculatedFieldsService, this.calculatedFieldsService,
this.translate, this.translate,
@ -83,6 +88,7 @@ export class CalculatedFieldsTableComponent {
this.importExportService, this.importExportService,
this.entityDebugSettingsService, this.entityDebugSettingsService,
this.utilsService, this.utilsService,
this.pageMode,
); );
this.cd.markForCheck(); 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>
<button type="button" <button type="button"
mat-icon-button mat-icon-button
[disabled]="disable"
(click)="onDelete($event, argument)" (click)="onDelete($event, argument)"
[matTooltip]="'action.delete' | translate" [matTooltip]="'action.delete' | translate"
matTooltipPosition="above"> matTooltipPosition="above">
@ -128,7 +129,7 @@
</div> </div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false" <div [class.!hidden]="(dataSource.isEmpty() | async) === false"
[class.required]="!disable" [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 }} {{ 'calculated-fields.no-arguments' | translate }}
</div> </div>
<div class="flex h-9 justify-between"> <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; return this.errorText ? { argumentsFormArray: false } : null;
} }
setDisabledState(isDisabled: boolean): void {
this.disable = isDisabled;
}
onDelete($event: Event, argument: CalculatedFieldArgumentValue): void { onDelete($event: Event, argument: CalculatedFieldArgumentValue): void {
$event.stopPropagation(); $event.stopPropagation();
const index = this.argumentsFormArray.controls.findIndex(control => isEqual(control.value, argument)); 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> <span class="flex-1"></span>
<div tb-help="calculatedField"></div> <div tb-help="calculatedField"></div>
<button mat-icon-button <button mat-icon-button
[disabled]="isLoading"
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -28,8 +29,8 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content class="flex-1"> <div mat-dialog-content class="flex-1">
<div class="tb-form-panel no-border no-padding"> <div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel"> <div class="tb-form-panel no-gap">
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div> <div class="tb-form-panel-title mb-4">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'entity-field.title' | translate }}</mat-label> <mat-label>{{ 'entity-field.title' | translate }}</mat-label>
@ -45,14 +46,40 @@
} }
</mat-error> </mat-error>
} }
<mat-hint></mat-hint>
</mat-form-field> </mat-form-field>
<tb-entity-debug-settings-button <tb-entity-debug-settings-button
formControlName="debugSettings" formControlName="debugSettings"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched" style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD" [entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig" [additionalActionConfig]="additionalDebugActionConfig"
/> />
</div> </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-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'common.type' | translate }}</mat-label> <mat-label>{{ 'common.type' | translate }}</mat-label>
<mat-select formControlName="type"> <mat-select formControlName="type">
@ -68,16 +95,18 @@
@switch (fieldFormGroup.get('type').value) { @switch (fieldFormGroup.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) { @case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration" <tb-geofencing-configuration formControlName="configuration"
[entityId]="data.entityId" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityName]="data.entityName" [entityId]="entityId"
[entityName]="entityName"
[ownerId]="data.ownerId" [ownerId]="data.ownerId"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
></tb-geofencing-configuration> ></tb-geofencing-configuration>
} }
@case (CalculatedFieldType.PROPAGATION) { @case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration" <tb-propagation-configuration formControlName="configuration"
[entityId]="data.entityId" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityName]="data.entityName" [entityId]="entityId"
[entityName]="entityName"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[ownerId]="data.ownerId" [ownerId]="data.ownerId"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
@ -85,23 +114,26 @@
} }
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration" <tb-related-entities-aggregation-component formControlName="configuration"
[entityId]="data.entityId" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityName]="data.entityName" [entityId]="entityId"
[entityName]="entityName"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
></tb-related-entities-aggregation-component> ></tb-related-entities-aggregation-component>
} }
@case (CalculatedFieldType.ENTITY_AGGREGATION) { @case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration" <tb-entity-aggregation-component formControlName="configuration"
[entityId]="data.entityId" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityName]="data.entityName" [entityId]="entityId"
[entityName]="entityName"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
></tb-entity-aggregation-component> ></tb-entity-aggregation-component>
} }
@default { @default {
<tb-simple-configuration formControlName="configuration" <tb-simple-configuration formControlName="configuration"
[entityId]="data.entityId" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityName]="data.entityName" [entityId]="entityId"
[entityName]="entityName"
[ownerId]="data.ownerId" [ownerId]="data.ownerId"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT" [isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT"
@ -115,12 +147,13 @@
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
cdkFocusInitial cdkFocusInitial
[disabled]="isLoading"
(click)="cancel()"> (click)="cancel()">
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
(click)="add()" (click)="add()"
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty"> [disabled]="isLoading || fieldFormGroup.invalid || !fieldFormGroup.dirty">
{{ data.buttonTitle | translate }} {{ data.buttonTitle | translate }}
</button> </button>
</div> </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. /// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -24,13 +24,15 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { import {
CalculatedField, CalculatedField,
CalculatedFieldConfiguration, CalculatedFieldConfiguration,
calculatedFieldsEntityTypeList,
CalculatedFieldTestScriptFn, CalculatedFieldTestScriptFn,
CalculatedFieldType, CalculatedFieldType,
calculatedFieldTypes,
CalculatedFieldTypeTranslations, CalculatedFieldTypeTranslations,
OutputStrategyType OutputStrategyType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; 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 { pairwise, switchMap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
@ -38,6 +40,9 @@ import { Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { deepTrim, isDefined } from '@core/utils'; import { deepTrim, 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 { export interface CalculatedFieldDialogData {
value?: CalculatedField; value?: CalculatedField;
@ -61,6 +66,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
fieldFormGroup = this.fb.group({ fieldFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], 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], type: [CalculatedFieldType.SIMPLE],
debugSettings: [], debugSettings: [],
configuration: this.fb.control<CalculatedFieldConfiguration>({} as CalculatedFieldConfiguration), 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 }), action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
} : null; } : null;
entityName = this.data.entityName;
disabledConfiguration = false;
isLoading = false;
readonly EntityType = EntityType; readonly EntityType = EntityType;
readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList;
readonly CalculatedFieldType = CalculatedFieldType; readonly CalculatedFieldType = CalculatedFieldType;
readonly fieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) as CalculatedFieldType[]; readonly fieldTypes = calculatedFieldTypes;
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
@ -84,9 +102,25 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.observeIsLoading();
this.observeType(); this.observeType();
this.applyDialogData(); 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 { get fromGroupValue(): CalculatedField {
@ -99,9 +133,17 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
add(): void { add(): void {
if (this.fieldFormGroup.valid) { if (this.fieldFormGroup.valid) {
this.isLoading = true;
this.calculatedFieldsService.saveCalculatedField({ entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}) this.calculatedFieldsService.saveCalculatedField({ entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue})
.pipe(takeUntilDestroyed(this.destroyRef)) .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); 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 { 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 (configuration.type !== CalculatedFieldType.ALARM) {
if (isDefined(configuration?.output) && !configuration?.output?.strategy) { if (isDefined(configuration?.output) && !configuration?.output?.strategy) {
configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN}; 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})); setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
} if (!this.data.entityId) {
this.fieldFormGroup.get('configuration').disable({emitEvent: false});
private observeIsLoading(): void { this.disabledConfiguration = true;
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();
}
}
});
} }
private observeType(): void { 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 #button
(click)="manageZone($event, button, geofenceZone)" (click)="manageZone($event, button, geofenceZone)"
[matTooltip]="'action.edit' | translate" [matTooltip]="'action.edit' | translate"
[disabled]="disable"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon [matBadgeHidden]="geofenceZone.refEntityId?.id !== NULL_UUID" <mat-icon [matBadgeHidden]="geofenceZone.refEntityId?.id !== NULL_UUID"
matBadgeColor="warn" matBadgeColor="warn"
@ -112,6 +113,7 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
(click)="onDelete($event, geofenceZone)" (click)="onDelete($event, geofenceZone)"
[disabled]="disable"
[matTooltip]="'action.delete' | translate" [matTooltip]="'action.delete' | translate"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
@ -128,7 +130,7 @@
} }
</div> </div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false" <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 }} {{ 'calculated-fields.no-zone-configured' | translate }}
</div> </div>
<div class="flex h-9 justify-between"> <div class="flex h-9 justify-between">
@ -137,7 +139,7 @@
color="primary" color="primary"
#button #button
(click)="manageZone($event, button)" (click)="manageZone($event, button)"
[disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF"> [disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF || disable">
{{ 'calculated-fields.add-zone-group' | translate }} {{ 'calculated-fields.add-zone-group' | translate }}
</button> </button>
@if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) { @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>(); entityNameMap = new Map<string, string>();
sortOrder = { direction: 'asc', property: '' }; sortOrder = { direction: 'asc', property: '' };
dataSource = new CalculatedFieldZoneDatasource(); dataSource = new CalculatedFieldZoneDatasource();
disable = false;
readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations;
readonly entityTypeTranslations = entityTypeTranslations; readonly entityTypeTranslations = entityTypeTranslations;
@ -135,6 +136,10 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
return this.errorText ? { zonesFormArray: false } : null; return this.errorText ? { zonesFormArray: false } : null;
} }
setDisabledState(isDisabled: boolean): void {
this.disable = isDisabled;
}
onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void { onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void {
$event.stopPropagation(); $event.stopPropagation();
const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); 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 mat-icon-button
#button #button
(click)="manageMetrics($event, button, metric)" (click)="manageMetrics($event, button, metric)"
[disabled]="disable"
[matTooltip]="'action.edit' | translate" [matTooltip]="'action.edit' | translate"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
@ -104,6 +105,7 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
(click)="onDelete($event, metric)" (click)="onDelete($event, metric)"
[disabled]="disable"
[matTooltip]="'action.delete' | translate" [matTooltip]="'action.delete' | translate"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
@ -116,7 +118,7 @@
</table> </table>
</div> </div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false" <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 }} {{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
</div> </div>
<div class="flex h-9 justify-between"> <div class="flex h-9 justify-between">
@ -125,7 +127,7 @@
color="primary" color="primary"
#button #button
(click)="manageMetrics($event, button)" (click)="manageMetrics($event, button)"
[disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF"> [disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF || disable">
{{ 'calculated-fields.metrics.add-metric' | translate }} {{ 'calculated-fields.metrics.add-metric' | translate }}
</button> </button>
@if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) { @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>([]); metricsFormArray = this.fb.array<CalculatedFieldAggMetricValue>([]);
sortOrder = { direction: 'asc' as SortDirection, property: '' }; sortOrder = { direction: 'asc' as SortDirection, property: '' };
dataSource = new CalculatedFieldMetricsDatasource(); dataSource = new CalculatedFieldMetricsDatasource();
disable = false;
displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions']; displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions'];
@ -141,6 +142,10 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
return this.errorText ? { metricsFormArray: false } : null; return this.errorText ? { metricsFormArray: false } : null;
} }
setDisabledState(isDisabled: boolean): void {
this.disable = isDisabled;
}
onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void { onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void {
$event.stopPropagation(); $event.stopPropagation();
const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); 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-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title> <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="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 }} {{ 'calculated-fields.output-strategy.strategy' | translate }}
</div> </div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill" (click)="$event.stopPropagation()"> <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}) @Input({required: true})
entityId: EntityId; entityId: EntityId;
disabled = false;
readonly outputTypes = Object.values(OutputType) as OutputType[]; readonly outputTypes = Object.values(OutputType) as OutputType[];
readonly OutputType = OutputType; readonly OutputType = OutputType;
readonly AttributeScope = AttributeScope; readonly AttributeScope = AttributeScope;
@ -175,6 +177,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
registerOnTouched(_: any): void { } registerOnTouched(_: any): void { }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) { if (isDisabled) {
this.outputForm.disable({emitEvent: false}); this.outputForm.disable({emitEvent: false});
} else { } 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 }}" matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
class="tb-mat-32" class="tb-mat-32"
[disabled]="propagateConfiguration.get('arguments').invalid" [disabled]="propagateConfiguration.get('arguments').invalid || disabled"
(click)="onTestScript()"> (click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> <mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button> </button>
@ -90,7 +90,7 @@
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
type="button" type="button"
(click)="onTestScript()" (click)="onTestScript()"
[disabled]="propagateConfiguration.get('arguments').invalid"> [disabled]="propagateConfiguration.get('arguments').invalid || disabled">
{{ 'calculated-fields.test-script-function' | translate }} {{ 'calculated-fields.test-script-function' | translate }}
</button> </button>
</div> </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), output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput),
}); });
disabled = false;
readonly ScriptLanguage = ScriptLanguage; readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType; readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType; readonly OutputType = OutputType;
@ -142,6 +144,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
registerOnTouched(_: any): void { } registerOnTouched(_: any): void { }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) { if (isDisabled) {
this.propagateConfiguration.disable({emitEvent: false}); this.propagateConfiguration.disable({emitEvent: false});
} else { } 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 }}" matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
class="tb-mat-32" class="tb-mat-32"
[disabled]="simpleConfiguration.get('arguments').invalid" [disabled]="simpleConfiguration.get('arguments').invalid || disabled"
(click)="onTestScript()"> (click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> <mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button> </button>
@ -81,7 +81,7 @@
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
type="button" type="button"
(click)="onTestScript()" (click)="onTestScript()"
[disabled]="simpleConfiguration.get('arguments').invalid"> [disabled]="simpleConfiguration.get('arguments').invalid || disabled">
{{ 'calculated-fields.test-script-function' | translate }} {{ 'calculated-fields.test-script-function' | translate }}
</button> </button>
</div> </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)) map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj))
); );
disabled = false;
private propagateChange: (config: SimpeConfiguration) => void = () => { }; private propagateChange: (config: SimpeConfiguration) => void = () => { };
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder) {
@ -127,7 +129,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
for (const propName of Object.keys(changes)) { for (const propName of Object.keys(changes)) {
const change = changes[propName]; const change = changes[propName];
if (change.currentValue !== change.previousValue) { if (change.currentValue !== change.previousValue) {
if (propName === 'isScript') { if (propName === 'isScript' && !this.disabled) {
this.updatedFormWithScript(); this.updatedFormWithScript();
if (!change.firstChange) { if (!change.firstChange) {
this.simpleConfiguration.updateValueAndValidity(); this.simpleConfiguration.updateValueAndValidity();
@ -163,6 +165,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) { if (isDisabled) {
this.simpleConfiguration.disable({emitEvent: false}); this.simpleConfiguration.disable({emitEvent: false});
} else { } 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. /// limitations under the License.
/// ///
import { Injectable, NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; 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 { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models'; import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models'; import { MenuId } from '@core/services/menu.models';
import { RouterTabsComponent } from "@home/components/router-tabs.component"; import { RouterTabsComponent } from "@home/components/router-tabs.component";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.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 = [ const routes: Routes = [
{ {
path: 'alarms', 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 { DeviceModule } from '@modules/home/pages/device/device.module';
import { AssetModule } from '@modules/home/pages/asset/asset.module'; import { AssetModule } from '@modules/home/pages/asset/asset.module';
import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module'; import { EntityViewModule } from '@modules/home/pages/entity-view/entity-view.module';
import { CalculatedFieldsModule } from '@home/pages/calculated-fields/calculated-fields.module';
import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module'; import { RuleChainModule } from '@modules/home/pages/rulechain/rulechain.module';
import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module'; import { WidgetLibraryModule } from '@modules/home/pages/widget/widget-library.module';
import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module'; import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module';
@ -69,6 +70,7 @@ import { AiModelModule } from '@home/pages/ai-model/ai-model.module';
EdgeModule, EdgeModule,
EntityViewModule, EntityViewModule,
CustomerModule, CustomerModule,
CalculatedFieldsModule,
RuleChainModule, RuleChainModule,
WidgetLibraryModule, WidgetLibraryModule,
DashboardModule, 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 = export type CalculatedFieldConfiguration =
| CalculatedFieldSimpleConfiguration | CalculatedFieldSimpleConfiguration
| CalculatedFieldScriptConfiguration | CalculatedFieldScriptConfiguration
@ -567,6 +569,8 @@ export type CalculatedFieldArgumentEventValue<ValueType = unknown> = CalculatedF
export type CalculatedFieldEventArguments<ValueType = unknown> = Record<string, CalculatedFieldArgumentEventValue<ValueType>>; 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 = { export const defaultCalculatedFieldOutput: CalculatedFieldOutputTimeSeries = {
type: OutputType.Timeseries, type: OutputType.Timeseries,
strategy: { strategy: {
@ -1062,8 +1066,8 @@ export function uniqueNameValidator(existingNames: string[]): ValidatorFn {
} }
export interface CalculatedFieldsQuery { export interface CalculatedFieldsQuery {
type: CalculatedFieldType; types: Array<CalculatedFieldType>;
entityType?: EntityType; entityType?: EntityType;
entities?: Array<string>; 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": "Deduplication interval",
"deduplication-interval-min": "Deduplication interval should be at least {{ sec }} seconds.", "deduplication-interval-min": "Deduplication interval should be at least {{ sec }} seconds.",
"deduplication-interval-required": "Deduplication interval is required.", "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", "metrics": "Metrics",
"metrics-empty": "At least one metric must be configured.", "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 { .mat-expansion-panel {
&.tb-settings { &.tb-settings {
box-shadow: none; 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 { .tb-form-panel-title {
font-weight: 500; font-weight: 500;
font-size: 16px; font-size: 16px;

Loading…
Cancel
Save