From a4fe68fdb3370b8d0dcfd577f2ce130ecf9b3a2a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 28 Apr 2026 16:31:10 +0300 Subject: [PATCH] feat(solutions): wire ALARM calculated field info and add display names - Introduce HasAppliedToEntity interface used by CreatedAlarmRuleInfo and CreatedCalculatedFieldInfo to share the entity page-link logic and cover DEVICE / ASSET in addition to the profile types. - Add static from(EntityId, name, CalculatedField) factories to both records; SolutionInstallContext now uses them and detects ALARM calculated fields via type instead of the previous TODO/hardcoded false. - AlarmSeverity and CalculatedFieldType expose display names used when formatting created-alarm-rule severities and CF type column. - DefaultSolutionService switches the CF arguments check from BaseCalculatedFieldConfiguration to ArgumentsBasedCalculatedFieldConfiguration and throws ThingsboardRuntimeException for missing references. --- .../solutions/DefaultSolutionService.java | 11 ++--- .../solutions/data/CreatedAlarmRuleInfo.java | 33 ++++++++------ .../data/CreatedCalculatedFieldInfo.java | 21 ++++----- .../solutions/data/HasAppliedToEntity.java | 43 +++++++++++++++++++ .../data/SolutionInstallContext.java | 7 +-- .../common/data/alarm/AlarmSeverity.java | 6 +++ .../common/data/cf/CalculatedFieldType.java | 23 +++++++--- 7 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/solutions/data/HasAppliedToEntity.java diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index 68a375752e..246e889ec3 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor; @@ -55,7 +56,6 @@ import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.configuration.BaseCalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AlarmId; @@ -94,6 +94,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.exception.EntitiesLimitExceededException; +import org.thingsboard.server.exception.ThingsboardRuntimeException; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; @@ -1329,8 +1330,8 @@ public class DefaultSolutionService implements SolutionService { throw new RuntimeException("Calculated field: " + cf.getName() + " references non existing entity."); } } - if (cf.getConfiguration() instanceof BaseCalculatedFieldConfiguration baseCfg) { - baseCfg.getArguments().forEach((key, argument) -> { + if (cf.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedCfg) { + argBasedCfg.getArguments().forEach((key, argument) -> { EntityId refEntityId = argument.getRefEntityId(); if (refEntityId != null) { if (refEntityId.getEntityType() == EntityType.TENANT) { @@ -1340,8 +1341,8 @@ public class DefaultSolutionService implements SolutionService { if (newId != null) { argument.setRefEntityId(EntityIdFactory.getByTypeAndUuid(refEntityId.getEntityType(), newId)); } else { - log.error("[{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), cf.getName()); - throw new RuntimeException("Calculated field: " + cf.getName() + " references non existing entity."); + log.error("[{}][{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), ctx.getSolutionId(), cf.getName()); + throw new ThingsboardRuntimeException(); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java index 90e5fc9f66..e03b28588f 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedAlarmRuleInfo.java @@ -15,25 +15,32 @@ */ package org.thingsboard.server.service.solutions.data; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.EntityId; import java.util.UUID; +import java.util.stream.Collectors; -public record CreatedAlarmRuleInfo(EntityId entityId, String entityName, String alarmType, String severities) { +public record CreatedAlarmRuleInfo(EntityId entityId, String entityName, String alarmType, + String severities) implements HasAppliedToEntity { - public String getCfPageLink(UUID cfId) { - return "/alarms/alarm-rules/" + cfId; - } - - public String getEntityPageLink() { - if (entityId == null) { - return null; + public static CreatedAlarmRuleInfo from(EntityId entityId, String entityName, CalculatedField calculatedField) { + if (calculatedField.getType() != CalculatedFieldType.ALARM) { + throw new UnsupportedOperationException("Only alarm calculated fields are supported"); } - return switch (entityId.getEntityType()) { - case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + entityId.getId(); - case ASSET_PROFILE -> "/profiles/assetProfiles/" + entityId.getId(); - default -> null; - }; + String severities = ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()) + .getCreateRules().keySet().stream() + .map(AlarmSeverity::getDisplayName) + .sorted() + .collect(Collectors.joining(", ")); + return new CreatedAlarmRuleInfo(entityId, entityName, calculatedField.getName(), severities); } + @Override + public String getCfPageLink(UUID cfId) { + return "/alarms/alarm-rules/" + cfId; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java index 9616d738af..7b19fbc0cf 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/CreatedCalculatedFieldInfo.java @@ -15,25 +15,20 @@ */ package org.thingsboard.server.service.solutions.data; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; import java.util.UUID; -public record CreatedCalculatedFieldInfo(EntityId entityId, String entityName, String type, String name) { +public record CreatedCalculatedFieldInfo(EntityId entityId, String entityName, String type, + String name) implements HasAppliedToEntity { - public String getCfPageLink(UUID cfId) { - return "/calculatedFields/" + cfId; + public static CreatedCalculatedFieldInfo from(EntityId entityId, String entityName, CalculatedField calculatedField) { + return new CreatedCalculatedFieldInfo(entityId, entityName, calculatedField.getType().getDisplayName(), calculatedField.getName()); } - public String getEntityPageLink() { - if (entityId == null) { - return null; - } - return switch (entityId.getEntityType()) { - case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + entityId.getId(); - case ASSET_PROFILE -> "/profiles/assetProfiles/" + entityId.getId(); - default -> null; - }; + @Override + public String getCfPageLink(UUID cfId) { + return "/calculatedFields/" + cfId; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/HasAppliedToEntity.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/HasAppliedToEntity.java new file mode 100644 index 0000000000..9b029bfa78 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/HasAppliedToEntity.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.solutions.data; + +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.UUID; + +public interface HasAppliedToEntity { + + EntityId entityId(); + + String getCfPageLink(UUID cfId); + + default String getEntityPageLink() { + EntityId id = entityId(); + if (id == null) { + return null; + } + String idStr = id.getId().toString(); + return switch (id.getEntityType()) { + case DEVICE_PROFILE -> "/profiles/deviceProfiles/" + idStr; + case ASSET_PROFILE -> "/profiles/assetProfiles/" + idStr; + case DEVICE -> "/entities/devices/" + idStr; + case ASSET -> "/entities/assets/" + idStr; + default -> null; + }; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java index 9ccf136678..4985ca1685 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -156,17 +157,17 @@ public class SolutionInstallContext { register(calculatedField.getId()); EntityId entityId = calculatedField.getEntityId(); CreatedEntityInfo entityInfo = createdEntities.get(entityId.getId()); - boolean alarmRule = false; // TODO: CE doesn't have ALARM calculated field type yet + boolean alarmRule = calculatedField.getType() == CalculatedFieldType.ALARM; if (entityInfo == null) { String target = alarmRule ? "Alarm rule" : "Calculated field"; throw new IllegalStateException("Failed to register " + target + " with name: " + calculatedField.getName() + " for non-existing entity with id: " + entityId); } if (alarmRule) { - createdAlarmRules.put(calculatedField.getUuidId(), new CreatedAlarmRuleInfo(entityId, entityInfo.getName(), calculatedField.getName(), null)); + createdAlarmRules.put(calculatedField.getUuidId(), CreatedAlarmRuleInfo.from(entityId, entityInfo.getName(), calculatedField)); return; } - createdCalculatedFields.put(calculatedField.getUuidId(), new CreatedCalculatedFieldInfo(entityId, entityInfo.getName(), calculatedField.getType().name(), calculatedField.getName())); + createdCalculatedFields.put(calculatedField.getUuidId(), CreatedCalculatedFieldInfo.from(entityId, entityInfo.getName(), calculatedField)); } public void putIdToMap(EntityDefinition entityDefinition, EntityId entityId) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java index e7776ce286..fbeac79982 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java @@ -15,8 +15,14 @@ */ package org.thingsboard.server.common.data.alarm; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + public enum AlarmSeverity { CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE; + @Getter + private final String displayName = StringUtils.capitalize(name().toLowerCase()); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java index fb944344e3..6dbf567003 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java @@ -15,20 +15,29 @@ */ package org.thingsboard.server.common.data.cf; +import lombok.Getter; + import java.util.Collections; import java.util.EnumSet; import java.util.Set; public enum CalculatedFieldType { - SIMPLE, - SCRIPT, - GEOFENCING, - ALARM, - PROPAGATION, - RELATED_ENTITIES_AGGREGATION, - ENTITY_AGGREGATION; + SIMPLE("Simple"), + SCRIPT("Script"), + GEOFENCING("Geofencing"), + ALARM("Alarm"), + PROPAGATION("Propagation"), + RELATED_ENTITIES_AGGREGATION("Related entities aggregation"), + ENTITY_AGGREGATION("Time series data aggregation"); public static final Set all = Collections.unmodifiableSet(EnumSet.allOf(CalculatedFieldType.class)); + @Getter + private final String displayName; + + CalculatedFieldType(String displayName) { + this.displayName = displayName; + } + }