diff --git a/.github/release.yml b/.github/release.yml index 53b2e3ae65..1dad5d274f 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -23,27 +23,19 @@ changelog: labels: - 'Major Core' - 'Major Rule Engine' - exclude: - labels: - - 'Bug' + - title: 'Major UI' labels: - 'Major UI' - exclude: - labels: - - 'Bug' + - title: 'Major Transport' labels: - 'Major Transport' - exclude: - labels: - - 'Bug' + - title: 'Major Edge' labels: - 'Major Edge' - exclude: - labels: - - 'Bug' + - title: 'Core & Rule Engine' labels: - 'Core' @@ -51,38 +43,63 @@ changelog: exclude: labels: - 'Bug' + - title: 'UI' labels: - 'UI' exclude: labels: - 'Bug' + - title: 'Transport' labels: - 'Transport' exclude: labels: - 'Bug' + - title: 'Edge' labels: - 'Edge' exclude: labels: - 'Bug' + - title: 'Bug: Core & Rule Engine' labels: - - 'Core' - - 'Rule Engine' - 'Bug' + exclude: + labels: + - 'UI' + - 'Transport' + - 'Edge' + - title: 'Bug: UI' labels: - - 'UI' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'Transport' + - 'Edge' + - title: 'Bug: Transport' labels: - - 'Transport' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'UI' + - 'Edge' + - title: 'Bug: Edge' labels: - - 'Edge' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'UI' + - 'Transport' diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 9848ac2fe6..a05a91434a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -846,7 +846,7 @@ public class ActorSystemContext { if (tbMsgType != null) { eventBuilder.msgType(tbMsgType); } - if (arguments != null) { + if (arguments != null && !arguments.isEmpty()) { eventBuilder.arguments(JacksonUtil.toString(arguments)); } if (result != null) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index d2ab4c85ac..a24c5fb732 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -572,24 +572,24 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArguments(CalculatedFieldCtx ctx, List data) { - return mapToArguments(entityId, ctx.getMainEntityArguments(), Collections.emptyMap(), data); + return mapToArguments(entityId, ctx, ctx.getMainEntityArguments(), Collections.emptyMap(), data); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, List data) { - return mapToArguments(entityId, ctx.getLinkedAndDynamicArgs(entityId), ctx.getRelatedEntityArguments(), data); + return mapToArguments(entityId, ctx, ctx.getLinkedAndDynamicArgs(entityId), ctx.getRelatedEntityArguments(), data); } - private Map mapToArguments(EntityId originator, Map> args, Map> relatedEntityArgs, List data) { + private Map mapToArguments(EntityId originator, CalculatedFieldCtx ctx, Map> args, Map> relatedEntityArgs, List data) { Map arguments = new HashMap<>(); if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) { for (TsKvProto item : data) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); SingleValueArgumentEntry relatedArgIncoming = new SingleValueArgumentEntry(originator, item); - mapLatest(relatedArgIncoming, relatedEntityArgs.get(key), arguments); + mapLatest(ctx, relatedArgIncoming, relatedEntityArgs.get(key), arguments); SingleValueArgumentEntry incoming = new SingleValueArgumentEntry(item); - mapLatest(incoming, args.get(key), arguments); + mapLatest(ctx, incoming, args.get(key), arguments); key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); mapRolling(item, args.get(key), arguments); @@ -598,7 +598,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM return arguments; } - private void mapLatest(SingleValueArgumentEntry incoming, + private void mapLatest(CalculatedFieldCtx ctx, + SingleValueArgumentEntry incoming, Set argNames, Map arguments) { if (argNames != null) { @@ -606,7 +607,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (existing == null) { return incoming; } - existing.updateEntry(incoming); + existing.updateEntry(incoming, ctx); return existing; })); } diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index f471c9e7b4..97792be3e0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -75,7 +75,7 @@ public class EntityQueryController extends BaseController { } @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping("/entitiesQuery/find") public PageData findEntityDataByQuery( @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index dc23ffa979..fc4f8ce365 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -52,7 +52,7 @@ public interface ArgumentEntry { Object getValue(); - boolean updateEntry(ArgumentEntry entry); + boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx); boolean isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 09ca35cc4b..b4b396b526 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -86,13 +86,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, validateNewEntry(key, newEntry); if (existingEntry instanceof RelatedEntitiesArgumentEntry || existingEntry instanceof EntityAggregationArgumentEntry) { - updateEntry(existingEntry, newEntry); + updateEntry(existingEntry, newEntry, ctx); } else { arguments.put(key, newEntry); } entryUpdated = true; } else { - entryUpdated = updateEntry(existingEntry, newEntry); + entryUpdated = updateEntry(existingEntry, newEntry, ctx); } if (entryUpdated) { @@ -111,8 +111,8 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, return updatedArguments; } - protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry) { - return existingEntry.updateEntry(newEntry); + protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry, CalculatedFieldCtx ctx) { + return existingEntry.updateEntry(newEntry, ctx); } @Override @@ -201,7 +201,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, @Override public JsonNode getArgumentsJson() { return JacksonUtil.valueToTree(arguments.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue()))); + .filter(entry -> !entry.getValue().isEmpty()) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue())) + ); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 4d0c4d7724..028c3c429b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -159,7 +159,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } @Override - public boolean updateEntry(ArgumentEntry entry) { + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) { if (singleValueEntry.getTs() < this.ts) { return false; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index 8cdc9ddcf9..8abddb3d4a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -100,7 +100,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs { } @Override - public boolean updateEntry(ArgumentEntry entry) { + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof TsRollingArgumentEntry tsRollingEntry) { updateTsRollingEntry(tsRollingEntry); } else if (entry instanceof SingleValueArgumentEntry singleValueEntry) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java index 219cf471ed..0a5c850e4f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java @@ -23,6 +23,7 @@ import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; @@ -63,7 +64,7 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs } @Override - public boolean updateEntry(ArgumentEntry entry) { + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); return true; @@ -74,7 +75,7 @@ public class RelatedEntitiesArgumentEntry implements ArgumentEntry, HasLatestTs } ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); if (argumentEntry != null) { - argumentEntry.updateEntry(singleValueArgumentEntry); + argumentEntry.updateEntry(singleValueArgumentEntry, ctx); } else { entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java index 7ec5098bc3..c0a0603390 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java @@ -19,11 +19,18 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.Map; +import java.util.concurrent.TimeUnit; @Data public class EntityAggregationArgumentEntry implements ArgumentEntry { @@ -47,28 +54,63 @@ public class EntityAggregationArgumentEntry implements ArgumentEntry { } @Override - public boolean updateEntry(ArgumentEntry entry) { - boolean updated = false; + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (entry instanceof EntityAggregationArgumentEntry entityAggEntry) { aggIntervals.putAll(entityAggEntry.getAggIntervals()); + return true; } else if (entry instanceof SingleValueArgumentEntry singleValueArgEntry) { long entryTs = singleValueArgEntry.getTs(); - long argUpdateTs = System.currentTimeMillis(); - for (Map.Entry aggIntervalEntry : aggIntervals.entrySet()) { - if (singleValueArgEntry.isForceResetPrevious()) { - aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs); - updated = true; - continue; - } - if (aggIntervalEntry.getKey().belongsToInterval(entryTs)) { - aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs); - return true; - } + long now = System.currentTimeMillis(); + if (updateExistingIntervals(singleValueArgEntry, entryTs, now)) { + return true; } + return createNewInterval(entryTs, now, ctx); } + return false; + } + + private boolean updateExistingIntervals(SingleValueArgumentEntry entry, long entryTs, long now) { + boolean updated = false; + + for (Map.Entry aggIntervalEntry : aggIntervals.entrySet()) { + AggIntervalEntry interval = aggIntervalEntry.getKey(); + AggIntervalEntryStatus status = aggIntervalEntry.getValue(); + if (entry.isForceResetPrevious()) { + status.setLastArgsRefreshTs(now); + updated = true; + continue; + } + if (interval.belongsToInterval(entryTs)) { + status.setLastArgsRefreshTs(now); + return true; + } + } + return updated; } + private boolean createNewInterval(long entryTs, long now, CalculatedFieldCtx ctx) { + if (!(ctx.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration config)) { + return false; + } + AggInterval interval = config.getInterval(); + Watermark watermark = config.getWatermark(); + long watermarkDuration = watermark == null ? 0 : TimeUnit.SECONDS.toMillis(watermark.getDuration()); + + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(entryTs), interval.getZoneId()); + + long startTs = interval.getDateTimeIntervalStartTs(zdt); + long endTs = interval.getDateTimeIntervalEndTs(zdt); + + if (now - endTs > watermarkDuration) { + return false; + } + + AggIntervalEntry newInterval = new AggIntervalEntry(startTs, endTs); + aggIntervals.computeIfAbsent(newInterval, i -> new AggIntervalEntryStatus(now)); + return true; + } + @Override public boolean isEmpty() { return aggIntervals.isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index 0f6e01a344..99bddc374c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -154,8 +154,10 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt } private void fillMissingIntervals() { + long now = System.currentTimeMillis(); ZoneId zoneId = interval.getZoneId(); long currentIntervalEndTs = interval.getCurrentIntervalEndTs(); + long watermarkThresholdTs = now - watermarkDuration; Map> intervals = getIntervals(); AggIntervalEntry lastIntervalEntry = intervals.keySet().stream().max(Comparator.comparing(AggIntervalEntry::getEndTs)).orElse(null); @@ -169,6 +171,13 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt while (nextEnd.toInstant().toEpochMilli() <= currentIntervalEndTs) { long nextStartTs = nextStart.toInstant().toEpochMilli(); long nextEndTs = nextEnd.toInstant().toEpochMilli(); + + if (nextEndTs < watermarkThresholdTs) { + nextStart = nextEnd; + nextEnd = interval.getNextIntervalStart(nextStart); + continue; + } + AggIntervalEntry missing = new AggIntervalEntry(nextStartTs, nextEndTs); arguments.forEach((argName, argumentEntry) -> { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java index 4f2cbede09..0853b923c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -150,7 +150,8 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { clearRuleState = null; } } - log.debug("Initialized create rule states {} and clear rule state {} for {}", createRuleStates, clearRuleState, configuration); + log.debug("Initialized create rule states {} and clear rule state {} for {}. Restored: {}, reeval needed: {}", + createRuleStates, clearRuleState, configuration, restored, reevalNeeded); if (reevalNeeded.get()) { initCurrentAlarm(ctx); @@ -224,17 +225,17 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { } @Override - protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry) { + protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry, CalculatedFieldCtx ctx) { if (!(existingArgumentEntry instanceof SingleValueArgumentEntry existingEntry) || !(newArgumentEntry instanceof SingleValueArgumentEntry newEntry)) { - return super.updateEntry(existingArgumentEntry, newArgumentEntry); + return super.updateEntry(existingArgumentEntry, newArgumentEntry, ctx); } if (newEntry.getTs() < existingEntry.getTs()) { if (existingEntry.isDefaultValue()) { existingEntry.setTs(newEntry.getTs()); } } - return super.updateEntry(existingEntry, newEntry); + return super.updateEntry(existingEntry, newEntry, ctx); } public void processAlarmAction(Alarm alarm, ActionType action) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java index 569bbe9310..cf7b1ce108 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java @@ -262,6 +262,14 @@ public class AlarmRuleState { } } + public void setDurationCheckFuture(ScheduledFuture durationCheckFuture) { + if (this.durationCheckFuture != null) { + log.warn("Setting new duration check future while previous is not null for state {}", this, new RuntimeException("stacktrace")); + this.durationCheckFuture.cancel(true); + } + this.durationCheckFuture = durationCheckFuture; + } + public boolean isEmpty() { return eventCount == 0L && firstEventTs == 0L && lastCheckTs == 0L && durationCheckFuture == null; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index 01c7119993..a3305ea52d 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.HasLatestTs; import java.util.Map; @@ -68,7 +69,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry, HasLatestTs { } @Override - public boolean updateEntry(ArgumentEntry entry) { + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java index 8536c0f65f..8130598b91 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -21,6 +21,7 @@ import org.thingsboard.script.api.tbel.TbelCfPropagationArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import java.util.ArrayList; import java.util.Collection; @@ -36,7 +37,7 @@ public class PropagationArgumentEntry implements ArgumentEntry { private transient EntityId removed; private transient boolean forceResetPrevious; - private transient boolean ignoreRemovedEntities; + private transient boolean syncWithDb; public PropagationArgumentEntry() { this.entityIds = new HashSet<>(); @@ -59,7 +60,7 @@ public class PropagationArgumentEntry implements ArgumentEntry { } @Override - public boolean updateEntry(ArgumentEntry entry) { + public boolean updateEntry(ArgumentEntry entry, CalculatedFieldCtx ctx) { if (!(entry instanceof PropagationArgumentEntry updated)) { throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); } @@ -69,14 +70,18 @@ public class PropagationArgumentEntry implements ArgumentEntry { if (updated.getRemoved() != null) { return entityIds.remove(updated.getRemoved()); } - if (updated.isIgnoreRemovedEntities()) { - Set updatedIds = updated.getEntityIds(); - if (updatedIds.isEmpty()) { + if (updated.isSyncWithDb()) { + Set dbEntityIds = updated.getEntityIds(); + if (dbEntityIds.isEmpty()) { + if (entityIds.isEmpty()) { + return false; + } entityIds.clear(); - return false; + return true; } - entityIds.retainAll(updatedIds); - return checkAdded(updatedIds); + boolean retained = entityIds.retainAll(dbEntityIds); + boolean added = checkAdded(dbEntityIds); + return retained || added; } if (updated.isEmpty()) { entityIds.clear(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index c4533aef37..8613e0c062 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -41,6 +42,7 @@ import java.util.Map; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; +@Slf4j public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState { private CalculatedFieldProcessingService cfProcessingService; @@ -67,12 +69,17 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState super.init(restored); if (restored) { cfProcessingService.fetchPropagationArgumentFromDb(ctx, entityId).ifPresent(fromDb -> { - fromDb.setIgnoreRemovedEntities(true); + fromDb.setSyncWithDb(true); var updatedArgs = update(Map.of(PROPAGATION_CONFIG_ARGUMENT, fromDb), ctx); if (updatedArgs.isEmpty()) { return; } - ctx.scheduleReevaluation(0L, actorCtx); + log.warn("[{}][{}] Propagation argument was out of sync during state restore and was reconciled with DB.", ctx.getCfId(), entityId); + var updatedPropagationArgument = (PropagationArgumentEntry) arguments.get(PROPAGATION_CONFIG_ARGUMENT); + if (updatedPropagationArgument.getAdded() != null) { + log.warn("[{}][{}] New propagation entities were detected during restore. Scheduling reevaluation for new entities...", ctx.getCfId(), entityId); + ctx.scheduleReevaluation(0L, actorCtx); + } }); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index e1c7f1198c..4aa7b2b856 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -27,6 +27,7 @@ import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.ai.TbAiNode; import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode; @@ -167,6 +168,14 @@ public class EdgeMsgConstructorUtils { ); public static final Map> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of( + EdgeVersion.V_4_1_0, + Set.of( + TbAiNode.class.getName() + ), + EdgeVersion.V_4_0_0, + Set.of( + TbAiNode.class.getName() + ), EdgeVersion.V_3_9_0, Set.of( TbCalculatedFieldsNode.class.getName() @@ -560,7 +569,7 @@ public class EdgeMsgConstructorUtils { .setEntityIdMSB(entityId.getId().getMostSignificantBits()) .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) .setEntityType(entityId.getEntityType().name()); - long ts = getTs(entityData.getAsJsonObject()); + long ts = extractTs(entityData.getAsJsonObject()); switch (actionType) { case TIMESERIES_UPDATED: try { @@ -613,8 +622,8 @@ public class EdgeMsgConstructorUtils { return builder.build(); } - private static long getTs(JsonObject data) { - if (data.get("ts") != null && !data.get("ts").isJsonNull()) { + private static long extractTs(JsonObject data) { + if (data.has("ts") && data.get("ts").isJsonPrimitive()) { return data.getAsJsonPrimitive("ts").getAsLong(); } return System.currentTimeMillis(); @@ -740,7 +749,7 @@ public class EdgeMsgConstructorUtils { result.sort(Comparator.comparingLong(EdgeEvent::getSeqId)); return result; } catch (Exception e) { - log.warn("Can't merge downlink duplicates, edgeEvents [{}]", edgeEvents, e); + log.info("Can't merge downlink duplicates. Sending downlinks without merge. Original edgeEvents [{}]", edgeEvents, e); return edgeEvents; } } @@ -751,6 +760,9 @@ public class EdgeMsgConstructorUtils { } String bodyStr = JacksonUtil.toString(body); var jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); + if (!jsonObject.has("ts")) { + return new AttrsTs(0L, List.of()); + } long ts = jsonObject.get("ts").getAsLong(); var kv = jsonObject.getAsJsonObject("kv"); List attrs = JsonConverter.convertToAttributes( @@ -761,22 +773,24 @@ public class EdgeMsgConstructorUtils { } private static JsonNode filterAttributesBody(JsonNode body, Map latestByKey) { - if (body == null || latestByKey == null || latestByKey.isEmpty()) { + if (body == null) { return null; } String bodyStr = JacksonUtil.toString(body); JsonObject jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); - long ts = jsonObject.get("ts").getAsLong(); - JsonObject kv = jsonObject.getAsJsonObject("kv"); - for (Iterator> it = kv.entrySet().iterator(); it.hasNext(); ) { - Map.Entry e = it.next(); - Long latestTs = latestByKey.get(e.getKey()); - if (latestTs == null || !latestTs.equals(ts)) { - it.remove(); + if (jsonObject.has("ts") && latestByKey != null && !latestByKey.isEmpty()) { + long ts = jsonObject.get("ts").getAsLong(); + JsonObject kv = jsonObject.getAsJsonObject("kv"); + for (Iterator> it = kv.entrySet().iterator(); it.hasNext(); ) { + Map.Entry e = it.next(); + Long latestTs = latestByKey.get(e.getKey()); + if (latestTs == null || !latestTs.equals(ts)) { + it.remove(); + } + } + if (kv.isEmpty()) { + return null; } - } - if (kv.isEmpty()) { - return null; } return JacksonUtil.toJsonNode(jsonObject.toString()); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 500ad60df0..d65ac6ba31 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -30,7 +30,8 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions that are compatible for the upgrade in 4 digits format (like 4.2.0.0, etc.). // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after a new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1.0"); + // TODO: don't check the "patch" number, since upgrade is not required for patch releases + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1.0", "4.2.1.1", "4.2.1.2"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 787111c28e..63c461dd7c 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1467,7 +1467,7 @@ device: pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}" gateway: # The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch - image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.7-stable}" + image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.8-stable}" # Edges parameters edges: diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index fa30dc9025..334755cea9 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -20,9 +20,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; @@ -76,7 +74,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public void testSimpleCalculatedFieldWhenAllTelemetryPresent() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); postTelemetry(testDevice.getId(), "{\"temperature\":25}"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"deviceTemperature\":40}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -259,15 +257,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testSimpleCalculatedFieldWhenEntityIdIsProfile() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":40}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":40}"); AssetProfile assetProfile = doPost("/api/assetProfile", createAssetProfile("Test Asset Profile"), AssetProfile.class); Asset asset1 = createAsset("Test asset 1", assetProfile.getId()); - doPost("/api/plugins/telemetry/ASSET/" + asset1.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":11}")); + postAttributes(asset1.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":11}"); Asset asset2 = createAsset("Test asset 2", assetProfile.getId()); - doPost("/api/plugins/telemetry/ASSET/" + asset2.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":12}")); + postAttributes(asset2.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":12}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(assetProfile.getId()); @@ -316,7 +314,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z2.get(0).get("value").asText()).isEqualTo("52.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":25}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":25}"); await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -332,7 +330,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z2.get(0).get("value").asText()).isEqualTo("37.0"); }); - doPost("/api/plugins/telemetry/ASSET/" + asset1.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":15}")); + postAttributes(asset1.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":15}"); await().alias("update asset 1 telemetry -> recalculate state only for asset 1").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -348,7 +346,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z2.get(0).get("value").asText()).isEqualTo("37.0"); }); - doPost("/api/plugins/telemetry/ASSET/" + asset2.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":5}")); + postAttributes(asset2.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":5}"); await().alias("update asset 2 telemetry -> recalculate state only for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -365,7 +363,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); Asset asset3 = createAsset("Test asset 3", assetProfile.getId()); - doPost("/api/plugins/telemetry/ASSET/" + asset3.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"y\":13}")); + postAttributes(asset3.getId(), AttributeScope.SERVER_SCOPE, "{\"y\":13}"); Asset finalAsset3 = asset3; await().alias("add new entity to profile -> calculate state for new entity").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -377,7 +375,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(z3.get(0).get("value").asText()).isEqualTo("38.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":20}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":20}"); await().alias("update device telemetry -> recalculate state for all assets").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -403,7 +401,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes asset3.setAssetProfileId(newAssetProfile.getId()); asset3 = doPost("/api/asset", asset3, Asset.class); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"x\":15}")); + postAttributes(testDevice.getId(), AttributeScope.SERVER_SCOPE, "{\"x\":15}"); Asset updatedAsset3 = asset3; await().alias("update device telemetry -> recalculate state for asset 1 and asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -662,7 +660,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(d.get("d").get(0).get("value").asText()).isEqualTo("600"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}")); + postTelemetry(testDevice.getId(), "{\"a\":10}"); await().alias("update telemetry -> save result with ts of 'a' argument").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -674,8 +672,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(keys.get("d").get(0).get("value").asText()).isEqualTo("510"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"b\":20}")); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"c\":30}")); + postTelemetry(testDevice.getId(), "{\"b\":20}"); + postTelemetry(testDevice.getId(), "{\"c\":30}"); await().alias("update telemetry -> save result with latest ts of updated arguments").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -726,7 +724,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}")); + postTelemetry(testDevice.getId(), "{\"m\":1}"); await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -740,7 +738,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes calculatedField.setConfiguration(config); calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}")); + postTelemetry(testDevice.getId(), "{\"m\":2}"); await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -761,15 +759,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Restricted zone polygon (square) String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"allowedZone\":" + allowedPolygon + "}")).andExpect(status().isOk()); - - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "{\"allowedZone\":" + allowedPolygon + "}"); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "{\"restrictedZone\":" + restrictedPolygon + "}"); // Initial device coordinates (inside Allowed, outside Restricted) - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + postTelemetry(device.getId(), "{\"latitude\":50.4730,\"longitude\":30.5050}"); // --- Build CF: GEOFENCING --- CalculatedField cf = new CalculatedField(); @@ -817,8 +811,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesStatus,restrictedZonesStatus", String.class); // --- Update restrictedZone by 'restrictedZone' attribute update - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "{\"restrictedZone\":" + restrictedPolygon + "}"); // --- Assert no transition --- // --- Assert attributes updated with the same values for restrictedZones --- @@ -848,30 +841,17 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; Asset allowedZoneAsset = createAsset("Allowed Zone", null); - doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygon + "}")).andExpect(status().isOk()); + postAttributes(allowedZoneAsset.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + allowedPolygon + "}"); Asset restrictedZoneAsset = createAsset("Restricted Zone", null); - doPost("/api/plugins/telemetry/ASSET/" + restrictedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"zone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + postAttributes(restrictedZoneAsset.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + restrictedPolygon + "}"); // Relations from device to zones - EntityRelation deviceToAllowedZoneRelation = new EntityRelation(); - deviceToAllowedZoneRelation.setFrom(device.getId()); - deviceToAllowedZoneRelation.setTo(allowedZoneAsset.getId()); - deviceToAllowedZoneRelation.setType("AllowedZone"); - - EntityRelation deviceToRestrictedZoneRelation = new EntityRelation(); - deviceToRestrictedZoneRelation.setFrom(device.getId()); - deviceToRestrictedZoneRelation.setTo(restrictedZoneAsset.getId()); - deviceToRestrictedZoneRelation.setType("RestrictedZone"); - - doPost("/api/relation", deviceToAllowedZoneRelation).andExpect(status().isOk()); - doPost("/api/relation", deviceToRestrictedZoneRelation).andExpect(status().isOk()); + createEntityRelation(device.getId(), allowedZoneAsset.getId(), "AllowedZone"); + createEntityRelation(device.getId(), restrictedZoneAsset.getId(), "RestrictedZone"); // Initial device coordinates (inside Allowed, outside Restricted) - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + postTelemetry(device.getId(), "{\"latitude\":50.4730,\"longitude\":30.5050}"); // --- Build CF: GEOFENCING --- CalculatedField cf = new CalculatedField(); @@ -922,8 +902,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); // --- Move the device into Restricted zone (and outside Allowed) --- - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")); + postTelemetry(device.getId(), "{\"latitude\":50.4760,\"longitude\":30.5110}"); // --- Assert transition (LEFT / ENTERED) --- await().alias("transition evaluation after movement") @@ -965,19 +944,13 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes String allowedPolygonA = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; Asset allowedZoneA = createAsset("Allowed Zone A", null); - doPost("/api/plugins/telemetry/ASSET/" + allowedZoneA.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygonA + "}")).andExpect(status().isOk()); + postAttributes(allowedZoneA.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + allowedPolygonA + "}"); // Relation from device to Allowed Zone A - EntityRelation relAllowedA = new EntityRelation(); - relAllowedA.setFrom(device.getId()); - relAllowedA.setTo(allowedZoneA.getId()); - relAllowedA.setType("AllowedZone"); - doPost("/api/relation", relAllowedA).andExpect(status().isOk()); + createEntityRelation(device.getId(), allowedZoneA.getId(), "AllowedZone"); // Initial device coordinates: INSIDE Zone A - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")).andExpect(status().isOk()); + postTelemetry(device.getId(), "{\"latitude\":50.4730,\"longitude\":30.5050}"); // --- Build CF: GEOFENCING with dynamic 'allowedZones' and short scheduled refresh --- CalculatedField cf = new CalculatedField(); @@ -1024,8 +997,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); // --- Move device OUTSIDE Zone A (expect LEFT) --- - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk()); + postTelemetry(device.getId(), "{\"latitude\":50.4760,\"longitude\":30.5110}"); await().alias("outside zone A (LEFT)") .atMost(TIMEOUT, TimeUnit.SECONDS) @@ -1042,21 +1014,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes String allowedPolygonB = "[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"; Asset allowedZoneB = createAsset("Allowed Zone B", null); - doPost("/api/plugins/telemetry/ASSET/" + allowedZoneB.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygonB + "}")).andExpect(status().isOk()); + postAttributes(allowedZoneB.getId(), AttributeScope.SERVER_SCOPE, "{\"zone\":" + allowedPolygonB + "}"); // Add a new relation - EntityRelation relAllowedB = new EntityRelation(); - relAllowedB.setFrom(device.getId()); - relAllowedB.setTo(allowedZoneB.getId()); - relAllowedB.setType("AllowedZone"); - doPost("/api/relation", relAllowedB).andExpect(status().isOk()); + createEntityRelation(device.getId(), allowedZoneB.getId(), "AllowedZone"); awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF); // --- Same coordinates as before, but now we expect INSIDE group status since a new zone is registered --- - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk()); + postTelemetry(device.getId(), "{\"latitude\":50.4760,\"longitude\":30.5110}"); // --- Assert dynamic refresh picks up a new relation and flips status back to INSIDE on the next telemetry update --- await().alias("dynamic refresh rebinds allowedZones") @@ -1079,14 +1045,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Asset asset2 = createAsset("Propagated Asset 2", null); // Create relations FROM assets TO device - EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); - EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); - doPost("/api/relation", rel1).andExpect(status().isOk()); - doPost("/api/relation", rel2).andExpect(status().isOk()); + createEntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + createEntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); // Telemetry on device - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"temperature\":12.5, \"humidity\":85}")).andExpect(status().isOk()); + postTelemetry(device.getId(), "{\"temperature\":12.5, \"humidity\":85}"); // --- Build CF: PROPAGATION with expression --- CalculatedField cf = new CalculatedField(); @@ -1136,8 +1099,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doDelete(deleteUrl).andExpect(status().isOk()); doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/SERVER_SCOPE?keys=testResult").andExpect(status().isOk()); - doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"temperature\":25}")).andExpect(status().isOk()); + postTelemetry(device.getId(), "{\"temperature\":25}"); // --- Assert propagated calculation (expression applied with new temperature argument and one relation removed) --- await().alias("propagation expr mode evaluation after temperature update") @@ -1160,10 +1122,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Asset asset2 = createAsset("Propagated Asset 2", null); // Create relations FROM assets TO device - EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); - EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); - doPost("/api/relation", rel1).andExpect(status().isOk()); - doPost("/api/relation", rel2).andExpect(status().isOk()); + createEntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + createEntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); // Telemetry on device long ts = System.currentTimeMillis() - 300000L; @@ -1245,8 +1205,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); Asset asset3 = createAsset("Propagated Asset 3", null); - EntityRelation rel3 = new EntityRelation(asset3.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); - doPost("/api/relation", rel3).andExpect(status().isOk()); + createEntityRelation(asset3.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); // --- Assert propagated calculation (arguments-only mode after update) --- await().alias("propagation args-only to new entity after relation creation") @@ -1266,7 +1225,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testCalculatedFieldWhenTheSameTelemetryKeysUsed() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":5}")); + postTelemetry(testDevice.getId(), "{\"a\":5}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -1301,7 +1260,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("10"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}")); + postTelemetry(testDevice.getId(), "{\"a\":10}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -1316,8 +1275,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public void testCalculatedFieldWhenBatchOfTelemetrySent() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long now = System.currentTimeMillis(); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5, \"b\":10}}", now - TimeUnit.MINUTES.toMillis(3)))); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":20}}", now - TimeUnit.MINUTES.toMillis(1)))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":5, \"b\":10}}", now - TimeUnit.MINUTES.toMillis(3))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"b\":20}}", now - TimeUnit.MINUTES.toMillis(1))); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -1358,7 +1317,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("15.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format(""" + postTelemetry(testDevice.getId(), String.format(""" [{ "ts": %s, "values": { @@ -1377,7 +1336,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes "a": 8, "b": 300 } - }]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5)))); + }]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5))); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java index 253d02ebe5..66c11a06a6 100644 --- a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -55,6 +55,8 @@ import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTE @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest { + private final String TZ = "Europe/Kyiv"; + private Tenant savedTenant; @Before @@ -93,7 +95,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest public void testCreateCfAndNoTelemetryDuringInterval_checkAggregation() throws Exception { Device device = createDevice("Device", "1234567890111"); - CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L); createConsumptionCF(device.getId(), customInterval, null); long interval = customInterval.getCurrentIntervalDurationMillis(); @@ -113,7 +115,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest public void testCreateCfWithoutWatermark_checkAggregation() throws Exception { Device device = createDevice("Device", "1234567890111"); - CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L); createConsumptionCF(device.getId(), customInterval, null); long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); @@ -156,7 +158,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest public void testCreateCfWithWatermark_checkAggregationDuringWatermark() throws Exception { Device device = createDevice("Device", "1234567890111"); - CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + CustomInterval customInterval = new CustomInterval(TZ, 0L, 5L); Watermark watermark = new Watermark(10); createConsumptionCF(device.getId(), customInterval, watermark); @@ -196,6 +198,51 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest }); } + @Test + public void testSendFutureTelemetry_checkAggregation() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval(TZ, 0L, 2L); + createConsumptionCF(device.getId(), customInterval, null); + + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); + + long tsBeforeInterval = currentIntervalStartTs - 1000; + long tsInInterval_1 = currentIntervalStartTs + 1000; + long tsInInterval_2 = currentIntervalStartTs + 500; + long tsInInterval_3 = currentIntervalStartTs + 200; + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + + await().alias("create CF -> perform aggregation after interval end") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("133"); + }); + + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", currentIntervalStartTs + 4500L)); + + await().alias("update telemetry that belongs to future interval -> check aggregation ") + .atMost(3 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption", "avgConsumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("500"); + assertThat(result.get("consumption").get(0).get("ts").asLong()).isEqualTo(currentIntervalStartTs + 4000L); + assertThat(result.get("avgConsumption").get(0).get("value").asText()).isEqualTo("500"); + assertThat(result.get("avgConsumption").get(0).get("ts").asLong()).isEqualTo(currentIntervalStartTs + 4000L); + }); + } + private CalculatedField createConsumptionCF(EntityId entityId, AggInterval aggInterval, Watermark watermark) { Map arguments = new HashMap<>(); Argument argument = new Argument(); 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 b9c6e17ded..3df3cf1d20 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1143,7 +1143,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void createEntityRelation(EntityId entityIdFrom, EntityId entityIdTo, String typeRelation) throws Exception { EntityRelation relation = new EntityRelation(entityIdFrom, entityIdTo, typeRelation); - doPost("/api/relation", relation); + doPost("/api/relation", relation).andExpect(status().isOk()); } protected void deleteEntityRelation(EntityRelation entityRelation) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index 724b1c622f..2213bb731c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -95,7 +95,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId coapDeviceProfileId; - @Value("${device.connectivity.gateway.image_version:3.7-stable}") + @Value("${device.connectivity.gateway.image_version:3.8-stable}") private String gatewayImageVersion; @Before diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index e32a2e8086..4d8f1508a8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -27,6 +28,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DataConstants; @@ -34,6 +36,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -63,6 +66,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.queue.QueueStats; @@ -70,6 +74,8 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edqs.util.EdqsRocksDb; @@ -216,6 +222,64 @@ public class EntityQueryControllerTest extends AbstractControllerTest { countByQueryAndCheck(countQuery, 97); } + @Test + public void testEDQForSysAdmin() throws Exception { + loginSysAdmin(); + + ObjectNode tenantAttr = JacksonUtil.newObjectNode(); + tenantAttr.put("attr", "tenantAttrValue"); + doPost("/api/plugins/telemetry/TENANT/" + tenantId + "/attributes/" + AttributeScope.SERVER_SCOPE, tenantAttr); + + loginTenantAdmin(); + Device tenantDevice = new Device(); + tenantDevice.setName("device " + StringUtils.randomAlphanumeric(10)); + tenantDevice.setType("default"); + tenantDevice = doPost("/api/device", tenantDevice, Device.class); + + loginSysAdmin(); + TenantProfile tenantProfile = doPost("/api/tenantProfile", createTenantProfile("Test tenant profile"), TenantProfile.class); + + ObjectNode tenantProfileAttr = JacksonUtil.newObjectNode(); + tenantProfileAttr.put("attr", "tenantProfileAttrValue"); + doPost("/api/plugins/telemetry/TENANT_PROFILE/" + tenantProfile.getId() + "/attributes/" + AttributeScope.SERVER_SCOPE, tenantProfileAttr); + + // check tenant telemetry is accessible for sysadmin + SingleEntityFilter filter = new SingleEntityFilter(); + filter.setSingleEntity(AliasEntityId.fromEntityId(tenantId)); + EntityDataSortOrder sortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + List latestValues = List.of(new EntityKey(EntityKeyType.ATTRIBUTE, "attr")); + EntityDataQuery dataQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); + + PageData loadedTenants = findByQueryAndCheck(dataQuery, 1); + String retrievedTenantAttr = loadedTenants.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("attr").getValue(); + assertThat(retrievedTenantAttr).isEqualTo("tenantAttrValue"); + + // check tenant profile telemetry is accessible for sysadmin + filter.setSingleEntity(AliasEntityId.fromEntityId(tenantProfile.getId())); + + PageData loadedTenantProfiles = findByQueryAndCheck(dataQuery, 1); + String retrievedProfileAttr = loadedTenantProfiles.getData().get(0).getLatest().get(EntityKeyType.ATTRIBUTE).get("attr").getValue(); + assertThat(retrievedProfileAttr).isEqualTo("tenantProfileAttrValue"); + + // check other tenant entities are prohibited + filter.setSingleEntity(AliasEntityId.fromEntityId(tenantDevice.getId())); + findByQueryAndCheck(dataQuery, 0); + } + + private TenantProfile createTenantProfile(String name) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName(name); + tenantProfile.setDescription(name + " Test"); + TenantProfileData tenantProfileData = new TenantProfileData(); + tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(tenantProfileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbRuleEngine(false); + return tenantProfile; + } + @Test public void testTenantCountAlarmsByQuery() throws Exception { loginTenantAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 380dec8015..5c3f43b451 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -49,8 +49,6 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.ota.OtaPackageType; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -671,8 +669,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { .atMost(10, TimeUnit.SECONDS) .until(() -> { String urlTemplate = "/api/plugins/telemetry/DEVICE/" + device.getId() + "/keys/attributes/" + scope; - List actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() { - }); + List actualKeys = doGetAsyncTyped(urlTemplate, new TypeReference<>() {}); return actualKeys != null && !actualKeys.isEmpty() && actualKeys.contains(expectedKey); }); diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java index 71722ad2a8..dd393e6849 100644 --- a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java @@ -78,8 +78,6 @@ public class EdgeStatsIntegrationTest extends AbstractEdgeTest { assertEquals(EXPECTED_MSGS_TMP_FAILED, counters.getMsgsTmpFailed().get()); }); - Thread.sleep(1000); - // WHEN edgeStatsService.reportStats(); diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 4594435626..56ded1eda6 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -438,7 +438,7 @@ public class EdgeImitator { Optional result; lock.lock(); try { - result = (Optional) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).findAny(); + result = (Optional) downlinkMsgs.stream().filter(downlinkMsg -> tClass.isAssignableFrom(downlinkMsg.getClass())).findAny(); } finally { lock.unlock(); } @@ -450,7 +450,7 @@ public class EdgeImitator { List result; lock.lock(); try { - result = (List) downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg.getClass().isAssignableFrom(tClass)).collect(Collectors.toList()); + result = (List) downlinkMsgs.stream().filter(downlinkMsg -> tClass.isAssignableFrom(downlinkMsg.getClass())).collect(Collectors.toList()); } finally { lock.unlock(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index 6da4bdc882..d274da2434 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -18,6 +18,9 @@ package org.thingsboard.server.service.cf.ctx.state; import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.geo.PerimeterDefinition; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.EntityId; @@ -33,6 +36,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(MockitoExtension.class) public class GeofencingValueArgumentEntryTest { private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); @@ -46,6 +50,9 @@ public class GeofencingValueArgumentEntryTest { private GeofencingArgumentEntry entry; + @Mock + private CalculatedFieldCtx ctx; + @BeforeEach void setUp() { entry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry)); @@ -58,14 +65,14 @@ public class GeofencingValueArgumentEntryTest { @Test void testUpdateEntryWhenSingleEntryPassed() { - assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); } @Test void testUpdateEntryWhenRollingEntryPassed() { - assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for geofencing argument entry: TS_ROLLING"); } @@ -74,7 +81,7 @@ public class GeofencingValueArgumentEntryTest { void testUpdateEntryWithTheSameTs() { BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 363L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry)); - assertThat(entry.updateEntry(updated)).isFalse(); + assertThat(entry.updateEntry(updated, ctx)).isFalse(); } @Test @@ -83,7 +90,7 @@ public class GeofencingValueArgumentEntryTest { BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, null); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry)); - assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.updateEntry(updated, ctx)).isTrue(); assertThat(entry.getValue()).isInstanceOf(Map.class); Map value = (Map) entry.getValue(); @@ -105,7 +112,7 @@ public class GeofencingValueArgumentEntryTest { BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); - assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.updateEntry(updated, ctx)).isTrue(); assertThat(entry.getValue()).isInstanceOf(Map.class); Map value = (Map) entry.getValue(); @@ -126,7 +133,7 @@ public class GeofencingValueArgumentEntryTest { BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 154L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); - assertThat(entry.updateEntry(updated)).isFalse(); + assertThat(entry.updateEntry(updated, ctx)).isFalse(); } @Test @@ -134,7 +141,7 @@ public class GeofencingValueArgumentEntryTest { BaseAttributeKvEntry newTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 364L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, newTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); - assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.updateEntry(updated, ctx)).isTrue(); } @Test @@ -142,7 +149,7 @@ public class GeofencingValueArgumentEntryTest { BaseAttributeKvEntry oldTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 362L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, oldTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); - assertThat(entry.updateEntry(updated)).isFalse(); + assertThat(entry.updateEntry(updated, ctx)).isFalse(); } @Test @@ -150,7 +157,7 @@ public class GeofencingValueArgumentEntryTest { final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3")); BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone)); - assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.updateEntry(updated, ctx)).isTrue(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java index 12f3e4298d..8a5e3df1ba 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfPropagationArg; import org.thingsboard.server.common.data.id.AssetId; @@ -31,6 +34,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(MockitoExtension.class) public class PropagationArgumentEntryTest { private final AssetId ENTITY_1_ID = new AssetId(UUID.fromString("b0a8637d-6d67-43d5-a483-c0e391afe805")); @@ -39,6 +43,9 @@ public class PropagationArgumentEntryTest { private PropagationArgumentEntry entry; + @Mock + private CalculatedFieldCtx ctx; + @BeforeEach void setUp() { List propagationEntityIds = new ArrayList<>(); @@ -68,14 +75,14 @@ public class PropagationArgumentEntryTest { @Test void testUpdateEntryWhenSingleEntryPassed() { - assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE"); } @Test void testUpdateEntryWhenRollingEntryPassed() { - assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for propagation argument entry: TS_ROLLING"); } @@ -85,7 +92,7 @@ public class PropagationArgumentEntryTest { var newIds = new ArrayList(List.of(ENTITY_3_ID, ENTITY_1_ID)); var updated = new PropagationArgumentEntry(newIds); - boolean changed = entry.updateEntry(updated); + boolean changed = entry.updateEntry(updated, ctx); assertThat(changed).isTrue(); assertThat(entry.getEntityIds()).containsExactlyElementsOf(newIds); @@ -95,7 +102,7 @@ public class PropagationArgumentEntryTest { void testUpdateEntryClearsWhenNewEntryIsEmpty() { var updatedEmpty = new PropagationArgumentEntry(List.of()); - boolean changed = entry.updateEntry(updatedEmpty); + boolean changed = entry.updateEntry(updatedEmpty, ctx); assertThat(changed).isTrue(); assertThat(entry.getEntityIds()).isEmpty(); @@ -106,7 +113,7 @@ public class PropagationArgumentEntryTest { var added = new PropagationArgumentEntry(); added.setAdded(List.of(ENTITY_3_ID)); - boolean changed = entry.updateEntry(added); + boolean changed = entry.updateEntry(added, ctx); assertThat(changed).isTrue(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); @@ -118,7 +125,7 @@ public class PropagationArgumentEntryTest { var added = new PropagationArgumentEntry(); added.setAdded(List.of(ENTITY_2_ID)); - boolean changed = entry.updateEntry(added); + boolean changed = entry.updateEntry(added, ctx); assertThat(changed).isFalse(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); @@ -130,7 +137,7 @@ public class PropagationArgumentEntryTest { var removed = new PropagationArgumentEntry(); removed.setRemoved(ENTITY_2_ID); - boolean changed = entry.updateEntry(removed); + boolean changed = entry.updateEntry(removed, ctx); assertThat(changed).isTrue(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); @@ -142,7 +149,7 @@ public class PropagationArgumentEntryTest { var removed = new PropagationArgumentEntry(); removed.setRemoved(ENTITY_3_ID); - boolean changed = entry.updateEntry(removed); + boolean changed = entry.updateEntry(removed, ctx); assertThat(changed).isFalse(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); @@ -152,72 +159,72 @@ public class PropagationArgumentEntryTest { @Test void testUpdateEntryWhenPartitionStateRestoreAddsMissingIds() { var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID)); - restore.setIgnoreRemovedEntities(true); + restore.setSyncWithDb(true); - boolean changed = entry.updateEntry(restore); + boolean changed = entry.updateEntry(restore, ctx); assertThat(changed).isTrue(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); assertThat(entry.getAdded()).containsExactly(ENTITY_3_ID); assertThat(entry.getRemoved()).isNull(); - assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + assertThat(entry.isSyncWithDb()).isFalse(); } @Test void testUpdateEntryWhenPartitionStateRestoreRemovesStaleIds() { var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); - restore.setIgnoreRemovedEntities(true); + restore.setSyncWithDb(true); - boolean changed = entry.updateEntry(restore); + boolean changed = entry.updateEntry(restore, ctx); - assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op + assertThat(changed).isTrue(); // expected to be changed, so we re-check readiness for the state assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); assertThat(entry.getAdded()).isNull(); assertThat(entry.getRemoved()).isNull(); - assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + assertThat(entry.isSyncWithDb()).isFalse(); } @Test void testUpdateEntryWhenPartitionStateRestoreAddsAndRemoves() { var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_3_ID)); - restore.setIgnoreRemovedEntities(true); + restore.setSyncWithDb(true); - boolean changed = entry.updateEntry(restore); + boolean changed = entry.updateEntry(restore, ctx); assertThat(changed).isTrue(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_3_ID); assertThat(entry.getAdded()).containsExactly(ENTITY_3_ID); assertThat(entry.getRemoved()).isNull(); - assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + assertThat(entry.isSyncWithDb()).isFalse(); } @Test void testUpdateEntryWhenPartitionStateRestoreNoChanges() { var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID)); - restore.setIgnoreRemovedEntities(true); + restore.setSyncWithDb(true); - boolean changed = entry.updateEntry(restore); + boolean changed = entry.updateEntry(restore, ctx); assertThat(changed).isFalse(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); assertThat(entry.getAdded()).isNull(); assertThat(entry.getRemoved()).isNull(); - assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + assertThat(entry.isSyncWithDb()).isFalse(); } @Test void testUpdateEntryWhenPartitionStateRestoreEmptySet() { var restore = new PropagationArgumentEntry(List.of()); - restore.setIgnoreRemovedEntities(true); + restore.setSyncWithDb(true); - boolean changed = entry.updateEntry(restore); + boolean changed = entry.updateEntry(restore, ctx); - assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op + assertThat(changed).isTrue(); // expected to be changed, so we re-check readiness for the state assertThat(entry.getEntityIds()).isEmpty(); assertThat(entry.getAdded()).isNull(); assertThat(entry.getRemoved()).isNull(); - assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + assertThat(entry.isSyncWithDb()).isFalse(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index a9bf6d54d1..2ceafd8c0a 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -317,7 +317,49 @@ public class PropagationCalculatedFieldStateTest { } @Test - void testPropapagationStateInitWithRestoredSetToTrue() { + void testPropagationStateInitRestoredTrueNoDbChanges_noReevaluation() { + initCtxAndState(false); + + // existing state already matches DB snapshot + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + + // DB returns the same set + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(propagationArgEntry)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropagationStateInitRestoredTrueOnlyRemovals_noReevaluation() { + initCtxAndState(false); + + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + + // DB snapshot contains only ASSET_ID_1 (ASSET_ID_2 should be removed) + var fromDb = new PropagationArgumentEntry(List.of(ASSET_ID_1)); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropagationStateIsNotReadyInitRestoredTrueAddedEntities_scheduleReevaluation() { initCtxAndState(false); Map initArgs = Map.of( TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, @@ -335,6 +377,75 @@ public class PropagationCalculatedFieldStateTest { verify(ctx).scheduleReevaluation(0L, state.getActorCtx()); } + @Test + void testPropagationStateInitRestoredTrueAddedEntities_scheduleReevaluation() { + initCtxAndState(false); + + // existing missing ASSET_ID_2 + var existing = new PropagationArgumentEntry(List.of(ASSET_ID_1)); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, existing + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); + + // DB snapshot contains both (ASSET_ID_2 should be "added") + var fromDb = new PropagationArgumentEntry(List.of(ASSET_ID_1, ASSET_ID_2)); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx).scheduleReevaluation(0L, state.getActorCtx()); + } + + @Test + void testPropagationStateInitRestoredTrueDbEmpty_clearsState_noReevaluation() { + initCtxAndState(false); + + // existing has something + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + + // DB snapshot empty -> clear + var fromDb = new PropagationArgumentEntry(Collections.emptyList()); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropagationStateInitRestoredTrueBothEmpty_noReevaluation() { + initCtxAndState(false); + + // Existing propagation argument is empty + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList()) + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isFalse(); + + // DB snapshot is also empty + var fromDb = new PropagationArgumentEntry(Collections.emptyList()); + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(fromDb)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setTenantId(TENANT_ID); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java index cc60b249ac..c357e2c30b 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -30,10 +33,14 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(MockitoExtension.class) public class RelatedEntitiesArgumentEntryTest { private RelatedEntitiesArgumentEntry entry; + @Mock + private CalculatedFieldCtx ctx; + private final DeviceId device1 = new DeviceId(UUID.fromString("1984e5f4-9ff0-4187-84ae-e4438bba4c8a")); private final DeviceId device2 = new DeviceId(UUID.fromString("937fc062-1a9d-438f-aa22-55a93fc908b7")); @@ -50,7 +57,7 @@ public class RelatedEntitiesArgumentEntryTest { @Test void testUpdateEntryWhenNotAggEntryPassed() { - assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for aggregation argument entry: " + ArgumentEntryType.TS_ROLLING); } @@ -65,7 +72,7 @@ public class RelatedEntitiesArgumentEntryTest { device4, new SingleValueArgumentEntry(device4, new BasicTsKvEntry(ts - 60, new LongDataEntry("key", 23L), 7L)) ), false); - assertThat(entry.updateEntry(relatedEntitiesArgumentEntry)).isTrue(); + assertThat(entry.updateEntry(relatedEntitiesArgumentEntry, ctx)).isTrue(); Map aggInputs = entry.getEntityInputs(); assertThat(aggInputs.size()).isEqualTo(4); @@ -79,7 +86,7 @@ public class RelatedEntitiesArgumentEntryTest { SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); - assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); + assertThat(entry.updateEntry(singleEntityArgumentEntry, ctx)).isTrue(); Map aggInputs = entry.getEntityInputs(); assertThat(aggInputs.size()).isEqualTo(3); @@ -90,7 +97,7 @@ public class RelatedEntitiesArgumentEntryTest { void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() { SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); - assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); + assertThat(entry.updateEntry(singleEntityArgumentEntry, ctx)).isTrue(); Map aggInputs = entry.getEntityInputs(); assertThat(aggInputs.size()).isEqualTo(2); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java index 4ada355054..e2d287c778 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.server.common.data.kv.JsonDataEntry; @@ -29,10 +32,14 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(MockitoExtension.class) public class SingleValueArgumentEntryTest { private SingleValueArgumentEntry entry; + @Mock + private CalculatedFieldCtx ctx; + private final long ts = System.currentTimeMillis(); @BeforeEach @@ -47,48 +54,48 @@ public class SingleValueArgumentEntryTest { @Test void testUpdateEntryWhenRollingEntryPassed() { - assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for single value argument entry: " + ArgumentEntryType.TS_ROLLING); } @Test void testUpdateEntryWithTheSameTs() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L))).isFalse(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L), ctx)).isFalse(); } @Test void testUpdateEntryWithTheSameTsAndDifferentVersion() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 364L))).isTrue(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 364L), ctx)).isTrue(); } @Test void testUpdateEntryWhenNewVersionIsNull() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null))).isTrue(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null), ctx)).isTrue(); assertThat(entry.getValue()).isEqualTo(13L); assertThat(entry.getVersion()).isNull(); } @Test void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 369L))).isTrue(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 369L), ctx)).isTrue(); assertThat(entry.getValue()).isEqualTo(18L); assertThat(entry.getVersion()).isEqualTo(369L); } @Test void testUpdateEntryWhenNewVersionIsLessThanCurrent() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 234L))).isFalse(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 18L), 234L), ctx)).isFalse(); } @Test void testUpdateEntryWhenValueWasNotChanged() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L), ctx)).isTrue(); } @Test void testUpdateEntryWithOldTs() { - assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts - 10, new LongDataEntry("key", 14L), 365L))).isFalse(); + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts - 10, new LongDataEntry("key", 14L), 365L), ctx)).isFalse(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java index b1f8063857..94b2a7b389 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntryTest.java @@ -17,6 +17,9 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; @@ -26,10 +29,14 @@ import java.util.TreeMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(MockitoExtension.class) public class TsRollingArgumentEntryTest { private TsRollingArgumentEntry entry; + @Mock + private CalculatedFieldCtx ctx; + private final long ts = System.currentTimeMillis(); @BeforeEach @@ -51,7 +58,7 @@ public class TsRollingArgumentEntryTest { void testUpdateEntryWhenSingleValueEntryPassed() { SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new DoubleDataEntry("key", 23.0), 123L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(4); assertThat(entry.getTsRecords().get(ts - 10)).isEqualTo(23.0); } @@ -64,7 +71,7 @@ public class TsRollingArgumentEntryTest { values.put(ts - 5, 1.0); newEntry.setTsRecords(values); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(5); assertThat(entry.getTsRecords()).isEqualTo(Map.of( ts - 40, 10.0, @@ -79,7 +86,7 @@ public class TsRollingArgumentEntryTest { void testUpdateEntryWhenValueIsNotNumber() { SingleValueArgumentEntry newEntry = new SingleValueArgumentEntry(ts - 10, new StringDataEntry("key", "string"), 123L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords().get(ts - 10)).isNaN(); } @@ -93,7 +100,7 @@ public class TsRollingArgumentEntryTest { newEntry.setTsRecords(values); entry = new TsRollingArgumentEntry(3, 30000L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(1); assertThat(entry.getTsRecords()).isEqualTo(Map.of( ts - 5, 0.0 @@ -111,7 +118,7 @@ public class TsRollingArgumentEntryTest { newEntry.setTsRecords(values); entry = new TsRollingArgumentEntry(3, 30000L); - assertThat(entry.updateEntry(newEntry)).isTrue(); + assertThat(entry.updateEntry(newEntry, ctx)).isTrue(); assertThat(entry.getTsRecords()).hasSize(3); assertThat(entry.getTsRecords()).isEqualTo(Map.of( ts - 18, 0.0, diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index ebb8305871..1e268e3bcb 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -230,10 +230,35 @@ public class EdgeMsgConstructorUtilsTest { Assertions.assertEquals(8, getIntValue(assetMergedAttrAD.getBody(), "d")); } + @Test + public void testMergeDownlinkDuplicates_attrBodyHasNoTs_returnOriginalList() { + UUID deviceId = UUID.randomUUID(); + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + + var deviceAttrUpdate1 = createEdgeEvent(tenantId, 1, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":1,\"b\":1,\"d\":1}")); + var deviceAttrUpdate2 = createEdgeEvent(tenantId, 2, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":2,\"b\":2,\"c\":2}")); + var deviceAttrUpdate3 = createEdgeEvent(tenantId, 3, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBodyWithoutTs("{\"a\":3,\"d\":3}")); + + List input = List.of(deviceAttrUpdate1, deviceAttrUpdate2, deviceAttrUpdate3); + List merged = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(input); + + Assertions.assertEquals(3, merged.size()); + Assertions.assertEquals(deviceAttrUpdate1, merged.get(0)); + Assertions.assertEquals(deviceAttrUpdate2, merged.get(1)); + Assertions.assertEquals(deviceAttrUpdate3, merged.get(2)); + } + private Integer getIntValue(JsonNode body, String key) { return body.get("kv").get(key) != null ? body.get("kv").get(key).asInt() : null; } + private static JsonNode createAttrBodyWithoutTs(String kvJson) { + return JacksonUtil.toJsonNode("{\"kv\":" + kvJson + "}"); + } + private static JsonNode createAttrBody(long ts, String kvJson) { return JacksonUtil.toJsonNode("{\"ts\":" + ts + ",\"kv\":" + kvJson + "}"); } diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 7806215a51..0a529e57f5 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -107,6 +107,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getSuccessfulCount()).isEqualTo(tasksCount); assertThat(job.getResult().getResults()).isEmpty(); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); }); } @@ -133,6 +135,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(failure.getError()).isEqualTo("error3"); // last error }); assertThat(jobResult.getCompletedCount()).isEqualTo(jobResult.getTotalCount()); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); }); } @@ -168,6 +172,9 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); + assertThat(job.getResult().getCancellationTs()).isPositive(); }); } @@ -194,6 +201,9 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getTotalCount()).isEqualTo(1); assertThat(job.getResult().getDiscardedCount()).isEqualTo(1); assertThat(job.getResult().getFailedCount()).isZero(); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); + assertThat(job.getResult().getCancellationTs()).isPositive(); }); } @@ -222,6 +232,9 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount - 1); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); + assertThat(job.getResult().getCancellationTs()).isPositive(); }); } @@ -275,6 +288,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); assertThat(job.getEntityId()).isEqualTo(jobEntity.getId()); assertThat(job.getEntityName()).isEqualTo(jobEntity.getName()); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); } }); @@ -385,6 +400,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getTotalCount()).isEqualTo(submittedTasks); assertThat(job.getResult().getFailedCount()).isZero(); assertThat(job.getResult().getDiscardedCount()).isZero(); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); }); } @@ -425,6 +442,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(job.getResult().getTotalCount()).isEqualTo(totalTasksCount); assertThat(job.getResult().getResults()).isEmpty(); assertThat(job.getConfiguration().getToReprocess()).isNullOrEmpty(); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); }); } @@ -449,6 +468,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks); assertThat(jobResult.getFailedCount()).isEqualTo(failedTasks + permanentlyFailedTasks); assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); + assertThat(jobResult.getStartTs()).isPositive(); + assertThat(jobResult.getFinishTs()).isPositive(); List failures = getFailures(jobResult); for (int i = 0, taskNumber = successfulTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { @@ -467,6 +488,8 @@ public class JobManagerTest extends AbstractControllerTest { assertThat(jobResult.getSuccessfulCount()).isEqualTo(successfulTasks + failedTasks); assertThat(jobResult.getFailedCount()).isEqualTo(permanentlyFailedTasks); assertThat(jobResult.getTotalCount()).isEqualTo(totalTasksCount); + assertThat(jobResult.getStartTs()).isPositive(); + assertThat(jobResult.getFinishTs()).isPositive(); List failures = getFailures(jobResult); for (int i = 0, taskNumber = successfulTasks + failedTasks + 1; taskNumber <= totalTasksCount; i++, taskNumber++) { @@ -478,6 +501,23 @@ public class JobManagerTest extends AbstractControllerTest { }); } + @Test + public void testSubmitJob_zeroTasks() { + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(0) + .build()).getId(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + Job job = findJobById(jobId); + assertThat(job.getStatus()).isEqualTo(JobStatus.COMPLETED); + assertThat(job.getResult().getSuccessfulCount()).isEqualTo(0); + assertThat(job.getResult().getResults()).isEmpty(); + assertThat(job.getResult().getCompletedCount()).isEqualTo(0); + assertThat(job.getResult().getStartTs()).isPositive(); + assertThat(job.getResult().getFinishTs()).isPositive(); + }); + } + private Job submitJob(DummyJobConfiguration configuration) { return submitJob(configuration, "test-job"); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 36cf1639db..7b3102a55c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -289,6 +289,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public String getObjectIdByKeyNameFromProfile(LwM2mClient client, String keyName) { Lwm2mDeviceProfileTransportConfiguration profile = getProfile(client.getRegistration()); + if (profile == null) throw new IllegalArgumentException(keyName + " is not configured in the device profile! Device profile is null"); for (Map.Entry entry : profile.getObserveAttr().getKeyName().entrySet()) { String k = entry.getKey(); String v = entry.getValue(); @@ -347,6 +348,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { PowerMode powerMode = lwM2MClient.getPowerMode(); if (powerMode == null) { Lwm2mDeviceProfileTransportConfiguration deviceProfile = getProfile(lwM2MClient.getRegistration()); + if (deviceProfile == null) return null; powerMode = deviceProfile.getClientLwM2mSettings().getPowerMode(); } return powerMode; @@ -377,8 +379,8 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } } } else { - log.warn("Device profile not found! The device profile ID is null. Return Lwm2mDeviceProfileTransportConfiguration with default."); - result = new Lwm2mDeviceProfileTransportConfiguration(); + log.warn("Device profile not found! The device profile ID is null. Return Lwm2mDeviceProfileTransportConfiguration with null."); + result = null; } return result; } @@ -413,6 +415,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { OtherConfiguration profileSettings = null; if (powerMode == null && client.getProfileId() != null) { var clientProfile = getProfile(client.getRegistration()); + if (clientProfile == null) return true; profileSettings = clientProfile.getClientLwM2mSettings(); powerMode = profileSettings.getPowerMode(); } @@ -459,8 +462,10 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { OtherConfiguration profileSettings = null; if (powerMode == null && client.getProfileId() != null) { var clientProfile = getProfile(client.getRegistration()); - profileSettings = clientProfile.getClientLwM2mSettings(); - powerMode = profileSettings.getPowerMode(); + if (clientProfile != null) { + profileSettings = clientProfile.getClientLwM2mSettings(); + powerMode = profileSettings.getPowerMode(); + } } if (powerMode == null || PowerMode.DRX.equals(powerMode)) { client.updateLastUplinkTime(); @@ -515,9 +520,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { timeout = client.getEdrxCycle(); } else { var clientProfile = getProfile(client.getRegistration()); - OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings(); - if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) { - timeout = clientLwM2mSettings.getEdrxCycle(); + if (clientProfile != null) { + OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings(); + if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) { + timeout = clientLwM2mSettings.getEdrxCycle(); + } } } if (timeout == null || timeout == 0L) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 325f1e2280..cc89fb8fd8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -200,7 +200,8 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl attributesToFetch.add(SOFTWARE_URL); } - var clientSettings = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings(); + var clientProfile = clientContext.getProfile(client.getRegistration()); + var clientSettings = clientProfile != null ? clientProfile.getClientLwM2mSettings() : null; if (clientSettings != null) { initFwStrategy(client, clientSettings); initSwStrategy(client, clientSettings); @@ -532,7 +533,8 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } else { strategy = info.getDeliveryMethod() == FirmwareDeliveryMethod.PULL.code ? LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL : LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY; } - Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); + var clientProfile = clientContext.getProfile(client.getRegistration()); + Boolean useObject19ForOtaInfo = clientProfile != null ? clientProfile.getClientLwM2mSettings().getUseObject19ForOtaInfo() : null; if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){ sendInfoToObject19ForOta(client, FW_INFO_19_INSTANCE_ID, response, otaPackageId); } @@ -558,7 +560,8 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) { UUID otaPackageId = new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB()); LwM2MSoftwareUpdateStrategy strategy = info.getStrategy(); - Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); + var clientProfile = clientContext.getProfile(client.getRegistration()); + Boolean useObject19ForOtaInfo = clientProfile != null ? clientProfile.getClientLwM2mSettings().getUseObject19ForOtaInfo() : null; if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){ sendInfoToObject19ForOta(client, SW_INFO_19_INSTANCE_ID, response, otaPackageId); } @@ -647,8 +650,9 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl LwM2MClientSwOtaInfo info = otaInfoStore.getSw(endpoint); if (info == null) { var profile = clientContext.getProfile(client.getRegistration()); - info = new LwM2MClientSwOtaInfo(endpoint, profile.getClientLwM2mSettings().getSwUpdateResource(), - LwM2MSoftwareUpdateStrategy.fromStrategySwByCode(profile.getClientLwM2mSettings().getSwUpdateStrategy())); + OtherConfiguration clientLwM2mSettings = profile == null ? new OtherConfiguration() : profile.getClientLwM2mSettings(); + info = new LwM2MClientSwOtaInfo(endpoint, clientLwM2mSettings.getSwUpdateResource(), + LwM2MSoftwareUpdateStrategy.fromStrategySwByCode(clientLwM2mSettings.getSwUpdateStrategy())); update(info); } return info; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 0ea3dbcced..f8ab16502f 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -420,7 +420,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl }); if (!clients.isEmpty()) { var oldProfile = clientContext.getProfile(clients.get(0).getRegistration()); - this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile); + if (oldProfile != null) this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile); } } catch (Exception e) { log.warn("[{}] failed to update profile: {} [{}]", deviceProfile.getId(), e.getMessage(), deviceProfile); @@ -725,8 +725,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void onDeviceUpdate(LwM2mClient lwM2MClient, Device device, Optional deviceProfileOpt) { var oldProfile = clientContext.getProfile(lwM2MClient.getRegistration()); - deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile)); - lwM2MClient.onDeviceUpdate(device, deviceProfileOpt); + if (oldProfile != null) { + deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile)); + lwM2MClient.onDeviceUpdate(device, deviceProfileOpt); + } } /** @@ -740,45 +742,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl Set paths = updateResource.getPaths(); ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); var profile = clientContext.getProfile(registration); - List resultAttributes = new ArrayList<>(); - Set attributes = profile.getObserveAttr().getAttribute().stream() - .filter(paths::contains) - .collect(Collectors.toSet()); - if (!attributes.isEmpty()){ - attributes.stream() - .map(attr -> this.getKvToThingsBoard(attr, registration)) - .filter(Objects::nonNull) - .forEach(resultAttributes::add); - } - List resultTelemetries = new ArrayList<>(); - Set telemetries = profile.getObserveAttr().getTelemetry().stream() - .filter(paths::contains) - .collect(Collectors.toSet()); - if (!telemetries.isEmpty()){ - telemetries.stream() - .map(telemetry -> this.getKvToThingsBoard(telemetry, registration)) - .filter(Objects::nonNull) - .forEach(resultTelemetries::add); - } - if (resultAttributes.size() > 0) { - results.setResultAttributes(resultAttributes); - } - if (resultTelemetries.size() > 0) { - results.setResultTelemetries(resultTelemetries); - } - return results; - } - - private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, Set path) { - if (!path.isEmpty()) { - ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); - var profile = clientContext.getProfile(registration); + if (profile != null) { List resultAttributes = new ArrayList<>(); Set attributes = profile.getObserveAttr().getAttribute().stream() - .map(LwM2MTransportUtil::fromVersionedIdToObjectId) - .filter(path::contains) + .filter(paths::contains) .collect(Collectors.toSet()); - if (!attributes.isEmpty()){ + if (!attributes.isEmpty()) { attributes.stream() .map(attr -> this.getKvToThingsBoard(attr, registration)) .filter(Objects::nonNull) @@ -786,10 +755,9 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } List resultTelemetries = new ArrayList<>(); Set telemetries = profile.getObserveAttr().getTelemetry().stream() - .map(LwM2MTransportUtil::fromVersionedIdToObjectId) - .filter(path::contains) + .filter(paths::contains) .collect(Collectors.toSet()); - if (!telemetries.isEmpty()){ + if (!telemetries.isEmpty()) { telemetries.stream() .map(telemetry -> this.getKvToThingsBoard(telemetry, registration)) .filter(Objects::nonNull) @@ -801,14 +769,15 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl if (resultTelemetries.size() > 0) { results.setResultTelemetries(resultTelemetries); } - return results; } - return null; + return results; } private TransportProtos.KeyValueProto getKvToThingsBoard(String pathIdVer, Registration registration) { LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint()); - Map names = clientContext.getProfile(lwM2MClient.getRegistration()).getObserveAttr().getKeyName(); + var clientProfile = clientContext.getProfile(lwM2MClient.getRegistration()); + if (clientProfile == null) return null; + Map names = clientProfile.getObserveAttr().getKeyName(); if (names != null && names.containsKey(pathIdVer)) { String resourceName = names.get(pathIdVer); if (resourceName != null && !resourceName.isEmpty()) { @@ -899,10 +868,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void onDeviceProfileUpdate(List clients, Lwm2mDeviceProfileTransportConfiguration oldProfileTransportConfiguration, DeviceProfile deviceProfile) { if (clientContext.profileUpdate(deviceProfile) != null) { var newProfileTransportConfiguration = clientContext.getProfile(clients.get(0).getRegistration()); - ParametersUpdateAnalyzeResult parametersUpdate = getParametersUpdate(oldProfileTransportConfiguration, newProfileTransportConfiguration); - ParametersObserveAnalyzeResult parametersObserve = getParametersObserve(oldProfileTransportConfiguration.getObserveAttr(), newProfileTransportConfiguration.getObserveAttr(), deviceProfile.getId().getId()); - compareAndSetWriteAttributesObservations(clients, parametersUpdate, parametersObserve); - updateValueOta(clients, newProfileTransportConfiguration, oldProfileTransportConfiguration); + if (newProfileTransportConfiguration != null) { + ParametersUpdateAnalyzeResult parametersUpdate = getParametersUpdate(oldProfileTransportConfiguration, newProfileTransportConfiguration); + ParametersObserveAnalyzeResult parametersObserve = getParametersObserve(oldProfileTransportConfiguration.getObserveAttr(), newProfileTransportConfiguration.getObserveAttr(), deviceProfile.getId().getId()); + compareAndSetWriteAttributesObservations(clients, parametersUpdate, parametersObserve); + updateValueOta(clients, newProfileTransportConfiguration, oldProfileTransportConfiguration); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index 6f22e95c87..e4f2796d3b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -85,7 +85,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService private String mqttsPemCertFile; @Value("${device.connectivity.coaps.pem_cert_file:}") private String coapsPemCertFile; - @Value("${device.connectivity.gateway.image_version:3.7-stable}") + @Value("${device.connectivity.gateway.image_version:3.8-stable}") private String gatewayImageVersion; @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index b512f353f5..d9deb5d891 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -135,7 +135,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe long startNs = System.nanoTime(); PageData result; - if (edqsService.isApiEnabled() && validForEdqs(query)) { + if (edqsService.isApiEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { EdqsRequest request = EdqsRequest.builder() .entityDataQuery(query) .build(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 06f219ca3d..f97805285c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -154,6 +154,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi job.setStatus(COMPLETED); publishEvent = true; } + if (result.getTotalCount() == 0 && lastFinishTs == 0) { + lastFinishTs = System.currentTimeMillis(); + } result.setFinishTs(lastFinishTs); job.getConfiguration().setToReprocess(null); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 8f3fe71f35..45515e16bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -540,7 +540,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } private String buildPermissionQuery(SqlQueryContext ctx, EntityFilter entityFilter) { - if (ctx.isIgnorePermissionCheck()) { + if (ctx.isIgnorePermissionCheck() || (ctx.getTenantId().isSysTenantId() && + (ctx.getEntityType() == EntityType.TENANT || ctx.getEntityType() == EntityType.TENANT_PROFILE))) { return "1=1"; } switch (entityFilter.getType()) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index ed791ec27e..441d4bb188 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -72,6 +72,7 @@ import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import java.nio.ByteBuffer; @@ -105,6 +106,8 @@ public class DeviceServiceTest extends AbstractServiceTest { @Autowired TenantProfileService tenantProfileService; @Autowired + TbTenantProfileCache tenantProfileCache; + @Autowired private CalculatedFieldService calculatedFieldService; @Autowired private PlatformTransactionManager platformTransactionManager; @@ -150,6 +153,7 @@ public class DeviceServiceTest extends AbstractServiceTest { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(Long.MAX_VALUE).build()); tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + tenantProfileCache.evict(defaultTenantProfile.getId()); Device device = this.saveDevice(tenantId, "My device"); deleteDevice(tenantId, device); @@ -185,6 +189,7 @@ public class DeviceServiceTest extends AbstractServiceTest { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(1).build()); tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + tenantProfileCache.evict(defaultTenantProfile.getId()); Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java index 48a8792452..79175d1d99 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import java.nio.ByteBuffer; @@ -75,6 +76,8 @@ public class OtaPackageServiceTest extends AbstractServiceTest { OtaPackageService otaPackageService; @Autowired TenantProfileService tenantProfileService; + @Autowired + TbTenantProfileCache tenantProfileCache; @Before public void before() { @@ -95,6 +98,7 @@ public class OtaPackageServiceTest extends AbstractServiceTest { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxOtaPackagesInBytes(DATA_SIZE).build()); tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + tenantProfileCache.evict(defaultTenantProfile.getId()); Assert.assertEquals(0, otaPackageService.sumDataSizeByTenantId(tenantId)); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 36f0ff5647..1490b85100 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -485,7 +485,7 @@ public class TestRestClient { .as(DeviceProfile.class); } - public void deleteDeviseProfile(DeviceProfileId deviceProfileId) { + public void deleteDeviceProfile(DeviceProfileId deviceProfileId) { given().spec(requestSpec) .delete("/api/deviceProfile/{deviceProfileId}", deviceProfileId.getId()) .then() diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 43fcbda5af..353302cb82 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -81,20 +82,22 @@ public class CalculatedFieldTest extends AbstractContainerTest { public final int TIMEOUT = 60; public final int POLL_INTERVAL = 1; - private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; + private final String exampleScript = """ + var avgTemperature = temperature.mean(); // Get average temperature + var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin + + // Estimate air pressure based on altitude + var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); + + // Air density formula + var airDensity = pressure / (287.05 * temperatureK); + + return { + "airDensity": toFixed(airDensity, 2) + } + """; - private final String exampleScript = "var avgTemperature = temperature.mean(); // Get average temperature\n" + - " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + - "\n" + - " // Estimate air pressure based on altitude\n" + - " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + - "\n" + - " // Air density formula\n" + - " var airDensity = pressure / (287.05 * temperatureK);\n" + - "\n" + - " return {\n" + - " \"airDensity\": toFixed(airDensity, 2)\n" + - " };"; + private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; private TenantId tenantId; private UserId tenantAdminId; @@ -117,26 +120,28 @@ public class CalculatedFieldTest extends AbstractContainerTest { tenantId = testRestClient.postTenant(EntityPrototypes.defaultTenantPrototype("Tenant")).getId(); tenantAdminId = testRestClient.createUserAndLogin(defaultTenantAdmin(tenantId, "tenantAdmin@thingsboard.org"), "tenant"); + } - deviceProfileId = testRestClient.postDeviceProfile(defaultDeviceProfile("Device Profile 1")).getId(); - device = testRestClient.postDevice(deviceToken, createDevice("Device 1", deviceProfileId)); + @BeforeMethod + public void beforeMethod() { + testRestClient.getAndSetUserToken(tenantAdminId); - assetProfileId = testRestClient.postAssetProfile(defaultAssetProfile("Asset Profile 1")).getId(); - asset = testRestClient.postAsset(createAsset("Asset 1", assetProfileId)); + deviceProfileId = testRestClient.postDeviceProfile(defaultDeviceProfile("Device Profile")).getId(); + device = testRestClient.postDevice(deviceToken, createDevice("Device", deviceProfileId)); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); - testRestClient.postTelemetryAttribute(device.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + assetProfileId = testRestClient.postAssetProfile(defaultAssetProfile("Asset Profile")).getId(); + asset = testRestClient.postAsset(createAsset("Asset", assetProfileId)); + } - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}")); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}")); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}")); + @AfterMethod + public void tearDown() { + testRestClient.getAndSetUserToken(tenantAdminId); - testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); - } + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteDeviceProfile(deviceProfileId); - @BeforeMethod - public void beforeMethod() { - testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + testRestClient.deleteAsset(asset.getId()); + testRestClient.deleteAssetProfile(assetProfileId); } @AfterClass @@ -148,12 +153,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testPerformInitialCalculationForSimpleType() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); - - CalculatedField savedCalculatedField = createSimpleCalculatedField(); - testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -169,10 +172,21 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testChangeConfigArgument() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); + testRestClient.postTelemetryAttribute(device.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); Argument savedArgument = ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).getArguments().get("T"); @@ -193,10 +207,19 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testChangeConfigOutput() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); AttributesOutput output = new AttributesOutput(); output.setScope(SERVER_SCOPE); @@ -219,12 +242,21 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testChangeConfigExpression() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + savedCalculatedField.setName("F to C"); ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setExpression("(T - 32) / 1.8"); testRestClient.postCalculatedField(savedCalculatedField); @@ -243,10 +275,19 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testTelemetryUpdated() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(device.getId()); - CalculatedField savedCalculatedField = createSimpleCalculatedField(); + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":30}")); @@ -264,8 +305,8 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testEntityIdIsProfile() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); @@ -283,46 +324,61 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testEntityAddedAndDeleted() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + waitInitialTelemetry(device.getId(), "temperature"); CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); - String newDeviceToken = "mmmXRIVRsq9lbnTP2XBE"; - Device newDevice = testRestClient.postDevice(newDeviceToken, createDevice("Device 2", deviceProfileId)); + await().alias("create CF -> perform initial calculation for device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + + String deviceToken2 = "beeUmKVqsq3lvnovt6BE"; + Device device2 = testRestClient.postDevice(deviceToken2, createDevice("Device 2", deviceProfileId)); await().alias("create device by profile -> perform initial calculation for new device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { // used default value since telemetry is not present - JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device2.getId()); assertThat(fahrenheitTemp).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); DeviceProfile newDeviceProfile = testRestClient.postDeviceProfile(defaultDeviceProfile("Test Profile")); - newDevice.setDeviceProfileId(newDeviceProfile.getId()); - testRestClient.postDevice(newDeviceToken, newDevice); + device2.setDeviceProfileId(newDeviceProfile.getId()); + testRestClient.postDevice(deviceToken2, device2); - testRestClient.postTelemetry(newDeviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + testRestClient.postTelemetry(deviceToken2, JacksonUtil.toJsonNode("{\"temperature\":25}")); await().alias("update telemetry -> no updates").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device2.getId()); assertThat(fahrenheitTemp).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + testRestClient.deleteDeviceIfExists(device2.getId()); + testRestClient.deleteDeviceProfileIfExists(newDeviceProfile); } @Test public void testEntityIdIsProfileAndRefEntityIsCommon() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}")); + waitInitialTelemetry(device.getId(), "temperatureInF"); + + testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); CalculatedField savedCalculatedField = createScriptCalculatedField(deviceProfileId, asset.getId()); @@ -351,9 +407,6 @@ public class CalculatedFieldTest extends AbstractContainerTest { @Test public void testPerformSerialsOfCalculationsForGeofencingType() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); - // Device and initial coords (inside Allowed, outside Restricted) String deviceToken = "geoDeviceTokenA"; Device device = testRestClient.postDevice(deviceToken, createDevice("GF Device", deviceProfileId)); @@ -433,13 +486,13 @@ public class CalculatedFieldTest extends AbstractContainerTest { }); testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteAsset(allowed.getId()); + testRestClient.deleteAsset(restricted.getId()); } @Test public void testPropagationCalculatedField_withExpression() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); - // --- Arrange entities --- String deviceToken = "propagationDeviceTokenA"; Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device With Expression", deviceProfileId)); @@ -516,13 +569,13 @@ public class CalculatedFieldTest extends AbstractContainerTest { }); testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteAsset(asset1.getId()); + testRestClient.deleteAsset(asset2.getId()); } @Test public void testPropagationCalculatedField_withoutExpression() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); - // --- Arrange entities --- String deviceToken = "propagationDeviceTokenB"; Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device Without Expression", deviceProfileId)); @@ -601,13 +654,13 @@ public class CalculatedFieldTest extends AbstractContainerTest { }); testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + testRestClient.deleteDeviceIfExists(device.getId()); + testRestClient.deleteAsset(asset1.getId()); + testRestClient.deleteAsset(asset2.getId()); } @Test public void testRelatedEntitiesAggregationCalculatedField() { - // login tenant admin - testRestClient.getAndSetUserToken(tenantAdminId); - // --- Create entities --- String device_1_1_token = "000000011"; Device device_1_1 = testRestClient.postDevice(device_1_1_token, createDevice("Device 1-1", deviceProfileId)); @@ -731,6 +784,11 @@ public class CalculatedFieldTest extends AbstractContainerTest { }); testRestClient.deleteCalculatedFieldIfExists(calculatedField.getId()); + testRestClient.deleteDeviceIfExists(device_1_1.getId()); + testRestClient.deleteDeviceIfExists(device_1_2.getId()); + testRestClient.deleteDeviceIfExists(device_2_1.getId()); + testRestClient.deleteDeviceIfExists(device_2_2.getId()); + testRestClient.deleteAsset(asset2.getId()); } private CalculatedField createOccupancyCF(EntityId entityId) { @@ -783,10 +841,6 @@ public class CalculatedFieldTest extends AbstractContainerTest { return testRestClient.postCalculatedField(calculatedField); } - private CalculatedField createSimpleCalculatedField() { - return createSimpleCalculatedField(device.getId()); - } - private CalculatedField createSimpleCalculatedField(EntityId entityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); @@ -799,7 +853,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { Argument argument = new Argument(); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); - argument.setDefaultValue("12"); // not used because real telemetry value in db is present + argument.setDefaultValue("12"); config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); @@ -844,6 +898,16 @@ public class CalculatedFieldTest extends AbstractContainerTest { return testRestClient.postCalculatedField(calculatedField); } + private void waitInitialTelemetry(EntityId entityId, String key) { + await().alias("wait initial telemetry").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode telemetry = testRestClient.getLatestTelemetry(entityId); + assertThat(telemetry).isNotNull(); + assertThat(telemetry.get(key)).isNotNull(); + }); + } + private Device createDevice(String name, DeviceProfileId deviceProfileId) { Device device = new Device(); device.setName(name); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java index 028bca0c48..44a89854f0 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java @@ -303,7 +303,7 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { public void deleteDeviceProfileByTitle(String deviceProfileTitle) { DeviceProfile deviceProfile = getDeviceProfileByName(deviceProfileTitle); if (deviceProfile != null) { - testRestClient.deleteDeviseProfile(deviceProfile.getId()); + testRestClient.deleteDeviceProfile(deviceProfile.getId()); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java index cee165a138..d50c9d8817 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java @@ -52,7 +52,7 @@ public class CreateDeviceProfileImportTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java index 3a97bf323b..1f8e7fb82c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java @@ -51,7 +51,7 @@ public class CreateDeviceProfileTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java index 9b0747ea7a..ae46f82bfb 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java @@ -51,7 +51,7 @@ public class DeviceProfileEditMenuTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java index 828197d2a6..3675b60e8a 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java @@ -46,7 +46,7 @@ public class SearchDeviceProfileTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java index bd90aedb7f..6aa7625ec5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java @@ -45,7 +45,7 @@ public class SortByNameTest extends AbstractDriverBaseTest { @AfterMethod public void delete() { if (name != null) { - testRestClient.deleteDeviseProfile(getDeviceProfileByName(name).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(name).getId()); name = null; } } @@ -83,9 +83,9 @@ public class SortByNameTest extends AbstractDriverBaseTest { profilesPage.setProfileName(2); String thirdDeviceProfile = profilesPage.getProfileName(); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfile).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileNumber).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfile).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileNumber).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); Assert.assertEquals(firstDeviceProfile, deviceProfileSymbol); Assert.assertEquals(secondDeviceProfile, deviceProfileNumber); @@ -126,9 +126,9 @@ public class SortByNameTest extends AbstractDriverBaseTest { profilesPage.setProfileName(lastIndex - 2); String thirdDeviceProfile = profilesPage.getProfileName(); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfile).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileNumber).getId()); - testRestClient.deleteDeviseProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfile).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileNumber).getId()); + testRestClient.deleteDeviceProfile(getDeviceProfileByName(deviceProfileSymbol).getId()); Assert.assertEquals(firstDeviceProfile, deviceProfileSymbol); Assert.assertEquals(secondDeviceProfile, deviceProfileNumber); diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock index 1d42a9cd96..7a54682fa7 100644 --- a/msa/js-executor/yarn.lock +++ b/msa/js-executor/yarn.lock @@ -1253,9 +1253,9 @@ pump@^3.0.0: once "^1.3.1" qs@^6.14.0: - version "6.14.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" - integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + version "6.14.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159" + integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== dependencies: side-channel "^1.1.0" diff --git a/msa/pom.xml b/msa/pom.xml index f41f7f0fc0..40d97d3882 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -32,7 +32,7 @@ ${basedir}/.. thingsboard - thingsboard/openjdk17:bookworm-slim + thingsboard/openjdk25:trixie-slim true true 1.4.13 @@ -125,6 +125,92 @@ + + push-lts-docker-amd-arm-images + + + push-lts-docker-amd-arm-images + + + + 4.3.0-latest + false + + + + + org.codehaus.mojo + exec-maven-plugin + + + push-latest-docker-amd-arm-images + ${docker.push-arm-amd-image.phase} + + exec + + + ${docker.skip.latest.tag} + docker + ${project.build.directory} + + buildx + build + -t + ${docker.repo}/${docker.name}:latest + --platform=linux/amd64,linux/arm64 + -o + type=registry + . + + + + + push-version-docker-amd-arm-images + ${docker.push-arm-amd-image.phase} + + exec + + + docker + ${project.build.directory} + + buildx + build + -t + ${docker.repo}/${docker.name}:${project.version} + --platform=linux/amd64,linux/arm64 + -o + type=registry + . + + + + + push-lts-docker-amd-arm-images + ${docker.push-arm-amd-image.phase} + + exec + + + docker + ${project.build.directory} + + buildx + build + -t + ${docker.repo}/${docker.name}:${docker.lts.tag} + --platform=linux/amd64,linux/arm64 + -o + type=registry + . + + + + + + + + diff --git a/msa/tb-node/docker/Dockerfile b/msa/tb-node/docker/Dockerfile index c084c9c15b..a588142152 100644 --- a/msa/tb-node/docker/Dockerfile +++ b/msa/tb-node/docker/Dockerfile @@ -16,6 +16,21 @@ FROM ${docker.base.image} +# For SVG renderer Debian Trixie slim fonts should be installed explicitly +RUN echo "Previously installed font libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz' || echo " (none)") \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libharfbuzz0b fontconfig fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* \ + && echo "Current fonts libraries:" \ + && (dpkg -l | grep -E 'font|dejavu|harfbuzz') \ + && echo "Font config list:" \ + && fc-list && test -n "$(fc-list)" \ + && (fc-list | grep -i dejavu) \ + && echo "Harfbuzz library test with ldconfig:" \ + && (ldconfig -p | grep harfbuzz) + COPY logback.xml start-tb-node.sh ${pkg.name}.deb /tmp/ RUN chmod a+x /tmp/*.sh \ diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock index 0f8b6f3eed..383d64d5f9 100644 --- a/msa/web-ui/yarn.lock +++ b/msa/web-ui/yarn.lock @@ -1330,9 +1330,9 @@ pump@^3.0.0: once "^1.3.1" qs@^6.14.0: - version "6.14.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" - integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + version "6.14.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159" + integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== dependencies: side-channel "^1.1.0" diff --git a/ui-ngx/patches/@mat-datetimepicker+core+14.0.0.patch b/ui-ngx/patches/@mat-datetimepicker+core+14.0.0.patch index 293ac10a21..ac4f7434e9 100644 --- a/ui-ngx/patches/@mat-datetimepicker+core+14.0.0.patch +++ b/ui-ngx/patches/@mat-datetimepicker+core+14.0.0.patch @@ -15,8 +15,39 @@ index 7ecfae7..08363d3 100644 date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); } this._timeChanged = true; +diff --git a/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/datetimepicker.mjs b/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/datetimepicker.mjs +index 5b82377..30e35ce 100644 +--- a/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/datetimepicker.mjs ++++ b/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/datetimepicker.mjs +@@ -48,7 +48,7 @@ export class MatDatetimepickerContentComponent { + } + } + /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.4", ngImport: i0, type: MatDatetimepickerContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.4", type: MatDatetimepickerContentComponent, selector: "mat-datetimepicker-content", host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "class.mat-datetimepicker-content-touch": "datetimepicker?.touchUi" }, classAttribute: "mat-datetimepicker-content" }, viewQueries: [{ propertyName: "_calendar", first: true, predicate: MatDatetimepickerCalendarComponent, descendants: true, static: true }], ngImport: i0, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;z-index:1000;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48}.cdk-overlay-dark-backdrop{background:#0009}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"], dependencies: [{ kind: "directive", type: i1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: i2.MatDatetimepickerCalendarComponent, selector: "mat-datetimepicker-calendar", inputs: ["multiYearSelector", "startView", "twelvehour", "timeInterval", "dateFilter", "ariaLabel", "ariaNextMonthLabel", "ariaPrevMonthLabel", "ariaNextYearLabel", "ariaPrevYearLabel", "ariaNextMultiYearLabel", "ariaPrevMultiYearLabel", "preventSameDateTimeSelection", "type", "startAt", "selected", "minDate", "maxDate"], outputs: ["_userSelection", "selectedChange", "viewChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } ++ /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.4", type: MatDatetimepickerContentComponent, selector: "mat-datetimepicker-content", host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "class.mat-datetimepicker-content-touch": "datetimepicker?.touchUi" }, classAttribute: "mat-datetimepicker-content" }, viewQueries: [{ propertyName: "_calendar", first: true, predicate: MatDatetimepickerCalendarComponent, descendants: true, static: true }], ngImport: i0, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"], dependencies: [{ kind: "directive", type: i1.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: i2.MatDatetimepickerCalendarComponent, selector: "mat-datetimepicker-calendar", inputs: ["multiYearSelector", "startView", "twelvehour", "timeInterval", "dateFilter", "ariaLabel", "ariaNextMonthLabel", "ariaPrevMonthLabel", "ariaNextYearLabel", "ariaPrevYearLabel", "ariaNextMultiYearLabel", "ariaPrevMultiYearLabel", "preventSameDateTimeSelection", "type", "startAt", "selected", "minDate", "maxDate"], outputs: ["_userSelection", "selectedChange", "viewChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.4", ngImport: i0, type: MatDatetimepickerContentComponent, decorators: [{ + type: Component, +@@ -56,7 +56,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.4", ngImpor + class: 'mat-datetimepicker-content', + '[class.mat-datetimepicker-content-touch]': 'datetimepicker?.touchUi', + '(keydown)': '_handleKeydown($event)', +- }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;z-index:1000;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48}.cdk-overlay-dark-backdrop{background:#0009}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"] }] ++ }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"] }] + }], propDecorators: { _calendar: [{ + type: ViewChild, + args: [MatDatetimepickerCalendarComponent, { static: true }] +@@ -301,7 +301,7 @@ export class MatDatetimepickerComponent { + const overlayConfig = new OverlayConfig({ + positionStrategy: this._createPopupPositionStrategy(), + hasBackdrop: true, +- backdropClass: 'mat-overlay-transparent-backdrop', ++ backdropClass: 'cdk-overlay-transparent-backdrop', + direction: this._dir ? this._dir.value : 'ltr', + scrollStrategy: this._scrollStrategy(), + panelClass: 'mat-datetimepicker-popup', diff --git a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs -index 00f4a52..df688e3 100644 +index 00f4a52..99c0ac1 100644 --- a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs +++ b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs @@ -946,9 +946,9 @@ class MatDatetimepickerClockComponent { @@ -32,3 +63,30 @@ index 00f4a52..df688e3 100644 date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); } this._timeChanged = true; +@@ -1911,7 +1911,7 @@ class MatDatetimepickerContentComponent { + } + } + /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.4", ngImport: i0, type: MatDatetimepickerContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.4", type: MatDatetimepickerContentComponent, selector: "mat-datetimepicker-content", host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "class.mat-datetimepicker-content-touch": "datetimepicker?.touchUi" }, classAttribute: "mat-datetimepicker-content" }, viewQueries: [{ propertyName: "_calendar", first: true, predicate: MatDatetimepickerCalendarComponent, descendants: true, static: true }], ngImport: i0, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;z-index:1000;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48}.cdk-overlay-dark-backdrop{background:#0009}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"], dependencies: [{ kind: "directive", type: i1$2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: MatDatetimepickerCalendarComponent, selector: "mat-datetimepicker-calendar", inputs: ["multiYearSelector", "startView", "twelvehour", "timeInterval", "dateFilter", "ariaLabel", "ariaNextMonthLabel", "ariaPrevMonthLabel", "ariaNextYearLabel", "ariaPrevYearLabel", "ariaNextMultiYearLabel", "ariaPrevMultiYearLabel", "preventSameDateTimeSelection", "type", "startAt", "selected", "minDate", "maxDate"], outputs: ["_userSelection", "selectedChange", "viewChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } ++ /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.4", type: MatDatetimepickerContentComponent, selector: "mat-datetimepicker-content", host: { listeners: { "keydown": "_handleKeydown($event)" }, properties: { "class.mat-datetimepicker-content-touch": "datetimepicker?.touchUi" }, classAttribute: "mat-datetimepicker-content" }, viewQueries: [{ propertyName: "_calendar", first: true, predicate: MatDatetimepickerCalendarComponent, descendants: true, static: true }], ngImport: i0, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"], dependencies: [{ kind: "directive", type: i1$2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "component", type: MatDatetimepickerCalendarComponent, selector: "mat-datetimepicker-calendar", inputs: ["multiYearSelector", "startView", "twelvehour", "timeInterval", "dateFilter", "ariaLabel", "ariaNextMonthLabel", "ariaPrevMonthLabel", "ariaNextYearLabel", "ariaPrevYearLabel", "ariaNextMultiYearLabel", "ariaPrevMultiYearLabel", "preventSameDateTimeSelection", "type", "startAt", "selected", "minDate", "maxDate"], outputs: ["_userSelection", "selectedChange", "viewChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.4", ngImport: i0, type: MatDatetimepickerContentComponent, decorators: [{ + type: Component, +@@ -1919,7 +1919,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.4", ngImpor + class: 'mat-datetimepicker-content', + '[class.mat-datetimepicker-content-touch]': 'datetimepicker?.touchUi', + '(keydown)': '_handleKeydown($event)', +- }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;z-index:1000;pointer-events:auto;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.48}.cdk-overlay-dark-backdrop{background:#0009}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"] }] ++ }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "\n\n", styles: [".mat-datetimepicker-content{box-shadow:0 5px 5px -3px #0003,0 8px 10px 1px #00000024,0 3px 14px 2px #0000001f;display:block;background-color:#fff;border-radius:2px;overflow:hidden}.mat-datetimepicker-calendar{width:296px;height:405px}.mat-datetimepicker-calendar[mode=landscape]{width:446px;height:328px}@media (min-width: 480px){.mat-datetimepicker-calendar[mode=auto]{width:446px;height:328px}}.mat-datetimepicker-content-touch{box-shadow:0 0 #0003,0 0 #00000024,0 0 #0000001f;display:block;box-shadow:0 11px 15px -7px #0003,0 24px 38px 3px #00000024,0 9px 46px 8px #0000001f}.mat-datetimepicker-dialog .mat-dialog-container{padding:0}\n"] }] + }], propDecorators: { _calendar: [{ + type: ViewChild, + args: [MatDatetimepickerCalendarComponent, { static: true }] +@@ -2164,7 +2164,7 @@ class MatDatetimepickerComponent { + const overlayConfig = new OverlayConfig({ + positionStrategy: this._createPopupPositionStrategy(), + hasBackdrop: true, +- backdropClass: 'mat-overlay-transparent-backdrop', ++ backdropClass: 'cdk-overlay-transparent-backdrop', + direction: this._dir ? this._dir.value : 'ltr', + scrollStrategy: this._scrollStrategy(), + panelClass: 'mat-datetimepicker-popup', diff --git a/ui-ngx/src/app/core/services/calculated-field-form.service.ts b/ui-ngx/src/app/core/services/calculated-field-form.service.ts new file mode 100644 index 0000000000..cc3e495c17 --- /dev/null +++ b/ui-ngx/src/app/core/services/calculated-field-form.service.ts @@ -0,0 +1,112 @@ +/// +/// 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 { DestroyRef, inject, Injectable } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { pairwise, switchMap } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { + CalculatedField, + CalculatedFieldConfiguration, + CalculatedFieldEventArguments, + CalculatedFieldType, + OutputStrategyType +} from '@shared/models/calculated-field.models'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { isDefined } from '@core/utils'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { CalculatedFieldsTableEntity } from '@home/components/calculated-fields/calculated-fields-table-config'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Injectable({ providedIn: 'root' }) +export class CalculatedFieldFormService { + private fb = inject(FormBuilder); + private calculatedFieldsService = inject(CalculatedFieldsService); + + buildForm(): FormGroup { + return this.fb.group({ + name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + entityId: [null, Validators.required], + type: [CalculatedFieldType.SIMPLE], + debugSettings: [], + configuration: this.fb.control({} as CalculatedFieldConfiguration), + }); + } + + buildAlarmRuleForm(): FormGroup { + return this.fb.group({ + name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + entityId: [null, Validators.required], + type: [CalculatedFieldType.ALARM], + debugSettings: [], + configuration: this.fb.group({ + type: [CalculatedFieldType.ALARM], + arguments: this.fb.control({}, Validators.required), + propagate: [false], + propagateToOwner: [false], + propagateToTenant: [false], + propagateRelationTypes: [null], + createRules: [null, Validators.required], + clearRule: [null], + }), + }); + } + + setupTypeChange(form: FormGroup, destroyRef: DestroyRef, isEditActive?: () => boolean): void { + form.get('type').valueChanges.pipe( + pairwise(), + takeUntilDestroyed(destroyRef) + ).subscribe(([prevType, nextType]) => { + const shouldCheck = isEditActive ? isEditActive() : true; + if (shouldCheck) { + if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) || + ![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) { + form.get('configuration').setValue({} as CalculatedFieldConfiguration, { emitEvent: false }); + } + } + }); + } + + prepareConfig(configuration: CalculatedFieldConfiguration): CalculatedFieldConfiguration { + const config = configuration || ({} as CalculatedFieldConfiguration); + if (config.type !== CalculatedFieldType.ALARM) { + if (isDefined(config?.output) && !config?.output?.strategy) { + config.output.strategy = { type: OutputStrategyType.RULE_CHAIN }; + } + } + return config; + } + + testScript( + calculatedFieldId: string, + formValue: CalculatedField, + testDialogFn: (calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit?: boolean, expression?: string) => Observable, + destroyRef: DestroyRef, + expression?: string, + ): Observable { + if (calculatedFieldId) { + return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) + .pipe( + switchMap(event => { + const args = event?.arguments ? JSON.parse(event.arguments) : null; + return testDialogFn(formValue, args, false, expression); + }), + takeUntilDestroyed(destroyRef) + ); + } + return testDialogFn(formValue, null, false, expression); + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index a57e3480b1..045725271c 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -454,11 +454,11 @@ export class UtilsService { return base64toObj(b64Encoded); } - public applyCssToElement(renderer: Renderer2, element: any, cssClassPrefix: string, css: string): string { + public applyCssToElement(renderer: Renderer2, element: any, cssClassPrefix: string, css: string, addTbDefaultClass: boolean = false): string { const cssParser = new cssjs(); cssParser.testMode = false; const cssClass = `${cssClassPrefix}-${guid()}`; - cssParser.cssPreviewNamespace = cssClass; + cssParser.cssPreviewNamespace = addTbDefaultClass ? 'tb-default .' + cssClass : cssClass; cssParser.createStyleElement(cssClass, css); renderer.addClass(element, cssClass); return cssClass; diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 85690447e3..9db2c7403d 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -121,6 +121,7 @@ import * as TbErrorComponent from '@shared/components/tb-error.component'; import * as TbCheatSheetComponent from '@shared/components/cheatsheet.component'; import * as BreadcrumbComponent from '@shared/components/breadcrumb.component'; import * as UserMenuComponent from '@shared/components/user-menu.component'; +import * as TimeUnitInputComponent from '@shared/components/time-unit-input.component'; import * as TimewindowComponent from '@shared/components/time/timewindow.component'; import * as TimewindowPanelComponent from '@shared/components/time/timewindow-panel.component'; import * as TimeintervalComponent from '@shared/components/time/timeinterval.component'; @@ -458,6 +459,7 @@ class ModulesMap implements IModulesMap { '@shared/components/cheatsheet.component': TbCheatSheetComponent, '@shared/components/breadcrumb.component': BreadcrumbComponent, '@shared/components/user-menu.component': UserMenuComponent, + '@shared/components/time-unit-input.component': TimeUnitInputComponent, '@shared/components/time/timewindow.component': TimewindowComponent, '@shared/components/time/timewindow-panel.component': TimewindowPanelComponent, '@shared/components/time/timeinterval.component': TimeintervalComponent, 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 869b707d98..2c3767c4c8 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 @@ -58,31 +58,16 @@ /> @if (!data.entityId) { -
- - - @if (fieldFormGroup.get('entityId.entityType').value) { - - } -
+ + } @@ -91,7 +76,7 @@ @@ -150,6 +135,7 @@ placeholder="{{ 'alarm-rule.alarm-rule-relation-types-list' | translate }}" hint="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}" [predefinedValues]="predefinedTypeValues" + allowUserValue formControlName="propagateRelationTypes"> } 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 a84aa52693..f0d3140ed0 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 @@ -18,12 +18,11 @@ import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@an import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormGroup } from '@angular/forms'; 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 { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { 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'; @@ -39,13 +38,15 @@ import { } from "@shared/models/alarm-rule.models"; import { deepTrim } from "@core/utils"; import { combineLatest, Observable } from "rxjs"; -import { debounceTime, startWith, switchMap } from "rxjs/operators"; -import { EntityTypeSelectComponent } from "@shared/components/entity/entity-type-select.component"; -import { EntityAutocompleteComponent } from "@shared/components/entity/entity-autocomplete.component"; -import { EntityService } from "@core/http/entity.service"; +import { debounceTime, startWith } from "rxjs/operators"; import { RelationTypes } from "@shared/models/relation.models"; import { StringItemsOption } from "@shared/components/string-items-list.component"; import { BaseData } from "@shared/models/base-data"; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { EntitySelectComponent } from '@shared/components/entity/entity-select.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; export interface AlarmRuleDialogData { value?: CalculatedField; @@ -67,24 +68,9 @@ export interface AlarmRuleDialogData { }) export class AlarmRuleDialogComponent extends DialogComponent { - fieldFormGroup = this.fb.group({ - name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], - type: [CalculatedFieldType.ALARM], - debugSettings: [], - entityId: this.fb.group({ - entityType: this.fb.control(EntityType.DEVICE_PROFILE, Validators.required), - id: [null as null | string, Validators.required], - }), - configuration: this.fb.group({ - arguments: this.fb.control({}, Validators.required), - propagate: [false], - propagateToOwner: [false], - propagateToTenant: [false], - propagateRelationTypes: [null], - createRules: [null, Validators.required], - clearRule: [null], - }), - }); + @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; + + fieldFormGroup: FormGroup ; additionalDebugActionConfig = this.data.value?.id ? { ...this.data.additionalDebugActionConfig, @@ -100,23 +86,21 @@ export class AlarmRuleDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, protected dialogRef: MatDialogRef, private calculatedFieldsService: CalculatedFieldsService, - private entityService: EntityService, private destroyRef: DestroyRef, - private fb: FormBuilder) { + private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); + this.fieldFormGroup = this.cfFormService.buildAlarmRuleForm(); this.applyDialogData(); this.updateRulesValidators(); @@ -132,7 +116,7 @@ export class AlarmRuleDialogComponent extends DialogComponent { - const calculatedFieldId = this.data.value?.id?.id; - if (calculatedFieldId) { - return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) - .pipe( - switchMap(event => { - const args = event?.arguments ? JSON.parse(event.arguments) : null; - return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, args, false); - }), - takeUntilDestroyed(this.destroyRef) - ) - } - return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, null, false); + return this.cfFormService.testScript( + this.data.value?.id?.id, + this.fromGroupValue, + this.data.getTestScriptDialogFn, + this.destroyRef, + expression + ); } private updateRulesValidators(): void { @@ -249,6 +227,12 @@ export class AlarmRuleDialogComponent extends DialogComponent): void { this.entityName = entity.name; + if (this.isAssignedToCustomer(entity as AssetInfo | DeviceInfo)) { + this.ownerId = (entity as AssetInfo | DeviceInfo).customerId; + } } + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts index 52d91188bc..f7adf5dab0 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 @@ -53,6 +53,7 @@ import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alar import { AlarmRuleFilterPredicateNoDataValueComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component"; +import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component'; @NgModule({ declarations: [ @@ -73,7 +74,8 @@ import { AlarmRuleDetailsDialogComponent, AlarmRuleFilterConfigComponent, AlarmRuleTableHeaderComponent, - AlarmRuleFilterPredicateNoDataValueComponent + AlarmRuleFilterPredicateNoDataValueComponent, + AlarmRulesComponent ], imports: [ CommonModule, @@ -83,6 +85,7 @@ import { ], exports: [ AlarmRuleDialogComponent, + AlarmRulesComponent ] }) export class AlarmRuleModule { } 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 3d1c066839..ad718408e4 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 @@ -21,7 +21,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; import { MatDialog } from '@angular/material/dialog'; @@ -36,7 +36,7 @@ import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; -import { catchError, filter, switchMap, tap } from 'rxjs/operators'; +import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; import { ArgumentEntityType, ArgumentType, @@ -67,6 +67,10 @@ import { CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData } from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component"; +import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.component'; +import { Router } from '@angular/router'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component'; type AlarmRuleTableEntity = CalculatedField | CalculatedFieldInfo; @@ -93,27 +97,26 @@ export class AlarmRulesTableConfig extends EntityTableConfig { - this.editCalculatedField($event, model); - return true; - }; } this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules'); - this.detailsPanelEnabled = false; + this.detailsPanelEnabled = this.pageMode; + this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD); this.entityType = EntityType.CALCULATED_FIELD; this.entityTranslations = { type: 'alarm-rule.alarm-rule', typePlural: 'alarm-rule.alarm-rules', list: 'alarm-rule.list', add: 'action.add', + details: 'alarm-rule.details', noEntities: 'alarm-rule.no-found', search: 'action.search', selectedEntities: 'alarm-rule.selected-fields' @@ -121,11 +124,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedAlarmDialog.bind(this); + this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id); + this.saveEntity = (alarmRule) => this.calculatedFieldsService.saveCalculatedField(alarmRule); + this.deleteEntityTitle = (field) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + + this.onEntityAction = action => this.onCFAction(action); + this.addActionDescriptors = [ { name: this.translate.instant('alarm-rule.create'), @@ -177,7 +186,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig true, - onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), + onAction: ($event, entity) => + this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity), }, { name: '', @@ -186,14 +196,18 @@ export class AlarmRulesTableConfig extends EntityTableConfig true, iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), - }, - { - name: this.translate.instant('action.edit'), - icon: 'edit', - isEnabled: () => true, - onAction: ($event, entity) => this.editCalculatedField($event, entity), } ); + if (!this.pageMode) { + this.cellActionDescriptors.push( + { + name: this.translate.instant('action.edit'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.editCalculatedField($event, entity), + } + ) + } } fetchCalculatedFields(pageLink: PageLink): Observable> { @@ -288,13 +302,29 @@ export class AlarmRulesTableConfig extends EntityTableConfig false + debugActionDisabled: true } }) .afterClosed() .subscribe(); } + private openDebugTab($event: Event, calculatedField: AlarmRuleTableEntity) { + const table = this.getTable(); + if (!table.isDetailsOpen) { + table.toggleEntityDetails($event, calculatedField); + if (table.entityDetailsPanel.matTabGroup._tabs.length > 1) { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + } else { + table.entityDetailsPanel.matTabGroup._tabs.changes.pipe( + first() + ).subscribe(() => { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + }) + } + } + } + private exportAlarmRule($event: Event, calculatedField: AlarmRuleTableEntity): void { $event?.stopPropagation(); this.importExportService.exportCalculatedField(calculatedField.id.id); @@ -348,7 +378,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, expression: string, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable { + private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { if (calculatedField.type === CalculatedFieldType.ALARM) { const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { const type = calculatedField.configuration.arguments[key].refEntityKey.type; @@ -384,4 +414,22 @@ export class AlarmRulesTableConfig extends EntityTableConfig): boolean { + switch (action.action) { + case 'open': + this.openCalculatedField(action.event, action.entity); + return true; + case 'export': + this.exportAlarmRule(action.event, action.entity); + return true; + } + return false; + } } 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 7f4c0b4ee7..aaddf94aac 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,7 +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"; +import { ActivatedRoute, Router } from "@angular/router"; @Component({ selector: 'tb-alarm-rules-table', @@ -70,6 +70,7 @@ export class AlarmRulesTableComponent { private utilsService: UtilsService, private destroyRef: DestroyRef, private route: ActivatedRoute, + private router: Router ) { this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { @@ -88,6 +89,7 @@ export class AlarmRulesTableComponent { this.importExportService, this.entityDebugSettingsService, this.utilsService, + this.router, this.pageMode, ); this.cd.markForCheck(); diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html new file mode 100644 index 0000000000..32107f6eda --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html @@ -0,0 +1,160 @@ + +
+ + + +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('pattern')) { + {{ 'common.hint.title-pattern' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + +
+ +
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
{{ 'alarm-rule.create-conditions' | translate }}
+
+ + +
+
+
+
{{ 'alarm-rule.clear-condition' | translate }}
+
+
+ + +
+ +
+
+ alarm-rule.no-clear-alarm-rule +
+
+ +
+
+
+ + + {{ 'alarm-rule.advanced-settings' | translate }} + + +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + + } +
+
+ + {{ 'alarm-rule.propagate-alarm-to-owner' | translate }} + +
+
+ + {{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts new file mode 100644 index 0000000000..0092bf2132 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts @@ -0,0 +1,199 @@ +/// +/// 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 { ChangeDetectorRef, Component, DestroyRef, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldArgument, + CalculatedFieldConfiguration, + CalculatedFieldInfo, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { RelationTypes } from '@shared/models/relation.models'; +import { + AlarmRule, + AlarmRuleConditionType, + alarmRuleEntityTypeList, + AlarmRuleExpressionType +} from '@shared/models/alarm-rule.models'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-alarm-rules', + templateUrl: './alarm-rules.component.html', + styleUrls: [] +}) +export class AlarmRulesComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly alarmRuleEntityTypeList = alarmRuleEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action( + { id: this.entity.id, ...this.entityFormValue() }, false, + (expression) => { + if (expression) { + this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); + this.entityForm.get('configuration').markAsDirty(); + } + }), + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldInfo): FormGroup { + return inject(CalculatedFieldFormService).buildAlarmRuleForm(); + } + + updateForm(entity: CalculatedFieldInfo) { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + this.entityForm.patchValue({ configuration, debugSettings, entityId, ...value }, {emitEvent: false}); + if (!entityId) { + this.entityForm.get('configuration').disable({emitEvent: false}); + } + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityFormValue(), + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + get arguments(): Record { + return this.entityForm.get('configuration.arguments').value; + } + + get predefinedTypeValues(): StringItemsOption[] { + return RelationTypes.map(type => ({ + name: type, + value: type + })); + } + + get configFormGroup(): FormGroup { + return this.entityForm.get('configuration') as FormGroup; + } + + public removeClearAlarmRule() { + this.configFormGroup.patchValue({clearRule: null}); + this.entityForm.markAsDirty(); + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + type: AlarmRuleConditionType.SIMPLE, + expression: { + type: AlarmRuleExpressionType.SIMPLE + } + } + }; + this.configFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index 20ad4a4bfb..6f691ecd21 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -184,11 +184,13 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode); + this.updateSpecText(this.conditionFormGroup.get('type').value); }); this.repeatingDynamicModeControl.valueChanges.pipe( takeUntilDestroyed() ).subscribe((mode) => { this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode); + this.updateSpecText(this.conditionFormGroup.get('type').value); }); this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); @@ -289,20 +291,20 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent -
+
{{ 'alarm-rule.condition' | translate }}
-
{{ 'alarm-rule.schedule-title' | 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 08004d7497..0fcb6d0135 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 @@ -137,19 +137,27 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali writeValue(value: AlarmRuleCondition): void { this.modelValue = value; this.updateConditionInfo(); - if (value) { - this.onValidatorChange(); - } + this.recalculateArgumentValidity(); } ngOnChanges(changes: SimpleChanges) { if (changes.arguments) { - if (changes.arguments && !changes.arguments.firstChange && this.modelValue) { - this.onValidatorChange(); + if (changes.arguments && !changes.arguments.firstChange) { + this.recalculateArgumentValidity(); } } } + private recalculateArgumentValidity(): void { + if (!this.modelValue || !this.arguments) { + this.filtersArgumentsValid = true; + this.schedulerArgumentsValid = true; + return; + } + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); + this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); + } + private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean { const arg = obj?.schedule?.dynamicValueArgument; return !arg || validArguments.includes(arg); @@ -173,21 +181,14 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali return true; } - public conditionSet() { - return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); + public conditionSet(): boolean { + return !!this.modelValue && !!(this.modelValue.expression?.expression || this.modelValue.expression?.filters); } public validate(control: AbstractControl): ValidationErrors | null { - this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); - this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); - this.onValidatorChange = () => { - control.updateValueAndValidity({ emitEvent: true }); - }; - return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : { - alarmRuleCondition: { - valid: false, - } - }; + const hasCondition = this.conditionSet(); + const argsValid = this.filtersArgumentsValid && this.schedulerArgumentsValid; + return (hasCondition && argsValid) ? null : { alarmRuleCondition: { valid: false } }; } public openFilterDialog($event: Event) { @@ -208,7 +209,6 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (result) { this.modelValue = {...this.modelValue, ...result}; this.updateModel(); - this.cd.detectChanges(); } }); } @@ -274,10 +274,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali private updateModel() { this.updateConditionInfo(); + this.recalculateArgumentValidity(); this.propagateChange(this.modelValue); - if (this.modelValue) { - this.onValidatorChange(); - } + this.onValidatorChange(); + this.cd.detectChanges(); } public openScheduleDialog($event: Event) { @@ -297,13 +297,12 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (result) { this.modelValue.schedule = result; this.updateModel(); - this.cd.detectChanges(); } }); } private updateScheduleText() { - let schedule = this.modelValue?.schedule; + const schedule = this.modelValue?.schedule; this.scheduleText = ''; if (isDefinedAndNotNull(schedule)) { if (schedule.dynamicValueArgument) { 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 a7ccc25fe6..f48819da74 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 @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts index 09a2d52756..d5ec210f34 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -114,13 +114,13 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida } createAlarmRulesControls.push(this.fb.group({ severity: [severity, Validators.required], - alarmRule: [createAlarmRule, Validators.required] + alarmRule: [{value: createAlarmRule, disabled: this.disabled}, Validators.required] })); }); } const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; - formArray.clear(); - createAlarmRulesControls.forEach(c => formArray.push(c)); + formArray.clear({emitEvent: false}); + createAlarmRulesControls.forEach(c => formArray.push(c, {emitEvent: false})); if (this.disabled) { this.createAlarmRulesFormGroup.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts index 800fdf2371..6d426cdcd0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts @@ -32,13 +32,15 @@ import { AlarmCommentInfo, AlarmCommentType, AlarmMessage, + AlarmSeverity, + alarmSeverityTranslations, getUserDisplayName } from '@shared/models/alarm.models'; import { UtilsService } from '@core/services/utils.service'; import { EntityType } from '@shared/models/entity-type.models'; import { DatePipe } from '@angular/common'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { isNotEmptyStr } from '@core/utils'; +import { deepClone, isNotEmptyStr } from '@core/utils'; interface AlarmCommentsDisplayData { commentId?: string; @@ -153,8 +155,21 @@ export class AlarmCommentComponent implements OnInit { private parseSystemComment(alarm: AlarmCommentInfo): string { const subTypeKey = alarm.comment?.subtype; if (subTypeKey && AlarmMessage[subTypeKey]) { + const alarmComment = deepClone(alarm.comment); const translationKey = AlarmMessage[subTypeKey]; - return this.translate.instant(translationKey, alarm.comment); + if (alarmComment?.newSeverity) { + alarmComment.newSeverity = + (alarmSeverityTranslations.has(alarmComment.newSeverity) + ? this.translate.instant(alarmSeverityTranslations.get(alarmComment.newSeverity)) + : alarmComment.newSeverity) as AlarmSeverity; + } + if (alarmComment?.oldSeverity) { + alarmComment.oldSeverity = + (alarmSeverityTranslations.has(alarmComment.oldSeverity) + ? this.translate.instant(alarmSeverityTranslations.get(alarmComment.oldSeverity)) + : alarmComment.oldSeverity) as AlarmSeverity; + } + return this.translate.instant(translationKey, alarmComment); } return alarm.comment.text; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html new file mode 100644 index 0000000000..1150b5b2a6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html @@ -0,0 +1,146 @@ + +
+ + + +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('pattern')) { + {{ 'common.hint.title-pattern' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + + + {{ 'common.type' | translate }} + + + {{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value) + ? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate) + : entityForm.get('type').value }} + + @for (type of fieldTypes; track type) { + + + {{ CalculatedFieldTypeTranslations.get(type).name | translate }} + +
+ + {{ CalculatedFieldTypeTranslations.get(type).hint | translate }} + +
+ } +
+
+
+ @switch (entityForm.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + + } + @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } + @default { + + } + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss new file mode 100644 index 0000000000..8dc0346d0c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss @@ -0,0 +1,52 @@ +/** + * 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-deep { + .tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; + } + + .tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts new file mode 100644 index 0000000000..fcf20b5421 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -0,0 +1,167 @@ +/// +/// 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 { ChangeDetectorRef, Component, DestroyRef, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldConfiguration, + CalculatedFieldInfo, + calculatedFieldsEntityTypeList, + CalculatedFieldType, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-calculated-field', + templateUrl: './calculated-field.component.html', + styleUrls: ['./calculated-field.component.scss'] +}) +export class CalculatedFieldComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + disabledConfiguration = false; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + readonly fieldTypes = calculatedFieldTypes; + readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action( + { id: this.entity.id, ...this.entityFormValue() }, false, + (expression) => { + if (expression) { + this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); + this.entityForm.get('configuration').markAsDirty(); + } + }), + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldInfo): FormGroup { + const form = inject(CalculatedFieldFormService).buildForm(); + inject(CalculatedFieldFormService).setupTypeChange(form, inject(DestroyRef), () => this.isEditValue); + return form; + } + + updateForm(entity: CalculatedFieldInfo) { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + const preparedConfig = this.cfFormService.prepareConfig(configuration); + this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true}); + setTimeout(() => { + this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); + }); + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityFormValue(), + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts index e3f5ac9cc2..2bcfcd7a1b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -50,6 +50,7 @@ import { import { CalculatedFieldsFilterConfigComponent } from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; @NgModule({ declarations: [ @@ -57,7 +58,8 @@ import { CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, CalculatedFieldsHeaderComponent, - CalculatedFieldsFilterConfigComponent + CalculatedFieldsFilterConfigComponent, + CalculatedFieldComponent, ], imports: [ CommonModule, @@ -72,6 +74,7 @@ import { exports: [ CalculatedFieldDialogComponent, CalculatedFieldScriptTestDialogComponent, + CalculatedFieldComponent, ] }) export class CalculatedFieldsModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 8385b5cb45..49cc6b75ca 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -20,7 +20,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; @@ -35,7 +35,7 @@ import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; -import { catchError, filter, switchMap, tap } from 'rxjs/operators'; +import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; import { ArgumentEntityType, ArgumentType, @@ -46,6 +46,7 @@ import { CalculatedFieldsQuery, CalculatedFieldType, CalculatedFieldTypeTranslations, + debugCfActionEnabled, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, PropagationWithExpression, @@ -62,20 +63,26 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; import { ActionNotificationShow } from "@core/notification/notification.actions"; -import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; import { CalculatedFieldsHeaderComponent } from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; +import { Router } from '@angular/router'; +import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component'; -type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; +export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { title: this.translate.instant('action.see-debug-events'), - action: (calculatedField: CalculatedFieldsTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField), + action: (calculatedField: CalculatedFieldsTableEntity, + openCalculatedFieldEdit = true, + afterCloseCallback?: (expression: string) => void) => this.openDebugEventsDialog.call(this, null, calculatedField, openCalculatedFieldEdit, afterCloseCallback), }; calculatedFieldFilterConfig: CalculatedFieldsQuery; @@ -93,30 +100,35 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - this.editCalculatedField($event, entity); - this.rowPointer = true; - return true; - }; + this.entityComponent = CalculatedFieldComponent; + this.entityTabsComponent = CalculatedFieldsTabsComponent; + this.rowPointer = true; } - this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields'); - this.detailsPanelEnabled = false; + this.tableTitle = this.translate.instant('entity.type-calculated-fields'); + this.detailsPanelEnabled = this.pageMode; this.entityType = EntityType.CALCULATED_FIELD; this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD); + this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD); this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedFieldDialog.bind(this); + this.saveEntity = (cf) => this.calculatedFieldsService.saveCalculatedField(cf); + this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id); this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + + this.onEntityAction = action => this.onCFAction(action); + this.addActionDescriptors = [ { name: this.translate.instant('calculated-fields.create'), @@ -163,7 +175,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, - onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), + onAction: ($event, entity) => + this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity), }, { name: '', @@ -172,14 +185,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), - }, - { + } + ); + if (!this.pageMode) { + this.cellActionDescriptors.push({ name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, onAction: ($event, entity) => this.editCalculatedField($event, entity), - } - ); + }) + } } fetchCalculatedFields(pageLink: PageLink): Observable> { @@ -188,7 +203,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig 1) { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + } else { + table.entityDetailsPanel.matTabGroup._tabs.changes.pipe( + first() + ).subscribe(() => { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + }) + } + } + } + private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void { $event?.stopPropagation(); this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty) @@ -222,7 +253,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false, disabledSelectType = false): Observable { const entityId = this.entityId || value?.entityId; const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; return this.dialog.open(CalculatedFieldDialogComponent, { @@ -234,10 +265,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig void ): void { $event?.stopPropagation(); - const debugActionEnabledFn = (event: DebugEvent) => { - return (calculatedField.type === CalculatedFieldType.SCRIPT || - (calculatedField.type === CalculatedFieldType.PROPAGATION && - calculatedField.configuration.applyExpressionToResolvedArguments) - ) && !!(event as DebugEvent).body.arguments; - }; const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef) => { - this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments)) + this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit) .subscribe(expression => dialogRef.close(expression)); }; @@ -270,11 +296,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + if (afterCloseCallback) { + afterCloseCallback(value) + } + }); } private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void { @@ -303,7 +333,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - if (calculatedField.type === CalculatedFieldType.ALARM) { + if (calculatedField.type === CalculatedFieldType.ALARM || !CalculatedFieldType[calculatedField.type]) { this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('calculated-fields.hint.import-invalid-calculated-field-type'), type: 'error', @@ -315,7 +345,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), + switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true, true)), filter(Boolean), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), filter(Boolean), @@ -354,7 +384,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { if ( calculatedField.type === CalculatedFieldType.SCRIPT || calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION || @@ -394,4 +424,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig): boolean { + switch (action.action) { + case 'open': + this.openCalculatedField(action.event, action.entity); + return true; + case 'export': + this.exportCalculatedField(action.event, action.entity); + return true; + } + return false; + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index 2a0c29e31a..7687277113 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -36,7 +36,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'tb-calculated-fields-table', @@ -70,6 +70,7 @@ export class CalculatedFieldsTableComponent { private utilsService: UtilsService, private destroyRef: DestroyRef, private route: ActivatedRoute, + private router: Router ) { this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { @@ -88,6 +89,7 @@ export class CalculatedFieldsTableComponent { this.importExportService, this.entityDebugSettingsService, this.utilsService, + this.router, this.pageMode, ); this.cd.markForCheck(); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html index 30533dc714..484f1f9238 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -188,13 +188,15 @@ (click)="cancel()"> {{ 'action.cancel' | translate }} - + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 38855ec79e..d6271409f8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -88,6 +88,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI maxlength: 'calculated-fields.hint.argument-name-max-length', forbidden: 'calculated-fields.hint.argument-name-forbidden' }; + @Input() readonly = false; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @@ -176,6 +177,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.argumentTypes = Object.values(ArgumentType) .filter(type => type !== ArgumentType.Rolling || this.isScript); + + if (this.readonly) { + this.argumentType.disable({emitEvent: false}); + this.argumentFormGroup.disable({emitEvent: false}); + } } ngAfterViewInit(): void { @@ -259,7 +265,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI }; } if (!onInit) { - this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); + this.argumentFormGroup.get('refEntityKey').get('key').setValue('', {emitEvents: !this.watchKeyChange}); + if (this.watchKeyChange && this.argumentFormGroup.get('argumentName').pristine) { + this.argumentFormGroup.get('argumentName').markAsUntouched({emitEvent: false}); + this.argumentFormGroup.get('argumentName').setValue('', {emitEvent: false}); + } } else if (this.predefinedEntityFilter) { entityFilter = this.predefinedEntityFilter; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index 99b4225db2..c61ce4c467 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -93,22 +93,33 @@
- + @if (disable) { + + } @else { + + }
@if (!data.entityId) { -
- - - @if (fieldFormGroup.get('entityId.entityType').value) { - - } -
+ + } {{ 'common.type' | translate }} + + {{ CalculatedFieldTypeTranslations.has(fieldFormGroup.get('type').value) + ? (CalculatedFieldTypeTranslations.get(fieldFormGroup.get('type').value).name | translate) + : fieldFormGroup.get('type').value }} + @for (type of fieldTypes; track type) { - {{ CalculatedFieldTypeTranslations.get(type).name | translate}} + + + {{ CalculatedFieldTypeTranslations.get(type).name | translate}} + +
+ + {{ CalculatedFieldTypeTranslations.get(type).hint | translate }} + +
}
- @if (CalculatedFieldTypeTranslations.get(fieldFormGroup.get('type').value).hint) { - {{ CalculatedFieldTypeTranslations.get(fieldFormGroup.get('type').value).hint | translate }} - }
@switch (fieldFormGroup.get('type').value) { @@ -98,7 +94,7 @@ [class.tb-form-panel-disabled]="disabledConfiguration" [entityId]="entityId" [entityName]="entityName" - [ownerId]="data.ownerId" + [ownerId]="ownerId" [tenantId]="data.tenantId" > } @@ -108,7 +104,7 @@ [entityId]="entityId" [entityName]="entityName" [tenantId]="data.tenantId" - [ownerId]="data.ownerId" + [ownerId]="ownerId" [testScript]="onTestScript.bind(this)" > } @@ -134,7 +130,7 @@ [class.tb-form-panel-disabled]="disabledConfiguration" [entityId]="entityId" [entityName]="entityName" - [ownerId]="data.ownerId" + [ownerId]="ownerId" [tenantId]="data.tenantId" [isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT" [testScript]="onTestScript.bind(this)" diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 4d2df6e416..7f1c9e376a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -14,11 +14,10 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { @@ -28,21 +27,21 @@ import { CalculatedFieldTestScriptFn, CalculatedFieldType, calculatedFieldTypes, - CalculatedFieldTypeTranslations, - OutputStrategyType + CalculatedFieldTypeTranslations } from '@shared/models/calculated-field.models'; -import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; -import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; -import { pairwise, switchMap } from 'rxjs/operators'; +import { EntityType } from '@shared/models/entity-type.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { deepTrim, isDefined } from '@core/utils'; -import { EntityTypeSelectComponent } from '@shared/components/entity/entity-type-select.component'; -import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; +import { deepTrim } from '@core/utils'; import { BaseData } from '@shared/models/base-data'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { FormGroup } from '@angular/forms'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -54,6 +53,7 @@ export interface CalculatedFieldDialogData { additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; getTestScriptDialogFn: CalculatedFieldTestScriptFn; isDirty?: boolean; + disabledSelectType?: boolean; } @Component({ @@ -64,16 +64,7 @@ export interface CalculatedFieldDialogData { }) export class CalculatedFieldDialogComponent extends DialogComponent { - fieldFormGroup = this.fb.group({ - name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], - entityId: this.fb.group({ - entityType: this.fb.control(EntityType.DEVICE_PROFILE, Validators.required), - id: [null as null | string, Validators.required], - }), - type: [CalculatedFieldType.SIMPLE], - debugSettings: [], - configuration: this.fb.control({} as CalculatedFieldConfiguration), - }); + fieldFormGroup: FormGroup; additionalDebugActionConfig = this.data.value?.id ? { ...this.data.additionalDebugActionConfig, @@ -81,6 +72,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData, protected dialogRef: MatDialogRef, private calculatedFieldsService: CalculatedFieldsService, private destroyRef: DestroyRef, - private fb: FormBuilder) { + private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); - this.observeType(); + this.fieldFormGroup = this.cfFormService.buildForm(); + this.cfFormService.setupTypeChange(this.fieldFormGroup, this.destroyRef); this.applyDialogData(); if (this.data.isDirty) { @@ -110,7 +100,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent { this.disabledConfiguration = !entityId; @@ -118,13 +108,18 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - const calculatedFieldId = this.data.value?.id?.id; - if (calculatedFieldId) { - return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) - .pipe( - switchMap(event => { - const args = event?.arguments ? JSON.parse(event.arguments) : null; - return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression); - }), - takeUntilDestroyed(this.destroyRef) - ) - } - return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression); + return this.cfFormService.testScript( + this.data.value?.id?.id, + this.fromGroupValue, + this.data.getTestScriptDialogFn, + this.destroyRef, + expression + ); } changeEntity(entity: BaseData): void { this.entityName = entity.name; + if (this.isAssignedToCustomer(entity as AssetInfo | DeviceInfo)) { + this.ownerId = (entity as AssetInfo | DeviceInfo).customerId; + } } get entityId(): EntityId { @@ -172,12 +163,8 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); if (!this.data.entityId) { this.fieldFormGroup.get('configuration').disable({emitEvent: false}); @@ -185,15 +172,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) || - ![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) { - this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false}); - } - }); + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html index 19600f8085..1eb0a63361 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -17,7 +17,7 @@ -->
-
+
{{ 'calculated-fields.arguments' | translate }}
@@ -29,7 +29,7 @@ [entityName]="entityName"/>
-
+
{{ 'calculated-fields.metrics.metrics' | translate }}
{ this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html index 9f4e331cc1..94a86b74e4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -95,7 +95,9 @@
- {{ 'calculated-fields.entity-zone-relationship' | translate }} + + {{ 'calculated-fields.entity-zone-relationship' | translate }} +
@@ -145,6 +147,7 @@
} @else { - } @@ -221,7 +224,7 @@
-
+
{{ 'calculated-fields.create-relation-with-matched-zones' | translate }} @@ -258,12 +261,14 @@ (click)="cancel()"> {{ 'action.cancel' | translate }} - + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts index 3484e717cb..1ec89e411a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts @@ -71,6 +71,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit @Input() entityName: string; @Input() ownerId: EntityId; @Input() usedNames: string[]; + @Input() readonly = false; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @@ -160,6 +161,10 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); this.updateEntityFilter(this.zone.refEntityId?.entityType); + + if (this.readonly) { + this.geofencingFormGroup.disable({emitEvent: false}); + } } fetchOptions(searchText: string): Observable> { @@ -303,6 +308,7 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit removeKey(index: number) { this.levelsFormArray().removeAt(index); + this.levelsFormArray().markAsDirty(); } addKey() { @@ -320,6 +326,6 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit } get dragEnabled(): boolean { - return this.levelsFormArray().controls.length > 1; + return this.levelsFormArray().controls.length > 1 && !this.readonly; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index fa0ba32f22..d661f5021a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -96,20 +96,31 @@
- + @if (disable) { + + } @else { + + } @@ -119,7 +119,7 @@
@@ -130,7 +130,10 @@ @if (!simpleMode) {
-
{{ 'calculated-fields.metrics.value-source' | translate }}
+
+ {{ 'calculated-fields.metrics.value-source' | translate }} +
@for (inputType of AggInputTypes; track inputType) { @@ -170,7 +173,7 @@ [highlightRules]="highlightRules" [editorCompleter]="editorCompleter" [helpPopupStyle]="{ width: '1200px' }" - helpId="calculated-field/expression_fn"> + helpId="calculated-field/map_expression_fn">
{{ 'api-usage.tbel' | translate }}
@@ -179,7 +182,7 @@ matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" matTooltipPosition="above" class="tb-mat-32" - [disabled]="!arguments.length" + [disabled]="!arguments.length || readonly" (click)="onTestScript('input.function')"> bug_report @@ -188,7 +191,7 @@
@@ -210,12 +213,14 @@ (click)="cancel()"> {{ 'action.cancel' | translate }} - + @if (!readonly) { + + }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts index 3d5a53ea4e..e8c8c4984a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts @@ -24,6 +24,8 @@ import { AggInputType, AggInputTypeTranslations, CalculatedFieldAggMetricValue, + calculatedFieldMetricFilterDefaultScript, + calculatedFieldMetricMapDefaultScript, FORBIDDEN_NAMES, forbiddenNamesValidator, uniqueNameValidator @@ -42,7 +44,7 @@ interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricVal @Component({ selector: 'tb-calculated-field-metrics-panel', templateUrl: './calculated-field-metrics-panel.component.html', - styleUrl: '../common/calculated-field-panel.scss', + styleUrls: ['../common/calculated-field-panel.scss', '../../calculated-field.component.scss'], }) export class CalculatedFieldMetricsPanelComponent implements OnInit { @@ -54,6 +56,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { @Input() editorCompleter: TbEditorCompleter; @Input() highlightRules: AceHighlightRules; @Input({required: true}) testScript: (expression?: string) => Observable; + @Input() readonly = false; metricDataApplied = output(); filterExpanded = false; @@ -63,18 +66,18 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { name: ['', [Validators.required, forbiddenNamesValidator(FORBIDDEN_NAMES), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], function: [AggFunction.AVG], allowFilter: [false], - filter: ['', Validators.required], + filter: [calculatedFieldMetricFilterDefaultScript, Validators.required], input: this.fb.group({ type: [AggInputType.key], key: ['', Validators.required], - function: ['', Validators.required], + function: [calculatedFieldMetricMapDefaultScript, Validators.required], }), defaultValue: [null] }); entityFilter: EntityFilter; - readonly AggFunctions = Object.values(AggFunction) as AggFunction[]; + AggFunctions = Object.values(AggFunction) as AggFunction[]; readonly AggFunctionTranslations = AggFunctionTranslations; readonly ScriptLanguage = ScriptLanguage; readonly AggInputType = AggInputType; @@ -103,6 +106,14 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { this.validateInputKey(); this.functionArgs = ['ctx', ...this.arguments]; + + if (this.simpleMode) { + this.AggFunctions = this.AggFunctions.filter(aggFunc => aggFunc !== AggFunction.COUNT_UNIQUE); + } + + if (this.readonly) { + this.metricForm.disable({emitEvent: false}); + } } saveMetric(): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html index 4693d3afb4..3b50b9e152 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -93,15 +93,26 @@
- + @if (disable) { + + } @else { + + } -
diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 0f2ec26ad3..6e9be088c7 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -20,13 +20,15 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { UserPasswordPolicy } from '@shared/models/settings.models'; import { passwordsMatchValidator, passwordStrengthValidator } from '@shared/models/password.models'; +import { finalize } from 'rxjs/operators'; +import { PageComponent } from '@shared/components/page.component'; @Component({ selector: 'tb-create-password', templateUrl: './create-password.component.html', styleUrls: ['./password.component.scss'] }) -export class CreatePasswordComponent { +export class CreatePasswordComponent extends PageComponent { passwordPolicy: UserPasswordPolicy; createPassword: FormGroup; @@ -38,7 +40,7 @@ export class CreatePasswordComponent { constructor(private route: ActivatedRoute, private authService: AuthService, private fb: FormBuilder) { - + super(); this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; @@ -60,11 +62,10 @@ export class CreatePasswordComponent { if (this.createPassword.invalid) { this.createPassword.markAllAsTouched(); } else { - this.isLoading = true - this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true) - .subscribe({ - error: () => {this.isLoading = false;} - }); + this.isLoading = true; + this.authService.activate(this.activateToken, this.createPassword.get('newPassword').value, true).pipe( + finalize(() => {this.isLoading = false;}) + ).subscribe(() => {}); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index f43ff18435..f2878bde27 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -22,9 +22,9 @@
- + - +
@if(oauth2Clients?.length) { @@ -70,7 +70,7 @@ lock -
diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index 5e75426952..e09086f95a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -26,6 +26,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatFormFieldAppearance } from '@angular/material/form-field'; +import { BaseData } from '@shared/models/base-data'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; @Component({ selector: 'tb-entity-select', @@ -37,7 +39,9 @@ import { MatFormFieldAppearance } from '@angular/material/form-field'; multi: true }] }) -export class EntitySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { +export class EntitySelectComponent implements ControlValueAccessor, OnInit { + + @ViewChild('entityAutocompleteComponent') entityAutocompleteComponent!: EntityAutocompleteComponent; entitySelectFormGroup: UntypedFormGroup; @@ -66,16 +70,26 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte @coerceBoolean() useEntityDisplayName = false; + @Input() + filterAllowedEntityTypes = true; + + @Input() + defaultEntityType: AliasEntityType | EntityType; + + @Input() + entityTypeLabel: string; + + @Output() + entityChanged = new EventEmitter>(); + displayEntityTypeSelect: boolean; AliasEntityType = AliasEntityType; - entityTypeNullUUID: Set = new Set([ + entityTypeNullUUID = new Set([ AliasEntityType.CURRENT_TENANT, AliasEntityType.CURRENT_USER, AliasEntityType.CURRENT_USER_OWNER ]); - private readonly defaultEntityType: EntityType | AliasEntityType = null; - private propagateChange = (v: any) => { }; constructor(private store: Store, @@ -86,15 +100,18 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, this.useAliasEntityTypes); + + let defaultEntityType: EntityType | AliasEntityType = null + if (entityTypes.length === 1) { this.displayEntityTypeSelect = false; - this.defaultEntityType = entityTypes[0]; + defaultEntityType = entityTypes[0]; } else { this.displayEntityTypeSelect = true; } this.entitySelectFormGroup = this.fb.group({ - entityType: [this.defaultEntityType], + entityType: [defaultEntityType], entityId: [null] }); } @@ -126,9 +143,19 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte if (additionNullUIIDEntityTypes.length > 0) { additionNullUIIDEntityTypes.forEach((entityType) => this.entityTypeNullUUID.add(entityType)); } - } - ngAfterViewInit(): void { + if (this.filterAllowedEntityTypes === false) { + if (this.allowedEntityTypes?.length === 1) { + this.displayEntityTypeSelect = false; + this.entitySelectFormGroup.get('entityType').setValue(this.allowedEntityTypes[0]) + } else { + this.displayEntityTypeSelect = true; + } + } + + if (this.defaultEntityType) { + this.entitySelectFormGroup.get('entityType').setValue(this.defaultEntityType); + } } setDisabledState(isDisabled: boolean): void { @@ -176,4 +203,12 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte } } } + + changeEntity(entity: BaseData): void { + this.entityChanged.emit(entity); + } + + entityAutocompleteMarkAsTouched() { + this.entityAutocompleteComponent.markAsTouched(); + } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index fcfe158da0..dcabc49906 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -52,8 +52,16 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, @coerceBoolean() showLabel: boolean; + private labelValue = this.translate.instant('entity.type'); + + get label(): string { + return this.labelValue; + } + @Input() - label = this.translate.instant('entity.type'); + set label(value: string) { + this.labelValue = value ?? this.translate.instant('entity.type'); + } @Input() @coerceBoolean() diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.html b/ui-ngx/src/app/shared/components/notification/notification.component.html index 674a074a85..0a667ff334 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.html +++ b/ui-ngx/src/app/shared/components/notification/notification.component.html @@ -42,7 +42,7 @@ check_circle_outline
+ [style.background-color]="alarmColorSeverityBackground()"> {{alarmSeverityTranslations.get(notification.info.alarmSeverity) | translate}} diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.ts b/ui-ngx/src/app/shared/components/notification/notification.component.ts index 204c9e3230..9e7398e457 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.ts +++ b/ui-ngx/src/app/shared/components/notification/notification.component.ts @@ -24,7 +24,11 @@ import { } from '@shared/models/notification.models'; import { UtilsService } from '@core/services/utils.service'; import { Router } from '@angular/router'; -import { alarmSeverityColors, alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { + alarmSeverityBackgroundColors, + alarmSeverityColors, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; import tinycolor from 'tinycolor2'; import { StateObject } from '@core/api/widget-api.models'; import { objToBase64URI } from '@core/utils'; @@ -133,8 +137,8 @@ export class NotificationComponent implements OnInit { } } - alarmColorSeverity(alpha: number) { - return tinycolor(alarmSeverityColors.get(this.notification.info.alarmSeverity)).setAlpha(alpha).toRgbString(); + alarmColorSeverityBackground() { + return alarmSeverityBackgroundColors.get(this.notification.info.alarmSeverity); } notificationColor(): string { diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 0469be2589..bcd0076269 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -52,7 +52,9 @@ @for (value of filteredValues | async; track value.value) { } @empty { - {{ 'common.not-found' | translate }} + @if (!allowUserValue) { + {{ 'common.not-found' | translate }} + } } diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index 1c210fdf1f..ab9f1f5831 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -210,8 +210,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { addOnBlur(event: FocusEvent) { const target: HTMLElement = event.relatedTarget as HTMLElement; - if (target && target.tagName !== 'MAT-OPTION') { - this.addItem(this.stringItemInput.nativeElement.value ?? '') + if ((target && target.tagName !== 'MAT-OPTION') || this.allowUserValue) { + this.addItem(this.stringItemInput.nativeElement.value ?? ''); } this.onTouched(); } diff --git a/ui-ngx/src/app/shared/models/alarm-rule.models.ts b/ui-ngx/src/app/shared/models/alarm-rule.models.ts index df137313be..5458d3ab77 100644 --- a/ui-ngx/src/app/shared/models/alarm-rule.models.ts +++ b/ui-ngx/src/app/shared/models/alarm-rule.models.ts @@ -21,7 +21,11 @@ import { TimeUnit } from "@shared/models/time/time.models"; import { ComplexOperation, EntityKeyValueType, FilterPredicateType } from "@shared/models/query/query.models"; import { EntityType } from "@shared/models/entity-type.models"; import { Observable } from "rxjs"; -import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { + CalculatedField, + CalculatedFieldArgument, + CalculatedFieldEventArguments +} from "@shared/models/calculated-field.models"; export const alarmRuleEntityTypeList = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; @@ -244,7 +248,7 @@ export const alarmRuleDefaultScript = '// Triggers when temperature is above 20 degrees\n' + 'return temperature > 20;' -export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, expression: string, argumentsObj?: Record, closeAllOnSave?: boolean) => Observable; +export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit?: boolean, expression?: string) => Observable; export function checkPredicates(predicates: any[], validSet: Set): boolean { for (const predicate of predicates) { diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index fda79a0154..76199faf2c 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -88,11 +88,21 @@ export const alarmSearchStatusTranslations = new Map( export const alarmSeverityColors = new Map( [ - [AlarmSeverity.CRITICAL, '#D12730'], - [AlarmSeverity.MAJOR, '#FEAC0C'], - [AlarmSeverity.MINOR, '#F2DA05'], - [AlarmSeverity.WARNING, '#F66716'], - [AlarmSeverity.INDETERMINATE, '#00000061'] + [AlarmSeverity.CRITICAL, 'var(--tb-alarm-severity-critical, rgb(209, 39, 48))'], + [AlarmSeverity.MAJOR, 'var(--tb-alarm-severity-major, rgb(246, 103, 22))'], + [AlarmSeverity.MINOR, 'var(--tb-alarm-severity-minor, rgb(250, 164, 5))'], + [AlarmSeverity.WARNING, 'var(--tb-alarm-severity-warning, rgb(242, 218, 5))'], + [AlarmSeverity.INDETERMINATE, 'var(--tb-alarm-severity-indeterminate, rgba(0, 0, 0, 0.38))'] + ] +); + +export const alarmSeverityBackgroundColors = new Map( + [ + [AlarmSeverity.CRITICAL, `var(--tb-alarm-severity-critical-bg, rgba(209, 39, 48, 0.06))`], + [AlarmSeverity.MAJOR, 'var(--tb-alarm-severity-major-bg, rgba(246, 103, 22, 0.06))'], + [AlarmSeverity.MINOR, 'var(--tb-alarm-severity-minor-bg, rgba(250, 164, 5, 0.06))'], + [AlarmSeverity.WARNING, 'var(--tb-alarm-severity-warning-bg, rgba(242, 218, 5, 0.06))'], + [AlarmSeverity.INDETERMINATE, 'var(--tb-alarm-severity-indeterminate-bg, rgba(0, 0, 0, 0.06))'] ] ); diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 825c08a69f..9370e59835 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -99,16 +99,20 @@ interface CalculatedFieldTypeTranslate { export const CalculatedFieldTypeTranslations = new Map( [ [CalculatedFieldType.SIMPLE, { - name: 'calculated-fields.type.simple' + name: 'calculated-fields.type.simple', + hint: 'calculated-fields.type.simple-hint', }], [CalculatedFieldType.SCRIPT, { - name: 'calculated-fields.type.script' + name: 'calculated-fields.type.script', + hint: 'calculated-fields.type.script-hint', }], [CalculatedFieldType.GEOFENCING, { - name: 'calculated-fields.type.geofencing' + name: 'calculated-fields.type.geofencing', + hint: 'calculated-fields.type.geofencing-hint', }], [CalculatedFieldType.PROPAGATION, { - name: 'calculated-fields.type.propagation' + name: 'calculated-fields.type.propagation', + hint: 'calculated-fields.type.propagation-hint', }], [CalculatedFieldType.RELATED_ENTITIES_AGGREGATION, { name: 'calculated-fields.type.related-entities-aggregation', @@ -521,7 +525,7 @@ export const ArgumentEntityTypeParamsMap =new Map { - switch (entityId.entityType) { + switch (entityId?.entityType) { case EntityType.ASSET_PROFILE: return { assetTypes: [entityName], @@ -540,6 +544,12 @@ export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entity } } +export const debugCfActionEnabled = (cf: CalculatedField) => { + return (cf.type === CalculatedFieldType.SCRIPT || + (cf.type === CalculatedFieldType.PROPAGATION && cf.configuration.applyExpressionToResolvedArguments) + ); +} + export interface CalculatedFieldArgumentValueBase { argumentName: string; type: ArgumentType; @@ -1073,3 +1083,14 @@ export interface CalculatedFieldsQuery { entities?: Array; name?: Array; } + +export const calculatedFieldMetricFilterDefaultScript = + '// Sample filter script to include only active and unoccupied parking spaces\n' + + '// Goal: Count only parking spaces that are active and currently free\n\n' + + 'return active == true && occupied == false;'; + +export const calculatedFieldMetricMapDefaultScript = + '// Sample map script to convert temperature from Fahrenheit to Celsius\n' + + '// Goal: Apply conversion per entity before aggregation (e.g., for average temperature)\n\n' + + 'var temperatureC = (temperature - 32) / 1.8;\n' + + 'return toFixed(temperatureC, 2);'; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 48428932e4..bc0d3fd470 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -490,7 +490,7 @@ export const entityTypeTranslations = new Map`** + **Example: Convert Temperature from Fahrenheit to Celsius** ```javascript @@ -26,7 +30,9 @@ return { } ``` -Alternatively, using `ctx` to access the argument as an object: +#### Accessing argument via **`ctx.args.`** + +In addition to direct access, arguments can be accessed via the `ctx.args.` object, which includes both the `value` of an argument and its timestamp as `ts`: ```json { @@ -37,7 +43,6 @@ Alternatively, using `ctx` to access the argument as an object: } ``` -You may notice that the object includes both the `value` of an argument and its timestamp as `ts`. Let's modify the function that converts Fahrenheit to Celsius to also return the timestamp information: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md index ce8ca22f51..22316fe418 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md @@ -1,10 +1,70 @@ ## Calculated Field TBEL Filter Function -The **filter()** function is a user-defined script that enables custom calculations using [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. +The **filter()** function is a [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) script used in aggregation metrics of a calculated field. + It receives arguments configured in the calculated field setup, along with an additional `ctx` object that stores `latestTs` and provides access to all arguments. +It allows you to include or exclude related entities from aggregation based on their telemetry or attribute values. + +The filter is evaluated per related entity before the aggregation function is applied. + ### Function Signature ```javascript -function calculate(ctx, arg1, arg2, ...): boolean +function filter(ctx, arg1, arg2, ...): boolean +``` + +### Supported Arguments + +Arguments are passed to the function by **name** defined in the calculated field configuration. + +There are two types of arguments supported in the calculated field configuration: **Attribute and Latest Telemetry Arguments** + +These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. + +#### Direct argument access via **``** + +**Example: Count free parking spaces** + +**Goal**: Include only parking spaces that are active and not occupied. + +```javascript +return active == true && occupied == false; +``` + +Only entities that satisfy this condition will be included in the aggregation. + +#### Accessing argument via **`ctx.args.`** + +In addition to direct access, arguments can be accessed via the `ctx.args.` object, which includes both the `value` of an argument and its timestamp as `ts`: + +```json +{ + "consumption": { + "ts": 1740644656669, + "value": 542.6 + } +} +``` + +The `ctx.latestTs` property represents the latest timestamp across all related entities and their arguments participating in the aggregation. + +**Example: Calculate the total consumption across multiple related pumps** + +**Scenario**: Each pump reports consumption telemetry approximately every 10 minutes, but reporting times may vary due to network delays (up to ~30 seconds). +To avoid counting outdated values, only recently updated telemetry should be included. + +**Goal**: Include only pumps whose consumption value was reported within 1 minute of the latest timestamp. + +```javascript +var ONE_MINUTE = 60 * 1000; +return (ctx.latestTs - ctx.args.consumption.ts) <= ONE_MINUTE; ``` + +### Function return format + +The function **must** return a boolean: +- `true` → include entity in aggregation +- `false` → exclude entity from aggregation + +Any other return type is considered invalid. diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/map_expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/map_expression_fn.md new file mode 100644 index 0000000000..41ccf3540b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/map_expression_fn.md @@ -0,0 +1,79 @@ +## Calculated Field TBEL Map Function + +The **map()** function is a [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) script used in aggregation metrics of a calculated field. + +It determines the value applied by each related entity to the aggregation. + +The function receives arguments configured in the calculated field setup, along with an additional `ctx` object that stores `latestTs` and provides access to all arguments. + +The function is evaluated **per related entity**, after filtering is applied. + +### Function Signature + +```javascript +function map(ctx, arg1, arg2, ...): number | boolean | string +``` + +### Supported Arguments + +Arguments are passed to the function by **name** defined in the calculated field configuration. + +There are two types of arguments supported in the calculated field configuration: **Attribute and Latest Telemetry Arguments** + +These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. + +#### Direct argument access via **``** + +**Example: Calculate average temperature across sensors** + +**Scenario**: Multiple related sensors report temperature in Fahrenheit. You want to aggregate temperature values, but the aggregation must be performed in Celsius. + +**Goal**: Convert temperature to Celsius before aggregation + +```javascript +var temperatureC = (temperature - 32) / 1.8; +return toFixed(temperatureC, 2); +``` + +Instead of creating a separate calculated field for conversion, the transformation is performed per entity, before aggregation. + +#### Accessing argument via **`ctx.args.`** + +In addition to direct access, arguments can be accessed via the `ctx.args.` object, which includes both the `value` of an argument and its timestamp as `ts`: + +```json +{ + "temperature": { + "ts": 1740644656761, + "value": 33.6 + } +} +``` + +The `ctx.latestTs` property represents the latest timestamp across all related entities and their arguments participating in the aggregation. + +**Example: Calculate average temperature** + +**Scenario**: Each sensor reports temperature approximately every 10 minutes, but reporting times may vary due to network delays (up to ~30 seconds). +If a temperature value is outdated, you want the entity to remain part of the aggregation, but contribute a default value instead of a stale one. + +**Goal**: Return a default value when the temperature was not reported within 1 minute of the latest aggregation timestamp. + +```javascript +var ONE_MINUTE = 60 * 1000; +if ((ctx.latestTs - ctx.args.temperature.ts) > ONE_MINUTE) { + return 0; +}; +return temperature; +``` + +### Function return format + +The returned value is passed directly to the aggregation engine: + +| Aggregation function | Expected return value | +|--------------------------------|-----------------------| +| Count, Count unique | any value | +| Sum, Average, Minimum, Maximum | number | + +Returning a value of an incompatible type may result in the entity being ignored or the aggregation producing incorrect results. diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e023179dd3..5e083ec165 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1086,7 +1086,7 @@ "filter-types": "Audit log types" }, "debug-settings": { - "label": "Debug Configuration", + "label": "Debug configuration", "on-failure": "Failures only (24/7)", "all-messages": "All messages ({{time}})", "failures": "Failures", @@ -1104,17 +1104,23 @@ "selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected", "type": { "simple": "Simple", + "simple-hint": "Simple arithmetic calculation based on input arguments.", "script": "Script", + "script-hint": "Calculation over defined arguments using a TBEL script.", "geofencing" : "Geofencing", + "geofencing-hint" : "Evaluation of entity GPS position and transitions against configured geofencing zone groups.", "propagation": "Propagation", + "propagation-hint": "Propagation of data to parent or child entities based on relation direction and type.", "related-entities-aggregation": "Related entities aggregation", - "related-entities-aggregation-hint": "Aggregation of data from related entities.", + "related-entities-aggregation-hint": "Aggregation of latest data from related entities.", "time-series-data-aggregation": "Time series data aggregation", "time-series-data-aggregation-hint": "Aggregation of historical data from a current entity." }, + "preview": "Preview", "arguments": "Arguments", "decimals-by-default": "Decimals by default", "debugging": "Calculated field debugging", + "calculated-field-details": "Calculated field details", "argument-name": "Argument name", "name": "Name", "datasource": "Datasource", @@ -1150,6 +1156,7 @@ "asset-name": "Asset name", "timeseries": "Time series", "output": "Output", + "output-hint": "Defines how output is processed.", "create": "Create new calculated field", "file": "Calculated field file", "invalid-file-error": "Invalid file format. Please make sure the file is a valid JSON file.", @@ -1182,7 +1189,7 @@ "perimeter-attribute-key": "Perimeter attribute key", "perimeter-attribute-key-required": "Perimeter attribute key is required.", "perimeter-attribute-key-pattern": "Perimeter attribute key is invalid.", - "entity-zone-relationship": "Path from Entity to Zones *", + "entity-zone-relationship": "Path from Entity to Zones", "direction": "Relation direction", "direction-from": "From entity to zone", "direction-to": "From zone to entity", @@ -1216,10 +1223,12 @@ "aggregation-path-related-entities": "Aggregation path to related entities", "deduplication-interval": "Deduplication interval", "deduplication-interval-min": "Deduplication interval should be at least {{ sec }} seconds.", + "deduplication-interval-hint": "Minimum time between telemetry aggregations.", "deduplication-interval-required": "Deduplication interval is required.", "calculated-field-filter-title": "Calculated field filter", "filter-title": "Filter", "calculated-field-types": "Calculated field types", + "events": "Events", "any-type": "Any type", "metrics": { "metrics": "Metrics", @@ -1243,6 +1252,7 @@ }, "filtered": "Filtered", "value-source": "Value source", + "value-source-hint": "Defines how the value for aggregation is obtained.", "value-source-type": { "key": "Key", "function": "Function" @@ -1304,9 +1314,10 @@ "aggregate-period-hint-offset-and-so-on": "Your aggregation interval will be: {{ interval }} and so on.", "entity-aggregation": { "argument-hint": "Data will be fetched from current entity.", + "argument-title-hint": "Defines the input arguments used for aggregation.", "argument-setting-hint": "Latest telemetry is the only available argument type for this calculated field.", "aggregation-interval": "Aggregation interval", - "aggregation-interval-hint": "Defines how often to perform aggregation. Example: every 1 hour aggregates data at 00:00, 01:00, 02:00, etc.", + "aggregation-interval-hint": "Defines how often aggregation is performed. Example: every 1 hour aggregates data at 00:00, 01:00, 02:00, etc. Aggregation results are stored with the timestamp corresponding to the start of the aggregation interval.", "apply-offset": "Apply offset to aggregation interval", "apply-offset-hint": "Defines how much to shift the start of each aggregation period (e.g., +10 minutes - 00:10, 01:10).", "offset-value": "Offset value", @@ -1370,11 +1381,16 @@ "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} seconds.", "propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type.", "data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.", - "aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported.", - "arguments-aggregation": "Defines input parameters used for filtering and aggregation.", + "aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities, based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported.", + "arguments-aggregation": "Defines the input arguments used for filtering and aggregation.", "setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.", - "metrics": "Defines metrics aggregated based on the configured arguments.", - "import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field structure." + "metrics": "Defines metrics aggregated based on configured arguments.", + "entity-aggregation-metrics": "Defines metrics aggregated based on configured arguments over the specified time intervals.", + "import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field structure.", + "simple-expression-title": "Arithmetic expression that defines how the calculated value is computed.", + "script-title": "TBEL script that defines the calculation logic and output values.", + "simple-arguments": "Arithmetic expression that defines how the calculated value is computed.", + "script-arguments": "Defines the input arguments available to the script." } }, "alarm-rule": { @@ -1392,6 +1408,7 @@ "create": "Create new alarm rule", "add": "Add alarm rule", "copy": "Copy alarm rule configuration", + "details": "Alarm rule details", "no-found": "No alarm rules found", "list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }", "selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected", @@ -1418,7 +1435,7 @@ "condition-during": "During {{during}}", "condition-during-dynamic": "During \"{{ attribute }}\"", "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", - "condition-repeat-times-dynamic": "Repeats \"{{ attribute }}\"", + "condition-repeat-times-dynamic": "Repeats \"{{ attribute }}\" times", "filter-preview": "Filter preview", "condition-settings": "Condition settings", "static": "Static", @@ -1535,7 +1552,7 @@ "mode": "Mode", "type": "Type", "value-required": "Value is required.", - "min-value": "Value must be 0 or greater.", + "min-value": "Value must be 1 or greater.", "argument-in-use": "Argument is used as general argument.", "import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule structure.", "no-filter-preview": "No filter specified", @@ -7240,7 +7257,7 @@ "load-relations": "Load relations", "load-attributes": "Load attributes", "load-credentials": "Load credentials", - "load-calculated-fields": "Load calculated fields", + "load-calculated-fields": "Load calculated fields and alarm rules", "compare-with-current": "Compare with current", "diff-entity-with-version": "Diff with entity version '{{versionName}}'", "previous-difference": "Previous Difference", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 56dc0f47a5..5f21abf7c8 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -2716,21 +2716,16 @@ dependencies: "@types/geojson" "*" -"@types/geojson@*", "@types/geojson@^7946.0.10": - version "7946.0.14" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" - integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== +"@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.16": + version "7946.0.16" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== "@types/geojson@7946.0.8": version "7946.0.8" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== -"@types/geojson@^7946.0.16": - version "7946.0.16" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" - integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== - "@types/hammerjs@^2.0.45": version "2.0.45" resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.45.tgz#ffa764bb68a66c08db6efb9c816eb7be850577b1" @@ -3629,23 +3624,23 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== +body-parser@~1.20.3: + version "1.20.4" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f" + integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA== dependencies: - bytes "3.1.2" + bytes "~3.1.2" content-type "~1.0.5" debug "2.6.9" depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" + destroy "~1.2.0" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + on-finished "~2.4.1" + qs "~6.14.0" + raw-body "~2.5.3" type-is "~1.6.18" - unpipe "1.0.0" + unpipe "~1.0.0" bonjour-service@^1.2.1: version "1.2.1" @@ -3717,7 +3712,7 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== -bytes@3.1.2: +bytes@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== @@ -3759,6 +3754,14 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -4074,7 +4077,7 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -content-disposition@0.5.4: +content-disposition@~0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== @@ -4096,15 +4099,15 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie-signature@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" + integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookie@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== copy-anything@^2.0.1: version "2.0.6" @@ -4689,7 +4692,7 @@ delegate@^3.1.2: resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== -depd@2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -4699,7 +4702,7 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -destroy@1.2.0: +destroy@1.2.0, destroy@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== @@ -4867,11 +4870,6 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - encodeurl@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" @@ -4978,14 +4976,7 @@ es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23 unbox-primitive "^1.0.2" which-typed-array "^1.1.15" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-define-property@^1.0.1: +es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== @@ -5000,30 +4991,14 @@ es-module-lexer@^1.2.1, es-module-lexer@^1.5.3: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== - dependencies: - es-errors "^1.3.0" - -es-object-atoms@^1.1.1: +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" -es-set-tostringtag@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" - integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== - dependencies: - get-intrinsic "^1.2.4" - has-tostringtag "^1.0.2" - hasown "^2.0.1" - -es-set-tostringtag@^2.1.0: +es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== @@ -5325,38 +5300,38 @@ exponential-backoff@^3.1.1: integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== express@^4.17.3: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + version "4.22.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069" + integrity sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" + body-parser "~1.20.3" + content-disposition "~0.5.4" content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" + cookie "~0.7.1" + cookie-signature "~1.0.6" debug "2.6.9" depd "2.0.0" encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" + finalhandler "~1.3.1" + fresh "~0.5.2" + http-errors "~2.0.0" merge-descriptors "1.0.3" methods "~1.1.2" - on-finished "2.4.1" + on-finished "~2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.10" + path-to-regexp "~0.1.12" proxy-addr "~2.0.7" - qs "6.13.0" + qs "~6.14.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" + send "~0.19.0" + serve-static "~1.16.2" setprototypeof "1.2.0" - statuses "2.0.1" + statuses "~2.0.1" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -5437,17 +5412,17 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== +finalhandler@~1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88" + integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg== dependencies: debug "2.6.9" encodeurl "~2.0.0" escape-html "~1.0.3" - on-finished "2.4.1" + on-finished "~2.4.1" parseurl "~1.3.3" - statuses "2.0.1" + statuses "~2.0.2" unpipe "~1.0.0" find-cache-dir@^4.0.0: @@ -5575,7 +5550,7 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== -fresh@0.5.2: +fresh@~0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== @@ -5660,18 +5635,7 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -5791,14 +5755,7 @@ good-listener@^1.2.2: dependencies: delegate "^3.1.2" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -gopd@^1.2.0: +gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== @@ -5850,17 +5807,12 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: +has-proto@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-symbols@^1.1.0: +has-symbols@^1.0.2, has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== @@ -5872,7 +5824,7 @@ has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -5929,17 +5881,6 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - http-errors@~1.6.2: version "1.6.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" @@ -5950,6 +5891,17 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" +http-errors@~2.0.0, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" @@ -6013,13 +5965,6 @@ hyperdyperid@^1.2.0: resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== -iconv-lite@0.4.24, iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@0.6, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -6027,6 +5972,13 @@ iconv-lite@0.6, iconv-lite@^0.6.2, iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +iconv-lite@^0.4.24, iconv-lite@~0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" @@ -6092,7 +6044,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7616,10 +7568,10 @@ object-hash@^3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.1, object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-keys@^1.1.1: version "1.1.1" @@ -7674,7 +7626,7 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -on-finished@2.4.1, on-finished@^2.4.1: +on-finished@^2.4.1, on-finished@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -7968,7 +7920,7 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@0.1.10, path-to-regexp@0.1.12: +path-to-regexp@0.1.12, path-to-regexp@~0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== @@ -8259,12 +8211,12 @@ qrcode@^1.5.4: pngjs "^5.0.0" yargs "^15.3.1" -qs@6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== +qs@~6.14.0: + version "6.14.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159" + integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== dependencies: - side-channel "^1.0.6" + side-channel "^1.1.0" queue-microtask@^1.2.2: version "1.2.3" @@ -8300,15 +8252,15 @@ raphael@^2.3.0: dependencies: eve-raphael "0.5.0" -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== +raw-body@~2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" + integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + unpipe "~1.0.0" rbush@^3.0.1: version "3.0.1" @@ -8706,7 +8658,7 @@ selfsigned@^2.4.1: "@types/node-forge" "^1.3.0" node-forge "^1" -semver@7.6.3, semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3: +semver@7.6.3: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -8721,24 +8673,29 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -send@0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== +semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +send@~0.19.0, send@~0.19.1: + version "0.19.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" + integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg== dependencies: debug "2.6.9" depd "2.0.0" destroy "1.2.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" + fresh "~0.5.2" + http-errors "~2.0.1" mime "1.6.0" ms "2.1.3" - on-finished "2.4.1" + on-finished "~2.4.1" range-parser "~1.2.1" - statuses "2.0.1" + statuses "~2.0.2" serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" @@ -8760,15 +8717,15 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== +serve-static@~1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" + integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA== dependencies: encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.19.0" + send "~0.19.1" set-blocking@^2.0.0: version "2.0.0" @@ -8807,7 +8764,7 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== -setprototypeof@1.2.0: +setprototypeof@1.2.0, setprototypeof@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== @@ -8836,15 +8793,45 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" @@ -9050,16 +9037,16 @@ ssri@^10.0.0: dependencies: minipass "^7.0.3" -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - "statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@~2.0.1, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -9399,7 +9386,7 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.1: +toidentifier@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== @@ -9661,7 +9648,7 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==