Merge branch 'rc' into features/add_tooltip_option_to_show_stack_mode_total_value_on_timeseries_chart_widgets

This commit is contained in:
Paolo Cristiani 2025-06-18 08:35:47 +02:00 committed by GitHub
commit f3b0cf8ac5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 101 additions and 30 deletions

View File

@ -335,6 +335,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
Edge edge = edgeGrpcSession.getEdge(); Edge edge = edgeGrpcSession.getEdge();
TenantId tenantId = edge.getTenantId(); TenantId tenantId = edge.getTenantId();
log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId);
if (sessions.containsKey(edgeId)) {
destroySession(sessions.get(edgeId));
}
sessions.put(edgeId, edgeGrpcSession); sessions.put(edgeId, edgeGrpcSession);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock(); newEventLock.lock();
@ -503,7 +506,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} finally { } finally {
newEventLock.unlock(); newEventLock.unlock();
} }
toRemove.destroy(); destroySession(toRemove);
TenantId tenantId = toRemove.getEdge().getTenantId(); TenantId tenantId = toRemove.getEdge().getTenantId();
save(tenantId, edgeId, ACTIVITY_STATE, false); save(tenantId, edgeId, ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis(); long lastDisconnectTs = System.currentTimeMillis();
@ -516,6 +519,12 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
edgeIdServiceIdCache.evict(edgeId); edgeIdServiceIdCache.evict(edgeId);
} }
private void destroySession(EdgeGrpcSession session) {
try (session) {
session.destroy();
}
}
private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { private void save(TenantId tenantId, EdgeId edgeId, String key, long value) {
log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value); log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value);
if (persistToTelemetry) { if (persistToTelemetry) {

View File

@ -165,7 +165,6 @@ public class JobManagerTest extends AbstractControllerTest {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId); Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED); assertThat(job.getStatus()).isEqualTo(JobStatus.CANCELLED);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount - 1); assertThat(job.getResult().getDiscardedCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount);
assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount); assertThat(job.getResult().getCompletedCount()).isEqualTo(tasksCount);

View File

@ -36,7 +36,7 @@ public abstract class AbstractEntityProfileNameQueryProcessor<T extends EntityFi
public AbstractEntityProfileNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter, EntityType entityType) { public AbstractEntityProfileNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query, T filter, EntityType entityType) {
super(repo, ctx, query, filter, entityType); super(repo, ctx, query, filter, entityType);
entityProfileNames = new HashSet<>(getProfileNames(this.filter)); entityProfileNames = new HashSet<>(getProfileNames(this.filter));
pattern = RepositoryUtils.toSqlLikePattern(getEntityNameFilter(filter)); pattern = RepositoryUtils.toContainsSqlLikePattern(getEntityNameFilter(filter));
} }
protected abstract String getEntityNameFilter(T filter); protected abstract String getEntityNameFilter(T filter);

View File

@ -43,7 +43,7 @@ public abstract class AbstractEntityProfileQueryProcessor<T extends EntityFilter
entityProfileIds.add(dp.getId()); entityProfileIds.add(dp.getId());
} }
} }
pattern = RepositoryUtils.toSqlLikePattern(getEntityNameFilter(filter)); pattern = RepositoryUtils.toContainsSqlLikePattern(getEntityNameFilter(filter));
} }
protected abstract String getEntityNameFilter(T filter); protected abstract String getEntityNameFilter(T filter);

View File

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.edqs.query.processor; package org.thingsboard.server.edqs.query.processor;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.permission.QueryContext; import org.thingsboard.server.common.data.permission.QueryContext;
import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.EntityFilter;
@ -106,7 +107,7 @@ public abstract class AbstractRelationQueryProcessor<T extends EntityFilter> ext
private Set<EntityData<?>> getEntitiesSet(RelationsRepo relations) { private Set<EntityData<?>> getEntitiesSet(RelationsRepo relations) {
Set<EntityData<?>> result = new HashSet<>(); Set<EntityData<?>> result = new HashSet<>();
Set<UUID> processed = new HashSet<>(); Set<RelationSearchTask> processed = new HashSet<>();
Queue<RelationSearchTask> tasks = new LinkedList<>(); Queue<RelationSearchTask> tasks = new LinkedList<>();
int maxLvl = getMaxLevel() == 0 ? MAXIMUM_QUERY_LEVEL : Math.max(1, getMaxLevel()); int maxLvl = getMaxLevel() == 0 ? MAXIMUM_QUERY_LEVEL : Math.max(1, getMaxLevel());
for (UUID uuid : getRootEntities()) { for (UUID uuid : getRootEntities()) {
@ -114,7 +115,7 @@ public abstract class AbstractRelationQueryProcessor<T extends EntityFilter> ext
} }
while (!tasks.isEmpty()) { while (!tasks.isEmpty()) {
RelationSearchTask task = tasks.poll(); RelationSearchTask task = tasks.poll();
if (processed.add(task.entityId)) { if (processed.add(task)) {
var entityLvl = task.lvl + 1; var entityLvl = task.lvl + 1;
Set<RelationInfo> entities = EntitySearchDirection.FROM.equals(getDirection()) ? relations.getFrom(task.entityId) : relations.getTo(task.entityId); Set<RelationInfo> entities = EntitySearchDirection.FROM.equals(getDirection()) ? relations.getFrom(task.entityId) : relations.getTo(task.entityId);
if (isFetchLastLevelOnly() && entities.isEmpty() && task.previous != null && check(task.previous)) { if (isFetchLastLevelOnly() && entities.isEmpty() && task.previous != null && check(task.previous)) {
@ -157,6 +158,7 @@ public abstract class AbstractRelationQueryProcessor<T extends EntityFilter> ext
protected abstract boolean check(RelationInfo relationInfo); protected abstract boolean check(RelationInfo relationInfo);
@RequiredArgsConstructor @RequiredArgsConstructor
@EqualsAndHashCode
private static class RelationSearchTask { private static class RelationSearchTask {
private final UUID entityId; private final UUID entityId;
private final int lvl; private final int lvl;

View File

@ -30,7 +30,7 @@ public class EntityNameQueryProcessor extends AbstractSimpleQueryProcessor<Entit
public EntityNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) { public EntityNameQueryProcessor(TenantRepo repo, QueryContext ctx, EdqsQuery query) {
super(repo, ctx, query, (EntityNameFilter) query.getEntityFilter(), ((EntityNameFilter) query.getEntityFilter()).getEntityType()); super(repo, ctx, query, (EntityNameFilter) query.getEntityFilter(), ((EntityNameFilter) query.getEntityFilter()).getEntityType());
pattern = RepositoryUtils.toSqlLikePattern(filter.getEntityNameFilter()); pattern = RepositoryUtils.toContainsSqlLikePattern(filter.getEntityNameFilter());
} }
@Override @Override

View File

@ -54,7 +54,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -249,11 +248,11 @@ public class RepositoryUtils {
} }
return switch (predicate.getOperation()) { return switch (predicate.getOperation()) {
case EQUAL -> value.equals(predicateValue); case EQUAL -> value.equals(predicateValue);
case STARTS_WITH -> value.startsWith(predicateValue); case STARTS_WITH -> toStartsWithSqlLikePattern(predicateValue).matcher(value).matches();
case ENDS_WITH -> value.endsWith(predicateValue); case ENDS_WITH -> toEndsWithSqlLikePattern(predicateValue).matcher(value).matches();
case NOT_EQUAL -> !value.equals(predicateValue); case NOT_EQUAL -> !value.equals(predicateValue);
case CONTAINS -> value.contains(predicateValue); case CONTAINS -> toContainsSqlLikePattern(predicateValue).matcher(value).matches();
case NOT_CONTAINS -> !value.contains(predicateValue); case NOT_CONTAINS -> !toContainsSqlLikePattern(predicateValue).matcher(value).matches();
case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue));
case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue));
}; };
@ -304,6 +303,15 @@ public class RepositoryUtils {
return true; return true;
} else if (filterPredicates.getOperation() == OR) { } else if (filterPredicates.getOperation() == OR) {
for (KeyFilterPredicate filterPredicate : filterPredicates.getPredicates()) { for (KeyFilterPredicate filterPredicate : filterPredicates.getPredicates()) {
// Emulate the SQL-like behavior of ThingsBoard's Entity Data Query service:
// for COMPLEX filters, return no results if filter value is empty
if (filterPredicate instanceof StringFilterPredicate stringFilterPredicate) {
if (StringUtils.isEmpty(stringFilterPredicate.getValue().getValue())) {
continue;
}
}
if (simpleKeyFilter.check(value, filterPredicate)) { if (simpleKeyFilter.check(value, filterPredicate)) {
return true; return true;
} }
@ -314,25 +322,40 @@ public class RepositoryUtils {
} }
} }
public static Pattern toSqlLikePattern(String nameFilter) { public static Pattern toContainsSqlLikePattern(String filter) {
if (StringUtils.isNotBlank(nameFilter)) { if (StringUtils.isNotBlank(filter)) {
boolean percentSymbolOnStart = nameFilter.startsWith("%"); return toSqlLikePattern(filter, ".*", ".*");
boolean percentSymbolOnEnd = nameFilter.endsWith("%");
if (percentSymbolOnStart) {
nameFilter = nameFilter.substring(1);
}
if (percentSymbolOnEnd) {
nameFilter = nameFilter.substring(0, nameFilter.length() - 1);
}
if (percentSymbolOnStart || percentSymbolOnEnd) {
return Pattern.compile((percentSymbolOnStart ? ".*" : "") + Pattern.quote(nameFilter) + (percentSymbolOnEnd ? ".*" : ""), Pattern.CASE_INSENSITIVE);
} else {
return Pattern.compile(Pattern.quote(nameFilter) + ".*", Pattern.CASE_INSENSITIVE);
}
} }
return null; return null;
} }
private static Pattern toStartsWithSqlLikePattern(String filter) {
return toSqlLikePattern(filter, "^", ".*");
}
private static Pattern toEndsWithSqlLikePattern(String filter) {
return toSqlLikePattern(filter, ".*", "$");
}
private static Pattern toSqlLikePattern(String value, String prefix, String suffix) {
if (value.contains("%") || value.contains("_")) {
String regexValue = value
.replace("_", ".")
.replace("%", ".*");
String regex;
if ("^".equals(prefix)) {
regex = "^" + regexValue + (regexValue.endsWith(".*") ? "" : ".*");
} else if ("$".equals(suffix)) {
regex = (regexValue.startsWith(".*") ? "" : ".*") + regexValue + "$";
} else {
regex = (regexValue.startsWith(".*") ? "" : ".*") + regexValue + (regexValue.endsWith(".*") ? "" : ".*");
}
return Pattern.compile(regex);
} else {
return Pattern.compile(prefix + Pattern.quote(value) + suffix);
}
}
@FunctionalInterface @FunctionalInterface
public interface SimpleKeyFilter<T> { public interface SimpleKeyFilter<T> {

View File

@ -70,7 +70,45 @@ public class RepositoryUtilsTest {
Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 123, loranet 124"), true), Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 123, loranet 124"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 125, loranet 126"), false), Arguments.of("loranet 123", getNameFilter(StringOperation.IN, "loranet 125, loranet 126"), false),
Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 125, loranet 126"), true), Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 125, loranet 126"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 123, loranet 126"), false) Arguments.of("loranet 123", getNameFilter(StringOperation.NOT_IN, "loranet 123, loranet 126"), false),
// Basic CONTAINS
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%loranet"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%ranet%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%123"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%loranx%"), false),
// Basic STARTS_WITH
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "loranet%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lora%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lorax%"), false),
// Basic ENDS_WITH
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "%123"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "%23"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "%124"), false),
// CONTAINS with _
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet_123"), true), // '_' = ' '
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loranet_12_"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "loran_t%"), true),
// STARTS_WITH with _
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "loranet_"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lora__t%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.STARTS_WITH, "lor_net%"), true),
// ENDS_WITH with _
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "_23"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "_2_"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.ENDS_WITH, "_3"), true),
// Mixed patterns
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "lora__t 1%"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "lora%net%3"), true),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "%o_anet%2_3"), false),
Arguments.of("loranet 123", getNameFilter(StringOperation.CONTAINS, "lora___ ___"), true)
); );
} }

View File

@ -54,7 +54,7 @@
<jjwt.version>0.12.5</jjwt.version> <jjwt.version>0.12.5</jjwt.version>
<slf4j.version>2.0.17</slf4j.version> <slf4j.version>2.0.17</slf4j.version>
<log4j.version>2.24.3</log4j.version> <log4j.version>2.24.3</log4j.version>
<logback.version>1.5.18</logback.version> <logback.version>1.5.6</logback.version>
<rat.version>0.10</rat.version> <!-- unused --> <rat.version>0.10</rat.version> <!-- unused -->
<cassandra.version>4.17.0</cassandra.version> <cassandra.version>4.17.0</cassandra.version>
<metrics.version>4.2.25</metrics.version> <metrics.version>4.2.25</metrics.version>

View File

@ -16,7 +16,7 @@
--> -->
<section [formGroup]="credentialsConfigFormGroup" class="flex flex-col"> <section [formGroup]="credentialsConfigFormGroup" class="flex flex-col">
<mat-expansion-panel class="tb-credentials-config-panel-group"> <mat-expansion-panel class="tb-credentials-config-panel-group" expanded="true">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title translate>rule-node-config.credentials</mat-panel-title> <mat-panel-title translate>rule-node-config.credentials</mat-panel-title>
<mat-panel-description> <mat-panel-description>