diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 9a24ab91fc..1c6b23084e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -44,6 +45,8 @@ import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; @@ -59,14 +62,18 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import java.util.ArrayList; import java.util.Collections; +import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; @@ -99,30 +106,30 @@ public class CalculatedFieldController extends BaseController { private static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" - + MARKDOWN_CODE_BLOCK_START - + "{\n" + - " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + - " \"arguments\": {\n" + - " \"temperature\": {\n" + - " \"type\": \"TS_ROLLING\",\n" + - " \"timeWindow\": {\n" + - " \"startTs\": 1739775630002,\n" + - " \"endTs\": 65432211,\n" + - " \"limit\": 5\n" + - " },\n" + - " \"values\": [\n" + - " { \"ts\": 1739775639851, \"value\": 23 },\n" + - " { \"ts\": 1739775664561, \"value\": 43 },\n" + - " { \"ts\": 1739775713079, \"value\": 15 },\n" + - " { \"ts\": 1739775999522, \"value\": 34 },\n" + - " { \"ts\": 1739776228452, \"value\": 22 }\n" + - " ]\n" + - " },\n" + - " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + - " }\n" + - "}" - + MARKDOWN_CODE_BLOCK_END - + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + + " \"arguments\": {\n" + + " \"temperature\": {\n" + + " \"type\": \"TS_ROLLING\",\n" + + " \"timeWindow\": {\n" + + " \"startTs\": 1739775630002,\n" + + " \"endTs\": 65432211,\n" + + " \"limit\": 5\n" + + " },\n" + + " \"values\": [\n" + + " { \"ts\": 1739775639851, \"value\": 23 },\n" + + " { \"ts\": 1739775664561, \"value\": 43 },\n" + + " { \"ts\": 1739775713079, \"value\": 15 },\n" + + " { \"ts\": 1739775999522, \"value\": 34 },\n" + + " { \"ts\": 1739776228452, \"value\": 22 }\n" + + " ]\n" + + " },\n" + + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; @ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)", notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK + @@ -183,6 +190,65 @@ public class CalculatedFieldController extends BaseController { return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink)); } + @ApiOperation(value = "Get calculated fields (getCalculatedFields)", + notes = "Fetch tenant calculated fields based on the filter.") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/calculatedFields") + public PageData getCalculatedFields(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Calculated field type filter.") + @RequestParam CalculatedFieldType type, + @Parameter(description = "Entity type filter. If not specified, calculated fields for all supported entity types will be returned.") + @RequestParam(required = false) EntityType entityType, + @Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.") + @RequestParam(required = false) List entities, + @Parameter(description = "Name filter. To specify multiple names, duplicate 'name' parameter for each name, for example '?name=name1&name=name2") + @RequestParam(required = false) String name, // for Swagger only, retrieved from MultiValueMap params (due to issues when name contains comma) + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder, + @RequestParam MultiValueMap params) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + SecurityUser user = getCurrentUser(); + Set entityTypes; + if (entityType == null) { + entityTypes = CalculatedField.SUPPORTED_ENTITIES.keySet(); + } else { + entityTypes = EnumSet.of(entityType); + } + + CalculatedFieldFilter filter = CalculatedFieldFilter.builder() + .type(type) + .entityTypes(entityTypes) + .entityIds(entities) + .names(params.get("name")) + .build(); + return calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); + } + + @ApiOperation(value = "Get calculated field names (getCalculatedFieldNames)", + notes = "Fetch the list of calculated field names for specified type.") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @GetMapping(value = "/calculatedFields/names") + public PageData getCalculatedFieldNames(@Parameter(description = "Calculated field type filter.") + @RequestParam CalculatedFieldType type, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder); + return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), type, pageLink); + } + @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", notes = "Deletes the calculated field. Referencing non-existing Calculated Field Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -295,7 +361,8 @@ public class CalculatedFieldController extends BaseController { return; } case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); + default -> + throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); } } } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 8cc9d477d2..7bc6f9876c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1421,7 +1421,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/calculatedField", calculatedField, CalculatedField.class); } - protected PageData getCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { + protected PageData getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); } diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 7b22694b87..635fc747e5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.core.type.TypeReference; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -40,16 +44,21 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoor import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -225,17 +234,84 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { } @Test - public void testGetCalculatedFields() throws Exception { + public void testGetEntityCalculatedFields() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - assertThat(getCalculatedFields(testDevice.getId(), null, new PageLink(10)).getData()) + assertThat(getEntityCalculatedFields(testDevice.getId(), null, new PageLink(10)).getData()) .singleElement().isEqualTo(calculatedField); - assertThat(getCalculatedFields(testDevice.getId(), CalculatedFieldType.SIMPLE, new PageLink(10)).getData()) + assertThat(getEntityCalculatedFields(testDevice.getId(), CalculatedFieldType.SIMPLE, new PageLink(10)).getData()) .singleElement().isEqualTo(calculatedField); } + @Test + public void testGetCalculatedFieldsByFilter() throws Exception { + Device device = createDevice("Device A", "1234567890"); + CalculatedFieldInfo deviceCalculatedField = new CalculatedFieldInfo( + doPost("/api/calculatedField", getSimpleCalculatedField(device.getId()), CalculatedField.class), + "Device A" + ); + + DeviceProfile deviceProfile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class); + CalculatedFieldInfo profileCalculatedField = new CalculatedFieldInfo( + doPost("/api/calculatedField", getSimpleCalculatedField(deviceProfile.getId()), CalculatedField.class), + "Profile A" + ); + + List allCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + null, null, null); + assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); + + List profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + EntityType.DEVICE_PROFILE, null, null); + assertThat(profileLevelCalculatedFields).containsOnly(profileCalculatedField); + + List specificDeviceCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + EntityType.DEVICE, List.of(device.getUuidId()), null); + assertThat(specificDeviceCalculatedFields).containsOnly(deviceCalculatedField); + + List byNameCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, + null, null, List.of(deviceCalculatedField.getName())); + assertThat(byNameCalculatedFields).containsOnly(deviceCalculatedField); + + byNameCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, null, null, + List.of(deviceCalculatedField.getName(), profileCalculatedField.getName())); + assertThat(byNameCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); + + PageData names = getCalculatedFieldNames(CalculatedFieldType.SIMPLE, new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.ASC))); + assertThat(names.getTotalElements()).isEqualTo(2); + assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder()); + assertThat(names.getData()).contains(deviceCalculatedField.getName(), profileCalculatedField.getName()); + + names = getCalculatedFieldNames(CalculatedFieldType.SIMPLE, new PageLink(10, 0, + null, new SortOrder("", SortOrder.Direction.DESC))); + assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder()); + + names = getCalculatedFieldNames(CalculatedFieldType.SIMPLE, new PageLink(10, 0, + device.getId().toString(), new SortOrder("", SortOrder.Direction.DESC))); + assertThat(names.getTotalElements()).isEqualTo(1); + assertThat(names.getData()).containsOnly(deviceCalculatedField.getName()); + } + + private PageData getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", + new TypeReference>() {}, pageLink); + } + + private List getCalculatedFields(CalculatedFieldType type, + EntityType entityType, + List entities, + List names) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" + + (entityType != null ? "entityType=" + entityType + "&" : "") + + (entities != null ? "entities=" + String.join(",", + entities.stream().map(UUID::toString).toList()) + "&" : "") + + (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), + new TypeReference>() {}, new PageLink(10)).getData(); + } + @Test public void testDeleteCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); @@ -262,7 +338,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(cfType); - calculatedField.setName("Test Calculated Field"); + calculatedField.setName("Test Calculated Field for " + entityId); calculatedField.setConfigurationVersion(1); if (customConfiguration != null) { calculatedField.setConfiguration(customConfiguration); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index bef4675e03..b33c8af33d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -44,6 +46,10 @@ public interface CalculatedFieldService extends EntityDaoService { PageData findCalculatedFieldsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findCalculatedFieldsByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink); + + PageData findCalculatedFieldNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink); + PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java new file mode 100644 index 0000000000..40283cbaa3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java @@ -0,0 +1,41 @@ +/** + * 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. + */ +package org.thingsboard.server.common.data.cf; + +import lombok.Builder; +import lombok.Data; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.thingsboard.server.common.data.EntityType; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Data +@Builder +public class CalculatedFieldFilter { + + @NonNull + private final CalculatedFieldType type; + @NonNull + private final Set entityTypes; + @Nullable + private final List entityIds; + @Nullable + private final List names; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldInfo.java new file mode 100644 index 0000000000..b72613e86f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldInfo.java @@ -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. + */ +package org.thingsboard.server.common.data.cf; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class CalculatedFieldInfo extends CalculatedField { + + private String entityName; + + public CalculatedFieldInfo(CalculatedField calculatedField, String entityName) { + super(calculatedField); + this.entityName = entityName; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index d3ca3ccff3..55023288bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -19,8 +19,11 @@ import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -30,6 +33,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -37,8 +41,10 @@ import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -53,6 +59,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements public static final String INCORRECT_CALCULATED_FIELD_ID = "Incorrect calculatedFieldId "; public static final String INCORRECT_ENTITY_ID = "Incorrect entityId "; + private final EntityService entityService; private final CalculatedFieldDao calculatedFieldDao; private final CalculatedFieldDataValidator calculatedFieldDataValidator; @@ -140,6 +147,24 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFieldDao.findAllByTenantId(tenantId, pageLink); } + @Override + public PageData findCalculatedFieldsByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink) { + PageData calculatedFields = calculatedFieldDao.findByTenantIdAndFilter(tenantId, filter, pageLink); + Set entityIds = calculatedFields.getData().stream() + .map(CalculatedField::getEntityId) + .collect(Collectors.toSet()); + Map entityInfos = entityService.fetchEntityInfos(tenantId, null, entityIds); + return calculatedFields.mapData(calculatedField -> { + EntityInfo entityInfo = entityInfos.get(calculatedField.getEntityId()); + return new CalculatedFieldInfo(calculatedField, entityInfo.getName()); + }); + } + + @Override + public PageData findCalculatedFieldNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink) { + return calculatedFieldDao.findNamesByTenantIdAndType(tenantId, type, pageLink); + } + @Override public PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink) { log.trace("Executing findAllByEntityId, entityId [{}], pageLink [{}]", entityId, pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index 9e8ee61d4c..5a44824cc0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -49,4 +50,8 @@ public interface CalculatedFieldDao extends Dao { long countByEntityIdAndTypeNot(TenantId tenantId, EntityId entityId, CalculatedFieldType type); + PageData findByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink); + + PageData findNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index 9f1f583108..76a7906b03 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -93,7 +93,7 @@ public abstract class DataValidator> { } public void validateString(String exceptionPrefix, String name) { - if (StringUtils.isEmpty(name) || name.trim().length() == 0) { + if (StringUtils.isBlank(name)) { throw new DataValidationException(exceptionPrefix + " should be specified!"); } if (StringUtils.contains0x00(name)) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 6725387fba..d7bcbd0568 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -42,6 +42,20 @@ public interface CalculatedFieldRepository extends JpaRepository findByTenantIdAndEntityIdAndTypes(UUID tenantId, UUID entityId, List types, String textSearch, Pageable pageable); + @Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + + "AND cf.type = :type " + + "AND cf.entityType IN :entityTypes " + + "AND (:entityIds IS NULL OR cf.entityId IN :entityIds) " + + "AND (:names IS NULL OR cf.name IN :names) " + + "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findByTenantIdAndFilter(UUID tenantId, String type, List entityTypes, + List entityIds, List names, String textSearch, Pageable pageable); + + @Query("SELECT DISTINCT cf.name FROM CalculatedFieldEntity cf " + + "WHERE cf.tenantId = :tenantId AND cf.type = :type AND " + + "(:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findNamesByTenantIdAndType(UUID tenantId, String type,String textSearch, Pageable pageable); + List findAllByTenantId(UUID tenantId); List removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 3cd9285d78..94aa11a920 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -15,13 +15,16 @@ */ package org.thingsboard.server.dao.sql.cf; +import com.google.common.base.Strings; import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldFilter; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -102,6 +105,21 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink) { + return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndFilter(tenantId.getId(), filter.getType().name(), + filter.getEntityTypes().stream().map(Enum::name).toList(), + CollectionUtils.isNotEmpty(filter.getEntityIds()) ? filter.getEntityIds() : null, + CollectionUtils.isNotEmpty(filter.getNames()) ? filter.getNames() : null, + pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findNamesByTenantIdAndType(TenantId tenantId, CalculatedFieldType type, PageLink pageLink) { + return DaoUtil.pageToPageData(calculatedFieldRepository.findNamesByTenantIdAndType(tenantId.getId(), type.name(), + Strings.emptyToNull(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink, false))); + } + @Override protected Class getEntityClass() { return CalculatedFieldEntity.class; diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 631550ae9e..0fba704277 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -15,12 +15,18 @@ /// import { Injectable } from '@angular/core'; -import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { + createDefaultHttpOptions, + defaultHttpOptionsFromConfig, + defaultHttpOptionsFromParams, + RequestConfig +} from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; import { CalculatedField, + CalculatedFieldsQuery, CalculatedFieldTestScriptInputParams, CalculatedFieldType } from '@shared/models/calculated-field.models'; @@ -50,8 +56,12 @@ export class CalculatedFieldsService { return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } + public getCalculatedFieldsFilter(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> { - return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, createDefaultHttpOptions(type ? {type} : null, config)); + return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type} , config)); } public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { @@ -61,4 +71,8 @@ export class CalculatedFieldsService { public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable { return this.http.get(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config)); } + + public getAlarmRuleNames(pageLink: PageLink, type: CalculatedFieldType, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields/names${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type}, config)); + } } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 5cd4167afa..4daae2115d 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -189,6 +189,12 @@ export class EntityService { case EntityType.AI_MODEL: observable = this.aiModelService.getAiModelById(entityId, config); break; + case EntityType.DEVICE_PROFILE: + observable = this.deviceProfileService.getDeviceProfile(entityId, config); + break; + case EntityType.ASSET_PROFILE: + observable = this.assetProfileService.getAssetProfile(entityId, config); + break; } return observable; } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 4277f96747..8909663771 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -85,7 +85,9 @@ export enum MenuId { domains = 'domains', clients = 'clients', audit_log = 'audit_log', + alarms_center = 'alarms_center', alarms = 'alarms', + alarm_rules = 'alarm_rules', dashboards = 'dashboards', entities = 'entities', devices = 'devices', @@ -494,16 +496,36 @@ export const menuSectionMap = new Map([ icon: 'track_changes' } ], + [ + MenuId.alarms_center, + { + id: MenuId.alarms_center, + name: 'alarm.alarms', + type: 'link', + path: '/alarms', + icon: 'mdi:alert-outline' + } + ], [ MenuId.alarms, { id: MenuId.alarms, name: 'alarm.alarms', type: 'link', - path: '/alarms', + path: '/alarms/alarms', icon: 'mdi:alert-outline' } ], + [ + MenuId.alarm_rules, + { + id: MenuId.alarm_rules, + name: 'alarm-rule.alarm-rules', + type: 'link', + path: '/alarms/alarm-rules', + icon: 'tune' + } + ], [ MenuId.dashboards, { @@ -792,7 +814,13 @@ const defaultUserMenuMap = new Map([ Authority.TENANT_ADMIN, [ {id: MenuId.home}, - {id: MenuId.alarms}, + { + id: MenuId.alarms_center, + pages: [ + {id: MenuId.alarms}, + {id: MenuId.alarm_rules} + ] + }, {id: MenuId.dashboards}, { id: MenuId.entities, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index e0f1a052d5..d4a93d461f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ 'alarm-rule.alarm-rule' | translate}}

@@ -47,30 +47,53 @@
+ @if (!data.entityId) { +
+ + + @if (fieldFormGroup.get('entityId.entityType').value) { + + } +
+ }
{{ 'calculated-fields.arguments' | translate }}
-
{{ 'alarm-rule.create-alarm-rules' | translate }}
+
{{ 'alarm-rule.create-conditions' | translate }}
-
{{ 'alarm-rule.clear-alarm-rule' | translate }}
+
{{ 'alarm-rule.clear-condition' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index 0ea3bd036e..b3176414ab 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -23,7 +23,7 @@ import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models'; import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; -import { EntityType } from '@shared/models/entity-type.models'; +import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; @@ -57,13 +57,17 @@ export class AlarmRuleDialogComponent extends DialogComponent(null, Validators.required), + id: ['', Validators.required], + }), configuration: this.fb.group({ arguments: this.fb.control({}), propagate: [false], propagateToOwner: [false], propagateToTenant: [false], propagateRelationTypes: [null], - createRules: [null], + createRules: [null, Validators.required], clearRule: [null], }), }); @@ -74,6 +78,8 @@ export class AlarmRuleDialogComponent extends DialogComponent + + + + + + + + + +
+ +
+ + + + +
+
+
+ +
+
+
alarm-rule.alarm-rule-type-list
+ + +
+
+
alarm-rule.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + + {{ entityTypeTranslations.get(type)?.type | translate }} + + + +
+ @if (alarmRuleFilterConfigForm.get('entityType').value) { +
+
alarm-rule.alarm-rule-entity-list
+ + +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss new file mode 100644 index 0000000000..12dd46a278 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss @@ -0,0 +1,58 @@ +/** + * 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-alarm-filter-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%; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts new file mode 100644 index 0000000000..c961cebe33 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts @@ -0,0 +1,296 @@ +/// +/// 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 { TranslateService } from '@ngx-translate/core'; +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 { UtilsService } from '@core/services/utils.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AlarmRuleFilterConfig } from "@shared/models/alarm-rule.models"; + +export const ALARM_FILTER_CONFIG_DATA = new InjectionToken('AlarmRuleFilterConfigData'); + +export interface AlarmRuleFilterConfigData { + panelMode: boolean; + userMode: boolean; + alarmRuleFilterConfig: AlarmRuleFilterConfig; + initialAlarmRuleFilterConfig?: AlarmRuleFilterConfig; +} + +@Component({ + selector: 'tb-alarm-rule-filter-config', + templateUrl: './alarm-rule-filter-config.component.html', + styleUrls: ['./alarm-rule-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterConfigComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAccessor { + + @ViewChild('alarmRuleFilterPanel') + alarmRuleFilterPanel: TemplateRef; + + @Input() disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @coerceBoolean() + @Input() + userMode = false; + + @coerceBoolean() + @Input() + propagatedFilter = true; + + @Input() + initialAlarmRuleFilterConfig: AlarmRuleFilterConfig = { + name: [], + entityType: null, + entities: [] + }; + + panelMode = false; + + buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter'); + + alarmRuleFilterConfigForm: FormGroup; + + alarmFilterOverlayRef: OverlayRef; + + panelResult: AlarmRuleFilterConfig = null; + + entityType = EntityType; + + listEntityTypes = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + entityTypeTranslations = entityTypeTranslations; + + private alarmRuleFilterConfig: AlarmRuleFilterConfig; + private resizeWindows: Subscription; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA) + private data: AlarmRuleFilterConfigData | undefined, + @Optional() + private overlayRef: OverlayRef, + private fb: FormBuilder, + private translate: TranslateService, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.userMode = this.data.userMode; + this.alarmRuleFilterConfig = this.data.alarmRuleFilterConfig; + this.initialAlarmRuleFilterConfig = this.data.initialAlarmRuleFilterConfig; + if (this.panelMode && !this.initialAlarmRuleFilterConfig) { + this.initialAlarmRuleFilterConfig = deepClone(this.alarmRuleFilterConfig); + } + } + this.alarmRuleFilterConfigForm = this.fb.group({ + name: [null, []], + entityType: [null, []], + entities: [null, []] + }); + this.alarmRuleFilterConfigForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + if (!this.buttonMode) { + this.alarmRuleConfigUpdated(this.alarmRuleFilterConfigForm.value); + } + } + ); + if (this.panelMode) { + this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleFilterConfigForm.disable({emitEvent: false}); + } else { + this.alarmRuleFilterConfigForm.enable({emitEvent: false}); + } + } + + writeValue(alarmRuleFilterConfig?: AlarmRuleFilterConfig): void { + this.alarmRuleFilterConfig = alarmRuleFilterConfig; + if (!this.initialAlarmRuleFilterConfig && alarmRuleFilterConfig) { + this.initialAlarmRuleFilterConfig = deepClone(alarmRuleFilterConfig); + } + this.updateButtonDisplayValue(); + this.updateAlarmRuleConfigForm(alarmRuleFilterConfig); + } + + toggleAlarmRuleFilterPanel($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.alarmFilterOverlayRef = this.overlay.create(config); + this.alarmFilterOverlayRef.backdropClick().subscribe(() => { + this.alarmFilterOverlayRef.dispose(); + }); + this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmRuleFilterPanel, + this.viewContainerRef)); + this.resizeWindows = fromEvent(window, 'resize').subscribe(() => { + this.alarmFilterOverlayRef.updatePosition(); + }); + } + + cancel() { + this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.alarmFilterOverlayRef.dispose(); + } + } + + update() { + this.alarmRuleConfigUpdated(this.alarmRuleFilterConfigForm.value); + this.alarmRuleFilterConfigForm.markAsPristine(); + if (this.panelMode) { + this.panelResult = this.alarmRuleFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.alarmFilterOverlayRef.dispose(); + } + } + + reset() { + const alarmRuleFilterConfig = this.alarmRuleFilterConfigFromFormValue(this.alarmRuleFilterConfigForm.value); + if (!this.alarmRuleFilterConfigEquals(alarmRuleFilterConfig, this.initialAlarmRuleFilterConfig)) { + this.updateAlarmRuleConfigForm(this.initialAlarmRuleFilterConfig); + this.alarmRuleFilterConfigForm.markAsDirty(); + } + } + + private alarmRuleFilterConfigEquals = (filter1?: AlarmRuleFilterConfig, filter2?: AlarmRuleFilterConfig): 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.name, filter2.name)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) { + return false; + } + return filter1.entityType !== filter2.entityType; + } + return false; + }; + + private updateAlarmRuleConfigForm(alarmRuleFilterConfig?: AlarmRuleFilterConfig) { + this.alarmRuleFilterConfigForm.patchValue({ + name: alarmRuleFilterConfig?.name ?? [], + entityType: alarmRuleFilterConfig?.entityType ?? null, + entities: alarmRuleFilterConfig?.entities ?? [], + }, {emitEvent: false}); + } + + private alarmRuleConfigUpdated(formValue: any) { + this.alarmRuleFilterConfig = this.alarmRuleFilterConfigFromFormValue(formValue); + this.updateButtonDisplayValue(); + this.propagateChange(this.alarmRuleFilterConfig); + } + + private alarmRuleFilterConfigFromFormValue(formValue: any): AlarmRuleFilterConfig { + return { + name: formValue?.name ?? [], + entityType: formValue?.entityType ?? null, + entities: formValue?.entities ?? [], + }; + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (this.alarmRuleFilterConfig?.name?.length) { + filterTextParts.push(this.alarmRuleFilterConfig.name.map((type) => this.customTranslate(type)).join(', ')); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + + private customTranslate(entity: string) { + return this.utils.customTranslation(entity, entity); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html new file mode 100644 index 0000000000..807dd5c9f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html @@ -0,0 +1,20 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss new file mode 100644 index 0000000000..ee0899b5ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-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%; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts new file mode 100644 index 0000000000..cf5462d1a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts @@ -0,0 +1,44 @@ +/// +/// 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 '../../components/entity/entity-table-header.component'; +import { AlarmFilterConfig } from '@shared/models/query/query.models'; +import { CalculatedFieldAlarmRule, CalculatedFieldsQuery } from "@shared/models/calculated-field.models"; +import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; + +@Component({ + selector: 'tb-alarm-rule-table-header', + templateUrl: './alarm-rule-table-header.component.html', + styleUrls: ['./alarm-rule-table-header.component.scss'] +}) +export class AlarmRuleTableHeaderComponent extends EntityTableHeaderComponent { + + get alarmRuleTableConfig(): AlarmRulesTableConfig { + return this.entitiesTableConfig as AlarmRulesTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + alarmRuleFilterChanged(alarmRuleFilterConfig: CalculatedFieldsQuery) { + this.alarmRuleTableConfig.alarmRuleFilterConfig = alarmRuleFilterConfig; + this.alarmRuleTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts index 7acfca7b0d..c02c11a170 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts @@ -48,6 +48,8 @@ import { import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component"; import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; +import { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.component"; +import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; @NgModule({ declarations: [ @@ -66,6 +68,8 @@ import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/al AlarmRuleFilterPredicateValueComponent, AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleDetailsDialogComponent, + AlarmRuleFilterConfigComponent, + AlarmRuleTableHeaderComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 5167e32853..a51fd6e6d7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -17,10 +17,11 @@ import { checkBoxCell, DateEntityTableColumn, + EntityLinkTableColumn, EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { EntityType } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; import { MatDialog } from '@angular/material/dialog'; @@ -40,6 +41,7 @@ import { ArgumentEntityType, CalculatedField, CalculatedFieldAlarmRule, + CalculatedFieldsQuery, CalculatedFieldType, } from '@shared/models/calculated-field.models'; import { ImportExportService } from '@shared/import-export/import-export.service'; @@ -55,6 +57,8 @@ import { } from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component"; import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; import { UtilsService } from "@core/services/utils.service"; +import { deepClone, getEntityDetailsPageURL } from "@core/utils"; +import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; export class AlarmRulesTableConfig extends EntityTableConfig { @@ -64,6 +68,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), }; + alarmRuleFilterConfig: CalculatedFieldsQuery; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, @@ -77,11 +83,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig { private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, private utilsService: UtilsService, + public pageMode: boolean = false, ) { super(); - this.tableTitle = this.translate.instant('alarm-rule.alarm-rules'); + if (this.pageMode) { + this.headerComponent = AlarmRuleTableHeaderComponent; + } + this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules'); this.detailsPanelEnabled = false; - this.pageMode = false; this.entityType = EntityType.CALCULATED_FIELD; this.entityTranslations = { type: 'alarm-rule.alarm-rule', @@ -117,14 +126,29 @@ export class AlarmRulesTableConfig extends EntityTableConfig { this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); - this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', '67%', + if (this.pageMode) { + this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.entity-type', '15%', + entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); + this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.entity-name', '30%', + entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), + entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); + } + this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%', entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), () => ({}), false)); this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '70px', entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); + this.cellActionDescriptors.push( + { + name: this.translate.instant('notification.copy-template'), + icon: 'content_copy', + isEnabled: () => true, + onAction: ($event, entity) => this.copyCalculatedField(entity) + } + ); this.cellActionDescriptors.push( { name: this.translate.instant('action.export'), @@ -156,7 +180,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } fetchCalculatedFields(pageLink: PageLink): Observable> { - return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM); + return this.pageMode ? + this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, {type: CalculatedFieldType.ALARM, ...this.alarmRuleFilterConfig}) : + this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM); } onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { @@ -195,6 +221,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }); } + private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void { + const copyCalculatedAlarmRule = deepClone(calculatedField); + copyCalculatedAlarmRule.entityId = null; + this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable { return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, @@ -205,7 +242,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { entityId: this.entityId, tenantId: this.tenantId, entityName: this.entityName, - ownerId: this.ownerId, + ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, additionalDebugActionConfig: this.additionalDebugActionConfig, isDirty, }, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html index e9eebb226c..fb0eab1447 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html @@ -16,5 +16,5 @@ --> @if (alarmRulesTableConfig) { - + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss index 0e0dacc392..0884f69200 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss @@ -14,7 +14,7 @@ * limitations under the License. */ :host ::ng-deep { - tb-entities-table { + .white-background { --mat-sidenav-content-background-color: white; } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index 4ade1c5461..de0dc87c12 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -36,6 +36,7 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity import { DatePipe } from '@angular/common'; import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; import { UtilsService } from "@core/services/utils.service"; +import { ActivatedRoute } from "@angular/router"; @Component({ selector: 'tb-alarm-rules-table', @@ -55,6 +56,8 @@ export class AlarmRulesTableComponent { alarmRulesTableConfig: AlarmRulesTableConfig; + pageMode: boolean = false; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, @@ -65,10 +68,12 @@ export class AlarmRulesTableComponent { private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, private utilsService: UtilsService, - private destroyRef: DestroyRef) { - + private destroyRef: DestroyRef, + private route: ActivatedRoute, + ) { + this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { - if (this.active()) { + if (this.active() || this.pageMode) { this.alarmRulesTableConfig = new AlarmRulesTableConfig( this.calculatedFieldsService, this.translate, @@ -83,6 +88,7 @@ export class AlarmRulesTableComponent { this.importExportService, this.entityDebugSettingsService, this.utilsService, + this.pageMode ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index d2f98ecd85..b3c0028f87 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -22,7 +22,7 @@
{{ 'alarm-rule.expression-type.simple' | translate }} - {{ 'alarm-rule.expression-type.tbel' | translate }} + {{ 'alarm-rule.expression-type.script' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 17ed11d892..2e8d116ad2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -74,12 +74,16 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali @coerceBoolean() disabled: boolean; + @Input() + @coerceBoolean() + required: boolean; + @Input() arguments: Record; alarmRuleConditionFormGroup = this.fb.group({ type: ['SIMPLE'], - expression: [{type: AlarmRuleExpressionType.SIMPLE}, Validators.required], + expression: [{type: AlarmRuleExpressionType.SIMPLE}], schedule: [null], }); @@ -114,19 +118,21 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali } writeValue(value: AlarmRuleCondition): void { - this.modelValue = value; - this.updateConditionInfo(); + if (value) { + this.modelValue = value; + this.updateConditionInfo(); + } } public conditionSet() { - return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); + return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters) || !this.required; } public validate(c: UntypedFormControl) { return this.conditionSet() ? null : { alarmRuleCondition: { valid: false, - }, + } }; } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index cd6b2c3796..14a88b2bf1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -16,7 +16,7 @@ -->
- + @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index 88ae1dd79d..f5e57310a2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -21,14 +21,13 @@ import { NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, - Validator, - Validators + Validator } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { isDefinedAndNotNull } from '@core/utils'; import { DashboardId } from '@shared/models/id/dashboard-id'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { AlarmRule, AlarmRuleCondition } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { AlarmRuleDetailsDialogComponent, @@ -69,7 +68,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid private modelValue: AlarmRule; alarmRuleFormGroup = this.fb.group({ - condition: this.fb.control({}, Validators.required), + condition: this.fb.control(null), alarmDetails: [null], dashboardId: [null] }); @@ -106,12 +105,14 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid } writeValue(value: AlarmRule): void { - this.modelValue = value; - const model = this.modelValue ? { - ...this.modelValue, - dashboardId: this.modelValue.dashboardId?.id - } : null; - this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); + if (value) { + this.modelValue = value; + const model = this.modelValue ? { + ...this.modelValue, + dashboardId: this.modelValue.dashboardId?.id + } : null; + this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); + } } public openEditDetailsDialog($event: Event) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss index 14df9a3972..b66573f417 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -20,41 +20,38 @@ display: grid; grid-template-rows: min-content minmax(auto, 1fr) min-content; } - - .tbel-script-lang-chip { - line-height: 20px; - font-size: 14px; - font-weight: 500; - color: white; - border-radius: 100px; - width: 70px; - min-width: 70px; - display: flex; - justify-content: center; - margin-top: 2px; - margin-right: 4px; - } - - .tb-js-func { - .ace_tb { - &.ace_calculated-field { - &-ctx { - color: #C52F00; - } - &-args { - color: #185F2A; - } - &-key { - color: #c24c1a; - } - &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { - color: #7214D0; - } - &-start-ts, &-end-ts { - color: #2CAA00; - } +} +.tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; +} +.tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; } } } } - diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html index 820418a6b0..b0e6164d30 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -55,6 +55,7 @@