From d9b77251133551e4b8f933fe0b9a268d06730877 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 26 Mar 2024 14:24:18 +0100 Subject: [PATCH 001/136] WIP Redis junit5 test container --- dao/pom.xml | 5 ++ .../server/dao/RedisJUnit5Test.java | 64 +++++++++++++++++++ pom.xml | 12 ++++ 3 files changed, 81 insertions(+) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java diff --git a/dao/pom.xml b/dao/pom.xml index ab62d8f013..700d2b0135 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -216,6 +216,11 @@ jdbc test + + org.testcontainers + junit-jupiter + test + org.springframework spring-context-support diff --git a/dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java b/dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java new file mode 100644 index 0000000000..43e788cccb --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +@Slf4j +public class RedisJUnit5Test { + + @Container + private static final GenericContainer REDIS = new GenericContainer("redis:7.2-bookworm") + .withLogConsumer(s -> log.error(((OutputFrame) s).getUtf8String().trim())) + .withExposedPorts(6379); + + @BeforeAll + static void beforeAll() { + log.warn("Starting redis..."); + REDIS.start(); + System.setProperty("cache.type", "redis"); + System.setProperty("redis.connection.type", "standalone"); + System.setProperty("redis.standalone.host", REDIS.getHost()); + System.setProperty("redis.standalone.port", String.valueOf(REDIS.getMappedPort(6379))); + + } + + @AfterAll + static void afterAll() { + List.of("cache.type", "redis.connection.type", "redis.standalone.host", "redis.standalone.port") + .forEach(System.getProperties()::remove); + REDIS.stop(); + log.warn("Redis is stopped"); + } + + @Test + void test() { + assertThat(REDIS.isRunning()).isTrue(); + } + +} diff --git a/pom.xml b/pom.xml index d2450e0a65..70a35d77d2 100755 --- a/pom.xml +++ b/pom.xml @@ -2021,6 +2021,18 @@ + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + junit + junit + + + org.zeroturnaround zt-exec From 3a86913e24068f020ed7f274c09bd483b6cd39d5 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 3 Apr 2024 10:05:01 +0200 Subject: [PATCH 002/136] RedisTbTransactionalCache refactored deprecated methods --- .../cache/RedisTbTransactionalCache.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index abfbb398c9..cd5db13995 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -29,7 +29,6 @@ import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.thingsboard.server.common.data.FstStatsService; -import redis.clients.jedis.Connection; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.util.JedisClusterCRC16; @@ -79,7 +78,7 @@ public abstract class RedisTbTransactionalCache get(K key) { try (var connection = connectionFactory.getConnection()) { byte[] rawKey = getRawKey(key); - byte[] rawValue = connection.get(rawKey); + byte[] rawValue = connection.stringCommands().get(rawKey); if (rawValue == null) { return null; } else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { @@ -113,7 +112,7 @@ public abstract class RedisTbTransactionalCache(this, connection); } - private RedisConnection getConnection(byte[] rawKey) { + protected RedisConnection getConnection(byte[] rawKey) { if (!connectionFactory.isRedisClusterAware()) { return connectionFactory.getConnection(); } @@ -168,7 +167,7 @@ public abstract class RedisTbTransactionalCache Date: Wed, 3 Apr 2024 11:46:03 +0200 Subject: [PATCH 003/136] WIP CachedRedisSqlTimeseriesLatestDao.java --- .../src/main/resources/thingsboard.yml | 7 ++ build.sh | 57 ++++++++++ .../server/cache/TbJavaRedisSerializer.java | 35 ++++++ .../util/SqlTsLatestAnyDaoCachedRedis.java | 26 +++++ .../server/common/data/CacheConstants.java | 1 + .../CachedRedisSqlTimeseriesLatestDao.java | 104 ++++++++++++++++++ .../dao/timeseries/TsLatestCacheKey.java | 40 +++++++ .../dao/timeseries/TsLatestRedisCache.java | 35 ++++++ .../resources/application-test.properties | 4 + dao/src/test/resources/logback.xml | 1 + 10 files changed, 310 insertions(+) create mode 100755 build.sh create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDaoCachedRedis.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index daf9f5d1c1..535a396762 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -489,6 +489,10 @@ cache: attributes: # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' if you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random' enabled: "${CACHE_ATTRIBUTES_ENABLED:true}" + ts_latest: + # Will enable cache-aside strategy for SQL timeseries latest DAO. + # make sure that if cache.type is 'redis' and cache.ts_latest.enabled is 'true' if you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random' + enabled: "${CACHE_TS_LATEST_ENABLED:true}" specs: relations: timeToLiveInMinutes: "${CACHE_SPECS_RELATIONS_TTL:1440}" # Relations cache TTL @@ -539,6 +543,9 @@ cache: attributes: timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" # Attributes cache TTL maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" # 0 means the cache is disabled + tsLatest: + timeToLiveInMinutes: "${CACHE_SPECS_TS_LATEST_TTL:1440}" # Timeseries latest cache TTL + maxSize: "${CACHE_SPECS_TS_LATEST_MAX_SIZE:100000}" # 0 means the cache is disabled userSessionsInvalidation: # The value of this TTL is ignored and replaced by the JWT refresh token expiration time timeToLiveInMinutes: "0" diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000..2c6a7d23fa --- /dev/null +++ b/build.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# +# Copyright © 2016-2024 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. +# + +set -e # exit on any error + +#PROJECTS="msa/tb-node,msa/web-ui,rule-engine-pe/rule-node-twilio-sms" +PROJECTS="" + +if [ "$1" ]; then + PROJECTS="--projects $1" +fi + +echo "Building and pushing [amd64,arm64] projects '$PROJECTS' ..." +echo "HELP: usage ./build.sh [projects]" +echo "HELP: example ./build.sh msa/web-ui,msa/web-report" +java -version +#echo "Cleaning ui-ngx/node_modules" && rm -rf ui-ngx/node_modules + +MAVEN_OPTS="-Xmx1024m" NODE_OPTIONS="--max_old_space_size=4096" DOCKER_CLI_EXPERIMENTAL=enabled DOCKER_BUILDKIT=0 \ +mvn -T2 license:format clean install -DskipTests \ + $PROJECTS --also-make +# \ +# -Dpush-docker-amd-arm-images +# -Ddockerfile.skip=false -Dpush-docker-image=true +# --offline +# --projects '!msa/web-report' --also-make + +# push all +# mvn -T 1C license:format clean install -DskipTests -Ddockerfile.skip=false -Dpush-docker-image=true + + +## Build and push AMD and ARM docker images using docker buildx +## Reference to article how to setup docker miltiplatform build environment: https://medium.com/@artur.klauser/building-multi-architecture-docker-images-with-buildx-27d80f7e2408 +## install docker-ce from docker repo https://docs.docker.com/engine/install/ubuntu/ +# sudo apt install -y qemu-user-static binfmt-support +# export DOCKER_CLI_EXPERIMENTAL=enabled +# docker version +# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes +# docker buildx create --name mybuilder +# docker buildx use mybuilder +# docker buildx inspect --bootstrap +# docker buildx ls +# mvn clean install -P push-docker-amd-arm-images \ No newline at end of file diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java new file mode 100644 index 0000000000..92c9f37900 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbJavaRedisSerializer.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache; + +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +public class TbJavaRedisSerializer implements TbRedisSerializer { + + final RedisSerializer serializer = RedisSerializer.java(); + + @Override + public byte[] serialize(V value) throws SerializationException { + return serializer.serialize(value); + } + + @Override + public V deserialize(K key, byte[] bytes) throws SerializationException { + return (V) serializer.deserialize(bytes); + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDaoCachedRedis.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDaoCachedRedis.java new file mode 100644 index 0000000000..634302a1f3 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/util/SqlTsLatestAnyDaoCachedRedis.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("('${database.ts_latest.type}'=='sql' || '${database.ts_latest.type}'=='timescale') && '${cache.ts_latest.enabled:false}'=='true' && '${cache.type:caffeine}'=='redis' ") +public @interface SqlTsLatestAnyDaoCachedRedis { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 9fda1c6bb5..50e02fca0a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -34,6 +34,7 @@ public class CacheConstants { public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; + public static final String TS_LATEST_CACHE = "tsLatest"; public static final String USERS_SESSION_INVALIDATION_CACHE = "userSessionsInvalidation"; public static final String OTA_PACKAGE_CACHE = "otaPackages"; public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java new file mode 100644 index 0000000000..6b32808f7d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -0,0 +1,104 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sqlts; + +import com.google.common.util.concurrent.ListenableFuture; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; +import org.thingsboard.server.common.stats.DefaultCounter; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.dao.cache.CacheExecutorService; +import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; +import org.thingsboard.server.dao.timeseries.TsLatestCacheKey; +import org.thingsboard.server.dao.util.SqlTsLatestAnyDaoCachedRedis; + +import java.util.List; +import java.util.Optional; + +@Slf4j +@Component +@SqlTsLatestAnyDaoCachedRedis +@RequiredArgsConstructor +@Primary +public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao implements TimeseriesLatestDao { + public static final String STATS_NAME = "ts_latest.cache"; + + DefaultCounter hitCounter; + DefaultCounter missCounter; + + final CacheExecutorService cacheExecutorService; + final SqlTimeseriesLatestDao sqlDao; + final StatsFactory statsFactory; + final TbTransactionalCache cache; + + @PostConstruct + public void init() { + log.info("Init Redis cache-aside SQL Timeseries Latest DAO"); + this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit"); + this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss"); + } + + @Override + public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + return sqlDao.saveLatest(tenantId, entityId, tsKvEntry); + } + + @Override + public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { + return sqlDao.removeLatest(tenantId, entityId, query); + } + + @Override + public ListenableFuture> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { + return sqlDao.findLatestOpt(tenantId, entityId, key); + } + + @Override + public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { + return sqlDao.findLatest(tenantId, entityId, key); + } + + @Override + public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) { + return sqlDao.findLatestSync(tenantId, entityId, key); + } + + @Override + public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { + return sqlDao.findAllLatest(tenantId, entityId); + } + + @Override + public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) { + return sqlDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId); + } + + @Override + public List findAllKeysByEntityIds(TenantId tenantId, List entityIds) { + return sqlDao.findAllKeysByEntityIds(tenantId, entityIds); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java new file mode 100644 index 0000000000..adb572922a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestCacheKey.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.timeseries; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.id.EntityId; + +import java.io.Serial; +import java.io.Serializable; + +@EqualsAndHashCode +@Getter +@AllArgsConstructor +public class TsLatestCacheKey implements Serializable { + private static final long serialVersionUID = 2024369077925351881L; + + private final EntityId entityId; + private final String key; + + @Override + public String toString() { + return "{" + entityId + "}" + key; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java new file mode 100644 index 0000000000..d67e9272e3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.timeseries; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJavaRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.kv.TsKvEntry; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("TsLatestCache") +public class TsLatestRedisCache extends RedisTbTransactionalCache { + + public TsLatestRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJavaRedisSerializer<>()); } + +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 73c1495af8..c34d0ad592 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -10,6 +10,7 @@ audit-log.sink.type=none #cache.type=caffeine # will be injected redis by RedisContainer or will be default (caffeine) cache.maximumPoolSize=16 cache.attributes.enabled=true +cache.ts_latest.enabled=true cache.specs.relations.timeToLiveInMinutes=1440 cache.specs.relations.maxSize=100000 @@ -53,6 +54,9 @@ cache.specs.assetProfiles.maxSize=100000 cache.specs.attributes.timeToLiveInMinutes=1440 cache.specs.attributes.maxSize=100000 +cache.specs.tsLatest.timeToLiveInMinutes=1440 +cache.specs.tsLatest.maxSize=100000 + cache.specs.tokensOutdatageTime.timeToLiveInMinutes=1440 cache.specs.tokensOutdatageTime.maxSize=100000 diff --git a/dao/src/test/resources/logback.xml b/dao/src/test/resources/logback.xml index 5e293b2982..8d4951658e 100644 --- a/dao/src/test/resources/logback.xml +++ b/dao/src/test/resources/logback.xml @@ -9,6 +9,7 @@ + From 44096a7cb7059c1f3d847d5af736dbe4c4c4a7d5 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 8 Apr 2024 14:57:20 +0200 Subject: [PATCH 004/136] TS latest dao put to Redis WIP --- .../cache/RedisTbTransactionalCache.java | 6 ++++- .../CachedRedisSqlTimeseriesLatestDao.java | 22 ++++++++++++++++++- .../dao/timeseries/TsLatestRedisCache.java | 19 ++++++++++++++-- .../{logback.xml => logback-test.xml} | 2 ++ 4 files changed, 45 insertions(+), 4 deletions(-) rename dao/src/test/resources/{logback.xml => logback-test.xml} (87%) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index cd5db13995..bb5af26c3e 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -214,8 +214,12 @@ public abstract class RedisTbTransactionalCache saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { - return sqlDao.saveLatest(tenantId, entityId, tsKvEntry); + ListenableFuture future = sqlDao.saveLatest(tenantId, entityId, tsKvEntry); + future = Futures.transform(future, x -> { + cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), tsKvEntry); + return x; + }, + cacheExecutorService); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Void result) { + log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry); + } + + @Override + public void onFailure(Throwable t) { + log.trace("saveLatest onFailure [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry, t); + } + }, MoreExecutors.directExecutor()); + return future; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java index d67e9272e3..c0bca607c9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.dao.timeseries; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.RedisTbTransactionalCache; @@ -25,11 +27,24 @@ import org.thingsboard.server.cache.TbJavaRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.TsKvEntry; +import java.io.Serializable; + @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("TsLatestCache") -public class TsLatestRedisCache extends RedisTbTransactionalCache { +@Slf4j +public class TsLatestRedisCache extends RedisTbTransactionalCache { public TsLatestRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJavaRedisSerializer<>()); } + super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJavaRedisSerializer<>()); + } + + @Override + public void put(TsLatestCacheKey key, TsKvEntry value) { + log.trace("put [{}][{}]", key, value); + final byte[] rawKey = getRawKey(key); + try (var connection = getConnection(rawKey)) { + put(connection, rawKey, value, RedisStringCommands.SetOption.UPSERT); + } + } } diff --git a/dao/src/test/resources/logback.xml b/dao/src/test/resources/logback-test.xml similarity index 87% rename from dao/src/test/resources/logback.xml rename to dao/src/test/resources/logback-test.xml index 8d4951658e..d7cff4b67b 100644 --- a/dao/src/test/resources/logback.xml +++ b/dao/src/test/resources/logback-test.xml @@ -10,6 +10,8 @@ + + From 3003fccf3b12f4f390b6e7860acbe1b5fc43db3b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 16 Apr 2024 16:43:20 +0200 Subject: [PATCH 005/136] BaseTimeseriesServiceTest refactored --- .../timeseries/BaseTimeseriesServiceTest.java | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 390cb9afac..58b28872be 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service.timeseries; import com.datastax.oss.driver.api.core.uuid.Uuids; import lombok.extern.slf4j.Slf4j; +import org.assertj.core.data.Offset; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; @@ -50,15 +52,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; /** * @author Andrew Shvayka @@ -89,6 +90,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE); protected TenantId tenantId; + DeviceId deviceId = new DeviceId(Uuids.timeBased()); @Before public void before() { @@ -106,8 +108,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindAllLatest() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS); @@ -150,8 +150,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindLatest() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); saveEntries(deviceId, TS); @@ -163,8 +161,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindLatestWithoutLatestUpdate() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); saveEntriesWithoutLatest(deviceId, TS); @@ -176,8 +172,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQueryAscOrder() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - saveEntries(deviceId, TS - 3); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); @@ -202,7 +196,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQuery_whenPeriodEqualsOneMilisecondPeriod() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 1L); saveEntries(deviceId, TS); saveEntries(deviceId, TS + 1L); @@ -222,7 +215,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQuery_whenPeriodEqualsInterval() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 1L); for (long i = TS; i <= TS + 100L; i += 10L) { saveEntries(deviceId, i); @@ -244,7 +236,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 1L); for (long i = TS; i <= TS + 100000L; i += 10000L) { saveEntries(deviceId, i); @@ -268,7 +259,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS - 1L); for (long i = TS; i <= TS + 80000L; i += 10000L) { saveEntries(deviceId, i); @@ -292,7 +282,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength_whereNotAllEntriesInRange() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000) { saveEntries(deviceId, i); } @@ -314,7 +303,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst_andNotAllEntriesInRange() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000L) { saveEntries(deviceId, i); } @@ -336,8 +324,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindByQueryDescOrder() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - saveEntries(deviceId, TS - 3); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 1); @@ -362,7 +348,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindAllByQueries_verifyQueryId() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 10); @@ -373,7 +358,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindAllByQueries_verifyQueryId_forEntityView() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); saveEntries(deviceId, TS); saveEntries(deviceId, TS - 2); saveEntries(deviceId, TS - 12); @@ -392,8 +376,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testDeleteDeviceTsDataWithOverwritingLatest() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - saveEntries(deviceId, 10000); saveEntries(deviceId, 20000); saveEntries(deviceId, 30000); @@ -412,7 +394,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindDeviceTsData() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); List entries = new ArrayList<>(); entries.add(save(deviceId, 5000, 100)); @@ -563,7 +544,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindDeviceLongAndDoubleTsData() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); List entries = new ArrayList<>(); entries.add(save(deviceId, 5000, 100)); @@ -654,8 +634,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testSaveTs_RemoveTs_AndSaveTsAgain() throws Exception { - DeviceId deviceId = new DeviceId(Uuids.timeBased()); - save(deviceId, 2000000L, 95); save(deviceId, 4000000L, 100); save(deviceId, 6000000L, 105); @@ -686,7 +664,6 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { BasicTsKvEntry jsonEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}")); List timeseries = List.of(booleanEntry, stringEntry, longEntry, doubleEntry, jsonEntry); - DeviceId deviceId = new DeviceId(Uuids.timeBased()); for (TsKvEntry tsKvEntry : timeseries) { save(tenantId, deviceId, tsKvEntry); } From 771d15a6ddc71d1e95511c17c3b03ad006e750c3 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 16 Apr 2024 16:43:47 +0200 Subject: [PATCH 006/136] BaseTimeseriesServiceTest: 3 findLatest related tests added --- .../timeseries/BaseTimeseriesServiceTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 58b28872be..83db90ad71 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -159,6 +159,36 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); } + @Test + public void testFindLatestOpt() throws Exception { + saveEntries(deviceId, TS - 2); + saveEntries(deviceId, TS - 1); + saveEntries(deviceId, TS); + + Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(entryOpt).isNotNull().isPresent(); + Assert.assertEquals(toTsEntry(TS, stringKvEntry), entryOpt.get()); + } + + @Test + public void testFindLatest_NotFound() throws Exception { + List entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(entries).hasSize(1); + TsKvEntry tsKvEntry = entries.get(0); + assertThat(tsKvEntry).isNotNull(); + // null ts latest representation + assertThat(tsKvEntry.getKey()).isEqualTo(STRING_KEY); + assertThat(tsKvEntry.getDataType()).isEqualTo(DataType.STRING); + assertThat(tsKvEntry.getValue()).isNull(); + assertThat(tsKvEntry.getTs()).isCloseTo(System.currentTimeMillis(), Offset.offset(TimeUnit.MINUTES.toMillis(1))); + } + + @Test + public void testFindLatestOpt_NotFound() throws Exception { + Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(entryOpt).isNotNull().isNotPresent(); + } + @Test public void testFindLatestWithoutLatestUpdate() throws Exception { saveEntries(deviceId, TS - 2); From 034b480a6ca641f9b2670c146b0a78e8190c72be Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 16 Apr 2024 16:47:27 +0200 Subject: [PATCH 007/136] RedisTbTransactionalCache refactored to make doGet using the rawKey and the Redis connection precalculated slot based on hash --- .../thingsboard/server/cache/RedisTbTransactionalCache.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index bb5af26c3e..35ef07f0ce 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -78,7 +78,7 @@ public abstract class RedisTbTransactionalCache get(K key) { try (var connection = connectionFactory.getConnection()) { byte[] rawKey = getRawKey(key); - byte[] rawValue = connection.stringCommands().get(rawKey); + byte[] rawValue = doGet(connection, rawKey); if (rawValue == null) { return null; } else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { @@ -95,6 +95,10 @@ public abstract class RedisTbTransactionalCache Date: Tue, 16 Apr 2024 16:50:36 +0200 Subject: [PATCH 008/136] SqlTimeseriesLatestDao refactored to use a common doFindLatestSync for any find request --- .../server/dao/sqlts/SqlTimeseriesLatestDao.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index a9fb85560a..0f773e83d9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -155,17 +155,18 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @Override public ListenableFuture> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { - return service.submit(() -> Optional.ofNullable(doFindLatest(entityId, key))); + return service.submit(() -> Optional.ofNullable(doFindLatestSync(entityId, key))); } @Override public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { - return service.submit(() -> getLatestTsKvEntry(entityId, key)); + log.trace("findLatest [{}][{}][{}]", tenantId, entityId, key); + return service.submit(() -> wrapNullTsKvEntry(key, doFindLatestSync(entityId, key))); } @Override public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) { - return getLatestTsKvEntry(entityId, key); + return wrapNullTsKvEntry(key, doFindLatestSync(entityId, key)); } @Override @@ -209,7 +210,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme ReadTsKvQueryResult::getData, MoreExecutors.directExecutor()); } - protected TsKvEntry doFindLatest(EntityId entityId, String key) { + protected TsKvEntry doFindLatestSync(EntityId entityId, String key) { TsKvLatestCompositeKey compositeKey = new TsKvLatestCompositeKey( entityId.getId(), @@ -225,7 +226,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme } protected ListenableFuture getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - ListenableFuture latestFuture = service.submit(() -> doFindLatest(entityId, query.getKey())); + ListenableFuture latestFuture = service.submit(() -> doFindLatestSync(entityId, query.getKey())); return Futures.transformAsync(latestFuture, latest -> { if (latest == null) { return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false)); @@ -266,10 +267,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return tsLatestQueue.add(latestEntity); } - private TsKvEntry getLatestTsKvEntry(EntityId entityId, String key) { - TsKvEntry latest = doFindLatest(entityId, key); + protected TsKvEntry wrapNullTsKvEntry(final String key, final TsKvEntry latest) { if (latest == null) { - latest = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); + return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)); } return latest; } From 551325b8c7afe8afa58ed2586f6d3e6258b0894e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 16 Apr 2024 16:52:31 +0200 Subject: [PATCH 009/136] added CachedRedisSqlTimeseriesLatestDao and TsLatestRedisCache (prototype impl with string commands). Caffeine cache not implemented yet --- .../CachedRedisSqlTimeseriesLatestDao.java | 99 +++++++++++++++---- .../dao/timeseries/TsLatestRedisCache.java | 53 ++++++++++ 2 files changed, 134 insertions(+), 18 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index b283832186..482b456e60 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -24,6 +24,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -48,14 +49,12 @@ import java.util.Optional; @Primary public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao implements TimeseriesLatestDao { public static final String STATS_NAME = "ts_latest.cache"; - - DefaultCounter hitCounter; - DefaultCounter missCounter; - final CacheExecutorService cacheExecutorService; final SqlTimeseriesLatestDao sqlDao; final StatsFactory statsFactory; final TbTransactionalCache cache; + DefaultCounter hitCounter; + DefaultCounter missCounter; @PostConstruct public void init() { @@ -72,37 +71,101 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries return x; }, cacheExecutorService); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(Void result) { - log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry); - } - - @Override - public void onFailure(Throwable t) { - log.trace("saveLatest onFailure [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry, t); - } - }, MoreExecutors.directExecutor()); + if (log.isTraceEnabled()) { + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry); + } + + @Override + public void onFailure(Throwable t) { + log.info("saveLatest onFailure [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry, t); + } + }, MoreExecutors.directExecutor()); + } return future; } @Override public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - return sqlDao.removeLatest(tenantId, entityId, query); + ListenableFuture future = sqlDao.removeLatest(tenantId, entityId, query); + future = Futures.transform(future, x -> { + cache.evict(new TsLatestCacheKey(entityId, query.getKey())); + return x; + }, + cacheExecutorService); + if (log.isTraceEnabled()) { + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(TsKvLatestRemovingResult result) { + log.trace("removeLatest onSuccess [{}][{}][{}]", entityId, query.getKey(), query); + } + + @Override + public void onFailure(Throwable t) { + log.info("removeLatest onFailure [{}][{}][{}]", entityId, query.getKey(), query, t); + } + }, MoreExecutors.directExecutor()); + } + return future; } @Override public ListenableFuture> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { - return sqlDao.findLatestOpt(tenantId, entityId, key); + log.trace("findLatestOpt"); + return doFindLatest(tenantId, entityId, key); } @Override public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { - return sqlDao.findLatest(tenantId, entityId, key); + return Futures.transform(doFindLatest(tenantId, entityId, key), x -> sqlDao.wrapNullTsKvEntry(key, x.orElse(null)), MoreExecutors.directExecutor()); + } + + public ListenableFuture> doFindLatest(TenantId tenantId, EntityId entityId, String key) { + final TsLatestCacheKey cacheKey = new TsLatestCacheKey(entityId, key); + ListenableFuture> cacheFuture = cacheExecutorService.submit(() -> cache.get(cacheKey)); + + return Futures.transformAsync(cacheFuture, (cacheValueWrap) -> { + if (cacheValueWrap != null) { + final TsKvEntry tsKvEntry = cacheValueWrap.get(); + log.debug("findLatest cache hit [{}][{}][{}]", entityId, key, tsKvEntry); + return Futures.immediateFuture(Optional.ofNullable(tsKvEntry)); + } + log.debug("findLatest cache miss [{}][{}]", entityId, key); + ListenableFuture> daoFuture = sqlDao.findLatestOpt(tenantId,entityId, key); + + return Futures.transformAsync(daoFuture, (daoValue) -> { + + if (daoValue.isEmpty()) { + //TODO implement the cache logic if no latest found in TS DAO. Currently we are always getting from DB to stay on the safe side + return Futures.immediateFuture(daoValue); + } + ListenableFuture> cachePutFuture = cacheExecutorService.submit(() -> { + cache.put(new TsLatestCacheKey(entityId, key), daoValue.get()); + return daoValue; + }); + + Futures.addCallback(cachePutFuture, new FutureCallback<>() { + @Override + public void onSuccess(Optional result) { + log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, key, result); + } + + @Override + public void onFailure(Throwable t) { + log.info("saveLatest onFailure [{}][{}][{}]", entityId, key, daoValue, t); + } + + }, MoreExecutors.directExecutor()); + return cachePutFuture; + }, MoreExecutors.directExecutor()); + }, MoreExecutors.directExecutor()); } @Override public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) { + log.trace("findLatestSync DEPRECATED [{}][{}]", entityId, key); return sqlDao.findLatestSync(tenantId, entityId, key); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java index c0bca607c9..61d2c8bdb2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java @@ -16,18 +16,24 @@ package org.thingsboard.server.dao.timeseries; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.NotImplementedException; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbCacheTransaction; +import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.TbJavaRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.io.Serializable; +import java.util.Collection; +import java.util.List; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("TsLatestCache") @@ -47,4 +53,51 @@ public class TsLatestRedisCache } } + @Override + public void putIfAbsent(TsLatestCacheKey key, TsKvEntry value) { + log.trace("putIfAbsent [{}][{}]", key, value); + throw new NotImplementedException("putIfAbsent is not supported by TsLatestRedisCache"); + } + + @Override + public TbCacheValueWrapper get(TsLatestCacheKey key) { + log.debug("get [{}]", key); + return super.get(key); + } + + @Override + protected byte[] doGet(RedisConnection connection, byte[] rawKey) { + log.trace("doGet [{}][{}]", connection, rawKey); + return connection.stringCommands().get(rawKey); + } + + @Override + public void evict(TsLatestCacheKey key){ + log.trace("evict [{}]", key); + final byte[] rawKey = getRawKey(key); + try (var connection = getConnection(rawKey)) { + connection.keyCommands().del(rawKey); + } + } + + @Override + public void evict(Collection keys) { + throw new NotImplementedException("evict by many keys is not supported by TsLatestRedisCache"); + } + + @Override + public void evictOrPut(TsLatestCacheKey key, TsKvEntry value) { + throw new NotImplementedException("evictOrPut is not supported by TsLatestRedisCache"); + } + + @Override + public TbCacheTransaction newTransactionForKey(TsLatestCacheKey key) { + throw new NotImplementedException("newTransactionForKey is not supported by TsLatestRedisCache"); + } + + @Override + public TbCacheTransaction newTransactionForKeys(List keys) { + throw new NotImplementedException("newTransactionForKeys is not supported by TsLatestRedisCache"); + } + } From c49d97f7d19d69883a789b2ac2b687f6c34f322a Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 16 Apr 2024 17:35:04 +0200 Subject: [PATCH 010/136] BaseTimeseriesServiceTest added overwrite TS cases and historical data load case, that fails without condition on update only if ts is the latest --- .../timeseries/BaseTimeseriesServiceTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 83db90ad71..d32933684a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -159,6 +159,40 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); } + @Test + public void testFindLatestOpt_givenSaveWithHistoricalNonOrderedTS() throws Exception { + save(tenantId, deviceId, toTsEntry(TS - 1, stringKvEntry)); + save(tenantId, deviceId, toTsEntry(TS, stringKvEntry)); + save(tenantId, deviceId, toTsEntry(TS - 10, stringKvEntry)); + save(tenantId, deviceId, toTsEntry(TS - 11, stringKvEntry)); + + Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(entryOpt).isNotNull().isPresent(); + Assert.assertEquals(toTsEntry(TS, stringKvEntry), entryOpt.orElse(null)); + } + + @Test + public void testFindLatestOpt_givenSaveWithSameTSOverwriteValue() throws Exception { + save(tenantId, deviceId, toTsEntry(TS, new StringDataEntry(STRING_KEY, "old"))); + save(tenantId, deviceId, toTsEntry(TS, new StringDataEntry(STRING_KEY, "new"))); + + Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(entryOpt).isNotNull().isPresent(); + Assert.assertEquals(toTsEntry(TS, new StringDataEntry(STRING_KEY, "new")), entryOpt.orElse(null)); + } + + public void testFindLatestOpt_givenSaveWithSameTSOverwriteTypeAndValue() throws Exception { + save(tenantId, deviceId, toTsEntry(TS, new JsonDataEntry("temp", "{\"hello\":\"world\"}"))); + save(tenantId, deviceId, toTsEntry(TS, new BooleanDataEntry("temp", true))); + save(tenantId, deviceId, toTsEntry(TS, new LongDataEntry("temp", 100L))); + save(tenantId, deviceId, toTsEntry(TS, new DoubleDataEntry("temp", Math.PI))); + save(tenantId, deviceId, toTsEntry(TS, new StringDataEntry("temp", "NOOP"))); + + Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(entryOpt).isNotNull().isPresent(); + Assert.assertEquals(toTsEntry(TS, new StringDataEntry("temp", "NOOP")), entryOpt.orElse(null)); + } + @Test public void testFindLatestOpt() throws Exception { saveEntries(deviceId, TS - 2); From a4469f69539ead14b1c926ce103cfdb704741823 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 17 Apr 2024 12:14:42 +0200 Subject: [PATCH 011/136] TsLatestRedisCache: added LUA upsert script. load script, eval, evalsha, test for script sha, fetch latest by zRange Redis command --- .../cache/RedisTbTransactionalCache.java | 1 + .../dao/timeseries/TsLatestRedisCache.java | 75 +++++++++++++++---- .../timeseries/TsLatestRedisCacheTest.java | 30 ++++++++ 3 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index 35ef07f0ce..4277a7bab9 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -51,6 +51,7 @@ public abstract class RedisTbTransactionalCache keySerializer = StringRedisSerializer.UTF_8; private final TbRedisSerializer valueSerializer; diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java index 61d2c8bdb2..e9f9ab522c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.dao.timeseries; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.NotImplementedException; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.connection.ReturnType; +import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.RedisTbTransactionalCache; @@ -32,33 +35,42 @@ import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.io.Serializable; +import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Set; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("TsLatestCache") @Slf4j public class TsLatestRedisCache extends RedisTbTransactionalCache { + static final byte[] UPSERT_TS_LATEST_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize("" + + "redis.call('ZREMRANGEBYSCORE', KEYS[1], ARGV[1], ARGV[1]); " + + "redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2]); " + + "local current_size = redis.call('ZCARD', KEYS[1]); " + + "if current_size > 1 then" + + " redis.call('ZREMRANGEBYRANK', KEYS[1], 0, -2) " + + "end;"); + static final byte[] UPSERT_TS_LATEST_SHA = StringRedisSerializer.UTF_8.serialize("24e226c3ea34e3e850113e8eb1f3cd2b88171988"); + public TsLatestRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJavaRedisSerializer<>()); } - @Override - public void put(TsLatestCacheKey key, TsKvEntry value) { - log.trace("put [{}][{}]", key, value); - final byte[] rawKey = getRawKey(key); - try (var connection = getConnection(rawKey)) { - put(connection, rawKey, value, RedisStringCommands.SetOption.UPSERT); + @PostConstruct + public void init() { + try (var connection = getConnection(UPSERT_TS_LATEST_SHA)) { + log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(UPSERT_TS_LATEST_SHA), connection.getNativeConnection()); + String sha = connection.scriptingCommands().scriptLoad(UPSERT_TS_LATEST_LUA_SCRIPT); + if (!Arrays.equals(UPSERT_TS_LATEST_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { + log.error("SHA for UPSERT_TS_LATEST_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(UPSERT_TS_LATEST_SHA), sha, connection.getNativeConnection()); + } + } catch (Throwable t) { + log.error("Error on Redis TS Latest cache init", t); } } - @Override - public void putIfAbsent(TsLatestCacheKey key, TsKvEntry value) { - log.trace("putIfAbsent [{}][{}]", key, value); - throw new NotImplementedException("putIfAbsent is not supported by TsLatestRedisCache"); - } - @Override public TbCacheValueWrapper get(TsLatestCacheKey key) { log.debug("get [{}]", key); @@ -68,11 +80,38 @@ public class TsLatestRedisCache @Override protected byte[] doGet(RedisConnection connection, byte[] rawKey) { log.trace("doGet [{}][{}]", connection, rawKey); - return connection.stringCommands().get(rawKey); + Set values = connection.commands().zRange(rawKey, -1, -1); + return values == null ? null : values.stream().findFirst().orElse(null); } @Override - public void evict(TsLatestCacheKey key){ + public void put(TsLatestCacheKey key, TsKvEntry value) { + log.trace("put [{}][{}]", key, value); + final byte[] rawKey = getRawKey(key); + try (var connection = getConnection(rawKey)) { + byte[] rawValue = getRawValue(value); + byte[] ts = StringRedisSerializer.UTF_8.serialize(String.valueOf(value.toTsValue().getTs())); + try { + connection.scriptingCommands().evalSha(UPSERT_TS_LATEST_SHA, ReturnType.VALUE, 1, rawKey, ts, rawValue); + } catch (InvalidDataAccessApiUsageException e) { + log.debug("loading LUA [{}]", connection.getNativeConnection()); + String sha = connection.scriptingCommands().scriptLoad(UPSERT_TS_LATEST_LUA_SCRIPT); + if (!Arrays.equals(UPSERT_TS_LATEST_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { + log.error("SHA for UPSERT_TS_LATEST_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(UPSERT_TS_LATEST_SHA), sha); + } + try { + connection.scriptingCommands().evalSha(UPSERT_TS_LATEST_SHA, ReturnType.VALUE, 1, rawKey, ts, rawValue); + } catch (InvalidDataAccessApiUsageException ignored) { + log.debug("Slowly executing eval instead of fast evalsha"); + connection.scriptingCommands().eval(UPSERT_TS_LATEST_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, ts, rawValue); + } + + } + } + } + + @Override + public void evict(TsLatestCacheKey key) { log.trace("evict [{}]", key); final byte[] rawKey = getRawKey(key); try (var connection = getConnection(rawKey)) { @@ -80,6 +119,12 @@ public class TsLatestRedisCache } } + @Override + public void putIfAbsent(TsLatestCacheKey key, TsKvEntry value) { + log.trace("putIfAbsent [{}][{}]", key, value); + throw new NotImplementedException("putIfAbsent is not supported by TsLatestRedisCache"); + } + @Override public void evict(Collection keys) { throw new NotImplementedException("evict by many keys is not supported by TsLatestRedisCache"); diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java new file mode 100644 index 0000000000..5de26c15d3 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java @@ -0,0 +1,30 @@ +package org.thingsboard.server.dao.timeseries; + +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.security.MessageDigest; + +import static org.assertj.core.api.Assertions.assertThat; + +class TsLatestRedisCacheTest { + + @Test + void testUpsertTsLatestLUAScriptHash() { + assertThat(getSHA1(TsLatestRedisCache.UPSERT_TS_LATEST_LUA_SCRIPT)).isEqualTo(new String(TsLatestRedisCache.UPSERT_TS_LATEST_SHA)); + } + + @SneakyThrows + String getSHA1(byte[] script) { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] hash = md.digest(script); + + StringBuilder sb = new StringBuilder(); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + + return sb.toString(); + } + +} \ No newline at end of file From 67c2392cccea99861bb1f869d4dce55f58a81f3a Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 17 Apr 2024 12:15:22 +0200 Subject: [PATCH 012/136] RedisClusterSqlTestSuite introduced --- .../dao/AbstractRedisClusterContainer.java | 92 +++++++++++++++++++ .../server/dao/RedisClusterSqlTestSuite.java | 31 +++++++ 2 files changed, 123 insertions(+) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/RedisClusterSqlTestSuite.java diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java new file mode 100644 index 0000000000..cfa4c00ddf --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import lombok.extern.slf4j.Slf4j; +import org.junit.ClassRule; +import org.junit.rules.ExternalResource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.OutputFrame; +import redis.clients.jedis.Jedis; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class AbstractRedisClusterContainer { + + static final String nodes = "127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375,127.0.0.1:6376"; + + @ClassRule(order = 0) + public static Network network = Network.newNetwork(); + @ClassRule(order = 1) + public static GenericContainer redis1 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER","6371").withNetworkMode("host").withLogConsumer(x->log.warn("{}", ((OutputFrame)x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD","yes").withEnv("REDIS_NODES",nodes); + @ClassRule(order = 2) + public static GenericContainer redis2 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER","6372").withNetworkMode("host").withLogConsumer(x->log.warn("{}", ((OutputFrame)x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD","yes").withEnv("REDIS_NODES",nodes); + @ClassRule(order = 3) + public static GenericContainer redis3 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER","6373").withNetworkMode("host").withLogConsumer(x->log.warn("{}", ((OutputFrame)x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD","yes").withEnv("REDIS_NODES",nodes); + @ClassRule(order = 4) + public static GenericContainer redis4 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER","6374").withNetworkMode("host").withLogConsumer(x->log.warn("{}", ((OutputFrame)x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD","yes").withEnv("REDIS_NODES",nodes); + @ClassRule(order = 5) + public static GenericContainer redis5 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER","6375").withNetworkMode("host").withLogConsumer(x->log.warn("{}", ((OutputFrame)x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD","yes").withEnv("REDIS_NODES",nodes); + @ClassRule(order = 6) + public static GenericContainer redis6 = new GenericContainer("bitnami/redis-cluster:latest").withEnv("REDIS_PORT_NUMBER","6376").withNetworkMode("host").withLogConsumer(x->log.warn("{}", ((OutputFrame)x).getUtf8StringWithoutLineEnding())).withEnv("ALLOW_EMPTY_PASSWORD","yes").withEnv("REDIS_NODES",nodes); + + + @ClassRule(order = 100) + public static ExternalResource resource = new ExternalResource() { + @Override + protected void before() throws Throwable { + redis1.start(); + redis2.start(); + redis3.start(); + redis4.start(); + redis5.start(); + redis6.start(); + + String clusterCreateCommand = "echo yes | redis-cli --cluster create " + + "127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375 127.0.0.1:6376 " + + "--cluster-replicas 1"; + + log.warn("Command to init Redis Cluster: {}", clusterCreateCommand); + log.warn("Connect to nodes : {}", nodes); + Thread.sleep(TimeUnit.SECONDS.toMillis(5)); + var result = redis6.execInContainer("/bin/sh", "-c", clusterCreateCommand); + log.warn("Init cluster result: {}", result); + + System.setProperty("cache.type", "redis"); + System.setProperty("redis.connection.type", "cluster"); + System.setProperty("redis.cluster.nodes", nodes); + System.setProperty("redis.password", ""); + + System.setProperty("redis.cluster.useDefaultPoolConfig", "false"); + } + + @Override + protected void after() { + redis1.stop(); + redis2.stop(); + redis3.stop(); + redis4.stop(); + redis5.stop(); + redis6.stop(); + List.of("cache.type", "redis.connection.type", "redis.standalone.host", "redis.standalone.port") + .forEach(System.getProperties()::remove); + } + }; + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/RedisClusterSqlTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/RedisClusterSqlTestSuite.java new file mode 100644 index 0000000000..be0bcc6fc7 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/RedisClusterSqlTestSuite.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; +import org.junit.runner.RunWith; + +@RunWith(ClasspathSuite.class) +@ClassnameFilters( + //All the same tests using redis instead of caffeine. + { + "org.thingsboard.server.dao.service.*ServiceSqlTest", + } +) +public class RedisClusterSqlTestSuite extends AbstractRedisClusterContainer { + +} From 743b8b1d801d751fd9fc55e31f5014853a670c5a Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 17 Apr 2024 12:16:31 +0200 Subject: [PATCH 013/136] logback-test temporary added TRACE for SqlTimeseriesLatestDao and CachedRedisSqlTimeseriesLatestDao --- dao/src/test/resources/logback-test.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dao/src/test/resources/logback-test.xml b/dao/src/test/resources/logback-test.xml index d7cff4b67b..87aa67a67e 100644 --- a/dao/src/test/resources/logback-test.xml +++ b/dao/src/test/resources/logback-test.xml @@ -10,6 +10,8 @@ + + From 168525d3b9e00636e979b36b75538ffb5ea41853 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 17 Apr 2024 12:24:16 +0200 Subject: [PATCH 014/136] mvn license:format --- .../dao/timeseries/TsLatestRedisCacheTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java index 5de26c15d3..c463b4630f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.dao.timeseries; import lombok.SneakyThrows; From 92793360f304687cd48f8dbf5a95834efcda53a1 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 17 Apr 2024 12:39:54 +0200 Subject: [PATCH 015/136] CachedRedisSqlTimeseriesLatestDao.java removed deprecated findLatestSync --- .../server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index 482b456e60..32d0ee91c9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -163,12 +163,6 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries }, MoreExecutors.directExecutor()); } - @Override - public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) { - log.trace("findLatestSync DEPRECATED [{}][{}]", entityId, key); - return sqlDao.findLatestSync(tenantId, entityId, key); - } - @Override public ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId) { return sqlDao.findAllLatest(tenantId, entityId); From 20ec210c320648a190d39ad40a78c08a6d4ce172 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 17 Apr 2024 12:43:14 +0200 Subject: [PATCH 016/136] AbstractRedisClusterContainer refactored --- .../server/dao/AbstractRedisClusterContainer.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java index cfa4c00ddf..a95f4b8c17 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java @@ -58,21 +58,19 @@ public class AbstractRedisClusterContainer { redis5.start(); redis6.start(); + Thread.sleep(TimeUnit.SECONDS.toMillis(5)); // otherwise not all containers have time to start + String clusterCreateCommand = "echo yes | redis-cli --cluster create " + "127.0.0.1:6371 127.0.0.1:6372 127.0.0.1:6373 127.0.0.1:6374 127.0.0.1:6375 127.0.0.1:6376 " + "--cluster-replicas 1"; - log.warn("Command to init Redis Cluster: {}", clusterCreateCommand); - log.warn("Connect to nodes : {}", nodes); - Thread.sleep(TimeUnit.SECONDS.toMillis(5)); var result = redis6.execInContainer("/bin/sh", "-c", clusterCreateCommand); log.warn("Init cluster result: {}", result); + log.warn("Connect to nodes: {}", nodes); System.setProperty("cache.type", "redis"); System.setProperty("redis.connection.type", "cluster"); System.setProperty("redis.cluster.nodes", nodes); - System.setProperty("redis.password", ""); - System.setProperty("redis.cluster.useDefaultPoolConfig", "false"); } @@ -84,7 +82,7 @@ public class AbstractRedisClusterContainer { redis4.stop(); redis5.stop(); redis6.stop(); - List.of("cache.type", "redis.connection.type", "redis.standalone.host", "redis.standalone.port") + List.of("cache.type", "redis.connection.type", "redis.cluster.nodes", "redis.cluster.useDefaultPoolConfig\"") .forEach(System.getProperties()::remove); } }; From 74ac5a949f2ff66e4e16213dc73d635b0ffdd2bf Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 14 May 2024 10:05:41 +0300 Subject: [PATCH 017/136] added rule engine controller and send rest api call reply node --- .../server/controller/BaseController.java | 4 + .../controller/ControllerConstants.java | 2 + .../controller/RuleEngineController.java | 237 ++++++++++++++++ .../server/controller/TbUrlConstants.java | 1 + .../queue/DefaultTbClusterService.java | 20 +- .../queue/DefaultTbCoreConsumerService.java | 13 +- .../rpc/DefaultTbRuleEngineRpcService.java | 11 + .../DefaultRuleEngineCallService.java | 100 +++++++ .../ruleengine/RuleEngineCallService.java | 31 +++ .../controller/AbstractNotifyEntityTest.java | 21 +- .../server/controller/AbstractWebTest.java | 10 + .../controller/RuleEngineControllerTest.java | 252 ++++++++++++++++++ .../queue/DefaultTbClusterServiceTest.java | 51 ++++ .../DefaultTbCoreConsumerServiceTest.java | 16 ++ .../DefaultTbRuleEngineRpcServiceTest.java | 61 +++++ .../DefaultRuleEngineCallServiceTest.java | 166 ++++++++++++ .../server/cluster/TbClusterService.java | 6 +- .../server/common/data/audit/ActionType.java | 1 + .../server/common/data/msg/TbMsgType.java | 1 + common/proto/src/main/proto/queue.proto | 7 + .../thingsboard/rest/client/RestClient.java | 45 ++++ .../rule/engine/api/RuleEngineRpcService.java | 3 + .../rest/TbSendRestApiCallReplyNode.java | 66 +++++ ...SendRestApiCallReplyNodeConfiguration.java | 45 ++++ .../rest/TbSendRestApiCallReplyNodeTest.java | 133 +++++++++ 25 files changed, 1292 insertions(+), 11 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/ruleengine/RuleEngineCallService.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java create mode 100644 application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeConfiguration.java create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 0942329f40..0ae129b119 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -140,6 +140,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; @@ -313,6 +314,9 @@ public abstract class BaseController { @Autowired protected ExportableEntitiesService entitiesService; + @Autowired + protected TbServiceInfoProvider serviceInfoProvider; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index d9232cda78..d41ffc8fc6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -1710,4 +1710,6 @@ public class ControllerConstants { MARKDOWN_CODE_BLOCK_START + "[{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}, {\"ts\":1634712588000,\"values\":{\"temperature\":25, \"humidity\":88}}]" + MARKDOWN_CODE_BLOCK_END ; + + protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities)."; } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java new file mode 100644 index 0000000000..16458e42db --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java @@ -0,0 +1,237 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.google.common.util.concurrent.FutureCallback; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.exception.ToErrorResponseEntity; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.ruleengine.RuleEngineCallService; +import org.thingsboard.server.service.security.AccessValidator; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.TimeoutException; + +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; + +@RestController +@TbCoreComponent +@RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX) +@Slf4j +public class RuleEngineController extends BaseController { + public static final int DEFAULT_TIMEOUT = 10000; + private static final String MSG_DESCRIPTION_PREFIX = "Creates the Message with type 'REST_API_REQUEST' and payload taken from the request body. "; + private static final String MSG_DESCRIPTION = "This method allows you to extend the regular platform API with the power of Rule Engine. You may use default and custom rule nodes to handle the message. " + + "The generated message contains two important metadata fields:\n\n" + + " * **'serviceId'** to identify the platform server that received the request;\n" + + " * **'requestUUID'** to identify the request and route possible response from the Rule Engine;\n\n" + + "Use **'rest call reply'** rule node to push the reply from rule engine back as a REST API call response. "; + + @Autowired + private RuleEngineCallService ruleEngineCallService; + @Autowired + private AccessValidator accessValidator; + + @ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequest)", + notes = MSG_DESCRIPTION_PREFIX + + "Uses current User Id ( the one which credentials is used to perform the request) as the Rule Engine message originator. " + + MSG_DESCRIPTION + + "The default timeout of the request processing is 10 seconds." + + "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/", method = RequestMethod.POST) + @ResponseBody + public DeferredResult handleRuleEngineRequest( + @Parameter(description = "A JSON value representing the message.", required = true) + @RequestBody String requestBody) throws ThingsboardException { + return handleRuleEngineRequest(null, null, null, DEFAULT_TIMEOUT, requestBody); + } + + @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", + notes = MSG_DESCRIPTION_PREFIX + + "Uses specified Entity Id as the Rule Engine message originator. " + + MSG_DESCRIPTION + + "The default timeout of the request processing is 10 seconds." + + "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/{entityType}/{entityId}", method = RequestMethod.POST) + @ResponseBody + public DeferredResult handleRuleEngineRequest( + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) + @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("entityId") String entityIdStr, + @Parameter(description = "A JSON value representing the message.", required = true) + @RequestBody String requestBody) throws ThingsboardException { + return handleRuleEngineRequest(entityType, entityIdStr, null, DEFAULT_TIMEOUT, requestBody); + } + + @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)", + notes = MSG_DESCRIPTION_PREFIX + + "Uses specified Entity Id as the Rule Engine message originator. " + + MSG_DESCRIPTION + + "The platform expects the timeout value in milliseconds." + + "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/{entityType}/{entityId}/{timeout}", method = RequestMethod.POST) + @ResponseBody + public DeferredResult handleRuleEngineRequest( + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) + @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("entityId") String entityIdStr, + @Parameter(description = "Timeout to process the request in milliseconds", required = true) + @PathVariable("timeout") int timeout, + @Parameter(description = "A JSON value representing the message.", required = true) + @RequestBody String requestBody) throws ThingsboardException { + return handleRuleEngineRequest(entityType, entityIdStr, null, timeout, requestBody); + } + + @ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequest)", + notes = MSG_DESCRIPTION_PREFIX + + "Uses specified Entity Id as the Rule Engine message originator. " + + MSG_DESCRIPTION + + "If request sent for Device/Device Profile or Asset/Asset Profile entity, specified queue will be used instead of the queue selected in the device or asset profile. " + + "The platform expects the timeout value in milliseconds." + + "\n\n" + ControllerConstants.SECURITY_WRITE_CHECK) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/{entityType}/{entityId}/{queueName}/{timeout}", method = RequestMethod.POST) + @ResponseBody + public DeferredResult handleRuleEngineRequest( + @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) + @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("entityId") String entityIdStr, + @Parameter(description = "Queue name to process the request in the rule engine", required = true) + @PathVariable("queueName") String queueName, + @Parameter(description = "Timeout to process the request in milliseconds", required = true) + @PathVariable("timeout") int timeout, + @Parameter(description = "A JSON value representing the message.", required = true) + @RequestBody String requestBody) throws ThingsboardException { + try { + SecurityUser currentUser = getCurrentUser(); + EntityId entityId; + if (StringUtils.isEmpty(entityType) || StringUtils.isEmpty(entityIdStr)) { + entityId = currentUser.getId(); + } else { + entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + } + //Check that this is a valid JSON + JacksonUtil.toJsonNode(requestBody); + final DeferredResult response = new DeferredResult<>(); + accessValidator.validate(currentUser, Operation.WRITE, entityId, new HttpValidationCallback(response, new FutureCallback>() { + @Override + public void onSuccess(@Nullable DeferredResult result) { + long expTime = System.currentTimeMillis() + timeout; + HashMap metaData = new HashMap<>(); + UUID requestId = UUID.randomUUID(); + metaData.put("serviceId", serviceInfoProvider.getServiceId()); + metaData.put("requestUUID", requestId.toString()); + metaData.put("expirationTime", Long.toString(expTime)); + TbMsg msg = TbMsg.newMsg(queueName, TbMsgType.REST_API_REQUEST, entityId, currentUser.getCustomerId(), new TbMsgMetaData(metaData), requestBody); + ruleEngineCallService.processRestApiCallToRuleEngine(currentUser.getTenantId(), requestId, msg, queueName != null, + reply -> reply(new LocalRequestMetaData(msg, currentUser, result), reply)); + } + + @Override + public void onFailure(Throwable e) { + ResponseEntity entity; + if (e instanceof ToErrorResponseEntity) { + entity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); + } else { + entity = new ResponseEntity(HttpStatus.UNAUTHORIZED); + } + logRuleEngineCall(currentUser, entityId, requestBody, null, e); + response.setResult(entity); + } + })); + return response; + } catch (IllegalArgumentException iae) { + throw new ThingsboardException("Invalid request body", iae, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + + private void reply(LocalRequestMetaData rpcRequest, TbMsg response) { + DeferredResult responseWriter = rpcRequest.responseWriter; + if (response == null) { + logRuleEngineCall(rpcRequest, null, new TimeoutException("Processing timeout detected!")); + responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT)); + } else { + String responseData = response.getData(); + if (!StringUtils.isEmpty(responseData)) { + try { + logRuleEngineCall(rpcRequest, response, null); + responseWriter.setResult(new ResponseEntity<>(JacksonUtil.toJsonNode(responseData), HttpStatus.OK)); + } catch (IllegalArgumentException e) { + log.debug("Failed to decode device response: {}", responseData, e); + logRuleEngineCall(rpcRequest, response, e); + responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE)); + } + } else { + logRuleEngineCall(rpcRequest, response, null); + responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK)); + } + } + } + + private void logRuleEngineCall(LocalRequestMetaData rpcRequest, TbMsg response, Throwable e) { + logRuleEngineCall(rpcRequest.user, rpcRequest.request.getOriginator(), rpcRequest.request.getData(), response, e); + } + + private void logRuleEngineCall(SecurityUser user, EntityId entityId, String request, TbMsg response, Throwable e) { + auditLogService.logEntityAction( + user.getTenantId(), + user.getCustomerId(), + user.getId(), + user.getName(), + entityId, + null, + ActionType.REST_API_RULE_ENGINE_CALL, + BaseController.toException(e), + request, + response != null ? response.getData() : ""); + } + + private record LocalRequestMetaData(TbMsg request, SecurityUser user, DeferredResult responseWriter) {} +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java b/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java index 99d94522ac..74575fd9d5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbUrlConstants.java @@ -22,4 +22,5 @@ public class TbUrlConstants { public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry"; public static final String RPC_V1_URL_PREFIX = "/api/plugins/rpc"; public static final String RPC_V2_URL_PREFIX = "/api/rpc"; + public static final String RULE_ENGINE_URL_PREFIX = "/api/rule-engine/"; } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 7af2579b89..0c62620fab 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasRuleEngineProfile; -import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -183,6 +182,14 @@ public class DefaultTbClusterService implements TbClusterService { toCoreNfs.incrementAndGet(); } + @Override + public void pushNotificationToCore(String targetServiceId, TransportProtos.RestApiCallResponseMsgProto responseMsgProto, TbQueueCallback callback) { + TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_CORE, targetServiceId); + ToCoreNotificationMsg msg = ToCoreNotificationMsg.newBuilder().setRestApiCallResponseMsg(responseMsgProto).build(); + producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); + toCoreNfs.incrementAndGet(); + } + @Override public void pushMsgToRuleEngine(TopicPartitionInfo tpi, UUID msgId, ToRuleEngineMsg msg, TbQueueCallback callback) { log.trace("PUSHING msg: {} to:{}", msg, tpi); @@ -192,6 +199,11 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbMsg tbMsg, TbQueueCallback callback) { + pushMsgToRuleEngine(tenantId, entityId, tbMsg, false, callback); + } + + @Override + public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbMsg tbMsg, boolean useQueueFromTbMsg, TbQueueCallback callback) { if (tenantId == null || tenantId.isNullUid()) { if (entityId.getEntityType().equals(EntityType.TENANT)) { tenantId = TenantId.fromUUID(entityId.getId()); @@ -201,7 +213,7 @@ public class DefaultTbClusterService implements TbClusterService { } } else { HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId); - tbMsg = transformMsg(tbMsg, ruleEngineProfile); + tbMsg = transformMsg(tbMsg, ruleEngineProfile, useQueueFromTbMsg); } TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId); log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); @@ -226,10 +238,10 @@ public class DefaultTbClusterService implements TbClusterService { return null; } - private TbMsg transformMsg(TbMsg tbMsg, HasRuleEngineProfile ruleEngineProfile) { + private TbMsg transformMsg(TbMsg tbMsg, HasRuleEngineProfile ruleEngineProfile, boolean useQueueFromTbMsg) { if (ruleEngineProfile != null) { RuleChainId targetRuleChainId = ruleEngineProfile.getDefaultRuleChainId(); - String targetQueueName = ruleEngineProfile.getDefaultQueueName(); + String targetQueueName = useQueueFromTbMsg ? tbMsg.getQueueName() : ruleEngineProfile.getDefaultQueueName(); boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index fbef0cfd29..1d016ee33d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -96,6 +96,7 @@ import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; +import org.thingsboard.server.service.ruleengine.RuleEngineCallService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; @@ -149,6 +150,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig> mainConsumer; @@ -177,7 +179,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> requests = new ConcurrentHashMap<>(); + + public DefaultRuleEngineCallService(TbClusterService clusterService) { + this.clusterService = clusterService; + } + + @PostConstruct + public void initExecutor() { + executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("re-rest-callback")); + } + + @PreDestroy + public void shutdownExecutor() { + if (executor != null) { + executor.shutdownNow(); + } + } + + @Override + public void processRestApiCallToRuleEngine(TenantId tenantId, UUID requestId, TbMsg request, boolean useQueueFromTbMsg, Consumer responseConsumer) { + log.trace("[{}] Processing REST API call to rule engine: [{}] for entity: [{}]", tenantId, requestId, request.getOriginator()); + requests.put(requestId, responseConsumer); + sendRequestToRuleEngine(tenantId, request, useQueueFromTbMsg); + scheduleTimeout(request, requestId, requests); + } + + @Override + public void onQueueMsg(TransportProtos.RestApiCallResponseMsgProto restApiCallResponseMsg, TbCallback callback) { + UUID requestId = new UUID(restApiCallResponseMsg.getRequestIdMSB(), restApiCallResponseMsg.getRequestIdLSB()); + Consumer consumer = requests.remove(requestId); + if (consumer != null) { + consumer.accept(TbMsg.fromBytes(null, restApiCallResponseMsg.getResponse().toByteArray(), TbMsgCallback.EMPTY)); + } else { + log.trace("[{}] Unknown or stale rest api call response received", requestId); + } + callback.onSuccess(); + } + + private void sendRequestToRuleEngine(TenantId tenantId, TbMsg msg, boolean useQueueFromTbMsg) { + clusterService.pushMsgToRuleEngine(tenantId, msg.getOriginator(), msg, useQueueFromTbMsg, null); + } + + private void scheduleTimeout(TbMsg request, UUID requestId, ConcurrentMap> requestsMap) { + long expirationTime = Long.parseLong(request.getMetaData().getValue("expirationTime")); + long timeout = Math.max(0, expirationTime - System.currentTimeMillis()); + log.trace("[{}] processing the request: [{}]", this.hashCode(), requestId); + executor.schedule(() -> { + Consumer consumer = requestsMap.remove(requestId); + if (consumer != null) { + log.trace("[{}] request timeout detected: [{}]", this.hashCode(), requestId); + consumer.accept(null); + } + }, timeout, TimeUnit.MILLISECONDS); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ruleengine/RuleEngineCallService.java b/application/src/main/java/org/thingsboard/server/service/ruleengine/RuleEngineCallService.java new file mode 100644 index 0000000000..cdaa90398a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ruleengine/RuleEngineCallService.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ruleengine; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.UUID; +import java.util.function.Consumer; + +public interface RuleEngineCallService { + + void processRestApiCallToRuleEngine(TenantId tenantId, UUID requestId, TbMsg request, boolean useQueueFromTbMsg, Consumer responseConsumer); + + void onQueueMsg(TransportProtos.RestApiCallResponseMsgProto restApiCallResponseMsg, TbCallback callback); +} diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 35c2ea65fa..9bb49c1d62 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -386,6 +386,16 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { actionType, cntTime, extractMatcherAdditionalInfo(additionalInfo)); } + protected void testLogEntityActionError(HasName entity, EntityId originatorId, TenantId tenantId, + CustomerId customerId, UserId userId, String userName, + ActionType actionType, Exception exp, Object... additionalInfo) { + ArgumentMatcher matcherError = argument -> argument.getMessage().contains(exp.getMessage()) + & argument.getClass().equals(exp.getClass()); + ArgumentMatcher matcherEntityEquals = entity == null ? Objects::isNull : argument -> argument.toString().equals(entity.toString()); + testLogEntityActionErrorAdditionalInfo(matcherEntityEquals, originatorId, tenantId, customerId, userId, userName, + actionType, 1, matcherError, extractMatcherAdditionalInfo(additionalInfo)); + } + private void testLogEntityActionAdditionalInfo(ArgumentMatcher matcherEntity, ArgumentMatcher matcherOriginatorId, TenantId tenantId, ArgumentMatcher matcherCustomerId, ArgumentMatcher matcherUserId, String userName, ActionType actionType, @@ -529,8 +539,9 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.argThat(matcherEntity), Mockito.eq(actionType), Mockito.argThat(matcherError), - Mockito.argThat(Mockito.eq(matcherAdditionalInfos.get(0))), - Mockito.argThat(Mockito.eq(matcherAdditionalInfos.get(1)))); + Mockito.argThat(matcherAdditionalInfos.get(0)), + Mockito.argThat(matcherAdditionalInfos.get(1))); + break; case 3: Mockito.verify(auditLogService, times(cntTime)) .logEntityAction(Mockito.eq(tenantId), @@ -541,9 +552,9 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.argThat(matcherEntity), Mockito.eq(actionType), Mockito.argThat(matcherError), - Mockito.argThat(Mockito.eq(matcherAdditionalInfos.get(0))), - Mockito.argThat(Mockito.eq(matcherAdditionalInfos.get(1))), - Mockito.argThat(Mockito.eq(matcherAdditionalInfos.get(2)))); + Mockito.argThat(matcherAdditionalInfos.get(0)), + Mockito.argThat(matcherAdditionalInfos.get(1)), + Mockito.argThat(matcherAdditionalInfos.get(2))); break; default: Mockito.verify(auditLogService, times(cntTime)) 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 b83a1b567c..32ff733e0c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -633,6 +633,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/device?accessToken=" + accessToken, device, Device.class); } + protected Device assignDeviceToCustomer(String name, String accessToken, CustomerId customerId) throws Exception { + Device device = createDevice(name, accessToken); + String deviceIdStr = String.valueOf(device.getId().getId()); + return doPost("/api/customer/" + customerId.getId() + "/device/" + deviceIdStr, device, Device.class); + } + protected MqttDeviceProfileTransportConfiguration createMqttDeviceProfileTransportConfiguration(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration, boolean sendAckOnValidationException) { MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration(); mqttDeviceProfileTransportConfiguration.setDeviceTelemetryTopic(MqttTopics.DEVICE_TELEMETRY_TOPIC); @@ -803,6 +809,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseType); } + protected R doPostAsyncWithTypedResponse(String urlTemplate, T content, TypeReference responseType, ResultMatcher resultMatcher, String... params) throws Exception { + return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseType); + } + protected T doPostAsync(String urlTemplate, T content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception { return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); } diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java new file mode 100644 index 0000000000..dac268588d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java @@ -0,0 +1,252 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MvcResult; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.ruleengine.RuleEngineCallService; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; + +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class RuleEngineControllerTest extends AbstractControllerTest { + + private static final String REQUEST_BODY = "{\"temperature\":23}"; + + @SpyBean + private RuleEngineCallService ruleEngineCallService; + + @Autowired + private JwtTokenFactory jwtTokenFactory; + + @Test + public void testHandleRuleEngineRequestWithMsgOriginatorUser() throws Exception { + loginSysAdmin(); + UserId sysAdminUserId = getCurrentUserId(); + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, sysAdminUserId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(4); + consumer.accept(msg); + return null; + }).when(ruleEngineCallService).processRestApiCallToRuleEngine(eq(TenantId.SYS_TENANT_ID), any(UUID.class), any(TbMsg.class), anyBoolean(), any()); + + var response = doPostAsyncWithTypedResponse("/api/rule-engine/", REQUEST_BODY, new TypeReference<>() { + }, status().isOk()); + + assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); + ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(TenantId.SYS_TENANT_ID), any(), captor.capture(), eq(false), any()); + TbMsg tbMsg = captor.getValue(); + assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); + assertThat(tbMsg.getType()).isEqualTo(msg.getType()); + assertThat(tbMsg.getOriginator()).isEqualTo(sysAdminUserId); + testLogEntityAction(null, sysAdminUserId, TenantId.SYS_TENANT_ID, new CustomerId(TenantId.SYS_TENANT_ID.getId()), sysAdminUserId, + SYS_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + } + + @Test + public void testHandleRuleEngineRequestWithMsgOriginatorDevice() throws Exception { + loginTenantAdmin(); + Device device = createDevice("Test", "123"); + DeviceId deviceId = device.getId(); + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); + mockSuccessfulRestApiCallToRuleEngine(msg); + + var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId(), REQUEST_BODY, new TypeReference<>() { + }, status().isOk()); + + assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); + ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); + TbMsg tbMsg = captor.getValue(); + assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); + assertThat(tbMsg.getType()).isEqualTo(msg.getType()); + assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); + testLogEntityAction(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, + TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + } + + @Test + public void testHandleRuleEngineRequestWithMsgOriginatorDeviceAndSpecifiedTimeout() throws Exception { + loginTenantAdmin(); + Device device = createDevice("Test", "123"); + DeviceId deviceId = device.getId(); + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); + mockSuccessfulRestApiCallToRuleEngine(msg); + + var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId() + "/15000", REQUEST_BODY, new TypeReference<>() { + }, status().isOk()); + + assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); + ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); + TbMsg tbMsg = captor.getValue(); + assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); + assertThat(tbMsg.getType()).isEqualTo(msg.getType()); + assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); + testLogEntityAction(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, + TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + } + + @Test + public void testHandleRuleEngineRequestWithMsgOriginatorDeviceAndResponseIsNull() throws Exception { + loginTenantAdmin(); + Device device = createDevice("Test", "123"); + DeviceId deviceId = device.getId(); + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); + mockSuccessfulRestApiCallToRuleEngine(null); + + doPostAsync("/api/rule-engine/DEVICE/" + deviceId.getId() + "/15000", REQUEST_BODY, String.class, status().isRequestTimeout()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); + TbMsg tbMsg = captor.getValue(); + assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); + assertThat(tbMsg.getType()).isEqualTo(msg.getType()); + assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); + Exception exception = new TimeoutException("Processing timeout detected!"); + testLogEntityActionError(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, + TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, exception, REQUEST_BODY, ""); + } + + @Test + public void testHandleRuleEngineRequestWithMsgOriginatorDeviceAndSpecifiedQueue() throws Exception { + loginTenantAdmin(); + Device device = createDevice("Test", "123"); + DeviceId deviceId = device.getId(); + TbMsg msg = TbMsg.newMsg("HighPriority", TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, REQUEST_BODY); + mockSuccessfulRestApiCallToRuleEngine(msg); + + var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId() + "/HighPriority/1000", REQUEST_BODY, new TypeReference<>() { + }, status().isOk()); + + assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); + ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(true), any()); + TbMsg tbMsg = captor.getValue(); + assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); + assertThat(tbMsg.getType()).isEqualTo(msg.getType()); + assertThat(tbMsg.getQueueName()).isEqualTo(msg.getQueueName()); + assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); + testLogEntityAction(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, + TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + } + + @Test + public void testHandleRuleEngineRequestWithInvalidRequestBody() throws Exception { + loginSysAdmin(); + + doPost("/api/rule-engine/", (Object) "@") + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Invalid request body"))); + + verifyNoMoreInteractions(ruleEngineCallService); + } + + @Test + public void testHandleRuleEngineRequestWithAuthorityCustomerUser() throws Exception { + loginTenantAdmin(); + Device device = assignDeviceToCustomer("test", "123", customerId); + DeviceId deviceId = device.getId(); + loginCustomerUser(); + + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, customerId, TbMsgMetaData.EMPTY, REQUEST_BODY); + mockSuccessfulRestApiCallToRuleEngine(msg); + + var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId(), REQUEST_BODY, new TypeReference<>() { + }, status().isOk()); + + assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); + ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); + TbMsg tbMsg = captor.getValue(); + assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); + assertThat(tbMsg.getType()).isEqualTo(msg.getType()); + assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); + testLogEntityAction(null, deviceId, tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, + ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + } + + @Test + public void testHandleRuleEngineRequestWithoutPermission() throws Exception { + loginTenantAdmin(); + Device device = createDevice("test", "123"); + loginCustomerUser(); + + MvcResult result = doPost("/api/rule-engine/DEVICE/" + device.getId().getId(), (Object) REQUEST_BODY).andReturn(); + + ResponseEntity response = (ResponseEntity) result.getAsyncResult(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(Objects.requireNonNull(response.getBody()).toString()).isEqualTo("You don't have permission to perform this operation!"); + verify(ruleEngineCallService, never()).processRestApiCallToRuleEngine(any(), any(), any(), anyBoolean(), any()); + } + + @Test + public void testHandleRuleEngineRequestUnauthorized() throws Exception { + doPost("/api/rule-engine/", (Object) REQUEST_BODY) + .andExpect(status().isUnauthorized()) + .andExpect(statusReason(containsString("Authentication failed"))); + } + + private void mockSuccessfulRestApiCallToRuleEngine(TbMsg msg) { + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(4); + consumer.accept(msg); + return null; + }).when(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(UUID.class), any(TbMsg.class), anyBoolean(), any()); + } + + private UserId getCurrentUserId() { + Jws jwsClaims = jwtTokenFactory.parseTokenClaims(token); + Claims claims = jwsClaims.getPayload(); + String userId = claims.get("userId", String.class); + return UserId.fromString(userId); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 33b0fc7801..5ece34cd7f 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -19,16 +19,22 @@ import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueProducer; @@ -43,6 +49,7 @@ import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.List; import java.util.UUID; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -237,6 +244,50 @@ public class DefaultTbClusterServiceTest { .send(eq(topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, monolith2)), any(TbProtoQueueMsg.class), isNull()); } + @Test + public void testPushNotificationToCoreWithRestApiCallResponseMsgProto() { + TbQueueProducer> tbCoreQueueProducer = mock(TbQueueProducer.class); + TopicPartitionInfo tpi = new TopicPartitionInfo(ServiceType.TB_CORE.name().toLowerCase() + ".notifications." + CORE, null, null, false); + + when(producerProvider.getTbCoreNotificationsMsgProducer()).thenReturn(tbCoreQueueProducer); + TransportProtos.RestApiCallResponseMsgProto responseMsgProto = TransportProtos.RestApiCallResponseMsgProto.getDefaultInstance(); + TransportProtos.ToCoreNotificationMsg toCoreNotificationMsg = TransportProtos.ToCoreNotificationMsg.newBuilder().setRestApiCallResponseMsg(responseMsgProto).build(); + + clusterService.pushNotificationToCore(CORE, responseMsgProto, null); + + verify(topicService).getNotificationsTopic(ServiceType.TB_CORE, CORE); + verify(producerProvider).getTbCoreNotificationsMsgProducer(); + ArgumentCaptor> protoQueueMsgArgumentCaptor = ArgumentCaptor.forClass(TbProtoQueueMsg.class); + verify(tbCoreQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), isNull()); + TbProtoQueueMsg protoQueueMsgArgumentCaptorValue = protoQueueMsgArgumentCaptor.getValue(); + assertThat(protoQueueMsgArgumentCaptorValue.getValue()).isEqualTo(toCoreNotificationMsg); + } + + @Test + public void testPushMsgToRuleEngineUsingQueueFromMsg() { + TenantId tenantId = TenantId.SYS_TENANT_ID; + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg tbMsg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, deviceId, null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TbQueueProducer> tbREQueueProducer = mock(TbQueueProducer.class); + TopicPartitionInfo tpi = new TopicPartitionInfo(ServiceType.TB_RULE_ENGINE.name().toLowerCase() + ".notifications." + CORE, tenantId, null, false); + TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTbMsg(TbMsg.toByteString(tbMsg)).build(); + + when(producerProvider.getRuleEngineMsgProducer()).thenReturn(tbREQueueProducer); + when(partitionService.resolve(any(), any(), any(), any())).thenReturn(tpi); + + clusterService.pushMsgToRuleEngine(tenantId, tenantId, tbMsg, true, null); + + verify(partitionService).resolve(ServiceType.TB_RULE_ENGINE, "main", tenantId, tenantId); + verify(producerProvider).getRuleEngineMsgProducer(); + ArgumentCaptor> protoQueueMsgArgumentCaptor = ArgumentCaptor.forClass(TbProtoQueueMsg.class); + verify(tbREQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), isNull()); + TbProtoQueueMsg protoQueueMsgArgumentCaptorValue = protoQueueMsgArgumentCaptor.getValue(); + assertThat(protoQueueMsgArgumentCaptorValue.getValue()).isEqualTo(msg); + } + protected Queue createTestQueue() { TenantId tenantId = TenantId.SYS_TENANT_ID; Queue queue = new Queue(new QueueId(UUID.randomUUID())); diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java index 621d4c5a91..ca6e6728ff 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerServiceTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.ruleengine.RuleEngineCallService; import org.thingsboard.server.service.state.DeviceStateService; import java.util.UUID; @@ -48,6 +49,8 @@ public class DefaultTbCoreConsumerServiceTest { private DeviceStateService stateServiceMock; @Mock private TbCoreConsumerStats statsMock; + @Mock + private RuleEngineCallService ruleEngineCallServiceMock; @Mock private TbCallback tbCallbackMock; @@ -529,4 +532,17 @@ public class DefaultTbCoreConsumerServiceTest { then(statsMock).should(never()).log(inactivityMsg); } + @Test + public void givenRestApiCallResponseMsgProto_whenForwardToRuleEngineCallService_thenCallOnQueueMsg() { + // GIVEN + ReflectionTestUtils.setField(defaultTbCoreConsumerServiceMock, "ruleEngineCallService", ruleEngineCallServiceMock); + var restApiCallResponseMsgProto = TransportProtos.RestApiCallResponseMsgProto.getDefaultInstance(); + doCallRealMethod().when(defaultTbCoreConsumerServiceMock).forwardToRuleEngineCallService(restApiCallResponseMsgProto, tbCallbackMock); + + // WHEN + defaultTbCoreConsumerServiceMock.forwardToRuleEngineCallService(restApiCallResponseMsgProto, tbCallbackMock); + + // THEN + then(ruleEngineCallServiceMock).should().onQueueMsg(restApiCallResponseMsgProto, tbCallbackMock); + } } diff --git a/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java new file mode 100644 index 0000000000..718648e143 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.rpc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.gen.transport.TransportProtos; +import java.util.UUID; + +import static org.mockito.BDDMockito.then; + +@ExtendWith(MockitoExtension.class) +class DefaultTbRuleEngineRpcServiceTest { + + @Mock + private TbClusterService tbClusterServiceMock; + + @InjectMocks + private DefaultTbRuleEngineRpcService tbRuleEngineRpcService; + + @Test + public void givenTbMsg_whenSendRestApiCallReply_thenPushNotificationToCore() { + // GIVEN + String serviceId = "tb-core-0"; + UUID requestId = UUID.fromString("f64a20df-eb1e-46a3-ba6f-0b3ae053ee0a"); + DeviceId deviceId = new DeviceId(UUID.fromString("1d9f771a-7cdc-4ac7-838c-ba193d05a012")); + TbMsg msg = TbMsg.newMsg(null, TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + var restApiCallResponseMsgProto = TransportProtos.RestApiCallResponseMsgProto.newBuilder() + .setRequestIdMSB(requestId.getMostSignificantBits()) + .setRequestIdLSB(requestId.getLeastSignificantBits()) + .setResponse(TbMsg.toByteString(msg)) + .build(); + + // WHEN + tbRuleEngineRpcService.sendRestApiCallReply(serviceId, requestId, msg); + + // THEN + then(tbClusterServiceMock).should().pushNotificationToCore(serviceId, restApiCallResponseMsgProto, null); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java new file mode 100644 index 0000000000..8976cd99f3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java @@ -0,0 +1,166 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ruleengine; + +import org.junit.jupiter.api.AfterEach; +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.springframework.test.util.ReflectionTestUtils; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class DefaultRuleEngineCallServiceTest { + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("d7210c7f-a152-4e91-8186-19ae85499a6b")); + + private final ConcurrentMap> requests = new ConcurrentHashMap<>(); + + @Mock + private TbClusterService tbClusterServiceMock; + + private DefaultRuleEngineCallService ruleEngineCallService; + private ScheduledExecutorService executor; + + @BeforeEach + void setUp() { + executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("rpc-callback")); + ruleEngineCallService = new DefaultRuleEngineCallService(tbClusterServiceMock); + ReflectionTestUtils.setField(ruleEngineCallService, "executor", executor); + ReflectionTestUtils.setField(ruleEngineCallService, "requests", requests); + } + + @AfterEach + void tearDown() { + requests.clear(); + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(10L, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + @Test + void givenRequest_whenProcessRestApiCallToRuleEngine_thenPushMsgToRuleEngine() { + long timeout = 100L; + long expTime = System.currentTimeMillis() + timeout; + HashMap metaData = new HashMap<>(); + UUID requestId = UUID.randomUUID(); + metaData.put("serviceId", "core"); + metaData.put("requestUUID", requestId.toString()); + metaData.put("expirationTime", Long.toString(expTime)); + TbMsg msg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); + Consumer anyConsumer = TbMsg::getData; + ruleEngineCallService.processRestApiCallToRuleEngine(TENANT_ID, requestId, msg, true, anyConsumer); + + assertThat(requests.size()).isEqualTo(1); + assertThat(requests.get(requestId)).isEqualTo(anyConsumer); + verify(tbClusterServiceMock).pushMsgToRuleEngine(TENANT_ID, TENANT_ID, msg, true, null); + } + + @Test + void givenSmallTimeout_whenProcessRestApiCallToRuleEngine_thenDoesNotReturnResponse() { + long timeout = 1L; + long expTime = System.currentTimeMillis() + timeout; + HashMap metaData = new HashMap<>(); + UUID requestId = UUID.randomUUID(); + metaData.put("serviceId", "core"); + metaData.put("requestUUID", requestId.toString()); + metaData.put("expirationTime", Long.toString(expTime)); + TbMsg msg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); + + Consumer anyConsumer = TbMsg::getData; + doAnswer(invocation -> { + //check the presence of request in the map after pushMsgToRuleEngine() + assertThat(requests.size()).isEqualTo(1); + assertThat(requests.get(requestId)).isEqualTo(anyConsumer); + return null; + }).when(tbClusterServiceMock).pushMsgToRuleEngine(any(), any(), any(), anyBoolean(), any()); + ruleEngineCallService.processRestApiCallToRuleEngine(TENANT_ID, requestId, msg, true, anyConsumer); + + verify(tbClusterServiceMock).pushMsgToRuleEngine(TENANT_ID, TENANT_ID, msg, true, null); + //check map is empty after scheduleTimeout() + Awaitility.await("Await until request was deleted from map due to timeout") + .pollDelay(25, TimeUnit.MILLISECONDS) + .atMost(10, TimeUnit.SECONDS) + .until(requests::isEmpty); + } + + @Test + void givenResponse_whenOnQueue_thenAcceptTbMsgResponse() { + long timeout = 10000L; + long expTime = System.currentTimeMillis() + timeout; + HashMap metaData = new HashMap<>(); + UUID requestId = UUID.randomUUID(); + metaData.put("serviceId", "core"); + metaData.put("requestUUID", requestId.toString()); + metaData.put("expirationTime", Long.toString(expTime)); + TbMsg msg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); + + Consumer anyConsumer = TbMsg::getData; + doAnswer(invocation -> { + //check the presence of request in the map after pushMsgToRuleEngine() + assertThat(requests.size()).isEqualTo(1); + assertThat(requests.get(requestId)).isEqualTo(anyConsumer); + ruleEngineCallService.onQueueMsg(getResponse(requestId, msg), TbCallback.EMPTY); + //check map is empty after onQueueMsg() + assertThat(requests.size()).isEqualTo(0); + return null; + }).when(tbClusterServiceMock).pushMsgToRuleEngine(any(), any(), any(), anyBoolean(), any()); + ruleEngineCallService.processRestApiCallToRuleEngine(TENANT_ID, requestId, msg, true, anyConsumer); + + verify(tbClusterServiceMock).pushMsgToRuleEngine(TENANT_ID, TENANT_ID, msg, true, null); + } + + private TransportProtos.RestApiCallResponseMsgProto getResponse(UUID requestId, TbMsg msg) { + return TransportProtos.RestApiCallResponseMsgProto.newBuilder() + .setResponse(TbMsg.toByteString(msg)) + .setRequestIdMSB(requestId.getMostSignificantBits()) + .setRequestIdLSB(requestId.getLeastSignificantBits()) + .build(); + } +} diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 301ef69dcc..ec37a5a864 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -18,7 +18,6 @@ package org.thingsboard.server.cluster; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -39,6 +38,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg; +import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueClusterService; @@ -58,10 +58,14 @@ public interface TbClusterService extends TbQueueClusterService { void pushNotificationToCore(String targetServiceId, FromDeviceRpcResponse response, TbQueueCallback callback); + void pushNotificationToCore(String targetServiceId, RestApiCallResponseMsgProto msg, TbQueueCallback callback); + void pushMsgToRuleEngine(TopicPartitionInfo tpi, UUID msgId, ToRuleEngineMsg msg, TbQueueCallback callback); void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbMsg msg, TbQueueCallback callback); + void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbMsg msg, boolean useQueueFromTbMsg, TbQueueCallback callback); + void pushNotificationToRuleEngine(String targetServiceId, FromDeviceRpcResponse response, TbQueueCallback callback); void pushNotificationToTransport(String targetServiceId, ToTransportMsg response, TbQueueCallback callback); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index d3c0889d6b..db56acb8b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -40,6 +40,7 @@ public enum ActionType { RELATION_ADD_OR_UPDATE(false, TbMsgType.RELATION_ADD_OR_UPDATE), RELATION_DELETED(false, TbMsgType.RELATION_DELETED), RELATIONS_DELETED(false, TbMsgType.RELATIONS_DELETED), + REST_API_RULE_ENGINE_CALL(false, null), // log call to rule engine from REST API ALARM_ACK(false, TbMsgType.ALARM_ACK), ALARM_CLEAR(false, TbMsgType.ALARM_CLEAR), ALARM_DELETE(false, TbMsgType.ALARM_DELETE), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java index 763f245a09..9aa1994ab6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -67,6 +67,7 @@ public enum TbMsgType { PROVISION_SUCCESS, PROVISION_FAILURE, SEND_EMAIL, + REST_API_REQUEST("REST API request"), // tellSelfOnly types GENERATOR_NODE_SELF_MSG(null, true), diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 7b2aa2fe1b..a37eb118c7 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -98,6 +98,12 @@ message SessionInfoProto { int64 customerIdLSB = 15; } +message RestApiCallResponseMsgProto { + int64 requestIdMSB = 1; + int64 requestIdLSB = 2; + bytes response = 5; +} + enum SessionEvent { OPEN = 0; CLOSED = 1; @@ -1465,6 +1471,7 @@ message ToCoreNotificationMsg { ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 11; FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 12; ResourceCacheInvalidateMsg resourceCacheInvalidateMsg = 13; + RestApiCallResponseMsgProto restApiCallResponseMsg = 50; } /* Messages that are handled by ThingsBoard RuleEngine Service */ diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 8783770be3..c116eddc74 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -3840,6 +3840,51 @@ public class RestClient implements Closeable { } } + public JsonNode handleRuleEngineRequest(JsonNode requestBody) { + return restTemplate.exchange( + baseURL + "/api/rule-engine", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference() { + }).getBody(); + } + + public JsonNode handleRuleEngineRequest(EntityId entityId, JsonNode requestBody) { + return restTemplate.exchange( + baseURL + "/api/rule-engine/{entityType}/{entityId}", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference() { + }, + entityId.getEntityType(), + entityId.getId()).getBody(); + } + + public JsonNode handleRuleEngineRequest(EntityId entityId, int timeout, JsonNode requestBody) { + return restTemplate.exchange( + baseURL + "/api/rule-engine/{entityType}/{entityId}/{timeout}", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference() { + }, + entityId.getEntityType(), + entityId.getId(), + timeout).getBody(); + } + + public JsonNode handleRuleEngineRequest(EntityId entityId, String queueName, int timeout, JsonNode requestBody) { + return restTemplate.exchange( + baseURL + "/api/rule-engine/{entityType}/{entityId}/{queueName}/{timeout}", + HttpMethod.POST, + new HttpEntity<>(requestBody), + new ParameterizedTypeReference() { + }, + entityId.getEntityType(), + entityId.getId(), + queueName, + timeout).getBody(); + } + private String getTimeUrlParams(TimePageLink pageLink) { String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java index 55f83f823b..99ab76b69d 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineRpcService.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rpc.Rpc; +import org.thingsboard.server.common.msg.TbMsg; import java.util.UUID; import java.util.function.Consumer; @@ -31,5 +32,7 @@ public interface RuleEngineRpcService { void sendRpcRequestToDevice(RuleEngineDeviceRpcRequest request, Consumer consumer); + void sendRestApiCallReply(String serviceId, UUID requestId, TbMsg msg); + Rpc findRpcById(TenantId tenantId, RpcId id); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java new file mode 100644 index 0000000000..84441fb213 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.rest; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.UUID; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "rest call reply", + configClazz = TbSendRestApiCallReplyNodeConfiguration.class, + nodeDescription = "Sends reply to REST API call to rule engine", + nodeDetails = "Expects messages with any message type. Forwards incoming message as a reply to REST API call sent to rule engine.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbActionNodeSendRestApiCallReplyConfig", + icon = "call_merge" +) +public class TbSendRestApiCallReplyNode implements TbNode { + + private TbSendRestApiCallReplyNodeConfiguration config; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, TbSendRestApiCallReplyNodeConfiguration.class); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + String serviceIdStr = msg.getMetaData().getValue(config.getServiceIdMetaDataAttribute()); + String requestIdStr = msg.getMetaData().getValue(config.getRequestIdMetaDataAttribute()); + if (StringUtils.isEmpty(requestIdStr)) { + ctx.tellFailure(msg, new RuntimeException("Request id is not present in the metadata!")); + } else if (StringUtils.isEmpty(serviceIdStr)) { + ctx.tellFailure(msg, new RuntimeException("Service id is not present in the metadata!")); + } else if (StringUtils.isEmpty(msg.getData())) { + ctx.tellFailure(msg, new RuntimeException("Request body is empty!")); + } else { + ctx.getRpcService().sendRestApiCallReply(serviceIdStr, UUID.fromString(requestIdStr), msg); + ctx.tellSuccess(msg); + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeConfiguration.java new file mode 100644 index 0000000000..010a6ccb15 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeConfiguration.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.rest; + +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.StringUtils; + +@Data +public class TbSendRestApiCallReplyNodeConfiguration implements NodeConfiguration { + public static final String SERVICE_ID = "serviceId"; + public static final String REQUEST_UUID = "requestUUID"; + + private String serviceIdMetaDataAttribute; + private String requestIdMetaDataAttribute; + + @Override + public TbSendRestApiCallReplyNodeConfiguration defaultConfiguration() { + TbSendRestApiCallReplyNodeConfiguration configuration = new TbSendRestApiCallReplyNodeConfiguration(); + configuration.setRequestIdMetaDataAttribute(REQUEST_UUID); + configuration.setServiceIdMetaDataAttribute(SERVICE_ID); + return configuration; + } + + public String getServiceIdMetaDataAttribute() { + return !StringUtils.isEmpty(serviceIdMetaDataAttribute) ? serviceIdMetaDataAttribute : SERVICE_ID; + } + + public String getRequestIdMetaDataAttribute() { + return !StringUtils.isEmpty(requestIdMetaDataAttribute) ? requestIdMetaDataAttribute : REQUEST_UUID; + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java new file mode 100644 index 0000000000..c37d4b28a0 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.rest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleEngineRpcService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TbSendRestApiCallReplyNodeTest { + + private static final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("212445ad-9852-4bfd-819d-6b01ab6ee6b6")); + + private TbSendRestApiCallReplyNode node; + private TbSendRestApiCallReplyNodeConfiguration config; + + @Mock + private TbContext ctxMock; + @Mock + private RuleEngineRpcService rpcServiceMock; + + @BeforeEach + public void setUp() throws TbNodeException { + node = new TbSendRestApiCallReplyNode(); + config = new TbSendRestApiCallReplyNodeConfiguration().defaultConfiguration(); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, configuration); + } + + @Test + public void givenDefaultConfig_whenInit_thenDoesNotThrowException() { + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + assertThatNoException().isThrownBy(() -> node.init(ctxMock, configuration)); + } + + @ParameterizedTest + @MethodSource + public void givenValidRestApiRequest_whenOnMsg_thenTellSuccess(String requestIdAttribute, String serviceIdAttribute) throws TbNodeException { + config.setRequestIdMetaDataAttribute(requestIdAttribute); + config.setServiceIdMetaDataAttribute(serviceIdAttribute); + var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, configuration); + when(ctxMock.getRpcService()).thenReturn(rpcServiceMock); + String requestUUIDStr = "80b7883b-7ec6-4872-9dd3-b2afd5660fa6"; + String serviceIdStr = "tb-core-0"; + String data = """ + { + "temperature": 23, + } + """; + Map metadata = Map.of( + requestIdAttribute, requestUUIDStr, + serviceIdAttribute, serviceIdStr); + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, DEVICE_ID, new TbMsgMetaData(metadata), data); + + node.onMsg(ctxMock, msg); + + UUID requestUUID = UUID.fromString(requestUUIDStr); + verify(rpcServiceMock).sendRestApiCallReply(serviceIdStr, requestUUID, msg); + verify(ctxMock).tellSuccess(msg); + } + + private static Stream givenValidRestApiRequest_whenOnMsg_thenTellSuccess() { + return Stream.of( + Arguments.of("requestId", "service"), + Arguments.of("requestUUID", "serviceId"), + Arguments.of("some_custom_request_id_field", "some_custom_service_id_field") + ); + } + + @ParameterizedTest + @MethodSource + public void givenInvalidRequest_whenOnMsg_thenTellFailure(TbMsgMetaData metaData, String data, String errorMsg) { + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, DEVICE_ID, metaData, data); + + node.onMsg(ctxMock, msg); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); + verify(ctxMock).tellFailure(eq(msg), captor.capture()); + Throwable throwable = captor.getValue(); + assertThat(throwable).isInstanceOf(RuntimeException.class).hasMessage(errorMsg); + } + + private static Stream givenInvalidRequest_whenOnMsg_thenTellFailure() { + return Stream.of( + Arguments.of(TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING, "Request id is not present in the metadata!"), + Arguments.of(new TbMsgMetaData(Map.of("requestUUID", "e1dd3985-efad-45a0-b0d2-0ff5dff2ccac")), + TbMsg.EMPTY_STRING, "Service id is not present in the metadata!"), + Arguments.of(new TbMsgMetaData(Map.of("serviceId", "tb-core-0")), + TbMsg.EMPTY_STRING, "Request id is not present in the metadata!"), + Arguments.of(new TbMsgMetaData(Map.of("requestUUID", "e1dd3985-efad-45a0-b0d2-0ff5dff2ccac", "serviceId", "tb-core-0")), + TbMsg.EMPTY_STRING, "Request body is empty!") + ); + } +} From f538f2ec795f6801a38da2e0defd6be5777dab73 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 14 May 2024 10:28:03 +0300 Subject: [PATCH 018/136] fixed ActionTypeTest --- .../thingsboard/server/common/data/audit/ActionTypeTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java index f746f47e19..6a5ed54896 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java @@ -28,6 +28,7 @@ import static org.thingsboard.server.common.data.audit.ActionType.DELETED_COMMEN import static org.thingsboard.server.common.data.audit.ActionType.LOCKOUT; import static org.thingsboard.server.common.data.audit.ActionType.LOGIN; import static org.thingsboard.server.common.data.audit.ActionType.LOGOUT; +import static org.thingsboard.server.common.data.audit.ActionType.REST_API_RULE_ENGINE_CALL; import static org.thingsboard.server.common.data.audit.ActionType.RPC_CALL; import static org.thingsboard.server.common.data.audit.ActionType.SMS_SENT; import static org.thingsboard.server.common.data.audit.ActionType.SUSPENDED; @@ -45,7 +46,8 @@ class ActionTypeTest { LOGOUT, LOCKOUT, DELETED_COMMENT, - SMS_SENT + SMS_SENT, + REST_API_RULE_ENGINE_CALL ); // backward-compatibility tests From d10f7995d7d066e43811ebbb10e6e071537546c7 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 15 May 2024 12:39:19 +0300 Subject: [PATCH 019/136] added additional tests to DefaultTbClusterServiceTest --- .../controller/AbstractNotifyEntityTest.java | 11 +- .../server/controller/AbstractWebTest.java | 10 +- .../controller/RuleEngineControllerTest.java | 203 +++++++++--------- .../queue/DefaultTbClusterServiceTest.java | 116 ++++++++-- .../DefaultTbRuleEngineRpcServiceTest.java | 2 +- .../DefaultRuleEngineCallServiceTest.java | 19 +- .../rest/TbSendRestApiCallReplyNodeTest.java | 2 +- 7 files changed, 224 insertions(+), 139 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 9bb49c1d62..d549407c44 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -386,13 +386,12 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { actionType, cntTime, extractMatcherAdditionalInfo(additionalInfo)); } - protected void testLogEntityActionError(HasName entity, EntityId originatorId, TenantId tenantId, + protected void testLogEntityActionError(EntityId originatorId, TenantId tenantId, CustomerId customerId, UserId userId, String userName, - ActionType actionType, Exception exp, Object... additionalInfo) { - ArgumentMatcher matcherError = argument -> argument.getMessage().contains(exp.getMessage()) - & argument.getClass().equals(exp.getClass()); - ArgumentMatcher matcherEntityEquals = entity == null ? Objects::isNull : argument -> argument.toString().equals(entity.toString()); - testLogEntityActionErrorAdditionalInfo(matcherEntityEquals, originatorId, tenantId, customerId, userId, userName, + ActionType actionType, Exception exception, Object... additionalInfo) { + ArgumentMatcher matcherError = argument -> argument.getMessage().contains(exception.getMessage()) + & argument.getClass().equals(exception.getClass()); + testLogEntityActionErrorAdditionalInfo(Objects::isNull, originatorId, tenantId, customerId, userId, userName, actionType, 1, matcherError, extractMatcherAdditionalInfo(additionalInfo)); } 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 32ff733e0c..9faba0d2d8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -212,6 +212,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected UserId differentCustomerUserId; protected UserId differentTenantCustomerUserId; + protected UserId currentUserId; @SuppressWarnings("rawtypes") private HttpMessageConverter mappingJackson2HttpMessageConverter; @@ -567,6 +568,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); Assert.assertEquals(username, subject); + String userId = claims.get("userId", String.class); + this.currentUserId = UserId.fromString(userId); } protected void resetTokens() throws Exception { @@ -633,10 +636,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/device?accessToken=" + accessToken, device, Device.class); } - protected Device assignDeviceToCustomer(String name, String accessToken, CustomerId customerId) throws Exception { - Device device = createDevice(name, accessToken); - String deviceIdStr = String.valueOf(device.getId().getId()); - return doPost("/api/customer/" + customerId.getId() + "/device/" + deviceIdStr, device, Device.class); + protected Device assignDeviceToCustomer(DeviceId deviceId, CustomerId customerId) { + String deviceIdStr = String.valueOf(deviceId.getId()); + return doPost("/api/customer/" + customerId.getId() + "/device/" + deviceIdStr, Device.class); } protected MqttDeviceProfileTransportConfiguration createMqttDeviceProfileTransportConfiguration(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration, boolean sendAckOnValidationException) { diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java index dac268588d..e05a44de08 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java @@ -16,30 +16,25 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.web.servlet.MvcResult; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.ruleengine.RuleEngineCallService; -import org.thingsboard.server.service.security.model.token.JwtTokenFactory; -import java.util.Objects; +import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -53,42 +48,38 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest public class RuleEngineControllerTest extends AbstractControllerTest { - private static final String REQUEST_BODY = "{\"temperature\":23}"; + private final String REQUEST_BODY = "{\"request\":\"download\"}"; + private final String RESPONSE_BODY = "{\"response\":\"downloadOk\"}"; @SpyBean private RuleEngineCallService ruleEngineCallService; - @Autowired - private JwtTokenFactory jwtTokenFactory; - @Test public void testHandleRuleEngineRequestWithMsgOriginatorUser() throws Exception { loginSysAdmin(); - UserId sysAdminUserId = getCurrentUserId(); - TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, sysAdminUserId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); - doAnswer(invocation -> { - Consumer consumer = invocation.getArgument(4); - consumer.accept(msg); - return null; - }).when(ruleEngineCallService).processRestApiCallToRuleEngine(eq(TenantId.SYS_TENANT_ID), any(UUID.class), any(TbMsg.class), anyBoolean(), any()); + TbMsg responseMsg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, currentUserId, TbMsgMetaData.EMPTY, RESPONSE_BODY); + mockRestApiCallToRuleEngine(responseMsg); - var response = doPostAsyncWithTypedResponse("/api/rule-engine/", REQUEST_BODY, new TypeReference<>() { + JsonNode apiResponse = doPostAsyncWithTypedResponse("/api/rule-engine/", REQUEST_BODY, new TypeReference<>() { }, status().isOk()); - assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); - ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); - verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(TenantId.SYS_TENANT_ID), any(), captor.capture(), eq(false), any()); - TbMsg tbMsg = captor.getValue(); - assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); - assertThat(tbMsg.getType()).isEqualTo(msg.getType()); - assertThat(tbMsg.getOriginator()).isEqualTo(sysAdminUserId); - testLogEntityAction(null, sysAdminUserId, TenantId.SYS_TENANT_ID, new CustomerId(TenantId.SYS_TENANT_ID.getId()), sysAdminUserId, - SYS_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + assertThat(JacksonUtil.toString(apiResponse)).isEqualTo(RESPONSE_BODY); + ArgumentCaptor requestMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(TenantId.SYS_TENANT_ID), any(UUID.class), requestMsgCaptor.capture(), eq(false), any(Consumer.class)); + TbMsg requestMsgCaptorValue = requestMsgCaptor.getValue(); + assertThat(requestMsgCaptorValue.getData()).isEqualTo(REQUEST_BODY); + assertThat(requestMsgCaptorValue.getType()).isEqualTo(TbMsgType.REST_API_REQUEST.name()); + assertThat(requestMsgCaptorValue.getOriginator()).isEqualTo(currentUserId); + assertThat(requestMsgCaptorValue.getCustomerId()).isNull(); + checkMetadataProperties(requestMsgCaptorValue.getMetaData()); + testLogEntityAction(null, currentUserId, TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), currentUserId, + SYS_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, RESPONSE_BODY); } @Test @@ -96,21 +87,23 @@ public class RuleEngineControllerTest extends AbstractControllerTest { loginTenantAdmin(); Device device = createDevice("Test", "123"); DeviceId deviceId = device.getId(); - TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); - mockSuccessfulRestApiCallToRuleEngine(msg); + TbMsg responseMsg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, RESPONSE_BODY); + mockRestApiCallToRuleEngine(responseMsg); - var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId(), REQUEST_BODY, new TypeReference<>() { + JsonNode apiResponse = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId(), REQUEST_BODY, new TypeReference<>() { }, status().isOk()); - assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); - ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); - verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); - TbMsg tbMsg = captor.getValue(); - assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); - assertThat(tbMsg.getType()).isEqualTo(msg.getType()); - assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); - testLogEntityAction(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, - TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + assertThat(JacksonUtil.toString(apiResponse)).isEqualTo(RESPONSE_BODY); + ArgumentCaptor requestMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(UUID.class), requestMsgCaptor.capture(), eq(false), any(Consumer.class)); + TbMsg requestMsgCaptorValue = requestMsgCaptor.getValue(); + assertThat(requestMsgCaptorValue.getData()).isEqualTo(REQUEST_BODY); + assertThat(requestMsgCaptorValue.getType()).isEqualTo(TbMsgType.REST_API_REQUEST.name()); + assertThat(requestMsgCaptorValue.getOriginator()).isEqualTo(deviceId); + assertThat(requestMsgCaptorValue.getCustomerId()).isNull(); + checkMetadataProperties(requestMsgCaptorValue.getMetaData()); + testLogEntityAction(null, deviceId, tenantId, new CustomerId(EntityId.NULL_UUID), tenantAdminUserId, + TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, RESPONSE_BODY); } @Test @@ -118,21 +111,23 @@ public class RuleEngineControllerTest extends AbstractControllerTest { loginTenantAdmin(); Device device = createDevice("Test", "123"); DeviceId deviceId = device.getId(); - TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); - mockSuccessfulRestApiCallToRuleEngine(msg); + TbMsg responseMsg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, RESPONSE_BODY); + mockRestApiCallToRuleEngine(responseMsg); - var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId() + "/15000", REQUEST_BODY, new TypeReference<>() { + JsonNode apiResponse = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId() + "/15000", REQUEST_BODY, new TypeReference<>() { }, status().isOk()); - assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); - ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); - verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); - TbMsg tbMsg = captor.getValue(); - assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); - assertThat(tbMsg.getType()).isEqualTo(msg.getType()); - assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); - testLogEntityAction(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, - TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + assertThat(JacksonUtil.toString(apiResponse)).isEqualTo(RESPONSE_BODY); + ArgumentCaptor requestMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(UUID.class), requestMsgCaptor.capture(), eq(false), any(Consumer.class)); + TbMsg requestMsgCaptorValue = requestMsgCaptor.getValue(); + assertThat(requestMsgCaptorValue.getData()).isEqualTo(REQUEST_BODY); + assertThat(requestMsgCaptorValue.getType()).isEqualTo(TbMsgType.REST_API_REQUEST.name()); + assertThat(requestMsgCaptorValue.getOriginator()).isEqualTo(deviceId); + assertThat(requestMsgCaptorValue.getCustomerId()).isNull(); + checkMetadataProperties(requestMsgCaptorValue.getMetaData()); + testLogEntityAction(null, deviceId, tenantId, new CustomerId(EntityId.NULL_UUID), tenantAdminUserId, + TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, RESPONSE_BODY); } @Test @@ -140,19 +135,20 @@ public class RuleEngineControllerTest extends AbstractControllerTest { loginTenantAdmin(); Device device = createDevice("Test", "123"); DeviceId deviceId = device.getId(); - TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), TbMsgMetaData.EMPTY, REQUEST_BODY); - mockSuccessfulRestApiCallToRuleEngine(null); + mockRestApiCallToRuleEngine(null); doPostAsync("/api/rule-engine/DEVICE/" + deviceId.getId() + "/15000", REQUEST_BODY, String.class, status().isRequestTimeout()); - ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); - verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); - TbMsg tbMsg = captor.getValue(); - assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); - assertThat(tbMsg.getType()).isEqualTo(msg.getType()); - assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); + ArgumentCaptor requestMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(UUID.class), requestMsgCaptor.capture(), eq(false), any(Consumer.class)); + TbMsg requestMsgCaptorValue = requestMsgCaptor.getValue(); + assertThat(requestMsgCaptorValue.getData()).isEqualTo(REQUEST_BODY); + assertThat(requestMsgCaptorValue.getType()).isEqualTo(TbMsgType.REST_API_REQUEST.name()); + assertThat(requestMsgCaptorValue.getOriginator()).isEqualTo(deviceId); + assertThat(requestMsgCaptorValue.getCustomerId()).isNull(); + checkMetadataProperties(requestMsgCaptorValue.getMetaData()); Exception exception = new TimeoutException("Processing timeout detected!"); - testLogEntityActionError(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, + testLogEntityActionError(deviceId, tenantId, new CustomerId(EntityId.NULL_UUID), tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, exception, REQUEST_BODY, ""); } @@ -161,22 +157,24 @@ public class RuleEngineControllerTest extends AbstractControllerTest { loginTenantAdmin(); Device device = createDevice("Test", "123"); DeviceId deviceId = device.getId(); - TbMsg msg = TbMsg.newMsg("HighPriority", TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, REQUEST_BODY); - mockSuccessfulRestApiCallToRuleEngine(msg); + TbMsg responseMsg = TbMsg.newMsg(DataConstants.HP_QUEUE_NAME, TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, RESPONSE_BODY); + mockRestApiCallToRuleEngine(responseMsg); - var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId() + "/HighPriority/1000", REQUEST_BODY, new TypeReference<>() { + JsonNode apiResponse = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId() + "/HighPriority/1000", REQUEST_BODY, new TypeReference<>() { }, status().isOk()); - assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); - ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); - verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(true), any()); - TbMsg tbMsg = captor.getValue(); - assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); - assertThat(tbMsg.getType()).isEqualTo(msg.getType()); - assertThat(tbMsg.getQueueName()).isEqualTo(msg.getQueueName()); - assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); - testLogEntityAction(null, deviceId, tenantId, new CustomerId(TenantId.SYS_TENANT_ID.getId()), tenantAdminUserId, - TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + assertThat(JacksonUtil.toString(apiResponse)).isEqualTo(RESPONSE_BODY); + ArgumentCaptor requestMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(UUID.class), requestMsgCaptor.capture(), eq(true), any(Consumer.class)); + TbMsg requestMsgCaptorValue = requestMsgCaptor.getValue(); + assertThat(requestMsgCaptorValue.getData()).isEqualTo(REQUEST_BODY); + assertThat(requestMsgCaptorValue.getType()).isEqualTo(TbMsgType.REST_API_REQUEST.name()); + assertThat(requestMsgCaptorValue.getQueueName()).isEqualTo(DataConstants.HP_QUEUE_NAME); + assertThat(requestMsgCaptorValue.getOriginator()).isEqualTo(deviceId); + assertThat(requestMsgCaptorValue.getCustomerId()).isNull(); + checkMetadataProperties(requestMsgCaptorValue.getMetaData()); + testLogEntityAction(null, deviceId, tenantId, new CustomerId(EntityId.NULL_UUID), tenantAdminUserId, + TENANT_ADMIN_EMAIL, ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, RESPONSE_BODY); } @Test @@ -193,25 +191,28 @@ public class RuleEngineControllerTest extends AbstractControllerTest { @Test public void testHandleRuleEngineRequestWithAuthorityCustomerUser() throws Exception { loginTenantAdmin(); - Device device = assignDeviceToCustomer("test", "123", customerId); + Device device = createDevice("Test", "123"); DeviceId deviceId = device.getId(); + assignDeviceToCustomer(deviceId, customerId); loginCustomerUser(); - TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, customerId, TbMsgMetaData.EMPTY, REQUEST_BODY); - mockSuccessfulRestApiCallToRuleEngine(msg); + TbMsg responseMsg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, customerId, TbMsgMetaData.EMPTY, RESPONSE_BODY); + mockRestApiCallToRuleEngine(responseMsg); - var response = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId(), REQUEST_BODY, new TypeReference<>() { + JsonNode apiResponse = doPostAsyncWithTypedResponse("/api/rule-engine/DEVICE/" + deviceId.getId(), REQUEST_BODY, new TypeReference<>() { }, status().isOk()); - assertThat(Objects.requireNonNull(JacksonUtil.toString(response))).isEqualTo(REQUEST_BODY); - ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); - verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(), captor.capture(), eq(false), any()); - TbMsg tbMsg = captor.getValue(); - assertThat(tbMsg.getData()).isEqualTo(REQUEST_BODY); - assertThat(tbMsg.getType()).isEqualTo(msg.getType()); - assertThat(tbMsg.getOriginator()).isEqualTo(deviceId); + assertThat(JacksonUtil.toString(apiResponse)).isEqualTo(RESPONSE_BODY); + ArgumentCaptor requestMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(UUID.class), requestMsgCaptor.capture(), eq(false), any(Consumer.class)); + TbMsg requestMsgCaptorValue = requestMsgCaptor.getValue(); + assertThat(requestMsgCaptorValue.getData()).isEqualTo(REQUEST_BODY); + assertThat(requestMsgCaptorValue.getType()).isEqualTo(TbMsgType.REST_API_REQUEST.name()); + assertThat(requestMsgCaptorValue.getOriginator()).isEqualTo(deviceId); + assertThat(requestMsgCaptorValue.getCustomerId()).isEqualTo(customerId); + checkMetadataProperties(requestMsgCaptorValue.getMetaData()); testLogEntityAction(null, deviceId, tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, - ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, REQUEST_BODY); + ActionType.REST_API_RULE_ENGINE_CALL, 1, REQUEST_BODY, RESPONSE_BODY); } @Test @@ -220,12 +221,12 @@ public class RuleEngineControllerTest extends AbstractControllerTest { Device device = createDevice("test", "123"); loginCustomerUser(); - MvcResult result = doPost("/api/rule-engine/DEVICE/" + device.getId().getId(), (Object) REQUEST_BODY).andReturn(); + doPostAsync("/api/rule-engine/DEVICE/" + device.getId().getId(), (Object) REQUEST_BODY, -1L) + .andExpect(status().isForbidden()) + .andExpect(content().string("You don't have permission to perform this operation!")); - ResponseEntity response = (ResponseEntity) result.getAsyncResult(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - assertThat(Objects.requireNonNull(response.getBody()).toString()).isEqualTo("You don't have permission to perform this operation!"); - verify(ruleEngineCallService, never()).processRestApiCallToRuleEngine(any(), any(), any(), anyBoolean(), any()); + verify(ruleEngineCallService, never()) + .processRestApiCallToRuleEngine(any(TenantId.class), any(UUID.class), any(TbMsg.class), anyBoolean(), any(Consumer.class)); } @Test @@ -235,18 +236,18 @@ public class RuleEngineControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString("Authentication failed"))); } - private void mockSuccessfulRestApiCallToRuleEngine(TbMsg msg) { + private void mockRestApiCallToRuleEngine(TbMsg responseMsg) { doAnswer(invocation -> { Consumer consumer = invocation.getArgument(4); - consumer.accept(msg); + consumer.accept(responseMsg); return null; - }).when(ruleEngineCallService).processRestApiCallToRuleEngine(eq(tenantId), any(UUID.class), any(TbMsg.class), anyBoolean(), any()); + }).when(ruleEngineCallService).processRestApiCallToRuleEngine(any(TenantId.class), any(UUID.class), any(TbMsg.class), anyBoolean(), any(Consumer.class)); } - private UserId getCurrentUserId() { - Jws jwsClaims = jwtTokenFactory.parseTokenClaims(token); - Claims claims = jwsClaims.getPayload(); - String userId = claims.get("userId", String.class); - return UserId.fromString(userId); + public void checkMetadataProperties(TbMsgMetaData metaData) { + Map data = metaData.getData(); + assertThat(data.containsKey("serviceId")).isTrue(); + assertThat(data.containsKey("requestUUID")).isTrue(); + assertThat(data.containsKey("expirationTime")).isTrue(); } } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 5ece34cd7f..604fe5fe86 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -26,7 +26,10 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -37,7 +40,9 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueProducer; +import org.thingsboard.server.queue.common.DefaultTbQueueMsgHeaders; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TopicService; @@ -53,10 +58,12 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @Slf4j @@ -246,46 +253,129 @@ public class DefaultTbClusterServiceTest { @Test public void testPushNotificationToCoreWithRestApiCallResponseMsgProto() { + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + TbQueueCallback callbackMock = mock(TbQueueCallback.class); TbQueueProducer> tbCoreQueueProducer = mock(TbQueueProducer.class); - TopicPartitionInfo tpi = new TopicPartitionInfo(ServiceType.TB_CORE.name().toLowerCase() + ".notifications." + CORE, null, null, false); + doReturn(tpi).when(topicService).getNotificationsTopic(any(ServiceType.class), any(String.class)); when(producerProvider.getTbCoreNotificationsMsgProducer()).thenReturn(tbCoreQueueProducer); TransportProtos.RestApiCallResponseMsgProto responseMsgProto = TransportProtos.RestApiCallResponseMsgProto.getDefaultInstance(); TransportProtos.ToCoreNotificationMsg toCoreNotificationMsg = TransportProtos.ToCoreNotificationMsg.newBuilder().setRestApiCallResponseMsg(responseMsgProto).build(); - clusterService.pushNotificationToCore(CORE, responseMsgProto, null); + clusterService.pushNotificationToCore(CORE, responseMsgProto, callbackMock); verify(topicService).getNotificationsTopic(ServiceType.TB_CORE, CORE); verify(producerProvider).getTbCoreNotificationsMsgProducer(); ArgumentCaptor> protoQueueMsgArgumentCaptor = ArgumentCaptor.forClass(TbProtoQueueMsg.class); - verify(tbCoreQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), isNull()); + verify(tbCoreQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), eq(callbackMock)); TbProtoQueueMsg protoQueueMsgArgumentCaptorValue = protoQueueMsgArgumentCaptor.getValue(); + assertThat(protoQueueMsgArgumentCaptorValue.getKey()).isNotNull(); assertThat(protoQueueMsgArgumentCaptorValue.getValue()).isEqualTo(toCoreNotificationMsg); + assertThat(protoQueueMsgArgumentCaptorValue.getHeaders().getData()).isEqualTo(new DefaultTbQueueMsgHeaders().getData()); } @Test - public void testPushMsgToRuleEngineUsingQueueFromMsg() { + public void testPushMsgToRuleEngineWithTenantIdIsNullUuidAndEntityIsTenantUseQueueFromMsgIsTrue() { + TbQueueProducer> tbREQueueProducer = mock(TbQueueProducer.class); + TbQueueCallback callback = mock(TbQueueCallback.class); + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + + TenantId tenantId = TenantId.fromUUID(UUID.fromString("3c8bd350-1239-4a3b-b9c3-4dd76f8e20f1")); + TbMsg requestMsg = TbMsg.newMsg(DataConstants.HP_QUEUE_NAME, TbMsgType.REST_API_REQUEST, tenantId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TransportProtos.ToRuleEngineMsg toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTbMsg(TbMsg.toByteString(requestMsg)).build(); + + when(partitionService.resolve(any(ServiceType.class), any(String.class), any(TenantId.class), any(EntityId.class))).thenReturn(tpi); + when(producerProvider.getRuleEngineMsgProducer()).thenReturn(tbREQueueProducer); + + clusterService.pushMsgToRuleEngine(TenantId.SYS_TENANT_ID, tenantId, requestMsg, true, callback); + + verify(partitionService).resolve(ServiceType.TB_RULE_ENGINE, DataConstants.HP_QUEUE_NAME, tenantId, tenantId); + verify(producerProvider).getRuleEngineMsgProducer(); + ArgumentCaptor> protoQueueMsgArgumentCaptor = ArgumentCaptor.forClass(TbProtoQueueMsg.class); + verify(tbREQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), eq(callback)); + TbProtoQueueMsg protoQueueMsgArgumentCaptorValue = protoQueueMsgArgumentCaptor.getValue(); + assertThat(protoQueueMsgArgumentCaptorValue.getKey()).isEqualTo(requestMsg.getId()); + assertThat(protoQueueMsgArgumentCaptorValue.getValue()).isEqualTo(toRuleEngineMsg); + assertThat(protoQueueMsgArgumentCaptorValue.getHeaders().getData()).isEqualTo(new DefaultTbQueueMsgHeaders().getData()); + } + + @Test + public void testPushMsgToRuleEngineWithTenantIdIsNullUuidAndEntityIsDevice() { TenantId tenantId = TenantId.SYS_TENANT_ID; DeviceId deviceId = new DeviceId(UUID.randomUUID()); - TbMsg tbMsg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, deviceId, null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TbMsg requestMsg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TbQueueCallback callback = mock(TbQueueCallback.class); + + clusterService.pushMsgToRuleEngine(tenantId, deviceId, requestMsg, false, callback); + + verifyNoMoreInteractions(partitionService, producerProvider); + } + + @Test + public void testPushMsgToRuleEngineWithTenantIdIsNotNullUuidUseQueueFromMsgIsTrue() { TbQueueProducer> tbREQueueProducer = mock(TbQueueProducer.class); - TopicPartitionInfo tpi = new TopicPartitionInfo(ServiceType.TB_RULE_ENGINE.name().toLowerCase() + ".notifications." + CORE, tenantId, null, false); - TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder() + TbQueueCallback callback = mock(TbQueueCallback.class); + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + + TenantId tenantId = TenantId.fromUUID(UUID.fromString("3c8bd350-1239-4a3b-b9c3-4dd76f8e20f1")); + DeviceId deviceId = new DeviceId(UUID.fromString("adbb9d41-3367-40fd-9e74-7dd7cc5d30cf")); + DeviceProfile deviceProfile = new DeviceProfile(new DeviceProfileId(UUID.fromString("552f5d6d-0b2b-43e1-a7d2-a51cb2a96927"))); + TbMsg requestMsg = TbMsg.newMsg(DataConstants.HP_QUEUE_NAME, TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TransportProtos.ToRuleEngineMsg toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setTbMsg(TbMsg.toByteString(requestMsg)).build(); + + when(deviceProfileCache.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); + when(partitionService.resolve(any(ServiceType.class), any(String.class), any(TenantId.class), any(EntityId.class))).thenReturn(tpi); + when(producerProvider.getRuleEngineMsgProducer()).thenReturn(tbREQueueProducer); + + clusterService.pushMsgToRuleEngine(tenantId, deviceId, requestMsg, true, callback); + + verify(partitionService).resolve(ServiceType.TB_RULE_ENGINE, DataConstants.HP_QUEUE_NAME, tenantId, deviceId); + verify(producerProvider).getRuleEngineMsgProducer(); + ArgumentCaptor> protoQueueMsgArgumentCaptor = ArgumentCaptor.forClass(TbProtoQueueMsg.class); + verify(tbREQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), eq(callback)); + TbProtoQueueMsg protoQueueMsgArgumentCaptorValue = protoQueueMsgArgumentCaptor.getValue(); + assertThat(protoQueueMsgArgumentCaptorValue.getKey()).isEqualTo(requestMsg.getId()); + assertThat(protoQueueMsgArgumentCaptorValue.getValue()).isEqualTo(toRuleEngineMsg); + assertThat(protoQueueMsgArgumentCaptorValue.getHeaders().getData()).isEqualTo(new DefaultTbQueueMsgHeaders().getData()); + } + + @Test + public void testPushMsgToRuleEngineUseQueueFromMsgIsFalse() { + TbQueueProducer> tbREQueueProducer = mock(TbQueueProducer.class); + TbQueueCallback callback = mock(TbQueueCallback.class); + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + + TenantId tenantId = TenantId.fromUUID(UUID.fromString("5377c8d0-26e5-4d81-84c6-4344043973c8")); + DeviceId deviceId = new DeviceId(UUID.fromString("016c2abb-f46f-49f9-a83d-4d28b803cfe6")); + DeviceProfile deviceProfile = new DeviceProfile(new DeviceProfileId(UUID.fromString("dc5766e2-1a32-4022-859b-743050097ab7"))); + deviceProfile.setDefaultQueueName(DataConstants.MAIN_QUEUE_NAME); + TbMsg requestMsg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + TransportProtos.ToRuleEngineMsg toRuleEngineMsg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(tbMsg)).build(); + .setTbMsg(TbMsg.toByteString(requestMsg)).build(); + when(deviceProfileCache.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); + when(partitionService.resolve(any(ServiceType.class), any(String.class), any(TenantId.class), any(EntityId.class))).thenReturn(tpi); when(producerProvider.getRuleEngineMsgProducer()).thenReturn(tbREQueueProducer); - when(partitionService.resolve(any(), any(), any(), any())).thenReturn(tpi); - clusterService.pushMsgToRuleEngine(tenantId, tenantId, tbMsg, true, null); + clusterService.pushMsgToRuleEngine(tenantId, deviceId, requestMsg, false, callback); - verify(partitionService).resolve(ServiceType.TB_RULE_ENGINE, "main", tenantId, tenantId); + verify(partitionService).resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, tenantId, deviceId); verify(producerProvider).getRuleEngineMsgProducer(); ArgumentCaptor> protoQueueMsgArgumentCaptor = ArgumentCaptor.forClass(TbProtoQueueMsg.class); - verify(tbREQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), isNull()); + verify(tbREQueueProducer).send(eq(tpi), protoQueueMsgArgumentCaptor.capture(), eq(callback)); TbProtoQueueMsg protoQueueMsgArgumentCaptorValue = protoQueueMsgArgumentCaptor.getValue(); - assertThat(protoQueueMsgArgumentCaptorValue.getValue()).isEqualTo(msg); + assertThat(protoQueueMsgArgumentCaptorValue.getKey()).isEqualTo(requestMsg.getId()); + assertThat(protoQueueMsgArgumentCaptorValue.getValue()).isEqualTo(toRuleEngineMsg); + assertThat(protoQueueMsgArgumentCaptorValue.getHeaders().getData()).isEqualTo(new DefaultTbQueueMsgHeaders().getData()); } protected Queue createTestQueue() { diff --git a/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java index 718648e143..0ed53b9561 100644 --- a/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java @@ -45,7 +45,7 @@ class DefaultTbRuleEngineRpcServiceTest { String serviceId = "tb-core-0"; UUID requestId = UUID.fromString("f64a20df-eb1e-46a3-ba6f-0b3ae053ee0a"); DeviceId deviceId = new DeviceId(UUID.fromString("1d9f771a-7cdc-4ac7-838c-ba193d05a012")); - TbMsg msg = TbMsg.newMsg(null, TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + TbMsg msg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); var restApiCallResponseMsgProto = TransportProtos.RestApiCallResponseMsgProto.newBuilder() .setRequestIdMSB(requestId.getMostSignificantBits()) .setRequestIdLSB(requestId.getLeastSignificantBits()) diff --git a/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java index 8976cd99f3..e5dc414b2f 100644 --- a/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java @@ -25,6 +25,7 @@ import org.springframework.test.util.ReflectionTestUtils; import org.testcontainers.shaded.org.awaitility.Awaitility; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; @@ -62,7 +63,7 @@ public class DefaultRuleEngineCallServiceTest { @BeforeEach void setUp() { - executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("rpc-callback")); + executor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("re-rest-callback")); ruleEngineCallService = new DefaultRuleEngineCallService(tbClusterServiceMock); ReflectionTestUtils.setField(ruleEngineCallService, "executor", executor); ReflectionTestUtils.setField(ruleEngineCallService, "requests", requests); @@ -72,15 +73,7 @@ public class DefaultRuleEngineCallServiceTest { void tearDown() { requests.clear(); if (executor != null) { - executor.shutdown(); - try { - if (!executor.awaitTermination(10L, TimeUnit.SECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } + executor.shutdownNow(); } } @@ -93,7 +86,7 @@ public class DefaultRuleEngineCallServiceTest { metaData.put("serviceId", "core"); metaData.put("requestUUID", requestId.toString()); metaData.put("expirationTime", Long.toString(expTime)); - TbMsg msg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); + TbMsg msg = TbMsg.newMsg(DataConstants.MAIN_QUEUE_NAME, TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); Consumer anyConsumer = TbMsg::getData; ruleEngineCallService.processRestApiCallToRuleEngine(TENANT_ID, requestId, msg, true, anyConsumer); @@ -111,7 +104,7 @@ public class DefaultRuleEngineCallServiceTest { metaData.put("serviceId", "core"); metaData.put("requestUUID", requestId.toString()); metaData.put("expirationTime", Long.toString(expTime)); - TbMsg msg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); + TbMsg msg = TbMsg.newMsg(DataConstants.MAIN_QUEUE_NAME, TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); Consumer anyConsumer = TbMsg::getData; doAnswer(invocation -> { @@ -139,7 +132,7 @@ public class DefaultRuleEngineCallServiceTest { metaData.put("serviceId", "core"); metaData.put("requestUUID", requestId.toString()); metaData.put("expirationTime", Long.toString(expTime)); - TbMsg msg = TbMsg.newMsg("main", TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); + TbMsg msg = TbMsg.newMsg(DataConstants.MAIN_QUEUE_NAME, TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); Consumer anyConsumer = TbMsg::getData; doAnswer(invocation -> { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java index c37d4b28a0..c603e111e2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNodeTest.java @@ -47,7 +47,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class TbSendRestApiCallReplyNodeTest { - private static final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("212445ad-9852-4bfd-819d-6b01ab6ee6b6")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("212445ad-9852-4bfd-819d-6b01ab6ee6b6")); private TbSendRestApiCallReplyNode node; private TbSendRestApiCallReplyNodeConfiguration config; From 0e035acfbd1118b9db7daadf88c941aabd780b9a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 16 May 2024 16:11:27 +0300 Subject: [PATCH 020/136] used assertThat() from another lib --- .../controller/RuleEngineControllerTest.java | 14 +++++--------- .../service/queue/DefaultTbClusterServiceTest.java | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java index e05a44de08..9c9322dc38 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleEngineControllerTest.java @@ -39,15 +39,14 @@ import java.util.UUID; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -185,7 +184,7 @@ public class RuleEngineControllerTest extends AbstractControllerTest { .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString("Invalid request body"))); - verifyNoMoreInteractions(ruleEngineCallService); + verifyNoInteractions(ruleEngineCallService); } @Test @@ -225,8 +224,7 @@ public class RuleEngineControllerTest extends AbstractControllerTest { .andExpect(status().isForbidden()) .andExpect(content().string("You don't have permission to perform this operation!")); - verify(ruleEngineCallService, never()) - .processRestApiCallToRuleEngine(any(TenantId.class), any(UUID.class), any(TbMsg.class), anyBoolean(), any(Consumer.class)); + verifyNoInteractions(ruleEngineCallService); } @Test @@ -246,8 +244,6 @@ public class RuleEngineControllerTest extends AbstractControllerTest { public void checkMetadataProperties(TbMsgMetaData metaData) { Map data = metaData.getData(); - assertThat(data.containsKey("serviceId")).isTrue(); - assertThat(data.containsKey("requestUUID")).isTrue(); - assertThat(data.containsKey("expirationTime")).isTrue(); + assertThat(data).containsKeys("serviceId", "requestUUID", "expirationTime"); } } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 604fe5fe86..3eb1c5806a 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -305,7 +305,7 @@ public class DefaultTbClusterServiceTest { @Test public void testPushMsgToRuleEngineWithTenantIdIsNullUuidAndEntityIsDevice() { TenantId tenantId = TenantId.SYS_TENANT_ID; - DeviceId deviceId = new DeviceId(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.fromString("aa6d112d-2914-4a22-a9e3-bee33edbdb14")); TbMsg requestMsg = TbMsg.newMsg(TbMsgType.REST_API_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); TbQueueCallback callback = mock(TbQueueCallback.class); From 32028e1beec3f057ef3d63ae39d1e32d68e80555 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 17 May 2024 17:15:27 +0300 Subject: [PATCH 021/136] invoked methods to get values from record fields --- .../controller/RuleEngineController.java | 4 ++-- .../DefaultRuleEngineCallServiceTest.java | 20 +------------------ 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java index 16458e42db..8cda3d2212 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java @@ -193,7 +193,7 @@ public class RuleEngineController extends BaseController { } private void reply(LocalRequestMetaData rpcRequest, TbMsg response) { - DeferredResult responseWriter = rpcRequest.responseWriter; + DeferredResult responseWriter = rpcRequest.responseWriter(); if (response == null) { logRuleEngineCall(rpcRequest, null, new TimeoutException("Processing timeout detected!")); responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT)); @@ -216,7 +216,7 @@ public class RuleEngineController extends BaseController { } private void logRuleEngineCall(LocalRequestMetaData rpcRequest, TbMsg response, Throwable e) { - logRuleEngineCall(rpcRequest.user, rpcRequest.request.getOriginator(), rpcRequest.request.getData(), response, e); + logRuleEngineCall(rpcRequest.user(), rpcRequest.request().getOriginator(), rpcRequest.request().getData(), response, e); } private void logRuleEngineCall(SecurityUser user, EntityId entityId, String request, TbMsg response, Throwable e) { diff --git a/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java index e5dc414b2f..282563574f 100644 --- a/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java @@ -78,25 +78,7 @@ public class DefaultRuleEngineCallServiceTest { } @Test - void givenRequest_whenProcessRestApiCallToRuleEngine_thenPushMsgToRuleEngine() { - long timeout = 100L; - long expTime = System.currentTimeMillis() + timeout; - HashMap metaData = new HashMap<>(); - UUID requestId = UUID.randomUUID(); - metaData.put("serviceId", "core"); - metaData.put("requestUUID", requestId.toString()); - metaData.put("expirationTime", Long.toString(expTime)); - TbMsg msg = TbMsg.newMsg(DataConstants.MAIN_QUEUE_NAME, TbMsgType.REST_API_REQUEST, TENANT_ID, new TbMsgMetaData(metaData), "{\"key\":\"value\"}"); - Consumer anyConsumer = TbMsg::getData; - ruleEngineCallService.processRestApiCallToRuleEngine(TENANT_ID, requestId, msg, true, anyConsumer); - - assertThat(requests.size()).isEqualTo(1); - assertThat(requests.get(requestId)).isEqualTo(anyConsumer); - verify(tbClusterServiceMock).pushMsgToRuleEngine(TENANT_ID, TENANT_ID, msg, true, null); - } - - @Test - void givenSmallTimeout_whenProcessRestApiCallToRuleEngine_thenDoesNotReturnResponse() { + void givenRequest_whenProcessRestApiCallToRuleEngine_thenPushMsgToRuleEngineAndCheckRemovedDueTimeout() { long timeout = 1L; long expTime = System.currentTimeMillis() + timeout; HashMap metaData = new HashMap<>(); From c44ba6307c51cde84ea8f0253eb53f226ee6962d Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 10 Jun 2024 16:02:32 +0200 Subject: [PATCH 022/136] added sequence numver for lates timeseries --- .../latest/InsertLatestTsRepository.java | 2 +- .../sql/SqlLatestInsertTsRepository.java | 285 +++++++++++------- .../main/resources/sql/schema-entities.sql | 3 + .../sql/LatestTimeseriesPerformanceTest.java | 151 ++++++++++ dao/src/test/resources/logback.xml | 1 + .../resources/sql/psql/drop-all-tables.sql | 1 + 6 files changed, 330 insertions(+), 113 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/InsertLatestTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/InsertLatestTsRepository.java index 0aa95fa324..d85b66a258 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/InsertLatestTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/InsertLatestTsRepository.java @@ -21,6 +21,6 @@ import java.util.List; public interface InsertLatestTsRepository { - void saveOrUpdate(List entities); + List saveOrUpdate(List entities); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java index 530ac7ce23..48b54c2281 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java @@ -15,23 +15,28 @@ */ package org.thingsboard.server.dao.sqlts.insert.latest.sql; +import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; -import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.List; +import java.util.Map; @SqlTsLatestAnyDao @@ -44,129 +49,185 @@ public class SqlLatestInsertTsRepository extends AbstractInsertRepository implem private Boolean updateByLatestTs; private static final String BATCH_UPDATE = - "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json) WHERE entity_id = ? AND key = ?"; + "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), seq_number = nextval('ts_kv_latest_seq') WHERE entity_id = ? AND key = ?"; private static final String INSERT_OR_UPDATE = - "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json)) " + - "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json)"; + "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, seq_number) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), nextval('ts_kv_latest_seq')) " + + "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), seq_number = nextval('ts_kv_latest_seq')"; private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?"; private static final String INSERT_OR_UPDATE_BY_LATEST_TS = INSERT_OR_UPDATE + " WHERE ts_kv_latest.ts <= ?"; + private static final String RETURNING = " RETURNING seq_number"; + + private static final String SEQ_NUMBER = "seq_number"; + + private String batchUpdateQuery; + private String insertOrUpdateQuery; + + @PostConstruct + private void init() { + this.batchUpdateQuery = (updateByLatestTs ? BATCH_UPDATE_BY_LATEST_TS : BATCH_UPDATE) + RETURNING; + this.insertOrUpdateQuery = (updateByLatestTs ? INSERT_OR_UPDATE_BY_LATEST_TS : INSERT_OR_UPDATE) + RETURNING; + } + @Override - public void saveOrUpdate(List entities) { - transactionTemplate.execute(new TransactionCallbackWithoutResult() { + public List saveOrUpdate(List entities) { + return transactionTemplate.execute(status -> { + List seqNumbers = new ArrayList<>(entities.size()); + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + int[] updateResult = onBatchUpdate(entities, keyHolder); + + List> seqNumbersList = keyHolder.getKeyList(); + + int notUpdatedCount = entities.size() - seqNumbersList.size(); + + List toInsertIndexes = new ArrayList<>(notUpdatedCount); + List insertEntities = new ArrayList<>(notUpdatedCount); + int keyHolderIndex = 0; + for (int i = 0; i < updateResult.length; i++) { + if (updateResult[i] == 0) { + insertEntities.add(entities.get(i)); + seqNumbers.add(0L); + toInsertIndexes.add(i); + } else { + seqNumbers.add((Long) seqNumbersList.get(keyHolderIndex).get(SEQ_NUMBER)); + keyHolderIndex++; + } + } + + if (insertEntities.isEmpty()) { + return seqNumbers; + } + + onInsertOrUpdate(insertEntities, keyHolder); + + seqNumbersList = keyHolder.getKeyList(); + + for (int i = 0; i < seqNumbersList.size(); i++) { + seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(SEQ_NUMBER)); + } + + return seqNumbers; + }); + } + + private int[] onBatchUpdate(List entities, KeyHolder keyHolder) { + return jdbcTemplate.batchUpdate(new SimplePreparedStatementCreator(batchUpdateQuery), new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + TsKvLatestEntity tsKvLatestEntity = entities.get(i); + ps.setLong(1, tsKvLatestEntity.getTs()); + + if (tsKvLatestEntity.getBooleanValue() != null) { + ps.setBoolean(2, tsKvLatestEntity.getBooleanValue()); + } else { + ps.setNull(2, Types.BOOLEAN); + } + + ps.setString(3, replaceNullChars(tsKvLatestEntity.getStrValue())); + + if (tsKvLatestEntity.getLongValue() != null) { + ps.setLong(4, tsKvLatestEntity.getLongValue()); + } else { + ps.setNull(4, Types.BIGINT); + } + + if (tsKvLatestEntity.getDoubleValue() != null) { + ps.setDouble(5, tsKvLatestEntity.getDoubleValue()); + } else { + ps.setNull(5, Types.DOUBLE); + } + + ps.setString(6, replaceNullChars(tsKvLatestEntity.getJsonValue())); + + ps.setObject(7, tsKvLatestEntity.getEntityId()); + ps.setInt(8, tsKvLatestEntity.getKey()); + if (updateByLatestTs) { + ps.setLong(9, tsKvLatestEntity.getTs()); + } + } + @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - String batchUpdateQuery = updateByLatestTs ? BATCH_UPDATE_BY_LATEST_TS : BATCH_UPDATE; - String insertOrUpdateQuery = updateByLatestTs ? INSERT_OR_UPDATE_BY_LATEST_TS : INSERT_OR_UPDATE; - - int[] result = jdbcTemplate.batchUpdate(batchUpdateQuery, new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - TsKvLatestEntity tsKvLatestEntity = entities.get(i); - ps.setLong(1, tsKvLatestEntity.getTs()); - - if (tsKvLatestEntity.getBooleanValue() != null) { - ps.setBoolean(2, tsKvLatestEntity.getBooleanValue()); - } else { - ps.setNull(2, Types.BOOLEAN); - } - - ps.setString(3, replaceNullChars(tsKvLatestEntity.getStrValue())); - - if (tsKvLatestEntity.getLongValue() != null) { - ps.setLong(4, tsKvLatestEntity.getLongValue()); - } else { - ps.setNull(4, Types.BIGINT); - } - - if (tsKvLatestEntity.getDoubleValue() != null) { - ps.setDouble(5, tsKvLatestEntity.getDoubleValue()); - } else { - ps.setNull(5, Types.DOUBLE); - } - - ps.setString(6, replaceNullChars(tsKvLatestEntity.getJsonValue())); - - ps.setObject(7, tsKvLatestEntity.getEntityId()); - ps.setInt(8, tsKvLatestEntity.getKey()); - if (updateByLatestTs) { - ps.setLong(9, tsKvLatestEntity.getTs()); - } - } - - @Override - public int getBatchSize() { - return entities.size(); - } - }); - - int updatedCount = 0; - for (int i = 0; i < result.length; i++) { - if (result[i] == 0) { - updatedCount++; - } + public int getBatchSize() { + return entities.size(); + } + }, keyHolder); + } + + private void onInsertOrUpdate(List insertEntities, KeyHolder keyHolder) { + jdbcTemplate.batchUpdate(new SimplePreparedStatementCreator(insertOrUpdateQuery), new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i); + ps.setObject(1, tsKvLatestEntity.getEntityId()); + ps.setInt(2, tsKvLatestEntity.getKey()); + + ps.setLong(3, tsKvLatestEntity.getTs()); + ps.setLong(9, tsKvLatestEntity.getTs()); + if (updateByLatestTs) { + ps.setLong(15, tsKvLatestEntity.getTs()); } - List insertEntities = new ArrayList<>(updatedCount); - for (int i = 0; i < result.length; i++) { - if (result[i] == 0) { - insertEntities.add(entities.get(i)); - } + if (tsKvLatestEntity.getBooleanValue() != null) { + ps.setBoolean(4, tsKvLatestEntity.getBooleanValue()); + ps.setBoolean(10, tsKvLatestEntity.getBooleanValue()); + } else { + ps.setNull(4, Types.BOOLEAN); + ps.setNull(10, Types.BOOLEAN); + } + + ps.setString(5, replaceNullChars(tsKvLatestEntity.getStrValue())); + ps.setString(11, replaceNullChars(tsKvLatestEntity.getStrValue())); + + if (tsKvLatestEntity.getLongValue() != null) { + ps.setLong(6, tsKvLatestEntity.getLongValue()); + ps.setLong(12, tsKvLatestEntity.getLongValue()); + } else { + ps.setNull(6, Types.BIGINT); + ps.setNull(12, Types.BIGINT); } - jdbcTemplate.batchUpdate(insertOrUpdateQuery, new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i); - ps.setObject(1, tsKvLatestEntity.getEntityId()); - ps.setInt(2, tsKvLatestEntity.getKey()); - - ps.setLong(3, tsKvLatestEntity.getTs()); - ps.setLong(9, tsKvLatestEntity.getTs()); - if (updateByLatestTs) { - ps.setLong(15, tsKvLatestEntity.getTs()); - } - - if (tsKvLatestEntity.getBooleanValue() != null) { - ps.setBoolean(4, tsKvLatestEntity.getBooleanValue()); - ps.setBoolean(10, tsKvLatestEntity.getBooleanValue()); - } else { - ps.setNull(4, Types.BOOLEAN); - ps.setNull(10, Types.BOOLEAN); - } - - ps.setString(5, replaceNullChars(tsKvLatestEntity.getStrValue())); - ps.setString(11, replaceNullChars(tsKvLatestEntity.getStrValue())); - - if (tsKvLatestEntity.getLongValue() != null) { - ps.setLong(6, tsKvLatestEntity.getLongValue()); - ps.setLong(12, tsKvLatestEntity.getLongValue()); - } else { - ps.setNull(6, Types.BIGINT); - ps.setNull(12, Types.BIGINT); - } - - if (tsKvLatestEntity.getDoubleValue() != null) { - ps.setDouble(7, tsKvLatestEntity.getDoubleValue()); - ps.setDouble(13, tsKvLatestEntity.getDoubleValue()); - } else { - ps.setNull(7, Types.DOUBLE); - ps.setNull(13, Types.DOUBLE); - } - - ps.setString(8, replaceNullChars(tsKvLatestEntity.getJsonValue())); - ps.setString(14, replaceNullChars(tsKvLatestEntity.getJsonValue())); - } - - @Override - public int getBatchSize() { - return insertEntities.size(); - } - }); + if (tsKvLatestEntity.getDoubleValue() != null) { + ps.setDouble(7, tsKvLatestEntity.getDoubleValue()); + ps.setDouble(13, tsKvLatestEntity.getDoubleValue()); + } else { + ps.setNull(7, Types.DOUBLE); + ps.setNull(13, Types.DOUBLE); + } + + ps.setString(8, replaceNullChars(tsKvLatestEntity.getJsonValue())); + ps.setString(14, replaceNullChars(tsKvLatestEntity.getJsonValue())); } - }); + + @Override + public int getBatchSize() { + return insertEntities.size(); + } + }, keyHolder); + } + + private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider { + + private static final String[] COLUMNS = {SEQ_NUMBER}; + private final String sql; + + + public SimplePreparedStatementCreator(String sql) { + this.sql = sql; + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + return con.prepareStatement(sql, COLUMNS); + } + + @Override + public String getSql() { + return this.sql; + } } } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index f2978b7f4a..b92c27574c 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -538,6 +538,8 @@ CREATE TABLE IF NOT EXISTS entity_view ( CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id) ); +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_seq cache 1000; + CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, @@ -548,6 +550,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, + seq_number bigint, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java new file mode 100644 index 0000000000..00f458ca52 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java @@ -0,0 +1,151 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.timeseries.sql; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@DaoSqlTest +public class LatestTimeseriesPerformanceTest extends AbstractServiceTest { + + private static final String STRING_KEY = "stringKey"; + private static final String LONG_KEY = "longKey"; + private static final String DOUBLE_KEY = "doubleKey"; + private static final String BOOLEAN_KEY = "booleanKey"; + public static final int AMOUNT_OF_UNIQ_KEY = 10000; + + private final Random random = new Random(); + + @Autowired + private TimeseriesLatestDao timeseriesLatestDao; + + private ListeningExecutorService testExecutor; + + private EntityId entityId; + + private AtomicLong saveCounter; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + entityId = new DeviceId(UUID.randomUUID()); + saveCounter = new AtomicLong(0); + testExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(200, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + if (testExecutor != null) { + testExecutor.shutdownNow(); + } + } + + @Test + public void test_save_latest_timeseries() throws Exception { + warmup(); + saveCounter.set(0); + + long startTime = System.currentTimeMillis(); + List> futures = new ArrayList<>(); + for (int i = 0; i < 25_000; i++) { + futures.add(save(generateStrEntry(getRandomKey()))); + futures.add(save(generateLngEntry(getRandomKey()))); + futures.add(save(generateDblEntry(getRandomKey()))); + futures.add(save(generateBoolEntry(getRandomKey()))); + } + Futures.allAsList(futures).get(60, TimeUnit.SECONDS); + long endTime = System.currentTimeMillis(); + + long totalTime = endTime - startTime; + + System.out.println("Total time: " + totalTime); + System.out.println("Saved count: " + saveCounter.get()); + System.out.println("Saved per 1 sec: " + saveCounter.get() * 1000 / totalTime); + } + + private void warmup() throws Exception { + List> futures = new ArrayList<>(); + for (int i = 0; i < AMOUNT_OF_UNIQ_KEY; i++) { + futures.add(save(generateStrEntry(i))); + futures.add(save(generateLngEntry(i))); + futures.add(save(generateDblEntry(i))); + futures.add(save(generateBoolEntry(i))); + } + Futures.allAsList(futures).get(60, TimeUnit.SECONDS); + } + + private ListenableFuture save(TsKvEntry tsKvEntry) { + return Futures.transformAsync(testExecutor.submit(() -> timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)), result -> { + saveCounter.incrementAndGet(); + return result; + }, testExecutor); + } + + private TsKvEntry generateStrEntry(int keyIndex) { + return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(STRING_KEY + keyIndex, RandomStringUtils.random(10))); + } + + private TsKvEntry generateLngEntry(int keyIndex) { + return new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(LONG_KEY + keyIndex, random.nextLong())); + } + + private TsKvEntry generateDblEntry(int keyIndex) { + return new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(DOUBLE_KEY + keyIndex, random.nextDouble())); + } + + private TsKvEntry generateBoolEntry(int keyIndex) { + return new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(BOOLEAN_KEY + keyIndex, random.nextBoolean())); + } + + private int getRandomKey() { + return random.nextInt(AMOUNT_OF_UNIQ_KEY); + } + +} diff --git a/dao/src/test/resources/logback.xml b/dao/src/test/resources/logback.xml index 5e293b2982..4f7a2142df 100644 --- a/dao/src/test/resources/logback.xml +++ b/dao/src/test/resources/logback.xml @@ -12,6 +12,7 @@ + diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 9c772df45b..3b1b37242f 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -37,6 +37,7 @@ DROP TABLE IF EXISTS tenant; DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv_latest; DROP TABLE IF EXISTS ts_kv_dictionary; +DROP SEQUENCE IF EXISTS ts_kv_latest_seq; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widgets_bundle_widget; DROP TABLE IF EXISTS widget_type; From b666f9499ae0d7d41817edce15ff3fd7993dfe3f Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 11 Jun 2024 15:41:33 +0200 Subject: [PATCH 023/136] added sequence number for attributes --- .../server/controller/DeviceController.java | 4 +- .../DefaultTelemetrySubscriptionService.java | 2 +- .../dao/timeseries/TimeseriesService.java | 2 +- .../common/data/util/CollectionsUtil.java | 8 + .../dao/AbstractSequenceInsertRepository.java | 128 ++++++++++ .../server/dao/sql/TbSqlBlockingQueue.java | 47 ++-- .../dao/sql/TbSqlBlockingQueueParams.java | 1 + .../dao/sql/TbSqlBlockingQueueWrapper.java | 15 +- .../server/dao/sql/TbSqlQueue.java | 8 +- .../server/dao/sql/TbSqlQueueElement.java | 6 +- .../AttributeKvInsertRepository.java | 226 +++++++----------- .../dao/sql/attributes/JpaAttributeDao.java | 5 +- .../SqlAttributesInsertRepository.java | 27 --- .../dao/sql/edge/JpaBaseEdgeEventDao.java | 2 +- .../server/dao/sql/event/JpaBaseEventDao.java | 2 +- ...stractChunkedAggregationTimeseriesDao.java | 2 +- .../dao/sqlts/SqlTimeseriesLatestDao.java | 36 +-- .../sql/SqlLatestInsertTsRepository.java | 224 ++++++----------- .../timescale/TimescaleTimeseriesDao.java | 8 +- .../dao/timeseries/BaseTimeseriesService.java | 4 +- .../CassandraBaseTimeseriesLatestDao.java | 2 +- .../dao/timeseries/TimeseriesLatestDao.java | 2 +- .../main/resources/sql/schema-entities.sql | 3 + 23 files changed, 382 insertions(+), 382 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/AbstractSequenceInsertRepository.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index fedbd48503..8544f60ca0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -30,6 +31,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -488,7 +490,7 @@ public class DeviceController extends BaseController { @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) @ResponseBody public List getDevicesByIds( - @Parameter(description = "A list of devices ids, separated by comma ','") + @Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("deviceIds", strDeviceIds); SecurityUser user = getCurrentUser(); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index b19fde2983..3aace70533 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -280,7 +280,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback) { - ListenableFuture> saveFuture = tsService.saveLatest(tenantId, entityId, ts); + ListenableFuture> saveFuture = tsService.saveLatest(tenantId, entityId, ts); addVoidCallback(saveFuture, callback); addWsCallback(saveFuture, success -> onTimeSeriesUpdate(tenantId, entityId, ts)); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index ffea217da7..eaba144042 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -50,7 +50,7 @@ public interface TimeseriesService { ListenableFuture saveWithoutLatest(TenantId tenantId, EntityId entityId, List tsKvEntry, long ttl); - ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntry); + ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntry); ListenableFuture> remove(TenantId tenantId, EntityId entityId, List queries); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java index 4c9006e829..e89f3c1ad8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -37,6 +38,13 @@ public class CollectionsUtil { return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet()); } + /** + * Returns new list with elements that are present in list B(new) but absent in list A(old). + */ + public static List diffLists(List a, List b) { + return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toList()); + } + public static boolean contains(Collection collection, T element) { return isNotEmpty(collection) && collection.contains(element); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractSequenceInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractSequenceInsertRepository.java new file mode 100644 index 0000000000..0808f5341f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractSequenceInsertRepository.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public abstract class AbstractSequenceInsertRepository extends AbstractInsertRepository { + + public static final String SEQ_NUMBER = "seq_number"; + + public List saveOrUpdate(List entities) { + return transactionTemplate.execute(status -> { + List seqNumbers = new ArrayList<>(entities.size()); + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + int[] updateResult = onBatchUpdate(entities, keyHolder); + + List> seqNumbersList = keyHolder.getKeyList(); + + int notUpdatedCount = entities.size() - seqNumbersList.size(); + + List toInsertIndexes = new ArrayList<>(notUpdatedCount); + List insertEntities = new ArrayList<>(notUpdatedCount); + int keyHolderIndex = 0; + for (int i = 0; i < updateResult.length; i++) { + if (updateResult[i] == 0) { + insertEntities.add(entities.get(i)); + seqNumbers.add(0L); + toInsertIndexes.add(i); + } else { + seqNumbers.add((Long) seqNumbersList.get(keyHolderIndex).get(SEQ_NUMBER)); + keyHolderIndex++; + } + } + + if (insertEntities.isEmpty()) { + return seqNumbers; + } + + onInsertOrUpdate(insertEntities, keyHolder); + + seqNumbersList = keyHolder.getKeyList(); + + for (int i = 0; i < seqNumbersList.size(); i++) { + seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(SEQ_NUMBER)); + } + + return seqNumbers; + }); + } + + private int[] onBatchUpdate(List entities, KeyHolder keyHolder) { + return jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getBatchUpdateQuery()), new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + setOnBatchUpdateValues(ps, i, entities); + } + + @Override + public int getBatchSize() { + return entities.size(); + } + }, keyHolder); + } + + private void onInsertOrUpdate(List insertEntities, KeyHolder keyHolder) { + jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getInsertOrUpdateQuery()), new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + setOnInsertOrUpdateValues(ps, i, insertEntities); + } + + @Override + public int getBatchSize() { + return insertEntities.size(); + } + }, keyHolder); + } + + protected abstract void setOnBatchUpdateValues(PreparedStatement ps, int i, List entities) throws SQLException; + + protected abstract void setOnInsertOrUpdateValues(PreparedStatement ps, int i, List entities) throws SQLException; + + protected abstract String getBatchUpdateQuery(); + + protected abstract String getInsertOrUpdateQuery(); + + private record SequencePreparedStatementCreator(String sql) implements PreparedStatementCreator, SqlProvider { + + private static final String[] COLUMNS = {SEQ_NUMBER}; + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + return con.prepareStatement(sql, COLUMNS); + } + + @Override + public String getSql() { + return this.sql; + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java index 8f580811a1..68221ad0ae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.stats.MessagesStats; import java.util.ArrayList; @@ -29,14 +30,13 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; @Slf4j -public class TbSqlBlockingQueue implements TbSqlQueue { +public class TbSqlBlockingQueue implements TbSqlQueue { - private final BlockingQueue> queue = new LinkedBlockingQueue<>(); + private final BlockingQueue> queue = new LinkedBlockingQueue<>(); private final TbSqlBlockingQueueParams params; private ExecutorService executor; @@ -48,17 +48,17 @@ public class TbSqlBlockingQueue implements TbSqlQueue { } @Override - public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int index) { + public void init(ScheduledLogExecutorComponent logExecutor, Function, List> saveFunction, Comparator batchUpdateComparator, Function>, List>> filter, int index) { executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase())); executor.submit(() -> { String logName = params.getLogName(); int batchSize = params.getBatchSize(); long maxDelay = params.getMaxDelay(); - final List> entities = new ArrayList<>(batchSize); + final List> entities = new ArrayList<>(batchSize); while (!Thread.interrupted()) { try { long currentTs = System.currentTimeMillis(); - TbSqlQueueElement attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS); + TbSqlQueueElement attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS); if (attr == null) { continue; } else { @@ -70,12 +70,27 @@ public class TbSqlBlockingQueue implements TbSqlQueue { log.debug("[{}] Going to save {} entities", logName, entities.size()); log.trace("[{}] Going to save entities: {}", logName, entities); } - Stream entitiesStream = entities.stream().map(TbSqlQueueElement::getEntity); - saveFunction.accept( - (params.isBatchSortEnabled() ? entitiesStream.sorted(batchUpdateComparator) : entitiesStream) - .collect(Collectors.toList()) - ); - entities.forEach(v -> v.getFuture().set(null)); + + List> entitiesToSave = filter.apply(entities); + + if (params.isBatchSortEnabled()) { + entitiesToSave = entitiesToSave.stream().sorted((o1, o2) -> batchUpdateComparator.compare(o1.getEntity(), o2.getEntity())).toList(); + } + + List result = saveFunction.apply(entitiesToSave.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList())); + + if (params.isWithResponse()) { + for (int i = 0; i < entitiesToSave.size(); i++) { + entitiesToSave.get(i).getFuture().set(result.get(i)); + } + + if (entities.size() > entitiesToSave.size()) { + CollectionsUtil.diffLists(entitiesToSave, entities).forEach(v -> v.getFuture().set(null)); + } + } else { + entities.forEach(v -> v.getFuture().set(null)); + } + stats.incrementSuccessful(entities.size()); if (!fullPack) { long remainingDelay = maxDelay - (System.currentTimeMillis() - currentTs); @@ -104,7 +119,7 @@ public class TbSqlBlockingQueue implements TbSqlQueue { }); logExecutor.scheduleAtFixedRate(() -> { - if (queue.size() > 0 || stats.getTotal() > 0 || stats.getSuccessful() > 0 || stats.getFailed() > 0) { + if (!queue.isEmpty() || stats.getTotal() > 0 || stats.getSuccessful() > 0 || stats.getFailed() > 0) { log.info("Queue-{} [{}] queueSize [{}] totalAdded [{}] totalSaved [{}] totalFailed [{}]", index, params.getLogName(), queue.size(), stats.getTotal(), stats.getSuccessful(), stats.getFailed()); stats.reset(); @@ -120,8 +135,8 @@ public class TbSqlBlockingQueue implements TbSqlQueue { } @Override - public ListenableFuture add(E element) { - SettableFuture future = SettableFuture.create(); + public ListenableFuture add(E element) { + SettableFuture future = SettableFuture.create(); queue.add(new TbSqlQueueElement<>(future, element)); stats.incrementTotal(); return future; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java index 56b7ad3af7..42ab6be049 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java @@ -30,4 +30,5 @@ public class TbSqlBlockingQueueParams { private final long statsPrintIntervalMs; private final String statsNamePrefix; private final boolean batchSortEnabled; + private final boolean withResponse; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java index 9c5bbbc855..3e9620de76 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java @@ -29,10 +29,9 @@ import java.util.function.Function; @Slf4j @Data -public class TbSqlBlockingQueueWrapper { - private final CopyOnWriteArrayList> queues = new CopyOnWriteArrayList<>(); +public class TbSqlBlockingQueueWrapper { + private final CopyOnWriteArrayList> queues = new CopyOnWriteArrayList<>(); private final TbSqlBlockingQueueParams params; - private ScheduledLogExecutorComponent logExecutor; private final Function hashCodeFunction; private final int maxThreads; private final StatsFactory statsFactory; @@ -46,15 +45,19 @@ public class TbSqlBlockingQueueWrapper { * NOTE: you must use all of primary key parts in your comparator */ public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator) { + init(logExecutor, l -> { saveFunction.accept(l); return null; }, batchUpdateComparator, l -> l); + } + + public void init(ScheduledLogExecutorComponent logExecutor, Function, List> saveFunction, Comparator batchUpdateComparator, Function>, List>> filter) { for (int i = 0; i < maxThreads; i++) { MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i); - TbSqlBlockingQueue queue = new TbSqlBlockingQueue<>(params, stats); + TbSqlBlockingQueue queue = new TbSqlBlockingQueue<>(params, stats); queues.add(queue); - queue.init(logExecutor, saveFunction, batchUpdateComparator, i); + queue.init(logExecutor, saveFunction, batchUpdateComparator, filter, i); } } - public ListenableFuture add(E element) { + public ListenableFuture add(E element) { int queueIndex = element != null ? (hashCodeFunction.apply(element) & 0x7FFFFFFF) % maxThreads : 0; return queues.get(queueIndex).add(element); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java index 90b4e0fe67..e1ed8c299c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java @@ -19,13 +19,13 @@ import com.google.common.util.concurrent.ListenableFuture; import java.util.Comparator; import java.util.List; -import java.util.function.Consumer; +import java.util.function.Function; -public interface TbSqlQueue { +public interface TbSqlQueue { - void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int queueIndex); + void init(ScheduledLogExecutorComponent logExecutor, Function, List> saveFunction, Comparator batchUpdateComparator, Function>, List>> filter, int queueIndex); void destroy(); - ListenableFuture add(E element); + ListenableFuture add(E element); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java index 15031be244..016c5c9527 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java @@ -20,13 +20,13 @@ import lombok.Getter; import lombok.ToString; @ToString(exclude = "future") -public final class TbSqlQueueElement { +public final class TbSqlQueueElement { @Getter - private final SettableFuture future; + private final SettableFuture future; @Getter private final E entity; - public TbSqlQueueElement(SettableFuture future, E entity) { + public TbSqlQueueElement(SettableFuture future, E entity) { this.future = future; this.entity = entity; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java index 856b2be381..9e188d39b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java @@ -15,162 +15,110 @@ */ package org.thingsboard.server.dao.sql.attributes; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.jdbc.core.BatchPreparedStatementSetter; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.AbstractSequenceInsertRepository; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.util.SqlDao; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; -import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; @Repository -@Slf4j +@Transactional @SqlDao -public abstract class AttributeKvInsertRepository { +public class AttributeKvInsertRepository extends AbstractSequenceInsertRepository { - private static final ThreadLocal PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE))); - private static final String EMPTY_STR = ""; - - private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ? " + - "WHERE entity_id = ? and attribute_type =? and attribute_key = ?;"; + private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, seq_number = nextval('attribute_kv_latest_seq') " + + "WHERE entity_id = ? and attribute_type =? and attribute_key = ? RETURNING seq_number;"; private static final String INSERT_OR_UPDATE = - "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts) " + - "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?) " + + "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts, seq_number) " + + "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?, nextval('attribute_kv_latest_seq')) " + "ON CONFLICT (entity_id, attribute_type, attribute_key) " + - "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?;"; - - @Autowired - protected JdbcTemplate jdbcTemplate; - - @Autowired - private TransactionTemplate transactionTemplate; - - @Value("${sql.remove_null_chars:true}") - private boolean removeNullChars; - - public void saveOrUpdate(List entities) { - transactionTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - AttributeKvEntity kvEntity = entities.get(i); - ps.setString(1, replaceNullChars(kvEntity.getStrValue())); - - if (kvEntity.getLongValue() != null) { - ps.setLong(2, kvEntity.getLongValue()); - } else { - ps.setNull(2, Types.BIGINT); - } - - if (kvEntity.getDoubleValue() != null) { - ps.setDouble(3, kvEntity.getDoubleValue()); - } else { - ps.setNull(3, Types.DOUBLE); - } - - if (kvEntity.getBooleanValue() != null) { - ps.setBoolean(4, kvEntity.getBooleanValue()); - } else { - ps.setNull(4, Types.BOOLEAN); - } - - ps.setString(5, replaceNullChars(kvEntity.getJsonValue())); - - ps.setLong(6, kvEntity.getLastUpdateTs()); - ps.setObject(7, kvEntity.getId().getEntityId()); - ps.setInt(8, kvEntity.getId().getAttributeType()); - ps.setInt(9, kvEntity.getId().getAttributeKey()); - } - - @Override - public int getBatchSize() { - return entities.size(); - } - }); - - int updatedCount = 0; - for (int i = 0; i < result.length; i++) { - if (result[i] == 0) { - updatedCount++; - } - } - - List insertEntities = new ArrayList<>(updatedCount); - for (int i = 0; i < result.length; i++) { - if (result[i] == 0) { - insertEntities.add(entities.get(i)); - } - } - - jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - AttributeKvEntity kvEntity = insertEntities.get(i); - ps.setObject(1, kvEntity.getId().getEntityId()); - ps.setInt(2, kvEntity.getId().getAttributeType()); - ps.setInt(3, kvEntity.getId().getAttributeKey()); - - ps.setString(4, replaceNullChars(kvEntity.getStrValue())); - ps.setString(10, replaceNullChars(kvEntity.getStrValue())); - - if (kvEntity.getLongValue() != null) { - ps.setLong(5, kvEntity.getLongValue()); - ps.setLong(11, kvEntity.getLongValue()); - } else { - ps.setNull(5, Types.BIGINT); - ps.setNull(11, Types.BIGINT); - } - - if (kvEntity.getDoubleValue() != null) { - ps.setDouble(6, kvEntity.getDoubleValue()); - ps.setDouble(12, kvEntity.getDoubleValue()); - } else { - ps.setNull(6, Types.DOUBLE); - ps.setNull(12, Types.DOUBLE); - } - - if (kvEntity.getBooleanValue() != null) { - ps.setBoolean(7, kvEntity.getBooleanValue()); - ps.setBoolean(13, kvEntity.getBooleanValue()); - } else { - ps.setNull(7, Types.BOOLEAN); - ps.setNull(13, Types.BOOLEAN); - } - - ps.setString(8, replaceNullChars(kvEntity.getJsonValue())); - ps.setString(14, replaceNullChars(kvEntity.getJsonValue())); - - ps.setLong(9, kvEntity.getLastUpdateTs()); - ps.setLong(15, kvEntity.getLastUpdateTs()); - } - - @Override - public int getBatchSize() { - return insertEntities.size(); - } - }); - } - }); + "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, seq_number = nextval('attribute_kv_latest_seq') RETURNING seq_number;"; + + @Override + protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List entities) throws SQLException { + AttributeKvEntity kvEntity = entities.get(i); + ps.setString(1, replaceNullChars(kvEntity.getStrValue())); + + if (kvEntity.getLongValue() != null) { + ps.setLong(2, kvEntity.getLongValue()); + } else { + ps.setNull(2, Types.BIGINT); + } + + if (kvEntity.getDoubleValue() != null) { + ps.setDouble(3, kvEntity.getDoubleValue()); + } else { + ps.setNull(3, Types.DOUBLE); + } + + if (kvEntity.getBooleanValue() != null) { + ps.setBoolean(4, kvEntity.getBooleanValue()); + } else { + ps.setNull(4, Types.BOOLEAN); + } + + ps.setString(5, replaceNullChars(kvEntity.getJsonValue())); + + ps.setLong(6, kvEntity.getLastUpdateTs()); + ps.setObject(7, kvEntity.getId().getEntityId()); + ps.setInt(8, kvEntity.getId().getAttributeType()); + ps.setInt(9, kvEntity.getId().getAttributeKey()); } - private String replaceNullChars(String strValue) { - if (removeNullChars && strValue != null) { - return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR); + @Override + protected void setOnInsertOrUpdateValues(PreparedStatement ps, int i, List insertEntities) throws SQLException { + AttributeKvEntity kvEntity = insertEntities.get(i); + ps.setObject(1, kvEntity.getId().getEntityId()); + ps.setInt(2, kvEntity.getId().getAttributeType()); + ps.setInt(3, kvEntity.getId().getAttributeKey()); + + ps.setString(4, replaceNullChars(kvEntity.getStrValue())); + ps.setString(10, replaceNullChars(kvEntity.getStrValue())); + + if (kvEntity.getLongValue() != null) { + ps.setLong(5, kvEntity.getLongValue()); + ps.setLong(11, kvEntity.getLongValue()); + } else { + ps.setNull(5, Types.BIGINT); + ps.setNull(11, Types.BIGINT); } - return strValue; + + if (kvEntity.getDoubleValue() != null) { + ps.setDouble(6, kvEntity.getDoubleValue()); + ps.setDouble(12, kvEntity.getDoubleValue()); + } else { + ps.setNull(6, Types.DOUBLE); + ps.setNull(12, Types.DOUBLE); + } + + if (kvEntity.getBooleanValue() != null) { + ps.setBoolean(7, kvEntity.getBooleanValue()); + ps.setBoolean(13, kvEntity.getBooleanValue()); + } else { + ps.setNull(7, Types.BOOLEAN); + ps.setNull(13, Types.BOOLEAN); + } + + ps.setString(8, replaceNullChars(kvEntity.getJsonValue())); + ps.setString(14, replaceNullChars(kvEntity.getJsonValue())); + + ps.setLong(9, kvEntity.getLastUpdateTs()); + ps.setLong(15, kvEntity.getLastUpdateTs()); + } + + @Override + protected String getBatchUpdateQuery() { + return BATCH_UPDATE; + } + + @Override + protected String getInsertOrUpdateQuery() { + return INSERT_OR_UPDATE; } } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 320e0e67fb..d8b4a9fedf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -88,7 +88,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Value("${sql.batch_sort:true}") private boolean batchSortEnabled; - private TbSqlBlockingQueueWrapper queue; + private TbSqlBlockingQueueWrapper queue; @PostConstruct private void init() { @@ -99,6 +99,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl .statsPrintIntervalMs(statsPrintIntervalMs) .statsNamePrefix("attributes") .batchSortEnabled(batchSortEnabled) + .withResponse(true) .build(); Function hashcodeFunction = entity -> entity.getId().getEntityId().hashCode(); @@ -106,7 +107,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v), Comparator.comparing((AttributeKvEntity attributeKvEntity) -> attributeKvEntity.getId().getEntityId()) .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeType()) - .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey()), l -> l ); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java deleted file mode 100644 index a99f5a24e7..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.sql.attributes; - -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.dao.util.SqlDao; - -@Repository -@Transactional -@SqlDao -public class SqlAttributesInsertRepository extends AttributeKvInsertRepository { - -} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index 1c64973d94..d9d2282fca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -90,7 +90,7 @@ public class JpaBaseEdgeEventDao extends JpaPartitionedAbstractDao queue; + private TbSqlBlockingQueueWrapper queue; @Override protected Class getEntityClass() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index afd6608774..33e7567664 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -110,7 +110,7 @@ public class JpaBaseEventDao implements EventDao { @Value("${sql.batch_sort:true}") private boolean batchSortEnabled; - private TbSqlBlockingQueueWrapper queue; + private TbSqlBlockingQueueWrapper queue; private final Map> repositories = new ConcurrentHashMap<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 9a52f3a192..f3b7b96c87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -60,7 +60,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq @Autowired protected InsertTsRepository insertRepository; - protected TbSqlBlockingQueueWrapper tsQueue; + protected TbSqlBlockingQueueWrapper tsQueue; @Autowired private StatsFactory statsFactory; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index 425bb10a0e..513414b8ac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -46,6 +46,7 @@ import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; +import org.thingsboard.server.dao.sql.TbSqlQueueElement; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; import org.thingsboard.server.dao.sqlts.latest.TsKvLatestRepository; @@ -81,7 +82,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @Autowired private InsertLatestTsRepository insertLatestTsRepository; - private TbSqlBlockingQueueWrapper tsLatestQueue; + private TbSqlBlockingQueueWrapper tsLatestQueue; @Value("${sql.ts_latest.batch_size:1000}") private int tsLatestBatchSize; @@ -115,25 +116,26 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme .maxDelay(tsLatestMaxDelay) .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) .statsNamePrefix("ts.latest") - .batchSortEnabled(false) + .batchSortEnabled(batchSortEnabled) + .withResponse(true) .build(); java.util.function.Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); tsLatestQueue = new TbSqlBlockingQueueWrapper<>(tsLatestParams, hashcodeFunction, tsLatestBatchThreads, statsFactory); - tsLatestQueue.init(logExecutor, v -> { - Map trueLatest = new HashMap<>(); - v.forEach(ts -> { - TsKey key = new TsKey(ts.getEntityId(), ts.getKey()); - trueLatest.merge(key, ts, (oldTs, newTs) -> oldTs.getTs() <= newTs.getTs() ? newTs : oldTs); - }); - List latestEntities = new ArrayList<>(trueLatest.values()); - if (batchSortEnabled) { - latestEntities.sort(Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) - .thenComparingInt(AbstractTsKvEntity::getKey)); - } - insertLatestTsRepository.saveOrUpdate(latestEntities); - }, (l, r) -> 0); + tsLatestQueue.init(logExecutor, + v -> insertLatestTsRepository.saveOrUpdate(v), + Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparingInt(AbstractTsKvEntity::getKey), + v -> { + Map> trueLatest = new HashMap<>(); + v.forEach(element -> { + var entity = element.getEntity(); + TsKey key = new TsKey(entity.getEntityId(), entity.getKey()); + trueLatest.merge(key, element, (oldElement, newElement) -> oldElement.getEntity().getTs() <= newElement.getEntity().getTs() ? newElement : oldElement); + }); + return new ArrayList<>(trueLatest.values()); + }); } @PreDestroy @@ -144,7 +146,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme } @Override - public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { return getSaveLatestFuture(entityId, tsKvEntry); } @@ -247,7 +249,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme searchTsKvLatestRepository.findAllByEntityId(entityId.getId())))); } - protected ListenableFuture getSaveLatestFuture(EntityId entityId, TsKvEntry tsKvEntry) { + protected ListenableFuture getSaveLatestFuture(EntityId entityId, TsKvEntry tsKvEntry) { TsKvLatestEntity latestEntity = new TsKvLatestEntity(); latestEntity.setEntityId(entityId.getId()); latestEntity.setTs(tsKvEntry.getTs()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java index 48b54c2281..5b953dfe59 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java @@ -17,33 +17,25 @@ package org.thingsboard.server.dao.sqlts.insert.latest.sql; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; -import org.springframework.jdbc.core.BatchPreparedStatementSetter; -import org.springframework.jdbc.core.PreparedStatementCreator; -import org.springframework.jdbc.core.SqlProvider; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.AbstractSequenceInsertRepository; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; -import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; -import java.util.ArrayList; import java.util.List; -import java.util.Map; @SqlTsLatestAnyDao @Repository @Transactional @SqlDao -public class SqlLatestInsertTsRepository extends AbstractInsertRepository implements InsertLatestTsRepository { +public class SqlLatestInsertTsRepository extends AbstractSequenceInsertRepository implements InsertLatestTsRepository { @Value("${sql.ts_latest.update_by_latest_ts:true}") private Boolean updateByLatestTs; @@ -61,8 +53,6 @@ public class SqlLatestInsertTsRepository extends AbstractInsertRepository implem private static final String RETURNING = " RETURNING seq_number"; - private static final String SEQ_NUMBER = "seq_number"; - private String batchUpdateQuery; private String insertOrUpdateQuery; @@ -73,161 +63,89 @@ public class SqlLatestInsertTsRepository extends AbstractInsertRepository implem } @Override - public List saveOrUpdate(List entities) { - return transactionTemplate.execute(status -> { - List seqNumbers = new ArrayList<>(entities.size()); + protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List entities) throws SQLException { + TsKvLatestEntity tsKvLatestEntity = entities.get(i); + ps.setLong(1, tsKvLatestEntity.getTs()); + + if (tsKvLatestEntity.getBooleanValue() != null) { + ps.setBoolean(2, tsKvLatestEntity.getBooleanValue()); + } else { + ps.setNull(2, Types.BOOLEAN); + } - KeyHolder keyHolder = new GeneratedKeyHolder(); + ps.setString(3, replaceNullChars(tsKvLatestEntity.getStrValue())); - int[] updateResult = onBatchUpdate(entities, keyHolder); + if (tsKvLatestEntity.getLongValue() != null) { + ps.setLong(4, tsKvLatestEntity.getLongValue()); + } else { + ps.setNull(4, Types.BIGINT); + } - List> seqNumbersList = keyHolder.getKeyList(); + if (tsKvLatestEntity.getDoubleValue() != null) { + ps.setDouble(5, tsKvLatestEntity.getDoubleValue()); + } else { + ps.setNull(5, Types.DOUBLE); + } - int notUpdatedCount = entities.size() - seqNumbersList.size(); + ps.setString(6, replaceNullChars(tsKvLatestEntity.getJsonValue())); - List toInsertIndexes = new ArrayList<>(notUpdatedCount); - List insertEntities = new ArrayList<>(notUpdatedCount); - int keyHolderIndex = 0; - for (int i = 0; i < updateResult.length; i++) { - if (updateResult[i] == 0) { - insertEntities.add(entities.get(i)); - seqNumbers.add(0L); - toInsertIndexes.add(i); - } else { - seqNumbers.add((Long) seqNumbersList.get(keyHolderIndex).get(SEQ_NUMBER)); - keyHolderIndex++; - } - } + ps.setObject(7, tsKvLatestEntity.getEntityId()); + ps.setInt(8, tsKvLatestEntity.getKey()); + if (updateByLatestTs) { + ps.setLong(9, tsKvLatestEntity.getTs()); + } + } - if (insertEntities.isEmpty()) { - return seqNumbers; - } + @Override + protected void setOnInsertOrUpdateValues(PreparedStatement ps, int i, List insertEntities) throws SQLException { + TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i); + ps.setObject(1, tsKvLatestEntity.getEntityId()); + ps.setInt(2, tsKvLatestEntity.getKey()); + + ps.setLong(3, tsKvLatestEntity.getTs()); + ps.setLong(9, tsKvLatestEntity.getTs()); + if (updateByLatestTs) { + ps.setLong(15, tsKvLatestEntity.getTs()); + } - onInsertOrUpdate(insertEntities, keyHolder); + if (tsKvLatestEntity.getBooleanValue() != null) { + ps.setBoolean(4, tsKvLatestEntity.getBooleanValue()); + ps.setBoolean(10, tsKvLatestEntity.getBooleanValue()); + } else { + ps.setNull(4, Types.BOOLEAN); + ps.setNull(10, Types.BOOLEAN); + } - seqNumbersList = keyHolder.getKeyList(); + ps.setString(5, replaceNullChars(tsKvLatestEntity.getStrValue())); + ps.setString(11, replaceNullChars(tsKvLatestEntity.getStrValue())); - for (int i = 0; i < seqNumbersList.size(); i++) { - seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(SEQ_NUMBER)); - } + if (tsKvLatestEntity.getLongValue() != null) { + ps.setLong(6, tsKvLatestEntity.getLongValue()); + ps.setLong(12, tsKvLatestEntity.getLongValue()); + } else { + ps.setNull(6, Types.BIGINT); + ps.setNull(12, Types.BIGINT); + } - return seqNumbers; - }); - } + if (tsKvLatestEntity.getDoubleValue() != null) { + ps.setDouble(7, tsKvLatestEntity.getDoubleValue()); + ps.setDouble(13, tsKvLatestEntity.getDoubleValue()); + } else { + ps.setNull(7, Types.DOUBLE); + ps.setNull(13, Types.DOUBLE); + } - private int[] onBatchUpdate(List entities, KeyHolder keyHolder) { - return jdbcTemplate.batchUpdate(new SimplePreparedStatementCreator(batchUpdateQuery), new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - TsKvLatestEntity tsKvLatestEntity = entities.get(i); - ps.setLong(1, tsKvLatestEntity.getTs()); - - if (tsKvLatestEntity.getBooleanValue() != null) { - ps.setBoolean(2, tsKvLatestEntity.getBooleanValue()); - } else { - ps.setNull(2, Types.BOOLEAN); - } - - ps.setString(3, replaceNullChars(tsKvLatestEntity.getStrValue())); - - if (tsKvLatestEntity.getLongValue() != null) { - ps.setLong(4, tsKvLatestEntity.getLongValue()); - } else { - ps.setNull(4, Types.BIGINT); - } - - if (tsKvLatestEntity.getDoubleValue() != null) { - ps.setDouble(5, tsKvLatestEntity.getDoubleValue()); - } else { - ps.setNull(5, Types.DOUBLE); - } - - ps.setString(6, replaceNullChars(tsKvLatestEntity.getJsonValue())); - - ps.setObject(7, tsKvLatestEntity.getEntityId()); - ps.setInt(8, tsKvLatestEntity.getKey()); - if (updateByLatestTs) { - ps.setLong(9, tsKvLatestEntity.getTs()); - } - } - - @Override - public int getBatchSize() { - return entities.size(); - } - }, keyHolder); + ps.setString(8, replaceNullChars(tsKvLatestEntity.getJsonValue())); + ps.setString(14, replaceNullChars(tsKvLatestEntity.getJsonValue())); } - private void onInsertOrUpdate(List insertEntities, KeyHolder keyHolder) { - jdbcTemplate.batchUpdate(new SimplePreparedStatementCreator(insertOrUpdateQuery), new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - TsKvLatestEntity tsKvLatestEntity = insertEntities.get(i); - ps.setObject(1, tsKvLatestEntity.getEntityId()); - ps.setInt(2, tsKvLatestEntity.getKey()); - - ps.setLong(3, tsKvLatestEntity.getTs()); - ps.setLong(9, tsKvLatestEntity.getTs()); - if (updateByLatestTs) { - ps.setLong(15, tsKvLatestEntity.getTs()); - } - - if (tsKvLatestEntity.getBooleanValue() != null) { - ps.setBoolean(4, tsKvLatestEntity.getBooleanValue()); - ps.setBoolean(10, tsKvLatestEntity.getBooleanValue()); - } else { - ps.setNull(4, Types.BOOLEAN); - ps.setNull(10, Types.BOOLEAN); - } - - ps.setString(5, replaceNullChars(tsKvLatestEntity.getStrValue())); - ps.setString(11, replaceNullChars(tsKvLatestEntity.getStrValue())); - - if (tsKvLatestEntity.getLongValue() != null) { - ps.setLong(6, tsKvLatestEntity.getLongValue()); - ps.setLong(12, tsKvLatestEntity.getLongValue()); - } else { - ps.setNull(6, Types.BIGINT); - ps.setNull(12, Types.BIGINT); - } - - if (tsKvLatestEntity.getDoubleValue() != null) { - ps.setDouble(7, tsKvLatestEntity.getDoubleValue()); - ps.setDouble(13, tsKvLatestEntity.getDoubleValue()); - } else { - ps.setNull(7, Types.DOUBLE); - ps.setNull(13, Types.DOUBLE); - } - - ps.setString(8, replaceNullChars(tsKvLatestEntity.getJsonValue())); - ps.setString(14, replaceNullChars(tsKvLatestEntity.getJsonValue())); - } - - @Override - public int getBatchSize() { - return insertEntities.size(); - } - }, keyHolder); + @Override + protected String getBatchUpdateQuery() { + return batchUpdateQuery; } - private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider { - - private static final String[] COLUMNS = {SEQ_NUMBER}; - private final String sql; - - - public SimplePreparedStatementCreator(String sql) { - this.sql = sql; - } - - @Override - public PreparedStatement createPreparedStatement(Connection con) throws SQLException { - return con.prepareStatement(sql, COLUMNS); - } - - @Override - public String getSql() { - return this.sql; - } + @Override + protected String getInsertOrUpdateQuery() { + return insertOrUpdateQuery; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java index c8bff8ceb9..cd9f397bb3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -18,8 +18,9 @@ package org.thingsboard.server.dao.sqlts.timescale; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -38,7 +39,6 @@ import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; -import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao; @@ -47,8 +47,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; import org.thingsboard.server.dao.util.TimeUtils; import org.thingsboard.server.dao.util.TimescaleDBTsDao; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -77,7 +75,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements @Autowired protected KeyDictionaryDao keyDictionaryDao; - protected TbSqlBlockingQueueWrapper tsQueue; + protected TbSqlBlockingQueueWrapper tsQueue; @PostConstruct protected void init() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 60056b2b8f..f9c4218d64 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -187,8 +187,8 @@ public class BaseTimeseriesService implements TimeseriesService { } @Override - public ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntries) { - List> futures = new ArrayList<>(tsKvEntries.size()); + public ListenableFuture> saveLatest(TenantId tenantId, EntityId entityId, List tsKvEntries) { + List> futures = new ArrayList<>(tsKvEntries.size()); for (TsKvEntry tsKvEntry : tsKvEntries) { futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 7a5904eb6b..f774f18dc7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -100,7 +100,7 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes } @Override - public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind()); stmtBuilder.setString(0, entityId.getEntityType().name()) .setUuid(1, entityId.getId()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index d339f49e11..9f62fd033a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -42,7 +42,7 @@ public interface TimeseriesLatestDao { ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); - ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); + ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry); ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index b92c27574c..a26262970c 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -102,6 +102,8 @@ CREATE TABLE IF NOT EXISTS audit_log ( action_failure_details varchar(1000000) ) PARTITION BY RANGE (created_time); +CREATE SEQUENCE IF NOT EXISTS attribute_kv_latest_seq cache 1000; + CREATE TABLE IF NOT EXISTS attribute_kv ( entity_id uuid, attribute_type int, @@ -112,6 +114,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv ( dbl_v double precision, json_v json, last_update_ts bigint, + seq_number bigint, CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) ); From 4807a7cbdd9156da4f9912fd51f30e8d40bc8fca Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 11 Jun 2024 17:15:55 +0200 Subject: [PATCH 024/136] ranamed seq_number to version and refactoring --- .../server/controller/DeviceController.java | 4 +--- ...java => AbstractVersionedInsertRepository.java} | 10 +++++----- .../attributes/AttributeKvInsertRepository.java | 14 +++++++------- .../latest/sql/SqlLatestInsertTsRepository.java | 12 ++++++------ dao/src/main/resources/sql/schema-entities.sql | 8 ++++---- dao/src/main/resources/sql/schema-timescale.sql | 3 +++ .../main/resources/sql/schema-ts-latest-psql.sql | 10 +++------- .../test/resources/sql/psql/drop-all-tables.sql | 3 ++- 8 files changed, 31 insertions(+), 33 deletions(-) rename dao/src/main/java/org/thingsboard/server/dao/{AbstractSequenceInsertRepository.java => AbstractVersionedInsertRepository.java} (93%) diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 8544f60ca0..fedbd48503 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -31,7 +30,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -490,7 +488,7 @@ public class DeviceController extends BaseController { @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) @ResponseBody public List getDevicesByIds( - @Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @Parameter(description = "A list of devices ids, separated by comma ','") @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException { checkArrayParameter("deviceIds", strDeviceIds); SecurityUser user = getCurrentUser(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractSequenceInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java similarity index 93% rename from dao/src/main/java/org/thingsboard/server/dao/AbstractSequenceInsertRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java index 0808f5341f..cb92ca5e1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/AbstractSequenceInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java @@ -29,9 +29,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -public abstract class AbstractSequenceInsertRepository extends AbstractInsertRepository { +public abstract class AbstractVersionedInsertRepository extends AbstractInsertRepository { - public static final String SEQ_NUMBER = "seq_number"; + public static final String VERSION_COLUMN = "version"; public List saveOrUpdate(List entities) { return transactionTemplate.execute(status -> { @@ -54,7 +54,7 @@ public abstract class AbstractSequenceInsertRepository extends AbstractInsert seqNumbers.add(0L); toInsertIndexes.add(i); } else { - seqNumbers.add((Long) seqNumbersList.get(keyHolderIndex).get(SEQ_NUMBER)); + seqNumbers.add((Long) seqNumbersList.get(keyHolderIndex).get(VERSION_COLUMN)); keyHolderIndex++; } } @@ -68,7 +68,7 @@ public abstract class AbstractSequenceInsertRepository extends AbstractInsert seqNumbersList = keyHolder.getKeyList(); for (int i = 0; i < seqNumbersList.size(); i++) { - seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(SEQ_NUMBER)); + seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(VERSION_COLUMN)); } return seqNumbers; @@ -113,7 +113,7 @@ public abstract class AbstractSequenceInsertRepository extends AbstractInsert private record SequencePreparedStatementCreator(String sql) implements PreparedStatementCreator, SqlProvider { - private static final String[] COLUMNS = {SEQ_NUMBER}; + private static final String[] COLUMNS = {VERSION_COLUMN}; @Override public PreparedStatement createPreparedStatement(Connection con) throws SQLException { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java index 9e188d39b1..586ee3c0ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.sql.attributes; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.dao.AbstractSequenceInsertRepository; +import org.thingsboard.server.dao.AbstractVersionedInsertRepository; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.util.SqlDao; @@ -29,16 +29,16 @@ import java.util.List; @Repository @Transactional @SqlDao -public class AttributeKvInsertRepository extends AbstractSequenceInsertRepository { +public class AttributeKvInsertRepository extends AbstractVersionedInsertRepository { - private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, seq_number = nextval('attribute_kv_latest_seq') " + - "WHERE entity_id = ? and attribute_type =? and attribute_key = ? RETURNING seq_number;"; + private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') " + + "WHERE entity_id = ? and attribute_type =? and attribute_key = ? RETURNING version;"; private static final String INSERT_OR_UPDATE = - "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts, seq_number) " + - "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?, nextval('attribute_kv_latest_seq')) " + + "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts, version) " + + "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?, nextval('attribute_kv_version_seq')) " + "ON CONFLICT (entity_id, attribute_type, attribute_key) " + - "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, seq_number = nextval('attribute_kv_latest_seq') RETURNING seq_number;"; + "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') RETURNING version;"; @Override protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List entities) throws SQLException { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java index 5b953dfe59..323eec0851 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java @@ -19,7 +19,7 @@ import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.dao.AbstractSequenceInsertRepository; +import org.thingsboard.server.dao.AbstractVersionedInsertRepository; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.util.SqlDao; @@ -35,23 +35,23 @@ import java.util.List; @Repository @Transactional @SqlDao -public class SqlLatestInsertTsRepository extends AbstractSequenceInsertRepository implements InsertLatestTsRepository { +public class SqlLatestInsertTsRepository extends AbstractVersionedInsertRepository implements InsertLatestTsRepository { @Value("${sql.ts_latest.update_by_latest_ts:true}") private Boolean updateByLatestTs; private static final String BATCH_UPDATE = - "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), seq_number = nextval('ts_kv_latest_seq') WHERE entity_id = ? AND key = ?"; + "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq') WHERE entity_id = ? AND key = ?"; private static final String INSERT_OR_UPDATE = - "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, seq_number) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), nextval('ts_kv_latest_seq')) " + - "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), seq_number = nextval('ts_kv_latest_seq')"; + "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, version) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), nextval('ts_kv_latest_version_seq')) " + + "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq')"; private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?"; private static final String INSERT_OR_UPDATE_BY_LATEST_TS = INSERT_OR_UPDATE + " WHERE ts_kv_latest.ts <= ?"; - private static final String RETURNING = " RETURNING seq_number"; + private static final String RETURNING = " RETURNING version"; private String batchUpdateQuery; private String insertOrUpdateQuery; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index a26262970c..45deaf931d 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -102,7 +102,7 @@ CREATE TABLE IF NOT EXISTS audit_log ( action_failure_details varchar(1000000) ) PARTITION BY RANGE (created_time); -CREATE SEQUENCE IF NOT EXISTS attribute_kv_latest_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1000; CREATE TABLE IF NOT EXISTS attribute_kv ( entity_id uuid, @@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv ( dbl_v double precision, json_v json, last_update_ts bigint, - seq_number bigint, + version bigint, CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) ); @@ -541,7 +541,7 @@ CREATE TABLE IF NOT EXISTS entity_view ( CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id) ); -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; CREATE TABLE IF NOT EXISTS ts_kv_latest ( @@ -553,7 +553,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, - seq_number bigint, + version bigint, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-timescale.sql b/dao/src/main/resources/sql/schema-timescale.sql index ecd8aa1e9d..e5d44cb24a 100644 --- a/dao/src/main/resources/sql/schema-timescale.sql +++ b/dao/src/main/resources/sql/schema-timescale.sql @@ -34,6 +34,8 @@ CREATE TABLE IF NOT EXISTS key_dictionary ( CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key) ); +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; + CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, key int NOT NULL, @@ -43,6 +45,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest ( long_v bigint, dbl_v double precision, json_v json, + version bigint, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-ts-latest-psql.sql b/dao/src/main/resources/sql/schema-ts-latest-psql.sql index c6701315b4..adae100823 100644 --- a/dao/src/main/resources/sql/schema-ts-latest-psql.sql +++ b/dao/src/main/resources/sql/schema-ts-latest-psql.sql @@ -14,6 +14,8 @@ -- limitations under the License. -- +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; + CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, @@ -24,12 +26,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, + version bigint, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) -); - -CREATE TABLE IF NOT EXISTS ts_kv_dictionary -( - key varchar(255) NOT NULL, - key_id serial UNIQUE, - CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); \ No newline at end of file diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 3b1b37242f..c731e239af 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -23,6 +23,7 @@ DROP TABLE IF EXISTS alarm_type; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; +DROP SEQUENCE IF EXISTS attribute_kv_version_seq; DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; DROP TABLE IF EXISTS device; @@ -37,7 +38,7 @@ DROP TABLE IF EXISTS tenant; DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv_latest; DROP TABLE IF EXISTS ts_kv_dictionary; -DROP SEQUENCE IF EXISTS ts_kv_latest_seq; +DROP SEQUENCE IF EXISTS ts_kv_latest_version_seq; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widgets_bundle_widget; DROP TABLE IF EXISTS widget_type; From e2be5c6492c2baab95200764da93ac595d206324 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 12 Jun 2024 12:23:19 +0200 Subject: [PATCH 025/136] return version instead of key for attributes --- .../device/DeviceProvisionServiceImpl.java | 2 +- .../service/edge/rpc/EdgeGrpcSession.java | 6 +-- .../DefaultSystemDataLoaderService.java | 2 +- .../DefaultTelemetrySubscriptionService.java | 4 +- .../dao/attributes/AttributesService.java | 22 ++------ .../server/dao/attributes/AttributesDao.java | 2 +- .../dao/attributes/BaseAttributesService.java | 42 ++-------------- .../attributes/CachedAttributesService.java | 50 +++++-------------- .../dao/sql/attributes/JpaAttributeDao.java | 10 ++-- .../server/dao/service/EntityServiceTest.java | 16 +++--- .../sql/LatestTimeseriesPerformanceTest.java | 8 +-- 11 files changed, 45 insertions(+), 119 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index bedf9c7537..f5b6411bf7 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -240,7 +240,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); } - private ListenableFuture> saveProvisionStateAttribute(Device device) { + private ListenableFuture> saveProvisionStateAttribute(Device device) { return attributesService.save(device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE, Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), System.currentTimeMillis()))); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 55e8e051ab..81f90fee51 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -371,10 +371,10 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onSuccess(@Nullable Pair newStartTsAndSeqId) { if (newStartTsAndSeqId != null) { - ListenableFuture> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); + ListenableFuture> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId); Futures.addCallback(updateFuture, new FutureCallback<>() { @Override - public void onSuccess(@Nullable List list) { + public void onSuccess(@Nullable List list) { log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId); if (fetcher.isSeqIdNewCycleStarted()) { seqIdEnd = fetcher.getSeqIdEnd(); @@ -626,7 +626,7 @@ public final class EdgeGrpcSession implements Closeable { return startSeqId; } - private ListenableFuture> updateQueueStartTsAndSeqId(Pair pair) { + private ListenableFuture> updateQueueStartTsAndSeqId(Pair pair) { this.newStartTs = pair.getFirst(); this.newStartSeqId = pair.getSecond(); log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 7588726a73..e49a42b57b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -577,7 +577,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); } else { - ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE, + ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE, Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value) , System.currentTimeMillis()))); addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value)); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 3aace70533..70962e821a 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -260,14 +260,14 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @Override public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback) { - ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); + ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); addVoidCallback(saveFuture, callback); addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice)); } @Override public void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes, boolean notifyDevice, FutureCallback callback) { - ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); + ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes); addVoidCallback(saveFuture, callback); addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope.name(), attributes, notifyDevice)); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index d869471fa3..a205ee9b84 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.attributes; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -32,30 +31,18 @@ import java.util.Optional; */ public interface AttributesService { - @Deprecated(since = "3.7.0") - ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey); - ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey); - @Deprecated(since = "3.7.0") - ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, Collection attributeKeys); - ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, Collection attributeKeys); - @Deprecated(since = "3.7.0") - ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope); - ListenableFuture> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope); @Deprecated(since = "3.7.0") - ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes); - - ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes); + ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes); - @Deprecated(since = "3.7.0") - ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute); + ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes); - ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute); + ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute); @Deprecated(since = "3.7.0") ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys); @@ -64,9 +51,6 @@ public interface AttributesService { List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); - @Deprecated(since = "3.7.0") - List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds); - List findAllKeysByEntityIds(TenantId tenantId, List entityIds); List findAllKeysByEntityIds(TenantId tenantId, List entityIds, String scope); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index 64ca5d9dd9..34f67fb5ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -38,7 +38,7 @@ public interface AttributesDao { List findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope); - ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute); + ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute); List> removeAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 1b1f2d005d..6f49b2a94e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -56,13 +56,6 @@ public class BaseAttributesService implements AttributesService { this.attributesDao = attributesDao; } - @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { - validate(entityId, scope); - Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k); - return Futures.immediateFuture(attributesDao.find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKey)); - } - @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) { validate(entityId, scope); @@ -70,13 +63,6 @@ public class BaseAttributesService implements AttributesService { return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKey)); } - @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, Collection attributeKeys) { - validate(entityId, scope); - attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k)); - return Futures.immediateFuture(attributesDao.find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKeys)); - } - @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, Collection attributeKeys) { validate(entityId, scope); @@ -84,12 +70,6 @@ public class BaseAttributesService implements AttributesService { return Futures.immediateFuture(attributesDao.find(tenantId, entityId, scope, attributeKeys)); } - @Override - public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { - validate(entityId, scope); - return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, AttributeScope.valueOf(scope))); - } - @Override public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope) { validate(entityId, scope); @@ -101,11 +81,6 @@ public class BaseAttributesService implements AttributesService { return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId); } - @Override - public List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds) { - return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); - } - @Override public List findAllKeysByEntityIds(TenantId tenantId, List entityIds) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); @@ -121,32 +96,25 @@ public class BaseAttributesService implements AttributesService { } @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { - validate(entityId, scope); - AttributeUtils.validate(attribute, valueNoXssValidation); - return attributesDao.save(tenantId, entityId, AttributeScope.valueOf(scope), attribute); - } - - @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); AttributeUtils.validate(attribute, valueNoXssValidation); return attributesDao.save(tenantId, entityId, scope, attribute); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { + public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); AttributeUtils.validate(attributes, valueNoXssValidation); - List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, AttributeScope.valueOf(scope), attribute)).collect(Collectors.toList()); + List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, AttributeScope.valueOf(scope), attribute)).collect(Collectors.toList()); return Futures.allAsList(saveFutures); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { + public ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { validate(entityId, scope); AttributeUtils.validate(attributes, valueNoXssValidation); - List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); + List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); return Futures.allAsList(saveFutures); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index f51bc6f0b8..375b56b1f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -29,7 +29,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -109,12 +108,6 @@ public class CachedAttributesService implements AttributesService { return cacheExecutorService.executor(); } - - @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { - return find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKey); - } - @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey) { validate(entityId, scope); @@ -144,16 +137,11 @@ public class CachedAttributesService implements AttributesService { }); } - @Override - public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, final Collection attributeKeysNonUnique) { - return find(tenantId, entityId, AttributeScope.valueOf(scope), attributeKeysNonUnique); - } - @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, final Collection attributeKeysNonUnique) { validate(entityId, scope); final var attributeKeys = new LinkedHashSet<>(attributeKeysNonUnique); // deduplicate the attributes - attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k ->"Incorrect attribute key " + k)); + attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, k -> "Incorrect attribute key " + k)); //CacheExecutor for Redis or DirectExecutor for local Caffeine return Futures.transformAsync(cacheExecutor.submit(() -> findCachedAttributes(entityId, scope, attributeKeys)), @@ -216,11 +204,6 @@ public class CachedAttributesService implements AttributesService { return cachedAttributes; } - @Override - public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { - return findAll(tenantId, entityId, AttributeScope.valueOf(scope)); - } - @Override public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, AttributeScope scope) { validate(entityId, scope); @@ -233,11 +216,6 @@ public class CachedAttributesService implements AttributesService { return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId); } - @Override - public List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds) { - return findAllKeysByEntityIds(tenantId, entityIds); - } - @Override public List findAllKeysByEntityIds(TenantId tenantId, List entityIds) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); @@ -253,42 +231,38 @@ public class CachedAttributesService implements AttributesService { } @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { - return save(tenantId, entityId, AttributeScope.valueOf(scope), attribute); - } - - @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); AttributeUtils.validate(attribute, valueNoXssValidation); - ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - return Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor); + ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); + return Futures.transform(future, version -> evict(entityId, scope, attribute, version), cacheExecutor); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { + public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { return save(tenantId, entityId, AttributeScope.valueOf(scope), attributes); } @Override - public ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { + public ListenableFuture> save(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributes) { validate(entityId, scope); AttributeUtils.validate(attributes, valueNoXssValidation); - List> futures = new ArrayList<>(attributes.size()); + List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { - ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - futures.add(Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor)); + ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); + futures.add(Futures.transform(future, version -> evict(entityId, scope, attribute, version), cacheExecutor)); } return Futures.allAsList(futures); } - private String evict(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, String key) { + private Long evict(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, Long version) { + String key = attribute.getKey(); log.trace("[{}][{}][{}] Before cache evict: {}", entityId, scope, key, attribute); cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute); log.trace("[{}][{}][{}] after cache evict.", entityId, scope, key); - return key; + return version; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index d8b4a9fedf..05cdfce221 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -16,9 +16,7 @@ package org.thingsboard.server.dao.sql.attributes; import com.google.common.collect.Lists; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; @@ -179,7 +177,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl } @Override - public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute) { + public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute) { AttributeKvEntity entity = new AttributeKvEntity(); entity.setId(new AttributeKvCompositeKey(entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(attribute.getKey()))); entity.setLastUpdateTs(attribute.getLastUpdateTs()); @@ -188,11 +186,11 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl entity.setLongValue(attribute.getLongValue().orElse(null)); entity.setBooleanValue(attribute.getBooleanValue().orElse(null)); entity.setJsonValue(attribute.getJsonValue().orElse(null)); - return addToQueue(entity, attribute.getKey()); + return addToQueue(entity); } - private ListenableFuture addToQueue(AttributeKvEntity entity, String key) { - return Futures.transform(queue.add(entity), v -> key, MoreExecutors.directExecutor()); + private ListenableFuture addToQueue(AttributeKvEntity entity) { + return queue.add(entity); } @Override diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java index 1fc2ff2ce6..6c87b0128f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java @@ -412,7 +412,7 @@ public class EntityServiceTest extends AbstractServiceTest { List highTemperatures = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); @@ -591,7 +591,7 @@ public class EntityServiceTest extends AbstractServiceTest { List highTemperatures = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); @@ -666,7 +666,7 @@ public class EntityServiceTest extends AbstractServiceTest { List highConsumptions = new ArrayList<>(); createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < assets.size(); i++) { Asset asset = assets.get(i); attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), AttributeScope.SERVER_SCOPE)); @@ -1586,7 +1586,7 @@ public class EntityServiceTest extends AbstractServiceTest { } } - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); for (AttributeScope currentScope : AttributeScope.values()) { @@ -1688,7 +1688,7 @@ public class EntityServiceTest extends AbstractServiceTest { } } - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveLongAttribute(device.getId(), "temperature", temperatures.get(i), AttributeScope.CLIENT_SCOPE)); @@ -1966,7 +1966,7 @@ public class EntityServiceTest extends AbstractServiceTest { } } - List>> attributeFutures = new ArrayList<>(); + List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); attributeFutures.add(saveStringAttribute(device.getId(), "attributeString", attributeStrings.get(i), AttributeScope.CLIENT_SCOPE)); @@ -2428,13 +2428,13 @@ public class EntityServiceTest extends AbstractServiceTest { return filter; } - private ListenableFuture> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) { + private ListenableFuture> saveLongAttribute(EntityId entityId, String key, long value, AttributeScope scope) { KvEntry attrValue = new LongDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); } - private ListenableFuture> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) { + private ListenableFuture> saveStringAttribute(EntityId entityId, String key, String value, AttributeScope scope) { KvEntry attrValue = new StringDataEntry(key, value); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); return attributesService.save(SYSTEM_TENANT_ID, entityId, scope, Collections.singletonList(attr)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java index 00f458ca52..d81eea997c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; @@ -48,6 +49,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @DaoSqlTest +@Slf4j public class LatestTimeseriesPerformanceTest extends AbstractServiceTest { private static final String STRING_KEY = "stringKey"; @@ -105,9 +107,9 @@ public class LatestTimeseriesPerformanceTest extends AbstractServiceTest { long totalTime = endTime - startTime; - System.out.println("Total time: " + totalTime); - System.out.println("Saved count: " + saveCounter.get()); - System.out.println("Saved per 1 sec: " + saveCounter.get() * 1000 / totalTime); + log.info("Total time: {}", totalTime); + log.info("Saved count: {}", saveCounter.get()); + log.warn("Saved per 1 sec: {}", saveCounter.get() * 1000 / totalTime); } private void warmup() throws Exception { From e57b9471dcac00a3b30e3f75702061b50c772e66 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 13 Jun 2024 19:18:01 +0200 Subject: [PATCH 026/136] used version + 1 instead of sequence next value --- .../sql/attributes/AttributeKvInsertRepository.java | 10 +++++----- .../insert/latest/sql/SqlLatestInsertTsRepository.java | 7 +++---- dao/src/main/resources/sql/schema-entities.sql | 8 ++------ dao/src/main/resources/sql/schema-timescale.sql | 4 +--- dao/src/main/resources/sql/schema-ts-latest-psql.sql | 4 +--- dao/src/test/resources/sql/psql/drop-all-tables.sql | 2 -- 6 files changed, 12 insertions(+), 23 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java index 586ee3c0ca..c2b9d53d47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java @@ -31,14 +31,14 @@ import java.util.List; @SqlDao public class AttributeKvInsertRepository extends AbstractVersionedInsertRepository { - private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') " + + private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = version + 1 " + "WHERE entity_id = ? and attribute_type =? and attribute_key = ? RETURNING version;"; private static final String INSERT_OR_UPDATE = - "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts, version) " + - "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?, nextval('attribute_kv_version_seq')) " + + "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts) " + + "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?) " + "ON CONFLICT (entity_id, attribute_type, attribute_key) " + - "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') RETURNING version;"; + "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = attribute_kv.version + 1 RETURNING version;"; @Override protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List entities) throws SQLException { @@ -121,4 +121,4 @@ public class AttributeKvInsertRepository extends AbstractVersionedInsertReposito protected String getInsertOrUpdateQuery() { return INSERT_OR_UPDATE; } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java index 323eec0851..0484337a08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java @@ -30,7 +30,6 @@ import java.sql.SQLException; import java.sql.Types; import java.util.List; - @SqlTsLatestAnyDao @Repository @Transactional @@ -41,11 +40,11 @@ public class SqlLatestInsertTsRepository extends AbstractVersionedInsertReposito private Boolean updateByLatestTs; private static final String BATCH_UPDATE = - "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq') WHERE entity_id = ? AND key = ?"; + "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = version + 1 WHERE entity_id = ? AND key = ?"; private static final String INSERT_OR_UPDATE = - "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, version) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), nextval('ts_kv_latest_version_seq')) " + - "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq')"; + "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json)) " + + "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = ts_kv_latest.version + 1"; private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?"; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 45deaf931d..9293636f0b 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -102,8 +102,6 @@ CREATE TABLE IF NOT EXISTS audit_log ( action_failure_details varchar(1000000) ) PARTITION BY RANGE (created_time); -CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1000; - CREATE TABLE IF NOT EXISTS attribute_kv ( entity_id uuid, attribute_type int, @@ -114,7 +112,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv ( dbl_v double precision, json_v json, last_update_ts bigint, - version bigint, + version bigint default 0, CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) ); @@ -541,8 +539,6 @@ CREATE TABLE IF NOT EXISTS entity_view ( CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id) ); -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; - CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, @@ -553,7 +549,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, - version bigint, + version bigint default 0, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-timescale.sql b/dao/src/main/resources/sql/schema-timescale.sql index e5d44cb24a..2d2381403d 100644 --- a/dao/src/main/resources/sql/schema-timescale.sql +++ b/dao/src/main/resources/sql/schema-timescale.sql @@ -34,8 +34,6 @@ CREATE TABLE IF NOT EXISTS key_dictionary ( CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key) ); -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; - CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, key int NOT NULL, @@ -45,7 +43,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest ( long_v bigint, dbl_v double precision, json_v json, - version bigint, + version bigint default 0, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-ts-latest-psql.sql b/dao/src/main/resources/sql/schema-ts-latest-psql.sql index adae100823..058104d4f2 100644 --- a/dao/src/main/resources/sql/schema-ts-latest-psql.sql +++ b/dao/src/main/resources/sql/schema-ts-latest-psql.sql @@ -14,8 +14,6 @@ -- limitations under the License. -- -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; - CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, @@ -26,6 +24,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, - version bigint, + version bigint default 0, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); \ No newline at end of file diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index c731e239af..9c772df45b 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -23,7 +23,6 @@ DROP TABLE IF EXISTS alarm_type; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; -DROP SEQUENCE IF EXISTS attribute_kv_version_seq; DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; DROP TABLE IF EXISTS device; @@ -38,7 +37,6 @@ DROP TABLE IF EXISTS tenant; DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv_latest; DROP TABLE IF EXISTS ts_kv_dictionary; -DROP SEQUENCE IF EXISTS ts_kv_latest_version_seq; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widgets_bundle_widget; DROP TABLE IF EXISTS widget_type; From 37ad8a79b1cafb3b11c5551ce844748aa3341b2c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 19 Jun 2024 13:31:54 +0300 Subject: [PATCH 027/136] Async transport api requests processing --- .../transport/DefaultTransportApiService.java | 127 ++++++++---------- 1 file changed, 55 insertions(+), 72 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index f5bd42e29d..8b29af34ed 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -18,7 +18,6 @@ package org.thingsboard.server.service.transport; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; 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.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; @@ -101,13 +100,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; @@ -136,7 +133,6 @@ public class DefaultTransportApiService implements TransportApiService { private final DeviceProfileService deviceProfileService; private final RelationService relationService; private final DeviceCredentialsService deviceCredentialsService; - private final DbCallbackExecutorService dbCallbackExecutorService; private final TbClusterService tbClusterService; private final DeviceProvisionService deviceProvisionService; private final ResourceService resourceService; @@ -170,52 +166,54 @@ public class DefaultTransportApiService implements TransportApiService { @Override public ListenableFuture> handle(TbProtoQueueMsg tbProtoQueueMsg) { TransportApiRequestMsg transportApiRequestMsg = tbProtoQueueMsg.getValue(); - ListenableFuture result = null; + return handlerExecutor.submit(() -> { + TransportApiResponseMsg result = handle(transportApiRequestMsg); + return new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), result, tbProtoQueueMsg.getHeaders()); + }); + } + private TransportApiResponseMsg handle(TransportApiRequestMsg transportApiRequestMsg) { if (transportApiRequestMsg.hasValidateTokenRequestMsg()) { ValidateDeviceTokenRequestMsg msg = transportApiRequestMsg.getValidateTokenRequestMsg(); final String token = msg.getToken(); - result = handlerExecutor.submit(() -> validateCredentials(token, DeviceCredentialsType.ACCESS_TOKEN)); + return validateCredentials(token, DeviceCredentialsType.ACCESS_TOKEN); } else if (transportApiRequestMsg.hasValidateBasicMqttCredRequestMsg()) { TransportProtos.ValidateBasicMqttCredRequestMsg msg = transportApiRequestMsg.getValidateBasicMqttCredRequestMsg(); - result = handlerExecutor.submit(() -> validateCredentials(msg)); + return validateCredentials(msg); } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); final String hash = msg.getHash(); - result = handlerExecutor.submit(() -> validateCredentials(hash, DeviceCredentialsType.X509_CERTIFICATE)); + return validateCredentials(hash, DeviceCredentialsType.X509_CERTIFICATE); } else if (transportApiRequestMsg.hasValidateOrCreateX509CertRequestMsg()) { TransportProtos.ValidateOrCreateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateOrCreateX509CertRequestMsg(); final String certChain = msg.getCertificateChain(); - result = handlerExecutor.submit(() -> validateOrCreateDeviceX509Certificate(certChain)); + return validateOrCreateDeviceX509Certificate(certChain); } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { - result = handlerExecutor.submit(() -> handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg())); + return handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { - result = handle(transportApiRequestMsg.getEntityProfileRequestMsg()); + return handle(transportApiRequestMsg.getEntityProfileRequestMsg()); } else if (transportApiRequestMsg.hasLwM2MRequestMsg()) { - result = handle(transportApiRequestMsg.getLwM2MRequestMsg()); + return handle(transportApiRequestMsg.getLwM2MRequestMsg()); } else if (transportApiRequestMsg.hasValidateDeviceLwM2MCredentialsRequestMsg()) { ValidateDeviceLwM2MCredentialsRequestMsg msg = transportApiRequestMsg.getValidateDeviceLwM2MCredentialsRequestMsg(); final String credentialsId = msg.getCredentialsId(); - result = handlerExecutor.submit(() -> validateCredentials(credentialsId, DeviceCredentialsType.LWM2M_CREDENTIALS)); + return validateCredentials(credentialsId, DeviceCredentialsType.LWM2M_CREDENTIALS); } else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) { - result = handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()); + return handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()); } else if (transportApiRequestMsg.hasResourceRequestMsg()) { - result = handle(transportApiRequestMsg.getResourceRequestMsg()); + return handle(transportApiRequestMsg.getResourceRequestMsg()); } else if (transportApiRequestMsg.hasSnmpDevicesRequestMsg()) { - result = handle(transportApiRequestMsg.getSnmpDevicesRequestMsg()); + return handle(transportApiRequestMsg.getSnmpDevicesRequestMsg()); } else if (transportApiRequestMsg.hasDeviceRequestMsg()) { - result = handle(transportApiRequestMsg.getDeviceRequestMsg()); + return handle(transportApiRequestMsg.getDeviceRequestMsg()); } else if (transportApiRequestMsg.hasDeviceCredentialsRequestMsg()) { - result = handle(transportApiRequestMsg.getDeviceCredentialsRequestMsg()); + return handle(transportApiRequestMsg.getDeviceCredentialsRequestMsg()); } else if (transportApiRequestMsg.hasOtaPackageRequestMsg()) { - result = handle(transportApiRequestMsg.getOtaPackageRequestMsg()); + return handle(transportApiRequestMsg.getOtaPackageRequestMsg()); } else if (transportApiRequestMsg.hasGetAllQueueRoutingInfoRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetAllQueueRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + return handle(transportApiRequestMsg.getGetAllQueueRoutingInfoRequestMsg()); } - - return Futures.transform(Optional.ofNullable(result).orElseGet(this::getEmptyTransportApiResponseFuture), - value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), - MoreExecutors.directExecutor()); + return getEmptyTransportApiResponse(); } private TransportApiResponseMsg validateCredentials(String credentialsId, DeviceCredentialsType credentialsType) { @@ -405,10 +403,10 @@ public class DefaultTransportApiService implements TransportApiService { } } - private ListenableFuture handle(ProvisionDeviceRequestMsg requestMsg) { - ListenableFuture provisionResponseFuture; + private TransportApiResponseMsg handle(ProvisionDeviceRequestMsg requestMsg) { + ProvisionResponse provisionResponse; try { - provisionResponseFuture = Futures.immediateFuture(deviceProvisionService.provisionDevice( + provisionResponse = deviceProvisionService.provisionDevice( new ProvisionRequest( requestMsg.getDeviceName(), requestMsg.getCredentialsType() != null ? DeviceCredentialsType.valueOf(requestMsg.getCredentialsType().name()) : null, @@ -419,18 +417,14 @@ public class DefaultTransportApiService implements TransportApiService { requestMsg.getCredentialsDataProto().getValidateDeviceX509CertRequestMsg().getHash()), new ProvisionDeviceProfileCredentials( requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(), - requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret())))); + requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()))); } catch (ProvisionFailedException e) { - return Futures.immediateFuture(getTransportApiResponseMsg( - new DeviceCredentials(), - TransportProtos.ResponseStatus.valueOf(e.getMessage()))); + return getTransportApiResponseMsg(new DeviceCredentials(), TransportProtos.ResponseStatus.valueOf(e.getMessage())); } - return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(provisionResponse.getDeviceCredentials(), TransportProtos.ResponseStatus.SUCCESS), - dbCallbackExecutorService); + return getTransportApiResponseMsg(provisionResponse.getDeviceCredentials(), TransportProtos.ResponseStatus.SUCCESS); } - private TransportApiResponseMsg getTransportApiResponseMsg(DeviceCredentials - deviceCredentials, TransportProtos.ResponseStatus status) { + private TransportApiResponseMsg getTransportApiResponseMsg(DeviceCredentials deviceCredentials, TransportProtos.ResponseStatus status) { if (!status.equals(TransportProtos.ResponseStatus.SUCCESS)) { return TransportApiResponseMsg.newBuilder().setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder().setStatus(status).build()).build(); } @@ -453,7 +447,7 @@ public class DefaultTransportApiService implements TransportApiService { .build(); } - private ListenableFuture handle(GetEntityProfileRequestMsg requestMsg) { + private TransportApiResponseMsg handle(GetEntityProfileRequestMsg requestMsg) { EntityType entityType = EntityType.valueOf(requestMsg.getEntityType()); UUID entityUuid = new UUID(requestMsg.getEntityIdMSB(), requestMsg.getEntityIdLSB()); GetEntityProfileResponseMsg.Builder builder = GetEntityProfileResponseMsg.newBuilder(); @@ -470,10 +464,10 @@ public class DefaultTransportApiService implements TransportApiService { } else { throw new RuntimeException("Invalid entity profile request: " + entityType); } - return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setEntityProfileResponseMsg(builder).build()); + return TransportApiResponseMsg.newBuilder().setEntityProfileResponseMsg(builder).build(); } - private ListenableFuture handle(GetDeviceRequestMsg requestMsg) { + private TransportApiResponseMsg handle(GetDeviceRequestMsg requestMsg) { DeviceId deviceId = new DeviceId(new UUID(requestMsg.getDeviceIdMSB(), requestMsg.getDeviceIdLSB())); Device device = deviceService.findDeviceById(TenantId.SYS_TENANT_ID, deviceId); @@ -491,21 +485,20 @@ public class DefaultTransportApiService implements TransportApiService { } else { responseMsg = TransportApiResponseMsg.getDefaultInstance(); } - - return Futures.immediateFuture(responseMsg); + return responseMsg; } - private ListenableFuture handle(GetDeviceCredentialsRequestMsg requestMsg) { + private TransportApiResponseMsg handle(GetDeviceCredentialsRequestMsg requestMsg) { DeviceId deviceId = new DeviceId(new UUID(requestMsg.getDeviceIdMSB(), requestMsg.getDeviceIdLSB())); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(TenantId.SYS_TENANT_ID, deviceId); - return Futures.immediateFuture(TransportApiResponseMsg.newBuilder() + return TransportApiResponseMsg.newBuilder() .setDeviceCredentialsResponseMsg(TransportProtos.GetDeviceCredentialsResponseMsg.newBuilder() .setDeviceCredentialsData(ProtoUtils.toProto(deviceCredentials))) - .build()); + .build(); } - private ListenableFuture handle(GetResourceRequestMsg requestMsg) { + private TransportApiResponseMsg handle(GetResourceRequestMsg requestMsg) { TenantId tenantId = TenantId.fromUUID(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(requestMsg.getResourceType()); String resourceKey = requestMsg.getResourceKey(); @@ -520,10 +513,10 @@ public class DefaultTransportApiService implements TransportApiService { builder.setResource(ProtoUtils.toProto(resource)); } - return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setResourceResponseMsg(builder).build()); + return TransportApiResponseMsg.newBuilder().setResourceResponseMsg(builder).build(); } - private ListenableFuture handle(GetSnmpDevicesRequestMsg requestMsg) { + private TransportApiResponseMsg handle(GetSnmpDevicesRequestMsg requestMsg) { PageLink pageLink = new PageLink(requestMsg.getPageSize(), requestMsg.getPage()); PageData result = deviceService.findDevicesIdsByDeviceProfileTransportType(DeviceTransportType.SNMP, pageLink); @@ -534,9 +527,9 @@ public class DefaultTransportApiService implements TransportApiService { .setHasNextPage(result.hasNext()) .build(); - return Futures.immediateFuture(TransportApiResponseMsg.newBuilder() + return TransportApiResponseMsg.newBuilder() .setSnmpDevicesResponseMsg(responseMsg) - .build()); + .build(); } TransportApiResponseMsg getDeviceInfo(DeviceCredentials credentials) { @@ -565,31 +558,26 @@ public class DefaultTransportApiService implements TransportApiService { } } - private ListenableFuture getEmptyTransportApiResponseFuture() { - return Futures.immediateFuture(getEmptyTransportApiResponse()); - } - private TransportApiResponseMsg getEmptyTransportApiResponse() { return TransportApiResponseMsg.newBuilder() .setValidateCredResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); } - private ListenableFuture handle(TransportProtos.LwM2MRequestMsg requestMsg) { + private TransportApiResponseMsg handle(TransportProtos.LwM2MRequestMsg requestMsg) { if (requestMsg.hasRegistrationMsg()) { return handleRegistration(requestMsg.getRegistrationMsg()); } else { - return Futures.immediateFailedFuture(new RuntimeException("Not supported!")); + throw new RuntimeException("Not supported!"); } } - private ListenableFuture handle(TransportProtos.GetOtaPackageRequestMsg requestMsg) { + private TransportApiResponseMsg handle(TransportProtos.GetOtaPackageRequestMsg requestMsg) { TenantId tenantId = TenantId.fromUUID(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(requestMsg.getDeviceIdMSB(), requestMsg.getDeviceIdLSB())); OtaPackageType otaPackageType = OtaPackageType.valueOf(requestMsg.getType()); Device device = deviceService.findDeviceById(tenantId, deviceId); - if (device == null) { - return getEmptyTransportApiResponseFuture(); + return getEmptyTransportApiResponse(); } OtaPackageId otaPackageId = OtaPackageUtil.getOtaPackageId(device, otaPackageType); @@ -626,14 +614,12 @@ public class DefaultTransportApiService implements TransportApiService { } } - return Futures.immediateFuture( - TransportApiResponseMsg.newBuilder() - .setOtaPackageResponseMsg(builder.build()) - .build()); + return TransportApiResponseMsg.newBuilder() + .setOtaPackageResponseMsg(builder.build()) + .build(); } - private ListenableFuture handleRegistration - (TransportProtos.LwM2MRegistrationRequestMsg msg) { + private TransportApiResponseMsg handleRegistration(TransportProtos.LwM2MRegistrationRequestMsg msg) { TenantId tenantId = TenantId.fromUUID(UUID.fromString(msg.getTenantId())); String deviceName = msg.getEndpoint(); Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(deviceName, id -> new ReentrantLock()); @@ -651,21 +637,18 @@ public class DefaultTransportApiService implements TransportApiService { TransportProtos.LwM2MRegistrationResponseMsg.newBuilder() .setDeviceInfo(ProtoUtils.toDeviceInfoProto(device)).build(); TransportProtos.LwM2MResponseMsg responseMsg = TransportProtos.LwM2MResponseMsg.newBuilder().setRegistrationMsg(registrationResponseMsg).build(); - return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setLwM2MResponseMsg(responseMsg).build()); + return TransportApiResponseMsg.newBuilder().setLwM2MResponseMsg(responseMsg).build(); } catch (JsonProcessingException e) { - log.warn("[{}][{}] Failed to lookup device by gateway id and name", tenantId, deviceName, e); + log.warn("[{}][{}] Failed to lookup device by name", tenantId, deviceName, e); throw new RuntimeException(e); } finally { deviceCreationLock.unlock(); } } - private ListenableFuture handle(TransportProtos.GetAllQueueRoutingInfoRequestMsg requestMsg) { - return queuesToTransportApiResponseMsg(queueService.findAllQueues()); - } - - private ListenableFuture queuesToTransportApiResponseMsg(List queues) { - return Futures.immediateFuture(TransportApiResponseMsg.newBuilder() + private TransportApiResponseMsg handle(TransportProtos.GetAllQueueRoutingInfoRequestMsg requestMsg) { + List queues = queueService.findAllQueues(); + return TransportApiResponseMsg.newBuilder() .addAllGetQueueRoutingInfoResponseMsgs(queues.stream() .map(queue -> TransportProtos.GetQueueRoutingInfoResponseMsg.newBuilder() .setTenantIdMSB(queue.getTenantId().getId().getMostSignificantBits()) @@ -676,7 +659,7 @@ public class DefaultTransportApiService implements TransportApiService { .setQueueTopic(queue.getTopic()) .setPartitions(queue.getPartitions()) .setDuplicateMsgToAllPartitions(queue.isDuplicateMsgToAllPartitions()) - .build()).collect(Collectors.toList())).build()); + .build()).collect(Collectors.toList())).build(); } private ProvisionRequest createProvisionRequest(String certificateValue) { From 118407d982cea7e2cfff7937511560cf96b8736a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 26 Jun 2024 15:51:32 +0200 Subject: [PATCH 028/136] implemented versioned cache --- .../cache/RedisTbTransactionalCache.java | 15 +- ...VersionedCaffeineTbTransactionalCache.java | 91 ++++++++++ .../VersionedRedisTbTransactionalCache.java | 163 ++++++++++++++++++ .../cache/VersionedTbTransactionalCache.java | 33 ++++ .../server/common/data/HasVersion.java | 20 +++ .../common/data/kv/AttributeKvEntry.java | 4 +- .../common/data/kv/BaseAttributeKvEntry.java | 14 ++ .../server/common/data/util/TbPair.java | 7 + .../AbstractVersionedInsertRepository.java | 18 +- .../attributes/AttributeCaffeineCache.java | 4 +- .../dao/attributes/AttributeRedisCache.java | 33 ++-- .../server/dao/attributes/AttributesDao.java | 3 + .../attributes/CachedAttributesService.java | 73 ++++---- .../server/dao/model/BaseVersionedEntity.java | 20 +++ .../server/dao/model/ModelConstants.java | 1 + .../dao/model/sql/AttributeKvEntity.java | 6 +- .../server/dao/model/sql/VersionedEntity.java | 30 ++++ .../AttributeKvInsertRepository.java | 8 +- .../sql/attributes/AttributeKvRepository.java | 12 +- .../dao/sql/attributes/JpaAttributeDao.java | 15 +- .../sql/SqlLatestInsertTsRepository.java | 6 +- .../main/resources/sql/schema-entities.sql | 8 +- .../main/resources/sql/schema-timescale.sql | 4 +- .../resources/sql/schema-ts-latest-psql.sql | 6 +- .../resources/sql/psql/drop-all-tables.sql | 2 + 25 files changed, 492 insertions(+), 104 deletions(-) create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbTransactionalCache.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbTransactionalCache.java create mode 100644 common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbTransactionalCache.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/VersionedEntity.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index abfbb398c9..543739aad4 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -29,7 +29,6 @@ import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.thingsboard.server.common.data.FstStatsService; -import redis.clients.jedis.Connection; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.util.JedisClusterCRC16; @@ -44,7 +43,7 @@ import java.util.concurrent.TimeUnit; @Slf4j public abstract class RedisTbTransactionalCache implements TbTransactionalCache { - private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); + static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE); static final JedisPool MOCK_POOL = new JedisPool(); //non-null pool required for JedisConnection to trigger closing jedis connection @Autowired @@ -79,7 +78,7 @@ public abstract class RedisTbTransactionalCache get(K key) { try (var connection = connectionFactory.getConnection()) { byte[] rawKey = getRawKey(key); - byte[] rawValue = connection.get(rawKey); + byte[] rawValue = doGet(connection, rawKey); if (rawValue == null) { return null; } else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { @@ -96,6 +95,10 @@ public abstract class RedisTbTransactionalCache(this, connection); } - private RedisConnection getConnection(byte[] rawKey) { + protected RedisConnection getConnection(byte[] rawKey) { if (!connectionFactory.isRedisClusterAware()) { return connectionFactory.getConnection(); } @@ -180,7 +183,7 @@ public abstract class RedisTbTransactionalCache implements VersionedTbTransactionalCache { + + private final CacheManager cacheManager; + private final String cacheName; + + private final Lock lock = new ReentrantLock(); + + @Override + public TbCacheValueWrapper get(K key) { + return SimpleTbCacheValueWrapper.wrap(doGet(key).getSecond()); + } + + @Override + public void put(K key, V value) { + Long version = value != null ? value.getVersion() : 0; + put(key, value, version); + } + + @Override + public void put(K key, V value, Long version) { + if (version == null) { + return; + } + lock.lock(); + try { + TbPair versionValuePair = doGet(key); + Long currentVersion = versionValuePair.getFirst(); + if (currentVersion == null || version >= currentVersion) { + cacheManager.getCache(cacheName).put(key, TbPair.of(version, value)); + } + } finally { + lock.unlock(); + } + } + + private TbPair doGet(K key) { + Cache.ValueWrapper source = cacheManager.getCache(cacheName).get(key); + return source == null ? TbPair.emptyPair() : (TbPair) source.get(); + } + + @Override + public void evict(K key) { + lock.lock(); + try { + cacheManager.getCache(cacheName).evict(key); + } finally { + lock.unlock(); + } + } + + @Override + public void evict(K key, Long version) { + if (version == null) { + return; + } + lock.lock(); + try { + put(key, null, version); + } finally { + lock.unlock(); + } + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbTransactionalCache.java new file mode 100644 index 0000000000..34b46d535a --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbTransactionalCache.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.ReturnType; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.thingsboard.server.common.data.HasVersion; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@Slf4j +public abstract class VersionedRedisTbTransactionalCache extends RedisTbTransactionalCache implements VersionedTbTransactionalCache { + + private static final int VERSION_SIZE = 8; + private static final int VALUE_END_OFFSET = -1; + + static final byte[] SET_VERSIONED_VALUE_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize(""" + -- KEYS[1] is the key + -- ARGV[1] is the new value + -- ARGV[2] is the new version + + local key = KEYS[1] + local newValue = ARGV[1] + local newVersion = tonumber(ARGV[2]) + + -- Function to set the new value with the version + local function setNewValue() + local newValueWithVersion = struct.pack(">I8", newVersion) .. newValue:sub(9) + redis.call('SET', key, newValueWithVersion) + end + + -- Get the current version (first 8 bytes) of the current value + local currentVersionBytes = redis.call('GETRANGE', key, 0, 7) + + if currentVersionBytes and #currentVersionBytes == 8 then + -- Extract the current version from the first 8 bytes + local currentVersion = tonumber(struct.unpack(">I8", currentVersionBytes)) + + if newVersion >= currentVersion then + setNewValue() + end + else + -- If the current value is absent or the current version is not found, set the new value + setNewValue() + end + """); + static final byte[] SET_VERSIONED_VALUE_SHA = StringRedisSerializer.UTF_8.serialize("041b109dd56f6c8afb55090076e754727a5d3da0"); + + public VersionedRedisTbTransactionalCache(String cacheName, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory, TBRedisCacheConfiguration configuration, TbRedisSerializer valueSerializer) { + super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer); + } + + @PostConstruct + public void init() { + try (var connection = getConnection(SET_VERSIONED_VALUE_SHA)) { + log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), connection.getNativeConnection()); + String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT); + if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { + log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), sha, connection.getNativeConnection()); + } + } catch (Throwable t) { + log.error("Error on Redis versioned cache init", t); + } + } + + @Override + protected byte[] doGet(RedisConnection connection, byte[] rawKey) { + return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET); + } + + @Override + public void put(K key, V value) { + Long version = value!= null ? value.getVersion() : 0; + put(key, value, version); + } + + @Override + public void put(K key, V value, Long version) { + //TODO: use expiration + log.trace("put [{}][{}][{}]", key, value, version); + if (version == null) { + return; + } + final byte[] rawKey = getRawKey(key); + try (var connection = getConnection(rawKey)) { + byte[] rawValue = getRawValue(value); + byte[] rawVersion = StringRedisSerializer.UTF_8.serialize(String.valueOf(version)); + try { + connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion); + } catch (InvalidDataAccessApiUsageException e) { + log.debug("loading LUA [{}]", connection.getNativeConnection()); + String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT); + if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { + log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(SET_VERSIONED_VALUE_SHA), sha); + } + try { + connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion); + } catch (InvalidDataAccessApiUsageException ignored) { + log.debug("Slowly executing eval instead of fast evalsha"); + connection.scriptingCommands().eval(SET_VERSIONED_VALUE_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion); + } + } + } + } + + @Override + public void evict(K key, Long version) { + log.trace("evict [{}][{}]", key, version); + if (version != null) { + //TODO: use evict expiration + put(key, null, version); + } + } + + @Override + public void putIfAbsent(K key, V value) { + log.trace("putIfAbsent [{}][{}]", key, value); + throw new NotImplementedException("putIfAbsent is not supported by versioned cache"); + } + + @Override + public void evict(Collection keys) { + throw new NotImplementedException("evict by many keys is not supported by versioned cache"); + } + + @Override + public void evictOrPut(K key, V value) { + throw new NotImplementedException("evictOrPut is not supported by versioned cache"); + } + + @Override + public TbCacheTransaction newTransactionForKey(K key) { + throw new NotImplementedException("newTransactionForKey is not supported by versioned cache"); + } + + @Override + public TbCacheTransaction newTransactionForKeys(List keys) { + throw new NotImplementedException("newTransactionForKeys is not supported by versioned cache"); + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbTransactionalCache.java new file mode 100644 index 0000000000..72f4a8c466 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbTransactionalCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache; + +import org.thingsboard.server.common.data.HasVersion; + +import java.io.Serializable; + +public interface VersionedTbTransactionalCache { + + TbCacheValueWrapper get(K key); + + void put(K key, V value); + + void put(K key, V value, Long version); + + void evict(K key); + + void evict(K key, Long version); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java new file mode 100644 index 0000000000..fbeb8ef3b6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasVersion { + long getVersion(); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java index 19057fb1aa..c63c953170 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.kv; +import org.thingsboard.server.common.data.HasVersion; + /** * @author Andrew Shvayka */ -public interface AttributeKvEntry extends KvEntry { +public interface AttributeKvEntry extends KvEntry, HasVersion { long getLastUpdateTs(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java index ced1486c14..6d48693511 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java @@ -29,9 +29,18 @@ public class BaseAttributeKvEntry implements AttributeKvEntry { @Valid private final KvEntry kv; + private final Long version; + public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs) { this.kv = kv; this.lastUpdateTs = lastUpdateTs; + this.version = null; + } + + public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs, Long version) { + this.kv = kv; + this.lastUpdateTs = lastUpdateTs; + this.version = version; } public BaseAttributeKvEntry(long lastUpdateTs, KvEntry kv) { @@ -88,6 +97,11 @@ public class BaseAttributeKvEntry implements AttributeKvEntry { return kv.getValue(); } + @Override + public long getVersion() { + return version; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java index 6e8b2e5696..315131bdad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java @@ -21,10 +21,17 @@ import lombok.Data; @Data @AllArgsConstructor public class TbPair { + public static final TbPair EMPTY = new TbPair<>(null, null); + private S first; private T second; public static TbPair of(S first, T second) { return new TbPair<>(first, second); } + + @SuppressWarnings("unchecked") + public static TbPair emptyPair() { + return (TbPair) EMPTY; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java index cb92ca5e1f..6380957f77 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java @@ -29,9 +29,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -public abstract class AbstractVersionedInsertRepository extends AbstractInsertRepository { +import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; - public static final String VERSION_COLUMN = "version"; +public abstract class AbstractVersionedInsertRepository extends AbstractInsertRepository { public List saveOrUpdate(List entities) { return transactionTemplate.execute(status -> { @@ -51,7 +51,7 @@ public abstract class AbstractVersionedInsertRepository extends AbstractInser for (int i = 0; i < updateResult.length; i++) { if (updateResult[i] == 0) { insertEntities.add(entities.get(i)); - seqNumbers.add(0L); + seqNumbers.add(null); toInsertIndexes.add(i); } else { seqNumbers.add((Long) seqNumbersList.get(keyHolderIndex).get(VERSION_COLUMN)); @@ -63,12 +63,14 @@ public abstract class AbstractVersionedInsertRepository extends AbstractInser return seqNumbers; } - onInsertOrUpdate(insertEntities, keyHolder); + int[] insertResult = onInsertOrUpdate(insertEntities, keyHolder); seqNumbersList = keyHolder.getKeyList(); - for (int i = 0; i < seqNumbersList.size(); i++) { - seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(VERSION_COLUMN)); + for (int i = 0; i < insertResult.length; i++) { + if (updateResult[i] != 0) { + seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(VERSION_COLUMN)); + } } return seqNumbers; @@ -89,8 +91,8 @@ public abstract class AbstractVersionedInsertRepository extends AbstractInser }, keyHolder); } - private void onInsertOrUpdate(List insertEntities, KeyHolder keyHolder) { - jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getInsertOrUpdateQuery()), new BatchPreparedStatementSetter() { + private int[] onInsertOrUpdate(List insertEntities, KeyHolder keyHolder) { + return jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(getInsertOrUpdateQuery()), new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { setOnInsertOrUpdateValues(ps, i, insertEntities); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java index 7df55a33fb..e00ea8d11c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java @@ -18,13 +18,13 @@ package org.thingsboard.server.dao.attributes; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; -import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.cache.VersionedCaffeineTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("AttributeCache") -public class AttributeCaffeineCache extends CaffeineTbTransactionalCache { +public class AttributeCaffeineCache extends VersionedCaffeineTbTransactionalCache { public AttributeCaffeineCache(CacheManager cacheManager) { super(cacheManager, CacheConstants.ATTRIBUTES_CACHE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java index 7932a8b8d4..68caf62ceb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -21,9 +21,9 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; -import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbRedisSerializer; +import org.thingsboard.server.cache.VersionedRedisTbTransactionalCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -38,7 +38,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("AttributeCache") -public class AttributeRedisCache extends RedisTbTransactionalCache { +public class AttributeRedisCache extends VersionedRedisTbTransactionalCache { public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>() { @@ -82,26 +82,15 @@ public class AttributeRedisCache extends RedisTbTransactionalCache new BooleanDataEntry(key.getKey(), hasValue ? proto.getBoolV() : null); + case LONG_V -> new LongDataEntry(key.getKey(), hasValue ? proto.getLongV() : null); + case DOUBLE_V -> new DoubleDataEntry(key.getKey(), hasValue ? proto.getDoubleV() : null); + case STRING_V -> new StringDataEntry(key.getKey(), hasValue ? proto.getStringV() : null); + case JSON_V -> new JsonDataEntry(key.getKey(), hasValue ? proto.getJsonV() : null); + default -> + throw new InvalidProtocolBufferException("Unrecognized type: " + proto.getType() + " !"); + }; return new BaseAttributeKvEntry(proto.getLastUpdateTs(), entry); } catch (InvalidProtocolBufferException e) { throw new SerializationException(e.getMessage()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index 34f67fb5ff..1805f72a8f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.util.TbPair; import java.util.Collection; import java.util.List; @@ -42,6 +43,8 @@ public interface AttributesDao { List> removeAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys); + List>> removeAllWithVersions(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys); + List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); List findAllKeysByEntityIds(TenantId tenantId, List entityIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 375b56b1f6..fb5c745a11 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -27,13 +27,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbCacheValueWrapper; -import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.cache.VersionedTbTransactionalCache; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.cache.CacheExecutorService; @@ -67,7 +68,7 @@ public class CachedAttributesService implements AttributesService { private final CacheExecutorService cacheExecutorService; private final DefaultCounter hitCounter; private final DefaultCounter missCounter; - private final TbTransactionalCache cache; + private final VersionedTbTransactionalCache cache; private ListeningExecutorService cacheExecutor; @Value("${cache.type:caffeine}") @@ -79,7 +80,7 @@ public class CachedAttributesService implements AttributesService { JpaExecutorService jpaExecutorService, StatsFactory statsFactory, CacheExecutorService cacheExecutorService, - TbTransactionalCache cache) { + VersionedTbTransactionalCache cache) { this.attributesDao = attributesDao; this.jpaExecutorService = jpaExecutorService; this.cacheExecutorService = cacheExecutorService; @@ -122,17 +123,9 @@ public class CachedAttributesService implements AttributesService { return Optional.ofNullable(cachedAttributeKvEntry); } else { missCounter.increment(); - var cacheTransaction = cache.newTransactionForKey(attributeCacheKey); - try { - Optional result = attributesDao.find(tenantId, entityId, scope, attributeKey); - cacheTransaction.putIfAbsent(attributeCacheKey, result.orElse(null)); - cacheTransaction.commit(); - return result; - } catch (Throwable e) { - cacheTransaction.rollback(); - log.debug("Could not find attribute from cache: [{}] [{}] [{}]", entityId, scope, attributeKey, e); - throw e; - } + Optional result = attributesDao.find(tenantId, entityId, scope, attributeKey); + cache.put(attributeCacheKey, result.orElse(null)); + return result; } }); } @@ -163,28 +156,19 @@ public class CachedAttributesService implements AttributesService { // DB call should run in DB executor, not in cache-related executor return jpaExecutorService.submit(() -> { - var cacheTransaction = cache.newTransactionForKeys(notFoundKeys); - try { - log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); - List result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); - for (AttributeKvEntry foundInDbAttribute : result) { - AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); - cacheTransaction.putIfAbsent(attributeCacheKey, foundInDbAttribute); - notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); - } - for (String key : notFoundAttributeKeys) { - cacheTransaction.putIfAbsent(new AttributeCacheKey(scope, entityId, key), null); - } - List mergedAttributes = new ArrayList<>(cachedAttributes); - mergedAttributes.addAll(result); - cacheTransaction.commit(); - log.trace("[{}][{}] Commit cache transaction: {}", entityId, scope, notFoundAttributeKeys); - return mergedAttributes; - } catch (Throwable e) { - cacheTransaction.rollback(); - log.debug("Could not find attributes from cache: [{}] [{}] [{}]", entityId, scope, notFoundAttributeKeys, e); - throw e; + log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); + List result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); + for (AttributeKvEntry foundInDbAttribute : result) { + put(entityId, scope, foundInDbAttribute, foundInDbAttribute.getVersion()); + notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); } + for (String key : notFoundAttributeKeys) { + cache.put(new AttributeCacheKey(scope, entityId, key), null); + } + List mergedAttributes = new ArrayList<>(cachedAttributes); + mergedAttributes.addAll(result); + log.trace("[{}][{}] Commit cache transaction: {}", entityId, scope, notFoundAttributeKeys); + return mergedAttributes; }); }, MoreExecutors.directExecutor()); // cacheExecutor analyse and returns results or submit to DB executor @@ -235,7 +219,7 @@ public class CachedAttributesService implements AttributesService { validate(entityId, scope); AttributeUtils.validate(attribute, valueNoXssValidation); ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - return Futures.transform(future, version -> evict(entityId, scope, attribute, version), cacheExecutor); + return Futures.transform(future, version -> put(entityId, scope, attribute, version), cacheExecutor); } @Override @@ -251,17 +235,17 @@ public class CachedAttributesService implements AttributesService { List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - futures.add(Futures.transform(future, version -> evict(entityId, scope, attribute, version), cacheExecutor)); + futures.add(Futures.transform(future, version -> put(entityId, scope, attribute, version), cacheExecutor)); } return Futures.allAsList(futures); } - private Long evict(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, Long version) { + private Long put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, Long version) { String key = attribute.getKey(); - log.trace("[{}][{}][{}] Before cache evict: {}", entityId, scope, key, attribute); - cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute); - log.trace("[{}][{}][{}] after cache evict.", entityId, scope, key); + log.trace("[{}][{}][{}] Before cache put: {}", entityId, scope, key, attribute); + cache.put(new AttributeCacheKey(scope, entityId, key), attribute, version); + log.trace("[{}][{}][{}] after cache put.", entityId, scope, key); return version; } @@ -273,9 +257,10 @@ public class CachedAttributesService implements AttributesService { @Override public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, AttributeScope scope, List attributeKeys) { validate(entityId, scope); - List> futures = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); - return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, key -> { - cache.evict(new AttributeCacheKey(scope, entityId, key)); + List>> futures = attributesDao.removeAllWithVersions(tenantId, entityId, scope, attributeKeys); + return Futures.allAsList(futures.stream().map(future -> Futures.transform(future, keyVersionPair -> { + String key = keyVersionPair.getFirst(); + cache.evict(new AttributeCacheKey(scope, entityId, key), keyVersionPair.getSecond()); return key; }, cacheExecutor)).collect(Collectors.toList())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.java new file mode 100644 index 0000000000..6b98348de9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseVersionedEntity.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model; + +public interface BaseVersionedEntity { + long getVersion(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index fb77ad6987..643c099829 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -56,6 +56,7 @@ public class ModelConstants { public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type"; public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key"; public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts"; + public static final String VERSION_COLUMN = "version"; /** * User constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java index 03aa0f98ad..d7bd23db4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.model.sql; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -41,9 +42,10 @@ import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN; @Data +@EqualsAndHashCode(callSuper = true) @Entity @Table(name = "attribute_kv") -public class AttributeKvEntity implements ToData, Serializable { +public class AttributeKvEntity extends VersionedEntity implements ToData, Serializable { @EmbeddedId private AttributeKvCompositeKey id; @@ -84,6 +86,6 @@ public class AttributeKvEntity implements ToData, Serializable kvEntry = new JsonDataEntry(strKey, jsonValue); } - return new BaseAttributeKvEntry(kvEntry, lastUpdateTs); + return new BaseAttributeKvEntry(kvEntry, lastUpdateTs, version); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/VersionedEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/VersionedEntity.java new file mode 100644 index 0000000000..2dfee6466a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/VersionedEntity.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; + +import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; + +@Data +@MappedSuperclass +public abstract class VersionedEntity { + + @Column(name = VERSION_COLUMN) + protected Long version; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java index c2b9d53d47..7050e61144 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java @@ -31,14 +31,14 @@ import java.util.List; @SqlDao public class AttributeKvInsertRepository extends AbstractVersionedInsertRepository { - private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = version + 1 " + + private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') " + "WHERE entity_id = ? and attribute_type =? and attribute_key = ? RETURNING version;"; private static final String INSERT_OR_UPDATE = - "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts) " + - "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?) " + + "INSERT INTO attribute_kv (entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, json_v, last_update_ts, version) " + + "VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), ?, nextval('attribute_kv_version_seq')) " + "ON CONFLICT (entity_id, attribute_type, attribute_key) " + - "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = attribute_kv.version + 1 RETURNING version;"; + "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, json_v = cast(? AS json), last_update_ts = ?, version = nextval('attribute_kv_version_seq') RETURNING version;"; @Override protected void setOnBatchUpdateValues(PreparedStatement ps, int i, List entities) throws SQLException { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index ddd6c3c881..beea5f2f2f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -30,15 +30,15 @@ public interface AttributeKvRepository extends JpaRepository findAllEntityIdAndAttributeType(@Param("entityId") UUID entityId, - @Param("attributeType") int attributeType); + List findAllByEntityIdAndAttributeType(@Param("entityId") UUID entityId, + @Param("attributeType") int attributeType); @Transactional @Modifying - @Query("DELETE FROM AttributeKvEntity a WHERE a.id.entityId = :entityId " + - "AND a.id.attributeType = :attributeType " + - "AND a.id.attributeKey = :attributeKey") - void delete(@Param("entityId") UUID entityId, + @Query(value = "DELETE FROM attribute_kv WHERE entity_id = :entityId " + + "AND attribute_type = :attributeType " + + "AND attribute_key = :attributeKey RETURNING version", nativeQuery = true) + Long delete(@Param("entityId") UUID entityId, @Param("attributeType") int attributeType, @Param("attributeKey") int attributeKey); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 05cdfce221..de4487381b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.attributes.AttributesDao; @@ -144,7 +145,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Override public List findAll(TenantId tenantId, EntityId entityId, AttributeScope attributeScope) { - List attributes = attributeKvRepository.findAllEntityIdAndAttributeType( + List attributes = attributeKvRepository.findAllByEntityIdAndAttributeType( entityId.getId(), attributeScope.getId()); attributes.forEach(attributeKvEntity -> attributeKvEntity.setStrKey(keyDictionaryDao.getKey(attributeKvEntity.getId().getAttributeKey()))); @@ -205,6 +206,18 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl return futuresList; } + @Override + public List>> removeAllWithVersions(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys) { + List>> futuresList = new ArrayList<>(keys.size()); + for (String key : keys) { + futuresList.add(service.submit(() -> { + Long version = attributeKvRepository.delete(entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(key)); + return TbPair.of(key, version); + })); + } + return futuresList; + } + @Transactional @Override public List> removeAllByEntityId(TenantId tenantId, EntityId entityId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java index 0484337a08..0a3ffad09f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java @@ -40,11 +40,11 @@ public class SqlLatestInsertTsRepository extends AbstractVersionedInsertReposito private Boolean updateByLatestTs; private static final String BATCH_UPDATE = - "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = version + 1 WHERE entity_id = ? AND key = ?"; + "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq') WHERE entity_id = ? AND key = ?"; private static final String INSERT_OR_UPDATE = - "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json)) " + - "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = ts_kv_latest.version + 1"; + "INSERT INTO ts_kv_latest (entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v, version) VALUES(?, ?, ?, ?, ?, ?, ?, cast(? AS json), nextval('ts_kv_latest_version_seq')) " + + "ON CONFLICT (entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?, json_v = cast(? AS json), version = nextval('ts_kv_latest_version_seq')"; private static final String BATCH_UPDATE_BY_LATEST_TS = BATCH_UPDATE + " AND ts_kv_latest.ts <= ?"; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9293636f0b..c2e3a2b108 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -102,6 +102,8 @@ CREATE TABLE IF NOT EXISTS audit_log ( action_failure_details varchar(1000000) ) PARTITION BY RANGE (created_time); +CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1000; + CREATE TABLE IF NOT EXISTS attribute_kv ( entity_id uuid, attribute_type int, @@ -112,7 +114,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv ( dbl_v double precision, json_v json, last_update_ts bigint, - version bigint default 0, + version bigint default 1, CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) ); @@ -539,6 +541,8 @@ CREATE TABLE IF NOT EXISTS entity_view ( CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id) ); +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; + CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, @@ -549,7 +553,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, - version bigint default 0, + version bigint default 1, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-timescale.sql b/dao/src/main/resources/sql/schema-timescale.sql index 2d2381403d..7c2a99fba3 100644 --- a/dao/src/main/resources/sql/schema-timescale.sql +++ b/dao/src/main/resources/sql/schema-timescale.sql @@ -34,6 +34,8 @@ CREATE TABLE IF NOT EXISTS key_dictionary ( CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key) ); +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; + CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, key int NOT NULL, @@ -43,7 +45,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest ( long_v bigint, dbl_v double precision, json_v json, - version bigint default 0, + version bigint default 1, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-ts-latest-psql.sql b/dao/src/main/resources/sql/schema-ts-latest-psql.sql index 058104d4f2..d50571c091 100644 --- a/dao/src/main/resources/sql/schema-ts-latest-psql.sql +++ b/dao/src/main/resources/sql/schema-ts-latest-psql.sql @@ -14,6 +14,8 @@ -- limitations under the License. -- +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; + CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, @@ -24,6 +26,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, - version bigint default 0, + version bigint default 1, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) -); \ No newline at end of file +); diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 9c772df45b..e1c35d7a21 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -23,6 +23,7 @@ DROP TABLE IF EXISTS alarm_type; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; +DROP SEQUENCE IF EXISTS attribute_kv_version_seq; DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; DROP TABLE IF EXISTS device; @@ -36,6 +37,7 @@ DROP TABLE IF EXISTS relation; DROP TABLE IF EXISTS tenant; DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv_latest; +DROP SEQUENCE IF EXISTS ts_kv_latest_version_seq; DROP TABLE IF EXISTS ts_kv_dictionary; DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widgets_bundle_widget; From 6162060e88a04e8883f54dec8f1d78e65d47fb74 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 1 Jul 2024 12:24:27 +0200 Subject: [PATCH 029/136] fixed insert result and used expiration --- .../cache/RedisTbTransactionalCache.java | 4 +- ...che.java => VersionedCaffeineTbCache.java} | 4 +- ...lCache.java => VersionedRedisTbCache.java} | 39 +++++++++---------- ...tionalCache.java => VersionedTbCache.java} | 2 +- .../AbstractVersionedInsertRepository.java | 2 +- .../attributes/AttributeCaffeineCache.java | 4 +- .../dao/attributes/AttributeRedisCache.java | 4 +- .../attributes/CachedAttributesService.java | 6 +-- .../sql/attributes/AttributeKvRepository.java | 3 +- .../main/resources/sql/schema-entities.sql | 4 +- .../main/resources/sql/schema-timescale.sql | 2 +- .../resources/sql/schema-ts-latest-psql.sql | 2 +- .../attributes/BaseAttributesServiceTest.java | 22 +---------- 13 files changed, 39 insertions(+), 59 deletions(-) rename common/cache/src/main/java/org/thingsboard/server/cache/{VersionedCaffeineTbTransactionalCache.java => VersionedCaffeineTbCache.java} (91%) rename common/cache/src/main/java/org/thingsboard/server/cache/{VersionedRedisTbTransactionalCache.java => VersionedRedisTbCache.java} (82%) rename common/cache/src/main/java/org/thingsboard/server/cache/{VersionedTbTransactionalCache.java => VersionedTbCache.java} (89%) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index 543739aad4..e33e13caa1 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -54,8 +54,8 @@ public abstract class RedisTbTransactionalCache keySerializer = StringRedisSerializer.UTF_8; private final TbRedisSerializer valueSerializer; - private final Expiration evictExpiration; - private final Expiration cacheTtl; + protected final Expiration evictExpiration; + protected final Expiration cacheTtl; public RedisTbTransactionalCache(String cacheName, CacheSpecsMap cacheSpecsMap, diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java similarity index 91% rename from common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbTransactionalCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java index f2120cf38a..e084668b90 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java @@ -26,7 +26,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @RequiredArgsConstructor -public abstract class VersionedCaffeineTbTransactionalCache implements VersionedTbTransactionalCache { +public abstract class VersionedCaffeineTbCache implements VersionedTbCache { private final CacheManager cacheManager; private final String cacheName; @@ -53,7 +53,7 @@ public abstract class VersionedCaffeineTbTransactionalCache versionValuePair = doGet(key); Long currentVersion = versionValuePair.getFirst(); - if (currentVersion == null || version >= currentVersion) { + if (currentVersion == null || version > currentVersion) { cacheManager.getCache(cacheName).put(key, TbPair.of(version, value)); } } finally { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java similarity index 82% rename from common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbTransactionalCache.java rename to common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java index 34b46d535a..5e9ba187c9 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java @@ -22,6 +22,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.ReturnType; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.thingsboard.server.common.data.HasVersion; @@ -31,34 +32,29 @@ import java.util.Collection; import java.util.List; @Slf4j -public abstract class VersionedRedisTbTransactionalCache extends RedisTbTransactionalCache implements VersionedTbTransactionalCache { +public abstract class VersionedRedisTbCache extends RedisTbTransactionalCache implements VersionedTbCache { private static final int VERSION_SIZE = 8; private static final int VALUE_END_OFFSET = -1; static final byte[] SET_VERSIONED_VALUE_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize(""" - -- KEYS[1] is the key - -- ARGV[1] is the new value - -- ARGV[2] is the new version - local key = KEYS[1] local newValue = ARGV[1] local newVersion = tonumber(ARGV[2]) + local expiration = tonumber(ARGV[3]) - -- Function to set the new value with the version local function setNewValue() local newValueWithVersion = struct.pack(">I8", newVersion) .. newValue:sub(9) - redis.call('SET', key, newValueWithVersion) + redis.call('SET', key, newValueWithVersion, 'EX', expiration) end -- Get the current version (first 8 bytes) of the current value local currentVersionBytes = redis.call('GETRANGE', key, 0, 7) if currentVersionBytes and #currentVersionBytes == 8 then - -- Extract the current version from the first 8 bytes local currentVersion = tonumber(struct.unpack(">I8", currentVersionBytes)) - if newVersion >= currentVersion then + if newVersion > currentVersion then setNewValue() end else @@ -66,9 +62,9 @@ public abstract class VersionedRedisTbTransactionalCache valueSerializer) { + public VersionedRedisTbCache(String cacheName, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory, TBRedisCacheConfiguration configuration, TbRedisSerializer valueSerializer) { super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer); } @@ -92,23 +88,27 @@ public abstract class VersionedRedisTbTransactionalCache { +public interface VersionedTbCache { TbCacheValueWrapper get(K key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java index 6380957f77..43ed41e52a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractVersionedInsertRepository.java @@ -68,7 +68,7 @@ public abstract class AbstractVersionedInsertRepository extends AbstractInser seqNumbersList = keyHolder.getKeyList(); for (int i = 0; i < insertResult.length; i++) { - if (updateResult[i] != 0) { + if (insertResult[i] != 0) { seqNumbers.set(toInsertIndexes.get(i), (Long) seqNumbersList.get(i).get(VERSION_COLUMN)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java index e00ea8d11c..140cfbc03e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCaffeineCache.java @@ -18,13 +18,13 @@ package org.thingsboard.server.dao.attributes; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; -import org.thingsboard.server.cache.VersionedCaffeineTbTransactionalCache; +import org.thingsboard.server.cache.VersionedCaffeineTbCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @Service("AttributeCache") -public class AttributeCaffeineCache extends VersionedCaffeineTbTransactionalCache { +public class AttributeCaffeineCache extends VersionedCaffeineTbCache { public AttributeCaffeineCache(CacheManager cacheManager) { super(cacheManager, CacheConstants.ATTRIBUTES_CACHE); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java index 68caf62ceb..a6f21f9c3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -23,7 +23,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbRedisSerializer; -import org.thingsboard.server.cache.VersionedRedisTbTransactionalCache; +import org.thingsboard.server.cache.VersionedRedisTbCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -38,7 +38,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("AttributeCache") -public class AttributeRedisCache extends VersionedRedisTbTransactionalCache { +public class AttributeRedisCache extends VersionedRedisTbCache { public AttributeRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { super(CacheConstants.ATTRIBUTES_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index fb5c745a11..d31ff9db79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -27,7 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbCacheValueWrapper; -import org.thingsboard.server.cache.VersionedTbTransactionalCache; +import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -68,7 +68,7 @@ public class CachedAttributesService implements AttributesService { private final CacheExecutorService cacheExecutorService; private final DefaultCounter hitCounter; private final DefaultCounter missCounter; - private final VersionedTbTransactionalCache cache; + private final VersionedTbCache cache; private ListeningExecutorService cacheExecutor; @Value("${cache.type:caffeine}") @@ -80,7 +80,7 @@ public class CachedAttributesService implements AttributesService { JpaExecutorService jpaExecutorService, StatsFactory statsFactory, CacheExecutorService cacheExecutorService, - VersionedTbTransactionalCache cache) { + VersionedTbCache cache) { this.attributesDao = attributesDao; this.jpaExecutorService = jpaExecutorService; this.cacheExecutorService = cacheExecutorService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index beea5f2f2f..8367431946 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -37,7 +37,7 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, @Param("attributeType") int attributeType); } - diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index c2e3a2b108..9e6ab22248 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv ( dbl_v double precision, json_v json, last_update_ts bigint, - version bigint default 1, + version bigint default 0, CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key) ); @@ -553,7 +553,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, - version bigint default 1, + version bigint default 0, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-timescale.sql b/dao/src/main/resources/sql/schema-timescale.sql index 7c2a99fba3..1d5b67a95e 100644 --- a/dao/src/main/resources/sql/schema-timescale.sql +++ b/dao/src/main/resources/sql/schema-timescale.sql @@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest ( long_v bigint, dbl_v double precision, json_v json, - version bigint default 1, + version bigint default 0, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/main/resources/sql/schema-ts-latest-psql.sql b/dao/src/main/resources/sql/schema-ts-latest-psql.sql index d50571c091..4892e40176 100644 --- a/dao/src/main/resources/sql/schema-ts-latest-psql.sql +++ b/dao/src/main/resources/sql/schema-ts-latest-psql.sql @@ -26,6 +26,6 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest long_v bigint, dbl_v double precision, json_v json, - version bigint default 1, + version bigint default 0, CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index 98a8cf9d47..ad2470a9b1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -26,7 +26,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -60,7 +60,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { private static final String NEW_VALUE = "NEW VALUE"; @Autowired - private TbTransactionalCache cache; + private VersionedTbCache cache; @Autowired private AttributesService attributesService; @@ -132,24 +132,6 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { Assert.assertTrue(result.isEmpty()); } - @Test - public void testConcurrentTransaction() throws Exception { - var tenantId = new TenantId(UUID.randomUUID()); - var deviceId = new DeviceId(UUID.randomUUID()); - var scope = AttributeScope.SERVER_SCOPE; - var key = "TEST"; - - var attrKey = new AttributeCacheKey(scope, deviceId, "TEST"); - var oldValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, OLD_VALUE)); - var newValue = new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(key, NEW_VALUE)); - - var trx = cache.newTransactionForKey(attrKey); - cache.putIfAbsent(attrKey, newValue); - trx.putIfAbsent(attrKey, oldValue); - Assert.assertFalse(trx.commit()); - Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); - } - @Test public void testConcurrentFetchAndUpdate() throws Exception { var tenantId = new TenantId(UUID.randomUUID()); From 7fa752854641e7aca149a6d863ffc6bfec29f8a7 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 2 Jul 2024 12:38:07 +0200 Subject: [PATCH 030/136] fixed delete attributes query --- .../server/dao/sql/attributes/AttributeKvRepository.java | 8 ++++---- .../server/dao/sql/attributes/JpaAttributeDao.java | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index 8367431946..90f49c57fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -35,10 +35,10 @@ public interface AttributeKvRepository extends JpaRepository>> removeAllWithVersions(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys) { List>> futuresList = new ArrayList<>(keys.size()); for (String key : keys) { futuresList.add(service.submit(() -> { - Long version = attributeKvRepository.delete(entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(key)); + Long version = jdbcTemplate.query("DELETE FROM attribute_kv WHERE entity_id = ? AND attribute_type = ? " + + "AND attribute_key = ? RETURNING nextval('attribute_kv_version_seq')", + rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(key)); return TbPair.of(key, version); })); } From 0b51baf38de1439224ecab046e8de938053e62de Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 3 Jul 2024 16:03:19 +0200 Subject: [PATCH 031/136] LUA script improvements and used versioned cache for latest ts --- .../cache/RedisTbTransactionalCache.java | 2 +- .../server/cache/VersionedRedisTbCache.java | 16 ++- .../server/cache}/TsLatestRedisCacheTest.java | 6 +- .../server/common/data/HasVersion.java | 2 +- .../common/data/kv/BaseAttributeKvEntry.java | 2 +- .../server/common/data/kv/BasicTsKvEntry.java | 13 ++ .../server/common/data/kv/TsKvEntry.java | 3 +- .../data/kv/TsKvLatestRemovingResult.java | 11 +- .../dao/model/sql/AbstractTsKvEntity.java | 7 +- .../model/sqlts/latest/TsKvLatestEntity.java | 11 +- ...paAbstractDaoListeningExecutorService.java | 4 + .../dao/sql/attributes/JpaAttributeDao.java | 5 +- .../CachedRedisSqlTimeseriesLatestDao.java | 51 +++---- .../dao/sqlts/SqlTimeseriesLatestDao.java | 12 +- .../latest/SearchTsKvLatestRepository.java | 2 +- .../CassandraBaseTimeseriesLatestDao.java | 2 +- .../dao/timeseries/TsLatestRedisCache.java | 127 +++--------------- .../attributes/BaseAttributesServiceTest.java | 3 - 18 files changed, 106 insertions(+), 173 deletions(-) rename {dao/src/test/java/org/thingsboard/server/dao/timeseries => common/cache/src/test/java/org/thingsboard/server/cache}/TsLatestRedisCacheTest.java (83%) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index dd371d96b6..9e51795be6 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -80,7 +80,7 @@ public abstract class RedisTbTransactionalCacheI8", newVersion) .. newValue:sub(9) + local newValueWithVersion = struct.pack(">I8", newVersion) .. newValue redis.call('SET', key, newValueWithVersion, 'EX', expiration) end + + local function bytes_to_number(bytes) + local n = 0 + for i = 1, 8 do + n = n * 256 + string.byte(bytes, i) + end + return n + end -- Get the current version (first 8 bytes) of the current value local currentVersionBytes = redis.call('GETRANGE', key, 0, 7) if currentVersionBytes and #currentVersionBytes == 8 then - local currentVersion = tonumber(struct.unpack(">I8", currentVersionBytes)) + local currentVersion = bytes_to_number(currentVersionBytes) - if newVersion > currentVersion then + if newVersion > currentVersion or newVersion == 1 and currentVersion > 1 then setNewValue() end else @@ -62,7 +70,7 @@ public abstract class VersionedRedisTbCache valueSerializer) { super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer); diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java b/common/cache/src/test/java/org/thingsboard/server/cache/TsLatestRedisCacheTest.java similarity index 83% rename from dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java rename to common/cache/src/test/java/org/thingsboard/server/cache/TsLatestRedisCacheTest.java index c463b4630f..d0f3042c61 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCacheTest.java +++ b/common/cache/src/test/java/org/thingsboard/server/cache/TsLatestRedisCacheTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.timeseries; +package org.thingsboard.server.cache; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; @@ -22,11 +22,11 @@ import java.security.MessageDigest; import static org.assertj.core.api.Assertions.assertThat; -class TsLatestRedisCacheTest { +class VersionedRedisTbCacheTest { @Test void testUpsertTsLatestLUAScriptHash() { - assertThat(getSHA1(TsLatestRedisCache.UPSERT_TS_LATEST_LUA_SCRIPT)).isEqualTo(new String(TsLatestRedisCache.UPSERT_TS_LATEST_SHA)); + assertThat(getSHA1(VersionedRedisTbCache.SET_VERSIONED_VALUE_LUA_SCRIPT)).isEqualTo(new String(VersionedRedisTbCache.SET_VERSIONED_VALUE_SHA)); } @SneakyThrows diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java index fbeb8ef3b6..b9af6f93c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasVersion.java @@ -16,5 +16,5 @@ package org.thingsboard.server.common.data; public interface HasVersion { - long getVersion(); + Long getVersion(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java index 6d48693511..001e061aa9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java @@ -98,7 +98,7 @@ public class BaseAttributeKvEntry implements AttributeKvEntry { } @Override - public long getVersion() { + public Long getVersion() { return version; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java index 396d1df5ba..ab39498ae0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java @@ -25,9 +25,18 @@ public class BasicTsKvEntry implements TsKvEntry { @Valid private final KvEntry kv; + private final Long version; + public BasicTsKvEntry(long ts, KvEntry kv) { this.ts = ts; this.kv = kv; + this.version = null; + } + + public BasicTsKvEntry(long ts, KvEntry kv, Long version) { + this.ts = ts; + this.kv = kv; + this.version = version; } @Override @@ -118,4 +127,8 @@ public class BasicTsKvEntry implements TsKvEntry { return Math.max(1, (length + MAX_CHARS_PER_DATA_POINT - 1) / MAX_CHARS_PER_DATA_POINT); } + @Override + public Long getVersion() { + return version; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java index c65f48e550..4b8887e893 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.kv; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.query.TsValue; /** @@ -24,7 +25,7 @@ import org.thingsboard.server.common.data.query.TsValue; * @author ashvayka * */ -public interface TsKvEntry extends KvEntry { +public interface TsKvEntry extends KvEntry, HasVersion { long getTs(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java index acdb745ad8..dae91bdd81 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvLatestRemovingResult.java @@ -22,15 +22,22 @@ public class TsKvLatestRemovingResult { private String key; private TsKvEntry data; private boolean removed; + private Long version; - public TsKvLatestRemovingResult(TsKvEntry data) { + public TsKvLatestRemovingResult(String key, boolean removed) { + this(key, removed, null); + } + + public TsKvLatestRemovingResult(TsKvEntry data, Long version) { this.key = data.getKey(); this.data = data; this.removed = true; + this.version = version; } - public TsKvLatestRemovingResult(String key, boolean removed) { + public TsKvLatestRemovingResult(String key, boolean removed, Long version) { this.key = key; this.removed = removed; + this.version = version; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java index a0a4664829..ae4f47a37a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java @@ -119,10 +119,13 @@ public abstract class AbstractTsKvEntity implements ToData { } if (aggValuesCount == null) { - return new BasicTsKvEntry(ts, kvEntry); + return new BasicTsKvEntry(ts, kvEntry, getVersion()); } else { return new AggTsKvEntry(ts, kvEntry, aggValuesCount); } } -} \ No newline at end of file + public Long getVersion() { + return null; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java index 0d34bb5195..23fe580fa3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.model.sqlts.latest; +import jakarta.persistence.Column; import jakarta.persistence.ColumnResult; import jakarta.persistence.ConstructorResult; import jakarta.persistence.Entity; @@ -30,6 +31,8 @@ import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; + @Data @Entity @Table(name = "ts_kv_latest") @@ -50,7 +53,7 @@ import java.util.UUID; @ColumnResult(name = "doubleValue", type = Double.class), @ColumnResult(name = "jsonValue", type = String.class), @ColumnResult(name = "ts", type = Long.class), - + @ColumnResult(name = "version", type = Long.class) } ), }) @@ -65,6 +68,9 @@ import java.util.UUID; }) public final class TsKvLatestEntity extends AbstractTsKvEntity { + @Column(name = VERSION_COLUMN) + private Long version; + @Override public boolean isNotEmpty() { return strValue != null || longValue != null || doubleValue != null || booleanValue != null || jsonValue != null; @@ -73,7 +79,7 @@ public final class TsKvLatestEntity extends AbstractTsKvEntity { public TsKvLatestEntity() { } - public TsKvLatestEntity(UUID entityId, Integer key, String strKey, String strValue, Boolean boolValue, Long longValue, Double doubleValue, String jsonValue, Long ts) { + public TsKvLatestEntity(UUID entityId, Integer key, String strKey, String strValue, Boolean boolValue, Long longValue, Double doubleValue, String jsonValue, Long ts, Long version) { this.entityId = entityId; this.key = key; this.ts = ts; @@ -83,5 +89,6 @@ public final class TsKvLatestEntity extends AbstractTsKvEntity { this.booleanValue = boolValue; this.jsonValue = jsonValue; this.strKey = strKey; + this.version = version; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java index b85e1bc724..3ac31bf831 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.support.TransactionTemplate; import javax.sql.DataSource; import java.sql.SQLException; @@ -36,6 +37,9 @@ public abstract class JpaAbstractDaoListeningExecutorService { @Autowired protected JdbcTemplate jdbcTemplate; + @Autowired + protected TransactionTemplate transactionTemplate; + protected void printWarnings(Statement statement) throws SQLException { SQLWarning warnings = statement.getWarnings(); if (warnings != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 172940d21d..a0e1f1447d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -206,15 +206,14 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl return futuresList; } - @Transactional @Override public List>> removeAllWithVersions(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, List keys) { List>> futuresList = new ArrayList<>(keys.size()); for (String key : keys) { futuresList.add(service.submit(() -> { - Long version = jdbcTemplate.query("DELETE FROM attribute_kv WHERE entity_id = ? AND attribute_type = ? " + + Long version = transactionTemplate.execute(status -> jdbcTemplate.query("DELETE FROM attribute_kv WHERE entity_id = ? AND attribute_type = ? " + "AND attribute_key = ? RETURNING nextval('attribute_kv_version_seq')", - rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(key)); + rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), attributeScope.getId(), keyDictionaryDao.getOrSaveKeyId(key))); return TbPair.of(key, version); })); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index 32d0ee91c9..42dc149342 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -25,7 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.TbCacheValueWrapper; -import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -52,7 +52,7 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries final CacheExecutorService cacheExecutorService; final SqlTimeseriesLatestDao sqlDao; final StatsFactory statsFactory; - final TbTransactionalCache cache; + final VersionedTbCache cache; DefaultCounter hitCounter; DefaultCounter missCounter; @@ -64,17 +64,17 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries } @Override - public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { - ListenableFuture future = sqlDao.saveLatest(tenantId, entityId, tsKvEntry); + public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { + ListenableFuture future = sqlDao.saveLatest(tenantId, entityId, tsKvEntry); future = Futures.transform(future, x -> { - cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), tsKvEntry); + cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), tsKvEntry, x); return x; }, cacheExecutorService); if (log.isTraceEnabled()) { Futures.addCallback(future, new FutureCallback<>() { @Override - public void onSuccess(Void result) { + public void onSuccess(Long result) { log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, tsKvEntry.getKey(), tsKvEntry); } @@ -91,7 +91,15 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries public ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { ListenableFuture future = sqlDao.removeLatest(tenantId, entityId, query); future = Futures.transform(future, x -> { - cache.evict(new TsLatestCacheKey(entityId, query.getKey())); + if (x.isRemoved()) { + TsLatestCacheKey key = new TsLatestCacheKey(entityId, query.getKey()); + Long version = x.getVersion(); + if (x.getData() != null) { + cache.put(key, x.getData(), version); + } else { + cache.evict(key, version); + } + } return x; }, cacheExecutorService); @@ -133,32 +141,11 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries return Futures.immediateFuture(Optional.ofNullable(tsKvEntry)); } log.debug("findLatest cache miss [{}][{}]", entityId, key); - ListenableFuture> daoFuture = sqlDao.findLatestOpt(tenantId,entityId, key); - - return Futures.transformAsync(daoFuture, (daoValue) -> { - - if (daoValue.isEmpty()) { - //TODO implement the cache logic if no latest found in TS DAO. Currently we are always getting from DB to stay on the safe side - return Futures.immediateFuture(daoValue); - } - ListenableFuture> cachePutFuture = cacheExecutorService.submit(() -> { - cache.put(new TsLatestCacheKey(entityId, key), daoValue.get()); - return daoValue; - }); - - Futures.addCallback(cachePutFuture, new FutureCallback<>() { - @Override - public void onSuccess(Optional result) { - log.trace("saveLatest onSuccess [{}][{}][{}]", entityId, key, result); - } - - @Override - public void onFailure(Throwable t) { - log.info("saveLatest onFailure [{}][{}][{}]", entityId, key, daoValue, t); - } + ListenableFuture> daoFuture = sqlDao.findLatestOpt(tenantId, entityId, key); - }, MoreExecutors.directExecutor()); - return cachePutFuture; + return Futures.transform(daoFuture, daoValue -> { + cache.put(cacheKey, daoValue.orElse(null)); + return daoValue; }, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index edb0e535d1..44859c26e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -190,7 +190,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return Futures.transformAsync(future, entryList -> { if (entryList.size() == 1) { TsKvEntry entry = entryList.get(0); - return Futures.transform(getSaveLatestFuture(entityId, entry), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); + return Futures.transform(getSaveLatestFuture(entityId, entry), v -> new TsKvLatestRemovingResult(entry, v), MoreExecutors.directExecutor()); } else { log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); } @@ -229,18 +229,18 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false)); } boolean isRemoved = false; + Long version = null; long ts = latest.getTs(); if (ts >= query.getStartTs() && ts < query.getEndTs()) { - TsKvLatestEntity latestEntity = new TsKvLatestEntity(); - latestEntity.setEntityId(entityId.getId()); - latestEntity.setKey(keyDictionaryDao.getOrSaveKeyId(query.getKey())); - tsKvLatestRepository.delete(latestEntity); + version = transactionTemplate.execute(status -> jdbcTemplate.query("DELETE FROM ts_kv_latest WHERE entity_id = ? " + + "AND key = ? RETURNING nextval('ts_kv_latest_version_seq')", + rs -> rs.next() ? rs.getLong(1) : null, entityId.getId(), keyDictionaryDao.getOrSaveKeyId(query.getKey()))); isRemoved = true; if (query.getRewriteLatestIfDeleted()) { return getNewLatestEntryFuture(tenantId, entityId, query); } } - return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved)); + return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved, version)); }, MoreExecutors.directExecutor()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java index 50652d4f60..5e9e3369a5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java @@ -31,7 +31,7 @@ public class SearchTsKvLatestRepository { public static final String FIND_ALL_BY_ENTITY_ID = "findAllByEntityId"; public static final String FIND_ALL_BY_ENTITY_ID_QUERY = "SELECT ts_kv_latest.entity_id AS entityId, ts_kv_latest.key AS key, key_dictionary.key AS strKey, ts_kv_latest.str_v AS strValue," + - " ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, ts_kv_latest.ts AS ts FROM ts_kv_latest " + + " ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, ts_kv_latest.ts AS ts, ts_kv_latest.version AS version FROM ts_kv_latest " + "INNER JOIN key_dictionary ON ts_kv_latest.key = key_dictionary.key_id WHERE ts_kv_latest.entity_id = cast(:id AS uuid)"; @PersistenceContext diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index f774f18dc7..01c91d4801 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -161,7 +161,7 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes var entryList = result.getData(); if (entryList.size() == 1) { TsKvEntry entry = entryList.get(0); - return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); + return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry, v), MoreExecutors.directExecutor()); } else { log.trace("Could not find new latest value for [{}], key - {}", entityId, query.getKey()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java index e9f9ab522c..851d66ae0c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java @@ -15,134 +15,41 @@ */ package org.thingsboard.server.dao.timeseries; -import jakarta.annotation.PostConstruct; +import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.NotImplementedException; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.ReturnType; -import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; import org.springframework.stereotype.Service; import org.thingsboard.server.cache.CacheSpecsMap; -import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; -import org.thingsboard.server.cache.TbCacheTransaction; -import org.thingsboard.server.cache.TbCacheValueWrapper; -import org.thingsboard.server.cache.TbJavaRedisSerializer; +import org.thingsboard.server.cache.TbRedisSerializer; +import org.thingsboard.server.cache.VersionedRedisTbCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.TsKvEntry; - -import java.io.Serializable; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import org.thingsboard.server.common.util.KvProtoUtil; +import org.thingsboard.server.gen.transport.TransportProtos; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("TsLatestCache") @Slf4j -public class TsLatestRedisCache extends RedisTbTransactionalCache { - - static final byte[] UPSERT_TS_LATEST_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize("" + - "redis.call('ZREMRANGEBYSCORE', KEYS[1], ARGV[1], ARGV[1]); " + - "redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2]); " + - "local current_size = redis.call('ZCARD', KEYS[1]); " + - "if current_size > 1 then" + - " redis.call('ZREMRANGEBYRANK', KEYS[1], 0, -2) " + - "end;"); - static final byte[] UPSERT_TS_LATEST_SHA = StringRedisSerializer.UTF_8.serialize("24e226c3ea34e3e850113e8eb1f3cd2b88171988"); +public class TsLatestRedisCache extends VersionedRedisTbCache { public TsLatestRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJavaRedisSerializer<>()); - } - - @PostConstruct - public void init() { - try (var connection = getConnection(UPSERT_TS_LATEST_SHA)) { - log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(UPSERT_TS_LATEST_SHA), connection.getNativeConnection()); - String sha = connection.scriptingCommands().scriptLoad(UPSERT_TS_LATEST_LUA_SCRIPT); - if (!Arrays.equals(UPSERT_TS_LATEST_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { - log.error("SHA for UPSERT_TS_LATEST_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(UPSERT_TS_LATEST_SHA), sha, connection.getNativeConnection()); + super(CacheConstants.TS_LATEST_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>() { + @Override + public byte[] serialize(TsKvEntry tsKvEntry) throws SerializationException { + return KvProtoUtil.toTsKvProto(tsKvEntry.getTs(), tsKvEntry).toByteArray(); } - } catch (Throwable t) { - log.error("Error on Redis TS Latest cache init", t); - } - } - - @Override - public TbCacheValueWrapper get(TsLatestCacheKey key) { - log.debug("get [{}]", key); - return super.get(key); - } - - @Override - protected byte[] doGet(RedisConnection connection, byte[] rawKey) { - log.trace("doGet [{}][{}]", connection, rawKey); - Set values = connection.commands().zRange(rawKey, -1, -1); - return values == null ? null : values.stream().findFirst().orElse(null); - } - @Override - public void put(TsLatestCacheKey key, TsKvEntry value) { - log.trace("put [{}][{}]", key, value); - final byte[] rawKey = getRawKey(key); - try (var connection = getConnection(rawKey)) { - byte[] rawValue = getRawValue(value); - byte[] ts = StringRedisSerializer.UTF_8.serialize(String.valueOf(value.toTsValue().getTs())); - try { - connection.scriptingCommands().evalSha(UPSERT_TS_LATEST_SHA, ReturnType.VALUE, 1, rawKey, ts, rawValue); - } catch (InvalidDataAccessApiUsageException e) { - log.debug("loading LUA [{}]", connection.getNativeConnection()); - String sha = connection.scriptingCommands().scriptLoad(UPSERT_TS_LATEST_LUA_SCRIPT); - if (!Arrays.equals(UPSERT_TS_LATEST_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { - log.error("SHA for UPSERT_TS_LATEST_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(UPSERT_TS_LATEST_SHA), sha); - } + @Override + public TsKvEntry deserialize(TsLatestCacheKey key, byte[] bytes) throws SerializationException { try { - connection.scriptingCommands().evalSha(UPSERT_TS_LATEST_SHA, ReturnType.VALUE, 1, rawKey, ts, rawValue); - } catch (InvalidDataAccessApiUsageException ignored) { - log.debug("Slowly executing eval instead of fast evalsha"); - connection.scriptingCommands().eval(UPSERT_TS_LATEST_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, ts, rawValue); + return KvProtoUtil.fromTsKvProto(TransportProtos.TsKvProto.parseFrom(bytes)); + } catch (InvalidProtocolBufferException e) { + throw new SerializationException(e.getMessage()); } - } - } - } - - @Override - public void evict(TsLatestCacheKey key) { - log.trace("evict [{}]", key); - final byte[] rawKey = getRawKey(key); - try (var connection = getConnection(rawKey)) { - connection.keyCommands().del(rawKey); - } - } - - @Override - public void putIfAbsent(TsLatestCacheKey key, TsKvEntry value) { - log.trace("putIfAbsent [{}][{}]", key, value); - throw new NotImplementedException("putIfAbsent is not supported by TsLatestRedisCache"); - } - - @Override - public void evict(Collection keys) { - throw new NotImplementedException("evict by many keys is not supported by TsLatestRedisCache"); - } - - @Override - public void evictOrPut(TsLatestCacheKey key, TsKvEntry value) { - throw new NotImplementedException("evictOrPut is not supported by TsLatestRedisCache"); - } - - @Override - public TbCacheTransaction newTransactionForKey(TsLatestCacheKey key) { - throw new NotImplementedException("newTransactionForKey is not supported by TsLatestRedisCache"); + }); } - - @Override - public TbCacheTransaction newTransactionForKeys(List keys) { - throw new NotImplementedException("newTransactionForKeys is not supported by TsLatestRedisCache"); - } - } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index ad2470a9b1..0603f4e3f6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -59,9 +59,6 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { private static final String OLD_VALUE = "OLD VALUE"; private static final String NEW_VALUE = "NEW VALUE"; - @Autowired - private VersionedTbCache cache; - @Autowired private AttributesService attributesService; From 84ea3b4afcbc30adb78b9be5453183059050f7cd Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 3 Jul 2024 17:51:52 +0200 Subject: [PATCH 032/136] test fixes --- .../dao/service/timeseries/BaseTimeseriesServiceTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index d32933684a..e4cc64d47c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -23,6 +23,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceId; @@ -74,6 +75,9 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Autowired EntityViewService entityViewService; + @Value("${database.ts.type}") + String databaseTsLatestType; + protected static final int MAX_TIMEOUT = 30; private static final String STRING_KEY = "stringKey"; @@ -161,6 +165,10 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { @Test public void testFindLatestOpt_givenSaveWithHistoricalNonOrderedTS() throws Exception { + if (databaseTsLatestType.equals("cassandra")) { + return; + } + save(tenantId, deviceId, toTsEntry(TS - 1, stringKvEntry)); save(tenantId, deviceId, toTsEntry(TS, stringKvEntry)); save(tenantId, deviceId, toTsEntry(TS - 10, stringKvEntry)); From 77a420b6d6aab641226c6cbea5b1a8bda17d8976 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 4 Jul 2024 19:02:19 +0200 Subject: [PATCH 033/136] fixed versioned caffeine cache --- .../server/cache/VersionedCaffeineTbCache.java | 11 +++++++---- .../thingsboard/server/common/data/util/TbPair.java | 7 ------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java index e084668b90..f2ac3c36d2 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java @@ -35,7 +35,11 @@ public abstract class VersionedCaffeineTbCache get(K key) { - return SimpleTbCacheValueWrapper.wrap(doGet(key).getSecond()); + TbPair versionValuePair = doGet(key); + if (versionValuePair != null) { + return SimpleTbCacheValueWrapper.wrap(versionValuePair.getSecond()); + } + return null; } @Override @@ -52,8 +56,7 @@ public abstract class VersionedCaffeineTbCache versionValuePair = doGet(key); - Long currentVersion = versionValuePair.getFirst(); - if (currentVersion == null || version > currentVersion) { + if (versionValuePair == null || version > versionValuePair.getFirst()) { cacheManager.getCache(cacheName).put(key, TbPair.of(version, value)); } } finally { @@ -63,7 +66,7 @@ public abstract class VersionedCaffeineTbCache doGet(K key) { Cache.ValueWrapper source = cacheManager.getCache(cacheName).get(key); - return source == null ? TbPair.emptyPair() : (TbPair) source.get(); + return source == null ? null : (TbPair) source.get(); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java index 315131bdad..6e8b2e5696 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java @@ -21,17 +21,10 @@ import lombok.Data; @Data @AllArgsConstructor public class TbPair { - public static final TbPair EMPTY = new TbPair<>(null, null); - private S first; private T second; public static TbPair of(S first, T second) { return new TbPair<>(first, second); } - - @SuppressWarnings("unchecked") - public static TbPair emptyPair() { - return (TbPair) EMPTY; - } } From cf821515fb215a9a6fb911f8a896019dacdc7413 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 9 Jul 2024 13:17:25 +0300 Subject: [PATCH 034/136] Refactoring for versioned caches --- .../cache/CaffeineTbCacheTransaction.java | 2 +- .../cache/CaffeineTbTransactionalCache.java | 25 +++++---- .../server/cache/RedisTbCacheTransaction.java | 5 +- .../cache/RedisTbTransactionalCache.java | 6 ++- .../server/cache/TbCacheTransaction.java | 2 +- .../server/cache/TbTransactionalCache.java | 4 +- .../cache/VersionedCaffeineTbCache.java | 29 ++++------ .../server/cache/VersionedRedisTbCache.java | 53 +++++++++---------- .../server/cache/VersionedTbCache.java | 3 +- .../resources/application-test.properties | 13 ++++- 10 files changed, 77 insertions(+), 65 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java index 54465b0b50..03d9099541 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java @@ -41,7 +41,7 @@ public class CaffeineTbCacheTransaction pendingPuts = new LinkedHashMap<>(); @Override - public void putIfAbsent(K key, V value) { + public void put(K key, V value) { pendingPuts.put(key, value); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java index 9c01b47b88..e0a1457d72 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java @@ -17,6 +17,7 @@ package org.thingsboard.server.cache; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import java.io.Serializable; @@ -26,6 +27,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.locks.Lock; @@ -34,17 +36,22 @@ import java.util.concurrent.locks.ReentrantLock; @RequiredArgsConstructor public abstract class CaffeineTbTransactionalCache implements TbTransactionalCache { - private final CacheManager cacheManager; @Getter - private final String cacheName; - - private final Lock lock = new ReentrantLock(); + protected final String cacheName; + protected final Cache cache; + protected final Lock lock = new ReentrantLock(); private final Map> objectTransactions = new HashMap<>(); private final Map> transactions = new HashMap<>(); + public CaffeineTbTransactionalCache(CacheManager cacheManager, String cacheName) { + this.cacheName = cacheName; + this.cache = Optional.ofNullable(cacheManager.getCache(cacheName)) + .orElseThrow(() -> new IllegalArgumentException("Cache '" + cacheName + "' is not configured")); + } + @Override public TbCacheValueWrapper get(K key) { - return SimpleTbCacheValueWrapper.wrap(cacheManager.getCache(cacheName).get(key)); + return SimpleTbCacheValueWrapper.wrap(cache.get(key)); } @Override @@ -52,7 +59,7 @@ public abstract class CaffeineTbTransactionalCache newTransaction(List keys) { @@ -181,7 +188,7 @@ public abstract class CaffeineTbTransactionalCache transactionsIds = objectTransactions.get(key); if (transactionsIds != null) { for (UUID otherTrId : transactionsIds) { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java index 0cb2d661db..fb852493ce 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java @@ -18,7 +18,6 @@ package org.thingsboard.server.cache; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisStringCommands; import java.io.Serializable; import java.util.Objects; @@ -31,8 +30,8 @@ public class RedisTbCacheTransaction { - void putIfAbsent(K key, V value); + void put(K key, V value); boolean commit(); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index be0b38b65e..7507ef99ff 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -68,7 +68,7 @@ public interface TbTransactionalCache implements VersionedTbCache { +public abstract class VersionedCaffeineTbCache extends CaffeineTbTransactionalCache implements VersionedTbCache { - private final CacheManager cacheManager; - private final String cacheName; - - private final Lock lock = new ReentrantLock(); + public VersionedCaffeineTbCache(CacheManager cacheManager, String cacheName) { + super(cacheManager, cacheName); + } @Override public TbCacheValueWrapper get(K key) { @@ -57,7 +52,8 @@ public abstract class VersionedCaffeineTbCache versionValuePair = doGet(key); if (versionValuePair == null || version > versionValuePair.getFirst()) { - cacheManager.getCache(cacheName).put(key, TbPair.of(version, value)); + failAllTransactionsByKey(key); + cache.put(key, TbPair.of(version, value)); } } finally { lock.unlock(); @@ -65,7 +61,7 @@ public abstract class VersionedCaffeineTbCache doGet(K key) { - Cache.ValueWrapper source = cacheManager.getCache(cacheName).get(key); + Cache.ValueWrapper source = cache.get(key); return source == null ? null : (TbPair) source.get(); } @@ -73,7 +69,8 @@ public abstract class VersionedCaffeineTbCache extends RedisTbTransactionalCache implements VersionedTbCache { @@ -47,7 +46,7 @@ public abstract class VersionedRedisTbCacheI8", newVersion) .. newValue redis.call('SET', key, newValueWithVersion, 'EX', expiration) end - + local function bytes_to_number(bytes) local n = 0 for i = 1, 8 do @@ -102,33 +101,44 @@ public abstract class VersionedRedisTbCache newTransactionForKey(K key) { - throw new NotImplementedException("newTransactionForKey is not supported by versioned cache"); - } - - @Override - public TbCacheTransaction newTransactionForKeys(List keys) { - throw new NotImplementedException("newTransactionForKeys is not supported by versioned cache"); - } - } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java index aaec7a47be..8fcfcff8d7 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java @@ -19,7 +19,7 @@ import org.thingsboard.server.common.data.HasVersion; import java.io.Serializable; -public interface VersionedTbCache { +public interface VersionedTbCache extends TbTransactionalCache { TbCacheValueWrapper get(K key); @@ -30,4 +30,5 @@ public interface VersionedTbCache Date: Tue, 9 Jul 2024 14:13:14 +0300 Subject: [PATCH 035/136] Fix Caffeine's getAndPutInTransaction; add compute method to VersionedTbCache --- .../server/cache/CaffeineTbCacheTransaction.java | 2 +- .../cache/CaffeineTbTransactionalCache.java | 4 ++-- .../server/cache/VersionedCaffeineTbCache.java | 11 ++++++++++- .../server/cache/VersionedRedisTbCache.java | 6 ------ .../server/cache/VersionedTbCache.java | 15 +++++++++++++++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java index 03d9099541..47778c96b6 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbCacheTransaction.java @@ -38,7 +38,7 @@ public class CaffeineTbCacheTransaction pendingPuts = new LinkedHashMap<>(); + private final Map pendingPuts = new LinkedHashMap<>(); @Override public void put(K key, V value) { diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java index e0a1457d72..d2ea960e68 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java @@ -116,7 +116,7 @@ public abstract class CaffeineTbTransactionalCache pendingPuts) { + public boolean commit(UUID trId, Map pendingPuts) { lock.lock(); try { var tr = transactions.get(trId); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java index a1f85611a7..86849586cc 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java @@ -53,7 +53,7 @@ public abstract class VersionedCaffeineTbCache versionValuePair = doGet(key); if (versionValuePair == null || version > versionValuePair.getFirst()) { failAllTransactionsByKey(key); - cache.put(key, TbPair.of(version, value)); + cache.put(key, wrapValue(value, version)); } } finally { lock.unlock(); @@ -84,4 +84,13 @@ public abstract class VersionedCaffeineTbCache wrapValue(V value, Long version) { + return TbPair.of(version, value); + } + } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java index b7722f90fd..bfb19ad01a 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java @@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.HasVersion; import java.io.Serializable; import java.util.Arrays; -import java.util.Collection; @Slf4j public abstract class VersionedRedisTbCache extends RedisTbTransactionalCache implements VersionedTbCache { @@ -156,11 +155,6 @@ public abstract class VersionedRedisTbCache keys) { - throw new NotImplementedException("evict by many keys is not supported by versioned cache"); - } - @Override public void evictOrPut(K key, V value) { throw new NotImplementedException("evictOrPut is not supported by versioned cache"); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java index 8fcfcff8d7..4d600bd363 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedTbCache.java @@ -18,17 +18,32 @@ package org.thingsboard.server.cache; import org.thingsboard.server.common.data.HasVersion; import java.io.Serializable; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Supplier; public interface VersionedTbCache extends TbTransactionalCache { TbCacheValueWrapper get(K key); + default V get(K key, Supplier supplier) { + return Optional.ofNullable(get(key)) + .map(TbCacheValueWrapper::get) + .orElseGet(() -> { + V value = supplier.get(); + put(key, value); + return value; + }); + } + void put(K key, V value); void put(K key, V value, Long version); void evict(K key); + void evict(Collection keys); + void evict(K key, Long version); } From d3fa7d912493ac7bba2dd167a2805a61d860067c Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 9 Jul 2024 14:43:25 +0300 Subject: [PATCH 036/136] OPC-UA UI fixes --- .../action-button-basic-config.component.html | 6 +- ...command-button-basic-config.component.html | 4 +- .../power-button-basic-config.component.html | 8 +- .../toggle-button-basic-config.component.html | 8 +- .../status-widget-basic-config.component.html | 4 +- .../single-switch-basic-config.component.html | 8 +- .../rpc/slider-basic-config.component.html | 6 +- .../mapping-data-keys-panel.component.html | 12 +- .../mapping-table.component.html | 6 +- .../mapping-table/mapping-table.component.ts | 3 +- .../server-config.component.html | 16 +-- .../server-config.component.scss | 8 -- .../workers-config-control.component.html | 8 +- .../dialog/mapping-dialog.component.html | 34 +++--- .../dialog/mapping-dialog.component.scss | 1 + .../dialog/mapping-dialog.component.ts | 7 +- .../lib/gateway/gateway-widget.models.ts | 8 ++ ...tion-button-widget-settings.component.html | 4 +- ...mand-button-widget-settings.component.html | 4 +- ...ower-button-widget-settings.component.html | 8 +- ...ggle-button-widget-settings.component.html | 8 +- ...value-action-settings-panel.component.html | 2 +- ...tatus-widget-state-settings.component.html | 8 +- ...ngle-switch-widget-settings.component.html | 8 +- .../slider-widget-settings.component.html | 6 +- .../status-widget-settings.component.html | 4 +- .../widget/widget-config.component.html | 4 +- .../hint-tooltip-icon.component.html | 14 ++- .../hint-tooltip-icon.component.scss | 34 +++--- .../src/app/shared/directives/public-api.ts | 17 +++ .../directives/tooltip/tooltip.directive.ts | 108 ++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 2 + 32 files changed, 259 insertions(+), 119 deletions(-) create mode 100644 ui-ngx/src/app/shared/directives/public-api.ts create mode 100644 ui-ngx/src/app/shared/directives/tooltip/tooltip.directive.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html index 6940cffc1f..a3030dec79 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html @@ -27,7 +27,7 @@
widgets.action-button.behavior
-
widgets.action-button.on-click
+
{{ 'widgets.action-button.on-click' | translate}}
-
widgets.button-state.activated-state
+
{{ 'widgets.button-state.activated-state' | translate }}
-
widgets.button-state.disabled-state
+
{{ 'widgets.button-state.disabled-state' | translate }}
widgets.command-button.behavior
-
widgets.command-button.on-click
+
{{ 'widgets.command-button.on-click' | translate }}
-
widgets.button-state.disabled-state
+
{{ 'widgets.button-state.disabled-state' | translate }}
widgets.power-button.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.power-button.power-on
+
{{ 'widgets.power-button.power-on' | translate }}
-
widgets.power-button.power-off
+
{{ 'widgets.power-button.power-off' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widgets.toggle-button.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.toggle-button.check
+
{{ 'widgets.toggle-button.check' | translate }}
-
widgets.toggle-button.uncheck
+
{{ 'widgets.toggle-button.uncheck' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widgets.status-widget.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widgets.single-switch.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.rpc-state.turn-on
+
{{ 'widgets.rpc-state.turn-on' | translate }}
-
widgets.rpc-state.turn-off
+
{{ 'widgets.rpc-state.turn-off' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widgets.slider.behavior
-
widgets.slider.initial-value
+
{{ 'widgets.slider.initial-value' | translate }}
-
widgets.slider.on-value-change
+
{{ 'widgets.slider.on-value-change' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
gateway.platform-side
- gateway.key + tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}"> + {{ 'gateway.key' | translate }}
@@ -97,8 +97,8 @@
- gateway.value + tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}"> + {{ 'gateway.value' | translate }}
-
- gateway.method-name +
+ {{ 'gateway.method-name' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html index 48e280bc67..7ebfb700bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html @@ -61,11 +61,11 @@ + [class.request-column]="mappingType === mappingTypeEnum.REQUESTS"> {{ column.title | translate }} - + {{ mapping[column.def] }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index 7042e5ae38..3e4ce09031 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts @@ -58,6 +58,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; +import { TooltipDirective } from '@shared/directives/public-api'; @Component({ selector: 'tb-mapping-table', @@ -77,7 +78,7 @@ import { CommonModule } from '@angular/common'; } ], standalone: true, - imports: [CommonModule, SharedModule] + imports: [CommonModule, SharedModule, TooltipDirective] }) export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html index 43b15de06c..5182fb108e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html @@ -18,7 +18,7 @@
-
gateway.server-url
+
gateway.server-url
@@ -35,8 +35,8 @@
-
- gateway.timeout +
+ {{ 'gateway.timeout' | translate }}
@@ -55,7 +55,7 @@
-
gateway.security
+
gateway.security
@@ -65,8 +65,8 @@
-
- gateway.scan-period +
+ {{ 'gateway.scan-period' | translate }}
@@ -86,8 +86,8 @@
-
- gateway.sub-check-period +
+ {{ 'gateway.sub-check-period' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss index 64e886ffef..416f368279 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss @@ -17,12 +17,4 @@ width: 100%; height: 100%; display: block; - - .server-conf-field-title { - min-width: 250px; - width: 30%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html index 37f7b1422e..0062e476c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html @@ -18,8 +18,8 @@
- gateway.max-number-of-workers + tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}"> + {{ 'gateway.max-number-of-workers' | translate }}
@@ -40,8 +40,8 @@
- gateway.max-messages-queue-for-worker + tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}"> + {{ 'gateway.max-messages-queue-for-worker' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html index 35ac90f671..17c002115c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html @@ -19,7 +19,7 @@

{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}

-
+
-
- gateway.mqtt-qos +
+ {{ 'gateway.mqtt-qos' | translate }}
@@ -140,8 +140,8 @@
- gateway.extension + tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}"> + {{ 'gateway.extension' | translate }}
@@ -369,8 +369,8 @@
- gateway.device-name-filter + tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}"> + {{ 'gateway.device-name-filter' | translate }}
@@ -388,8 +388,8 @@
-
- gateway.attribute-filter +
+ {{ 'gateway.attribute-filter' | translate }}
@@ -472,8 +472,8 @@
-
- gateway.device-name-filter +
+ {{ 'gateway.device-name-filter' | translate }}
@@ -491,8 +491,8 @@
-
- gateway.method-filter +
+ {{ 'gateway.method-filter' | translate }}
@@ -580,8 +580,8 @@
-
- gateway.response-topic-Qos +
+ {{ 'gateway.response-topic-Qos' | translate }}
@@ -618,8 +618,8 @@
-
- gateway.device-node +
+ {{ 'gateway.device-node' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss index 190422eeb0..4212e8f834 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss @@ -47,6 +47,7 @@ .mdc-evolution-chip-set__chips { justify-content: flex-end; align-items: center; + flex-wrap: nowrap; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index 3812fa161e..0e177df527 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -28,6 +28,7 @@ import { ConvertorTypeTranslationsMap, DataConversionTranslationsMap, DeviceInfoType, + HelpLinkByMappingTypeMap, MappingHintTranslationsMap, MappingInfo, MappingKeysAddKeyTranslationsMap, @@ -97,6 +98,8 @@ export class MappingDialogComponent extends DialogComponent(); @@ -187,10 +190,6 @@ export class MappingDialogComponent extends DialogComponent( ] ); +export const HelpLinkByMappingTypeMap = new Map( + [ + [MappingType.DATA, 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping'], + [MappingType.OPCUA, 'https://thingsboard.io/docs/iot-gateway/config/opc-ua/#section-mapping'], + [MappingType.REQUESTS, 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping'] + ] +); + export const QualityTypes = [0, 1 ,2]; export const QualityTypeTranslationsMap = new Map( diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html index 265a9c2501..a8cdf55efc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html @@ -19,7 +19,7 @@
widgets.action-button.behavior
-
widgets.button-state.activated-state
+
{{ 'widgets.button-state.activated-state' | translate }}
-
widgets.button-state.disabled-state
+
{{ 'widgets.button-state.disabled-state' | translate }}
widgets.command-button.behavior
-
widgets.command-button.on-click
+
{{ 'widgets.command-button.on-click' | translate }}
-
widgets.button-state.disabled-state
+
{{ 'widgets.button-state.disabled-state' | translate }}
widgets.power-button.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.power-button.power-on
+
{{ 'widgets.power-button.power-on' | translate }}
-
widgets.power-button.power-off
+
{{ 'widgets.power-button.power-off' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widgets.toggle-button.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.toggle-button.check
+
{{ 'widgets.toggle-button.check' | translate }}
-
widgets.toggle-button.uncheck
+
{{ 'widgets.toggle-button.uncheck' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
-
widgets.value-action.value
+
{{ 'widgets.value-action.value' | translate }}
-
widgets.status-widget.primary
+
{{ 'widgets.status-widget.primary' | translate }}
-
widgets.status-widget.secondary
+
{{ 'widgets.status-widget.secondary' | translate }}
@@ -88,14 +88,14 @@ [fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'" style="gap: 12px;">
-
widgets.status-widget.primary
+
{{ 'widgets.status-widget.primary' | translate }}
-
widgets.status-widget.secondary
+
{{ 'widgets.status-widget.secondary' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html index 9208efd68a..d5b732b6bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html @@ -19,7 +19,7 @@
widgets.single-switch.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.rpc-state.turn-on
+
{{ 'widgets.rpc-state.turn-on' | translate }}
-
widgets.rpc-state.turn-off
+
{{ 'widgets.rpc-state.turn-off' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widgets.slider.behavior
-
widgets.slider.initial-value
+
{{ 'widgets.slider.initial-value' | translate }}
-
widgets.slider.on-value-change
+
{{ 'widgets.slider.on-value-change' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widgets.status-widget.behavior
-
widgets.rpc-state.initial-state
+
{{ 'widgets.rpc-state.initial-state' | translate }}
-
widgets.rpc-state.disabled-state
+
{{ 'widgets.rpc-state.disabled-state' | translate }}
widget-config.data-settings
-
widget-config.units-by-default
+
{{ 'widget-config.units-by-default' | translate }}
-
widget-config.decimals-by-default
+
{{ 'widget-config.decimals-by-default' | translate }}
diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html index 32b66fd2b7..20593f042b 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html @@ -15,8 +15,12 @@ limitations under the License. --> - -{{ hintIcon }} +
+
+ +
+ {{ hintIcon }} +
diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss index 7ed9925300..fb428e9fc3 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss @@ -13,21 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host(.tb-hint-tooltip) { - display: flex; - flex-direction: row; - align-items: center; - gap: 4px; -} :host { - .tb-hint-tooltip-icon { - color: #E0E0E0; - overflow: visible; - order: 3; - margin-left: 4px; - &:hover { - color: #9E9E9E; + .tb-hint-tooltip { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + + &-content { + flex: 1 1 auto; + min-width: 0; + } + + &-icon { + color: #E0E0E0; + overflow: visible; + order: 3; + margin-left: 4px; + flex-shrink: 0; + + &:hover { + color: #9E9E9E; + } } } } diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts new file mode 100644 index 0000000000..a82428fc19 --- /dev/null +++ b/ui-ngx/src/app/shared/directives/public-api.ts @@ -0,0 +1,17 @@ +/// +/// Copyright © 2016-2024 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. +/// + +export * from './tooltip/tooltip.directive'; diff --git a/ui-ngx/src/app/shared/directives/tooltip/tooltip.directive.ts b/ui-ngx/src/app/shared/directives/tooltip/tooltip.directive.ts new file mode 100644 index 0000000000..4d26c1388c --- /dev/null +++ b/ui-ngx/src/app/shared/directives/tooltip/tooltip.directive.ts @@ -0,0 +1,108 @@ +/// +/// Copyright © 2016-2024 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 { + AfterViewInit, + Directive, + ElementRef, + inject, + Input, + OnDestroy, + OnInit, + Renderer2, +} from '@angular/core'; +import { fromEvent, Subject } from 'rxjs'; +import { filter, takeUntil, tap } from 'rxjs/operators'; +import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; + +@Directive({ + standalone: true, + selector: '[tbTruncateTooltip]', + providers: [MatTooltip], + +}) +export class TooltipDirective implements OnInit, AfterViewInit, OnDestroy { + @Input('tbTruncateTooltip') text: string; + @Input() tooltipEnabled = true; + @Input() position: TooltipPosition = 'above'; + + private elementRef = inject(ElementRef); + private renderer = inject(Renderer2); + private tooltip = inject(MatTooltip); + private destroy$ = new Subject(); + + ngOnInit(): void { + this.observeMouseEvents(); + } + + ngAfterViewInit(): void { + this.applyTruncationStyles(); + + if (!this.text) { + this.text = this.elementRef.nativeElement.innerText; + } + + this.tooltip.position = this.position; + } + + ngOnDestroy(): void { + if (this.tooltip._isTooltipVisible()) { + this.hideTooltip(); + } + this.destroy$.next(); + this.destroy$.complete(); + } + + private observeMouseEvents(): void { + fromEvent(this.elementRef.nativeElement, 'mouseenter') + .pipe( + filter(() => this.tooltipEnabled), + filter(() => this.isOverflown(this.elementRef.nativeElement)), + tap(() => this.showTooltip()), + takeUntil(this.destroy$), + ) + .subscribe(); + fromEvent(this.elementRef.nativeElement, 'mouseleave') + .pipe( + filter(() => this.tooltipEnabled), + filter(() => this.tooltip._isTooltipVisible()), + tap(() => this.hideTooltip()), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + private applyTruncationStyles(): void { + this.renderer.setStyle(this.elementRef.nativeElement, 'white-space', 'nowrap'); + this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'hidden'); + this.renderer.setStyle(this.elementRef.nativeElement, 'text-overflow', 'ellipsis'); + } + + private isOverflown(element: HTMLElement): boolean { + return element.clientWidth < element.scrollWidth; + } + + private showTooltip(): void { + this.tooltip.message = this.text; + + this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text); + this.tooltip.show(); + } + + private hideTooltip(): void { + this.tooltip.hide(); + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 567b80ed83..aa74c96f29 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -219,6 +219,7 @@ import { ImageGalleryDialogComponent } from '@shared/components/image/image-gall import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component'; import { WidgetButtonComponent } from '@shared/components/button/widget-button.component'; import { HexInputComponent } from '@shared/components/color-picker/hex-input.component'; +import { TooltipDirective } from '@shared/directives/public-api'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -472,6 +473,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) DndModule, NgxFlowModule, NgxFlowchartModule, + TooltipDirective, // ngx-markdown MarkdownModule.forRoot({ sanitize: SecurityContext.NONE, From 474162bb052b15687ef8ca479a785382978db6ab Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 9 Jul 2024 15:38:16 +0300 Subject: [PATCH 037/136] OPC-UA Data mapping crushing on creating empty connector fix --- .../mapping-table/mapping-table.component.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index 7042e5ae38..51847d2c70 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts @@ -33,6 +33,7 @@ import { debounceTime, distinctUntilChanged, map, take, takeUntil } from 'rxjs/o import { ControlValueAccessor, FormBuilder, + FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormArray, @@ -206,13 +207,26 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A if (isDefinedAndNotNull(index)) { this.mappingFormGroup.at(index).patchValue(res); } else { - this.mappingFormGroup.push(this.fb.group(res)); + this.mappingFormGroup.push(this.getMappedDialogDataFormGroup(res)); } this.mappingFormGroup.markAsDirty(); } }); } + private getMappedDialogDataFormGroup(mappingValue: MappingValue): FormGroup { + Object.keys(mappingValue).forEach(key => { + if (Array.isArray(mappingValue[key])) { + mappingValue = { + ...mappingValue, + [key]: this.fb.control(mappingValue[key]), + }; + } + }); + + return this.fb.group(mappingValue); + } + private updateTableData(value: ConnectorMapping[], textSearch?: string): void { let tableValue = value.map(mappingValue => this.getMappingValue(mappingValue)); if (textSearch) { From d6367c9680188b213826e3dcf252776ae90f0131 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 9 Jul 2024 15:19:01 +0200 Subject: [PATCH 038/136] added version to timeseries and attribute protos --- .../cache/VersionedCaffeineTbCache.java | 7 +- .../server/cache/VersionedRedisTbCache.java | 18 +-- .../server/cache/VersionedTbCache.java | 2 - .../common/data/kv/BaseAttributeKvEntry.java | 39 +----- .../server/common/data/kv/BasicTsKvEntry.java | 34 +----- .../server/common/util/KvProtoUtil.java | 9 +- .../server/common/util/ProtoUtils.java | 94 ++++++++------- common/proto/src/main/proto/queue.proto | 2 + .../dao/attributes/AttributeRedisCache.java | 54 +-------- .../attributes/CachedAttributesService.java | 22 ++-- .../CachedRedisSqlTimeseriesLatestDao.java | 12 +- .../dao/timeseries/TsLatestRedisCache.java | 2 +- .../attributes/BaseAttributesServiceTest.java | 23 ++-- .../sql/AttributeCacheServiceSqlTest.java | 114 ++++++++++++++++++ .../timeseries/BaseTimeseriesServiceTest.java | 22 +++- 15 files changed, 253 insertions(+), 201 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/attributes/sql/AttributeCacheServiceSqlTest.java diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java index f2ac3c36d2..8ed4928d97 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedCaffeineTbCache.java @@ -45,11 +45,10 @@ public abstract class VersionedCaffeineTbCacheI8", newVersion) .. newValue redis.call('SET', key, newValueWithVersion, 'EX', expiration) end - + local function bytes_to_number(bytes) local n = 0 for i = 1, 8 do @@ -96,13 +96,15 @@ public abstract class VersionedRedisTbCache>> 32)); - result = 31 * result + kv.hashCode(); - return result; - } - - @Override - public String toString() { - return "BaseAttributeKvEntry{" + - "lastUpdateTs=" + lastUpdateTs + - ", kv=" + kv + - '}'; - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java index ab39498ae0..82f96ea966 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java @@ -16,9 +16,12 @@ package org.thingsboard.server.common.data.kv; import jakarta.validation.Valid; +import lombok.Data; + import java.util.Objects; import java.util.Optional; +@Data public class BasicTsKvEntry implements TsKvEntry { private static final int MAX_CHARS_PER_DATA_POINT = 512; protected final long ts; @@ -79,33 +82,6 @@ public class BasicTsKvEntry implements TsKvEntry { return kv.getValue(); } - @Override - public long getTs() { - return ts; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof BasicTsKvEntry)) return false; - BasicTsKvEntry that = (BasicTsKvEntry) o; - return getTs() == that.getTs() && - Objects.equals(kv, that.kv); - } - - @Override - public int hashCode() { - return Objects.hash(getTs(), kv); - } - - @Override - public String toString() { - return "BasicTsKvEntry{" + - "ts=" + ts + - ", kv=" + kv + - '}'; - } - @Override public String getValueAsString() { return kv.getValueAsString(); @@ -127,8 +103,4 @@ public class BasicTsKvEntry implements TsKvEntry { return Math.max(1, (length + MAX_CHARS_PER_DATA_POINT - 1) / MAX_CHARS_PER_DATA_POINT); } - @Override - public Long getVersion() { - return version; - } } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java b/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java index 74674e1e45..4b206fd804 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java @@ -86,8 +86,15 @@ public class KvProtoUtil { .setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)).build(); } + public static TransportProtos.TsKvProto toTsKvProto(long ts, KvEntry kvEntry, long version) { + return TransportProtos.TsKvProto.newBuilder() + .setTs(ts) + .setVersion(version) + .setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)).build(); + } + public static TsKvEntry fromTsKvProto(TransportProtos.TsKvProto proto) { - return new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv())); + return new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv()), proto.getVersion()); } public static TransportProtos.KeyValueProto toKeyValueTypeProto(KvEntry kvEntry) { diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index e3d587e915..db1c1fe2e5 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -17,7 +17,9 @@ package org.thingsboard.server.common.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; @@ -256,42 +258,47 @@ public class ProtoUtils { if (msg.getValues() != null) { for (AttributeKvEntry attributeKvEntry : msg.getValues()) { - TransportProtos.AttributeValueProto.Builder attributeValueBuilder = TransportProtos.AttributeValueProto.newBuilder() - .setLastUpdateTs(attributeKvEntry.getLastUpdateTs()) - .setKey(attributeKvEntry.getKey()); - switch (attributeKvEntry.getDataType()) { - case BOOLEAN -> { - attributeKvEntry.getBooleanValue().ifPresent(attributeValueBuilder::setBoolV); - attributeValueBuilder.setHasV(attributeKvEntry.getBooleanValue().isPresent()); - attributeValueBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V); - } - case STRING -> { - attributeKvEntry.getStrValue().ifPresent(attributeValueBuilder::setStringV); - attributeValueBuilder.setHasV(attributeKvEntry.getStrValue().isPresent()); - attributeValueBuilder.setType(TransportProtos.KeyValueType.STRING_V); - } - case DOUBLE -> { - attributeKvEntry.getDoubleValue().ifPresent(attributeValueBuilder::setDoubleV); - attributeValueBuilder.setHasV(attributeKvEntry.getDoubleValue().isPresent()); - attributeValueBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V); - } - case LONG -> { - attributeKvEntry.getLongValue().ifPresent(attributeValueBuilder::setLongV); - attributeValueBuilder.setHasV(attributeKvEntry.getLongValue().isPresent()); - attributeValueBuilder.setType(TransportProtos.KeyValueType.LONG_V); - } - case JSON -> { - attributeKvEntry.getJsonValue().ifPresent(attributeValueBuilder::setJsonV); - attributeValueBuilder.setHasV(attributeKvEntry.getJsonValue().isPresent()); - attributeValueBuilder.setType(TransportProtos.KeyValueType.JSON_V); - } - } - builder.addValues(attributeValueBuilder.build()); + builder.addValues(toProto(attributeKvEntry)); } } return builder.build(); } + public static TransportProtos.AttributeValueProto toProto(AttributeKvEntry attributeKvEntry) { + TransportProtos.AttributeValueProto.Builder builder = TransportProtos.AttributeValueProto.newBuilder() + .setLastUpdateTs(attributeKvEntry.getLastUpdateTs()) + .setKey(attributeKvEntry.getKey()); + switch (attributeKvEntry.getDataType()) { + case BOOLEAN: + attributeKvEntry.getBooleanValue().ifPresent(builder::setBoolV); + builder.setHasV(attributeKvEntry.getBooleanValue().isPresent()); + builder.setType(TransportProtos.KeyValueType.BOOLEAN_V); + break; + case STRING: + attributeKvEntry.getStrValue().ifPresent(builder::setStringV); + builder.setHasV(attributeKvEntry.getStrValue().isPresent()); + builder.setType(TransportProtos.KeyValueType.STRING_V); + break; + case DOUBLE: + attributeKvEntry.getDoubleValue().ifPresent(builder::setDoubleV); + builder.setHasV(attributeKvEntry.getDoubleValue().isPresent()); + builder.setType(TransportProtos.KeyValueType.DOUBLE_V); + break; + case LONG: + attributeKvEntry.getLongValue().ifPresent(builder::setLongV); + builder.setHasV(attributeKvEntry.getLongValue().isPresent()); + builder.setType(TransportProtos.KeyValueType.LONG_V); + break; + case JSON: + attributeKvEntry.getJsonValue().ifPresent(builder::setJsonV); + builder.setHasV(attributeKvEntry.getJsonValue().isPresent()); + builder.setType(TransportProtos.KeyValueType.JSON_V); + break; + } + builder.setVersion(attributeKvEntry.getVersion()); + return builder.build(); + } + private static ToDeviceActorNotificationMsg fromProto(TransportProtos.DeviceAttributesEventMsgProto proto) { return new DeviceAttributesEventNotificationMsg( TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), @@ -500,20 +507,25 @@ public class ProtoUtils { } List result = new ArrayList<>(); for (TransportProtos.AttributeValueProto kvEntry : valuesList) { - boolean hasValue = kvEntry.getHasV(); - KvEntry entry = switch (kvEntry.getType()) { - case BOOLEAN_V -> new BooleanDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getBoolV() : null); - case LONG_V -> new LongDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getLongV() : null); - case DOUBLE_V -> new DoubleDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getDoubleV() : null); - case STRING_V -> new StringDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getStringV() : null); - case JSON_V -> new JsonDataEntry(kvEntry.getKey(), hasValue ? kvEntry.getJsonV() : null); - default -> null; - }; - result.add(new BaseAttributeKvEntry(kvEntry.getLastUpdateTs(), entry)); + result.add(fromProto(kvEntry)); } return result; } + public static AttributeKvEntry fromProto(TransportProtos.AttributeValueProto proto) { + boolean hasValue = proto.getHasV(); + String key = proto.getKey(); + KvEntry entry = switch (proto.getType()) { + case BOOLEAN_V -> new BooleanDataEntry(key, hasValue ? proto.getBoolV() : null); + case LONG_V -> new LongDataEntry(key, hasValue ? proto.getLongV() : null); + case DOUBLE_V -> new DoubleDataEntry(key, hasValue ? proto.getDoubleV() : null); + case STRING_V -> new StringDataEntry(key, hasValue ? proto.getStringV() : null); + case JSON_V -> new JsonDataEntry(key, hasValue ? proto.getJsonV() : null); + default -> null; + }; + return new BaseAttributeKvEntry(entry, proto.getLastUpdateTs(), proto.getVersion()); + } + public static TransportProtos.DeviceProto toProto(Device device) { var builder = TransportProtos.DeviceProto.newBuilder() .setTenantIdMSB(device.getTenantId().getId().getMostSignificantBits()) diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 581b993120..9b93812708 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -157,11 +157,13 @@ message AttributeValueProto { string string_v = 7; string json_v = 8; optional string key = 9; + int64 version = 10; } message TsKvProto { int64 ts = 1; KeyValueProto kv = 2; + int64 version = 3; } message TsKvListProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java index a6f21f9c3a..2b182c2b92 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeRedisCache.java @@ -26,15 +26,8 @@ import org.thingsboard.server.cache.TbRedisSerializer; import org.thingsboard.server.cache.VersionedRedisTbCache; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; -import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("AttributeCache") @@ -44,54 +37,13 @@ public class AttributeRedisCache extends VersionedRedisTbCache() { @Override public byte[] serialize(AttributeKvEntry attributeKvEntry) throws SerializationException { - AttributeValueProto.Builder builder = AttributeValueProto.newBuilder() - .setLastUpdateTs(attributeKvEntry.getLastUpdateTs()); - switch (attributeKvEntry.getDataType()) { - case BOOLEAN: - attributeKvEntry.getBooleanValue().ifPresent(builder::setBoolV); - builder.setHasV(attributeKvEntry.getBooleanValue().isPresent()); - builder.setType(KeyValueType.BOOLEAN_V); - break; - case STRING: - attributeKvEntry.getStrValue().ifPresent(builder::setStringV); - builder.setHasV(attributeKvEntry.getStrValue().isPresent()); - builder.setType(KeyValueType.STRING_V); - break; - case DOUBLE: - attributeKvEntry.getDoubleValue().ifPresent(builder::setDoubleV); - builder.setHasV(attributeKvEntry.getDoubleValue().isPresent()); - builder.setType(KeyValueType.DOUBLE_V); - break; - case LONG: - attributeKvEntry.getLongValue().ifPresent(builder::setLongV); - builder.setHasV(attributeKvEntry.getLongValue().isPresent()); - builder.setType(KeyValueType.LONG_V); - break; - case JSON: - attributeKvEntry.getJsonValue().ifPresent(builder::setJsonV); - builder.setHasV(attributeKvEntry.getJsonValue().isPresent()); - builder.setType(KeyValueType.JSON_V); - break; - - } - return builder.build().toByteArray(); + return ProtoUtils.toProto(attributeKvEntry).toByteArray(); } @Override public AttributeKvEntry deserialize(AttributeCacheKey key, byte[] bytes) throws SerializationException { try { - AttributeValueProto proto = AttributeValueProto.parseFrom(bytes); - boolean hasValue = proto.getHasV(); - KvEntry entry = switch (proto.getType()) { - case BOOLEAN_V -> new BooleanDataEntry(key.getKey(), hasValue ? proto.getBoolV() : null); - case LONG_V -> new LongDataEntry(key.getKey(), hasValue ? proto.getLongV() : null); - case DOUBLE_V -> new DoubleDataEntry(key.getKey(), hasValue ? proto.getDoubleV() : null); - case STRING_V -> new StringDataEntry(key.getKey(), hasValue ? proto.getStringV() : null); - case JSON_V -> new JsonDataEntry(key.getKey(), hasValue ? proto.getJsonV() : null); - default -> - throw new InvalidProtocolBufferException("Unrecognized type: " + proto.getType() + " !"); - }; - return new BaseAttributeKvEntry(proto.getLastUpdateTs(), entry); + return ProtoUtils.fromProto(AttributeValueProto.parseFrom(bytes)); } catch (InvalidProtocolBufferException e) { throw new SerializationException(e.getMessage()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index d31ff9db79..3de90355ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsFactory; @@ -159,7 +160,7 @@ public class CachedAttributesService implements AttributesService { log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys); List result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); for (AttributeKvEntry foundInDbAttribute : result) { - put(entityId, scope, foundInDbAttribute, foundInDbAttribute.getVersion()); + put(entityId, scope, foundInDbAttribute); notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); } for (String key : notFoundAttributeKeys) { @@ -218,8 +219,7 @@ public class CachedAttributesService implements AttributesService { public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); AttributeUtils.validate(attribute, valueNoXssValidation); - ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - return Futures.transform(future, version -> put(entityId, scope, attribute, version), cacheExecutor); + return doSave(tenantId, entityId, scope, attribute); } @Override @@ -234,19 +234,25 @@ public class CachedAttributesService implements AttributesService { List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { - ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); - futures.add(Futures.transform(future, version -> put(entityId, scope, attribute, version), cacheExecutor)); + futures.add(doSave(tenantId, entityId, scope, attribute)); } return Futures.allAsList(futures); } - private Long put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute, Long version) { + private ListenableFuture doSave(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { + ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); + return Futures.transform(future, version -> { + put(entityId, scope, new BaseAttributeKvEntry(((BaseAttributeKvEntry)attribute).getKv(), attribute.getLastUpdateTs(), version)); + return version; + }, cacheExecutor); + } + + private void put(EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { String key = attribute.getKey(); log.trace("[{}][{}][{}] Before cache put: {}", entityId, scope, key, attribute); - cache.put(new AttributeCacheKey(scope, entityId, key), attribute, version); + cache.put(new AttributeCacheKey(scope, entityId, key), attribute); log.trace("[{}][{}][{}] after cache put.", entityId, scope, key); - return version; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index 42dc149342..0be078a89f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -29,6 +29,7 @@ import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; @@ -66,9 +67,9 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries @Override public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { ListenableFuture future = sqlDao.saveLatest(tenantId, entityId, tsKvEntry); - future = Futures.transform(future, x -> { - cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), tsKvEntry, x); - return x; + future = Futures.transform(future, version -> { + cache.put(new TsLatestCacheKey(entityId, tsKvEntry.getKey()), new BasicTsKvEntry(tsKvEntry.getTs(), ((BasicTsKvEntry) tsKvEntry).getKv(), version)); + return version; }, cacheExecutorService); if (log.isTraceEnabled()) { @@ -94,8 +95,9 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries if (x.isRemoved()) { TsLatestCacheKey key = new TsLatestCacheKey(entityId, query.getKey()); Long version = x.getVersion(); - if (x.getData() != null) { - cache.put(key, x.getData(), version); + TsKvEntry newTsKvEntry = x.getData(); + if (newTsKvEntry != null) { + cache.put(key, new BasicTsKvEntry(newTsKvEntry.getTs(), ((BasicTsKvEntry) newTsKvEntry).getKv(), version)); } else { cache.evict(key, version); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java index 851d66ae0c..241132b4c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsLatestRedisCache.java @@ -39,7 +39,7 @@ public class TsLatestRedisCache extends VersionedRedisTbCache() { @Override public byte[] serialize(TsKvEntry tsKvEntry) throws SerializationException { - return KvProtoUtil.toTsKvProto(tsKvEntry.getTs(), tsKvEntry).toByteArray(); + return KvProtoUtil.toTsKvProto(tsKvEntry.getTs(), tsKvEntry, tsKvEntry.getVersion()).toByteArray(); } @Override diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index 0603f4e3f6..2d648f9db3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -26,7 +26,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -34,7 +33,6 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.attributes.AttributeCacheKey; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -74,7 +72,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { attributesService.save(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, Collections.singletonList(attr)).get(); Optional saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attr.getKey()).get(); Assert.assertTrue(saved.isPresent()); - Assert.assertEquals(attr, saved.get()); + equalsIgnoreVersion(attr, saved.get()); } @Test @@ -87,14 +85,15 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { Optional saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attrOld.getKey()).get(); Assert.assertTrue(saved.isPresent()); - Assert.assertEquals(attrOld, saved.get()); + equalsIgnoreVersion(attrOld, saved.get()); KvEntry attrNewValue = new StringDataEntry("attribute1", "value2"); AttributeKvEntry attrNew = new BaseAttributeKvEntry(attrNewValue, 73L); attributesService.save(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, Collections.singletonList(attrNew)).get(); saved = attributesService.find(SYSTEM_TENANT_ID, deviceId, AttributeScope.CLIENT_SCOPE, attrOld.getKey()).get(); - Assert.assertEquals(attrNew, saved.get()); + Assert.assertTrue(saved.isPresent()); + equalsIgnoreVersion(attrNew, saved.get()); } @Test @@ -117,8 +116,8 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { Assert.assertNotNull(saved); Assert.assertEquals(2, saved.size()); - Assert.assertEquals(attrANew, saved.get(0)); - Assert.assertEquals(attrBNew, saved.get(1)); + equalsIgnoreVersion(attrANew, saved.get(0)); + equalsIgnoreVersion(attrBNew, saved.get(1)); } @Test @@ -253,6 +252,11 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { })); futures.add(pool.submit(() -> saveAttribute(tenantId, deviceId, scope, key, NEW_VALUE))); Futures.allAsList(futures).get(10, TimeUnit.SECONDS); + + String attributeValue = getAttributeValue(tenantId, deviceId, scope, key); + if (!NEW_VALUE.equals(attributeValue)) { + System.out.println(); + } Assert.assertEquals(NEW_VALUE, getAttributeValue(tenantId, deviceId, scope, key)); } @@ -309,5 +313,10 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { } } + private void equalsIgnoreVersion(AttributeKvEntry expected, AttributeKvEntry actual) { + Assert.assertEquals(expected.getKey(), actual.getKey()); + Assert.assertEquals(expected.getValue(), actual.getValue()); + Assert.assertEquals(expected.getLastUpdateTs(), actual.getLastUpdateTs()); + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/sql/AttributeCacheServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/sql/AttributeCacheServiceSqlTest.java new file mode 100644 index 0000000000..95e3e15adc --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/sql/AttributeCacheServiceSqlTest.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.attributes.sql; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.cache.VersionedTbCache; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.attributes.AttributeCacheKey; +import org.thingsboard.server.dao.service.AbstractServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@DaoSqlTest +public class AttributeCacheServiceSqlTest extends AbstractServiceTest { + + private static final String TEST_KEY = "key"; + private static final String TEST_VALUE = "value"; + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + + @Autowired + VersionedTbCache cache; + + @Test + public void testPutAndGet() { + AttributeCacheKey testKey = new AttributeCacheKey(AttributeScope.CLIENT_SCOPE, DEVICE_ID, TEST_KEY); + AttributeKvEntry testValue = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 1L); + cache.put(testKey, testValue); + + TbCacheValueWrapper wrapper = cache.get(testKey); + assertNotNull(wrapper); + + assertEquals(testValue, wrapper.get()); + + AttributeKvEntry testValue2 = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 2L); + cache.put(testKey, testValue2); + + wrapper = cache.get(testKey); + assertNotNull(wrapper); + + assertEquals(testValue2, wrapper.get()); + + AttributeKvEntry testValue3 = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 0L); + cache.put(testKey, testValue3); + + wrapper = cache.get(testKey); + assertNotNull(wrapper); + + assertEquals(testValue2, wrapper.get()); + + cache.evict(testKey); + } + + @Test + public void testEvictWithVersion() { + AttributeCacheKey testKey = new AttributeCacheKey(AttributeScope.CLIENT_SCOPE, DEVICE_ID, TEST_KEY); + AttributeKvEntry testValue = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 1L); + cache.put(testKey, testValue); + + TbCacheValueWrapper wrapper = cache.get(testKey); + assertNotNull(wrapper); + + assertEquals(testValue, wrapper.get()); + + cache.evict(testKey, 2L); + + wrapper = cache.get(testKey); + assertNotNull(wrapper); + + assertNull(wrapper.get()); + + cache.evict(testKey); + } + + @Test + public void testEvict() { + AttributeCacheKey testKey = new AttributeCacheKey(AttributeScope.CLIENT_SCOPE, DEVICE_ID, TEST_KEY); + AttributeKvEntry testValue = new BaseAttributeKvEntry(new StringDataEntry(TEST_KEY, TEST_VALUE), 1, 1L); + cache.put(testKey, testValue); + + TbCacheValueWrapper wrapper = cache.get(testKey); + assertNotNull(wrapper); + + assertEquals(testValue, wrapper.get()); + + cache.evict(testKey); + + wrapper = cache.get(testKey); + assertNull(wrapper); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index e4cc64d47c..443052240a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -137,7 +137,12 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { toTsEntry(TS, booleanKvEntry)); Collections.sort(expected, Comparator.comparing(KvEntry::getKey)); - assertEquals(expected, tsList); + for (int i = 0; i < expected.size(); i++) { + var expectedEntry = expected.get(i); + var actualEntry = tsList.get(i); + equalsIgnoreVersion(expectedEntry, actualEntry); + + } } private EntityView saveAndCreateEntityView(DeviceId deviceId, List timeseries) { @@ -160,7 +165,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { List entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS); Assert.assertEquals(1, entries.size()); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entries.get(0)); + equalsIgnoreVersion(toTsEntry(TS, stringKvEntry), entries.get(0)); } @Test @@ -176,7 +181,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); assertThat(entryOpt).isNotNull().isPresent(); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entryOpt.orElse(null)); + equalsIgnoreVersion(toTsEntry(TS, stringKvEntry), entryOpt.get()); } @Test @@ -186,7 +191,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); assertThat(entryOpt).isNotNull().isPresent(); - Assert.assertEquals(toTsEntry(TS, new StringDataEntry(STRING_KEY, "new")), entryOpt.orElse(null)); + equalsIgnoreVersion(toTsEntry(TS, new StringDataEntry(STRING_KEY, "new")), entryOpt.get()); } public void testFindLatestOpt_givenSaveWithSameTSOverwriteTypeAndValue() throws Exception { @@ -209,7 +214,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { Optional entryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); assertThat(entryOpt).isNotNull().isPresent(); - Assert.assertEquals(toTsEntry(TS, stringKvEntry), entryOpt.get()); + equalsIgnoreVersion(toTsEntry(TS, stringKvEntry), entryOpt.get()); } @Test @@ -239,7 +244,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { List entries = tsService.findLatest(tenantId, deviceId, Collections.singleton(STRING_KEY)).get(MAX_TIMEOUT, TimeUnit.SECONDS); Assert.assertEquals(1, entries.size()); - Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(0)); + equalsIgnoreVersion(toTsEntry(TS - 1, stringKvEntry), entries.get(0)); } @Test @@ -794,5 +799,10 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { return new BasicTsKvEntry(ts, entry); } + private static void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) { + assertEquals(expected.getKey(), actual.getKey()); + assertEquals(expected.getValue(), actual.getValue()); + assertEquals(expected.getTs(), actual.getTs()); + } } From 2336cba89a61b5632742c80a5fc7d99774995952 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 9 Jul 2024 16:50:07 +0200 Subject: [PATCH 039/136] version in proto should be optional --- .../server/common/util/KvProtoUtil.java | 15 ++++++++++----- .../server/common/util/ProtoUtils.java | 8 ++++++-- common/proto/src/main/proto/queue.proto | 4 ++-- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java b/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java index 4b206fd804..1be79a28cc 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/KvProtoUtil.java @@ -86,15 +86,20 @@ public class KvProtoUtil { .setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)).build(); } - public static TransportProtos.TsKvProto toTsKvProto(long ts, KvEntry kvEntry, long version) { - return TransportProtos.TsKvProto.newBuilder() + public static TransportProtos.TsKvProto toTsKvProto(long ts, KvEntry kvEntry, Long version) { + var builder = TransportProtos.TsKvProto.newBuilder() .setTs(ts) - .setVersion(version) - .setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)).build(); + .setKv(KvProtoUtil.toKeyValueTypeProto(kvEntry)); + + if (version != null) { + builder.setVersion(version); + } + + return builder.build(); } public static TsKvEntry fromTsKvProto(TransportProtos.TsKvProto proto) { - return new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv()), proto.getVersion()); + return new BasicTsKvEntry(proto.getTs(), fromTsKvProto(proto.getKv()), proto.hasVersion() ? proto.getVersion() : null); } public static TransportProtos.KeyValueProto toKeyValueTypeProto(KvEntry kvEntry) { diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index db1c1fe2e5..b9377ac215 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -295,7 +295,11 @@ public class ProtoUtils { builder.setType(TransportProtos.KeyValueType.JSON_V); break; } - builder.setVersion(attributeKvEntry.getVersion()); + + if (attributeKvEntry.getVersion() != null) { + builder.setVersion(attributeKvEntry.getVersion()); + } + return builder.build(); } @@ -523,7 +527,7 @@ public class ProtoUtils { case JSON_V -> new JsonDataEntry(key, hasValue ? proto.getJsonV() : null); default -> null; }; - return new BaseAttributeKvEntry(entry, proto.getLastUpdateTs(), proto.getVersion()); + return new BaseAttributeKvEntry(entry, proto.getLastUpdateTs(), proto.hasVersion() ? proto.getVersion() : null); } public static TransportProtos.DeviceProto toProto(Device device) { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 9b93812708..3317391f01 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -157,13 +157,13 @@ message AttributeValueProto { string string_v = 7; string json_v = 8; optional string key = 9; - int64 version = 10; + optional int64 version = 10; } message TsKvProto { int64 ts = 1; KeyValueProto kv = 2; - int64 version = 3; + optional int64 version = 3; } message TsKvListProto { From 164185c0f34c095c8e2c24b598dd4b8b6331faec Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 9 Jul 2024 17:17:23 +0200 Subject: [PATCH 040/136] added upgrade script for attributes and latest ts --- .../main/data/upgrade/3.7.0/schema_update.sql | 23 +++++++++++++++++++ .../install/ThingsboardInstallService.java | 4 ++++ .../install/SqlDatabaseUpgradeService.java | 3 +++ .../update/DefaultCacheCleanupService.java | 6 ++++- 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 application/src/main/data/upgrade/3.7.0/schema_update.sql diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql new file mode 100644 index 0000000000..0e10667545 --- /dev/null +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -0,0 +1,23 @@ +-- +-- Copyright © 2016-2024 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. +-- + +-- UPDATE PUBLIC CUSTOMERS START + +CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; + +ALTER TABLE attribute_kv ADD COLUMN version bigint default 0; +ALTER TABLE ts_kv_latest ADD COLUMN version bigint default 0; diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 4bef0d420a..85eb64ffba 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -137,6 +137,10 @@ public class ThingsboardInstallService { entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists(); systemDataLoaderService.updateDefaultNotificationConfigs(false); systemDataLoaderService.updateSecuritySettings(); + break; + case "3.7.0": + log.info("Upgrading ThingsBoard from version 3.7.0 to 3.7.1 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.7.0"); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 86d922c305..000c0536cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -121,6 +121,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService case "3.6.4": updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null); break; + case "3.7.0": + updateSchema("3.7.0", 3007000, "3.7.1", 3007001, null); + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index 7fd92bc5c6..0085df35d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -61,6 +61,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService { log.info("Clearing cache to upgrade from version 3.6.4 to 3.7.0"); clearAll(); break; + case "3.7.0": + log.info("Clearing cache to upgrade from version 3.7.0 to 3.7.1"); + clearAll(); + break; default: //Do nothing, since cache cleanup is optional. } @@ -81,7 +85,7 @@ public class DefaultCacheCleanupService implements CacheCleanupService { if (redisTemplate.isPresent()) { log.info("Flushing all caches"); redisTemplate.get().execute((RedisCallback) connection -> { - connection.flushAll(); + connection.serverCommands().flushAll(); return null; }); return; From 9515bdea1d8c519cdda71ffe741bd2070ad00c23 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 9 Jul 2024 17:33:00 +0200 Subject: [PATCH 041/136] updated LUA SHA --- .../org/thingsboard/server/cache/VersionedRedisTbCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java index d0b3afec9c..3ddcc53f38 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java @@ -68,7 +68,7 @@ public abstract class VersionedRedisTbCache valueSerializer) { super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer); From d9807e797b3a21d77ffb6743c3713792d822c9be Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 10 Jul 2024 10:32:37 +0300 Subject: [PATCH 042/136] refactoring --- .../server-config/server-config.component.html | 12 ++++++------ .../server-config/server-config.component.ts | 2 ++ .../components/hint-tooltip-icon.component.html | 2 +- .../components/hint-tooltip-icon.component.scss | 1 + .../components/hint-tooltip-icon.component.ts | 3 +-- .../shared/directives/tooltip/tooltip.directive.ts | 13 +++++++------ ui-ngx/src/app/shared/shared.module.ts | 2 -- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html index 5182fb108e..dbabe3acf6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html @@ -18,7 +18,7 @@
-
gateway.server-url
+
gateway.server-url
@@ -36,7 +36,7 @@
- {{ 'gateway.timeout' | translate }} +
{{ 'gateway.timeout' | translate }}
@@ -55,7 +55,7 @@
-
gateway.security
+
gateway.security
@@ -66,7 +66,7 @@
- {{ 'gateway.scan-period' | translate }} +
{{ 'gateway.scan-period' | translate }}
@@ -87,7 +87,7 @@
- {{ 'gateway.sub-check-period' | translate }} +
{{ 'gateway.sub-check-period' | translate }}
@@ -110,7 +110,7 @@
- {{ 'gateway.enable-subscription' | translate }} +
{{ 'gateway.enable-subscription' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts index 98a36182a6..66af025267 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts @@ -36,6 +36,7 @@ import { CommonModule } from '@angular/common'; import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { TooltipDirective } from '@shared/directives/public-api'; @Component({ selector: 'tb-server-config', @@ -59,6 +60,7 @@ import { takeUntil } from 'rxjs/operators'; CommonModule, SharedModule, SecurityConfigComponent, + TooltipDirective, ] }) export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html index 20593f042b..cb98fa4608 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html @@ -16,7 +16,7 @@ -->
-
+
(); + constructor( + private elementRef: ElementRef, + private renderer: Renderer2, + private tooltip: MatTooltip + ) {} + ngOnInit(): void { this.observeMouseEvents(); + this.applyTruncationStyles(); } ngAfterViewInit(): void { - this.applyTruncationStyles(); - if (!this.text) { this.text = this.elementRef.nativeElement.innerText; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index aa74c96f29..567b80ed83 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -219,7 +219,6 @@ import { ImageGalleryDialogComponent } from '@shared/components/image/image-gall import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component'; import { WidgetButtonComponent } from '@shared/components/button/widget-button.component'; import { HexInputComponent } from '@shared/components/color-picker/hex-input.component'; -import { TooltipDirective } from '@shared/directives/public-api'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -473,7 +472,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) DndModule, NgxFlowModule, NgxFlowchartModule, - TooltipDirective, // ngx-markdown MarkdownModule.forRoot({ sanitize: SecurityContext.NONE, From a1d26e29b3cf6681701ffc68a1b09e6fe659dfd8 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 10 Jul 2024 11:07:38 +0300 Subject: [PATCH 043/136] refactoring --- .../workers-config-control.component.html | 4 +- .../workers-config-control.component.ts | 2 + .../hint-tooltip-icon.component.html | 14 +++---- .../hint-tooltip-icon.component.scss | 39 +++++++++---------- .../components/hint-tooltip-icon.component.ts | 3 +- 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html index 0062e476c2..b348b49252 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html @@ -19,7 +19,7 @@
- {{ 'gateway.max-number-of-workers' | translate }} +
{{ 'gateway.max-number-of-workers' | translate }}
@@ -41,7 +41,7 @@
- {{ 'gateway.max-messages-queue-for-worker' | translate }} +
{{ 'gateway.max-messages-queue-for-worker' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index 9a37bdb07c..c05b7c32b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -33,6 +33,7 @@ import { CommonModule } from '@angular/common'; import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { TooltipDirective } from '@shared/directives/tooltip/tooltip.directive'; @Component({ selector: 'tb-workers-config-control', @@ -42,6 +43,7 @@ import { takeUntil } from 'rxjs/operators'; imports: [ CommonModule, SharedModule, + TooltipDirective, ], providers: [ { diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html index cb98fa4608..f2413f0ad7 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html @@ -15,12 +15,8 @@ limitations under the License. --> -
-
- -
- {{ hintIcon }} -
+ +{{ hintIcon }} diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss index 60bd53aa43..b83c2ff1b9 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss @@ -13,30 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +:host(.tb-hint-tooltip) { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; +} :host { - .tb-hint-tooltip { - display: flex; - flex-direction: row; - align-items: center; - gap: 4px; - - &-content { - flex: 1 1 auto; - min-width: 0; - max-width: fit-content; - } + .tb-hint-tooltip-content { + flex: 1 1 auto; + min-width: 0; + max-width: fit-content; + } - &-icon { - color: #E0E0E0; - overflow: visible; - order: 3; - margin-left: 4px; - flex-shrink: 0; + .tb-hint-tooltip-icon { + color: #E0E0E0; + overflow: visible; + order: 3; + margin-left: 4px; + flex-shrink: 0; - &:hover { - color: #9E9E9E; - } + &:hover { + color: #9E9E9E; } } } diff --git a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts index 86a346012e..4518875bb6 100644 --- a/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts +++ b/ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input } from '@angular/core'; +import { Component, HostBinding, Input } from '@angular/core'; import { TooltipPosition } from '@angular/material/tooltip'; @Component({ @@ -24,6 +24,7 @@ import { TooltipPosition } from '@angular/material/tooltip'; }) export class HintTooltipIconComponent { + @HostBinding('class.tb-hint-tooltip') @Input('tb-hint-tooltip-icon') tooltipText: string; @Input() From b7508db08f791176987ab0a505b8f653ba07caeb Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 10 Jul 2024 12:16:27 +0300 Subject: [PATCH 044/136] changes timeseries to time series --- .../rule/engine/telemetry/TbMsgTimeseriesNode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 081804639a..ca2b73b634 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -43,10 +43,10 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE @Slf4j @RuleNode( type = ComponentType.ACTION, - name = "save timeseries", + name = "save time series", configClazz = TbMsgTimeseriesNodeConfiguration.class, - nodeDescription = "Saves timeseries data", - nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " + + nodeDescription = "Saves time series data", + nodeDetails = "Saves time series telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " + "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' message timestamp will be applied. " + "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.\n " + "
" + From c65b5724f54622965b273946aa700c3e55af81fc Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 10 Jul 2024 12:27:40 +0300 Subject: [PATCH 045/136] refactoring --- .../mapping-table/mapping-table.component.ts | 18 ++----------- .../dialog/mapping-dialog.component.ts | 26 +++++++++++-------- .../lib/gateway/gateway-widget.models.ts | 22 ++++++++++++---- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index 51847d2c70..9c04a32296 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts @@ -33,7 +33,6 @@ import { debounceTime, distinctUntilChanged, map, take, takeUntil } from 'rxjs/o import { ControlValueAccessor, FormBuilder, - FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormArray, @@ -192,7 +191,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A $event.stopPropagation(); } const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {}; - this.dialog.open(MappingDialogComponent, { + this.dialog.open(MappingDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { @@ -207,26 +206,13 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A if (isDefinedAndNotNull(index)) { this.mappingFormGroup.at(index).patchValue(res); } else { - this.mappingFormGroup.push(this.getMappedDialogDataFormGroup(res)); + this.pushDataAsFormArrays([res]); } this.mappingFormGroup.markAsDirty(); } }); } - private getMappedDialogDataFormGroup(mappingValue: MappingValue): FormGroup { - Object.keys(mappingValue).forEach(key => { - if (Array.isArray(mappingValue[key])) { - mappingValue = { - ...mappingValue, - [key]: this.fb.control(mappingValue[key]), - }; - } - }); - - return this.fb.group(mappingValue); - } - private updateTableData(value: ConnectorMapping[], textSearch?: string): void { let tableValue = value.map(mappingValue => this.getMappingValue(mappingValue)); if (textSearch) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index 3812fa161e..31a43314cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -24,9 +24,13 @@ import { Router } from '@angular/router'; import { Attribute, AttributesUpdate, + ConnectorMapping, + ConnectorMappingFormValue, + ConverterMappingFormValue, ConvertorType, ConvertorTypeTranslationsMap, DataConversionTranslationsMap, + DeviceConnectorMapping, DeviceInfoType, MappingHintTranslationsMap, MappingInfo, @@ -37,11 +41,11 @@ import { MappingKeysType, MappingType, MappingTypeTranslationsMap, - MappingValue, noLeadTrailSpacesRegex, OPCUaSourceTypes, QualityTypes, QualityTypeTranslationsMap, + RequestMappingFormValue, RequestType, RequestTypesTranslationsMap, RpcMethod, @@ -62,7 +66,7 @@ import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gatew templateUrl: './mapping-dialog.component.html', styleUrls: ['./mapping-dialog.component.scss'] }) -export class MappingDialogComponent extends DialogComponent implements OnDestroy { +export class MappingDialogComponent extends DialogComponent implements OnDestroy { mappingForm: UntypedFormGroup; @@ -104,7 +108,7 @@ export class MappingDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: MappingInfo, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, private fb: FormBuilder, private popoverService: TbPopoverService, private renderer: Renderer2, @@ -247,7 +251,7 @@ export class MappingDialogComponent extends DialogComponent & { + converter: { + type: ConvertorType; + } & Record; +}; + export interface DeviceConnectorMapping { deviceNodePattern: string; deviceNodeSource: string; deviceInfo: DeviceInfo; - attributes: Attribute[]; - timeseries: Timeseries[]; - rpc_methods: RpcMethod[]; - attributes_updates: AttributesUpdate[]; + attributes?: Attribute[]; + timeseries?: Timeseries[]; + rpc_methods?: RpcMethod[]; + attributes_updates?: AttributesUpdate[]; } export enum ConnectorType { @@ -597,6 +605,10 @@ export interface RequestMappingData { requestValue: RequestDataItem; } +export type RequestMappingFormValue = Omit & { + requestValue: Record; +}; + export interface RequestDataItem { type: string; details: string; From 7945669303cf45b9a336a5aa44e0edf9f862239d Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 10 Jul 2024 12:22:08 +0200 Subject: [PATCH 046/136] fixed sparkplug tests --- ...ctMqttV5ClientSparkplugConnectionTest.java | 21 +++++++++++++------ ...actMqttV5ClientSparkplugTelemetryTest.java | 17 +++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java index 603b42f6b6..561594f0bf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java @@ -63,7 +63,9 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra return finalFuture.get().get().isPresent(); }); TsKvEntry actualTsKvEntry = finalFuture.get().get().get(); - Assert.assertEquals(expectedTsKvEntry, actualTsKvEntry); + Assert.assertEquals(expectedTsKvEntry.getKey(), actualTsKvEntry.getKey()); + Assert.assertEquals(expectedTsKvEntry.getValue(), actualTsKvEntry.getValue()); + Assert.assertEquals(expectedTsKvEntry.getTs(), actualTsKvEntry.getTs()); } protected void processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test() throws Exception { @@ -95,20 +97,27 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name())); - AtomicReference>> finalFuture = new AtomicReference<>(); await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) .atMost(40, TimeUnit.SECONDS) .until(() -> { - finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); - return finalFuture.get().get().contains(tsKvEntry); + var foundEntry = tsService.findAllLatest(tenantId, savedGateway.getId()).get().stream() + .filter(tsKv -> tsKv.getKey().equals(tsKvEntry.getKey())) + .filter(tsKv -> tsKv.getValue().equals(tsKvEntry.getValue())) + .filter(tsKv -> tsKv.getTs() == tsKvEntry.getTs()) + .findFirst(); + return foundEntry.isPresent(); }); for (Device device : devices) { await(alias + messageName(STATE) + ", device: " + device.getName()) .atMost(40, TimeUnit.SECONDS) .until(() -> { - finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); - return finalFuture.get().get().contains(tsKvEntry); + var foundEntry = tsService.findAllLatest(tenantId, device.getId()).get().stream() + .filter(tsKv -> tsKv.getKey().equals(tsKvEntry.getKey())) + .filter(tsKv -> tsKv.getValue().equals(tsKvEntry.getValue())) + .filter(tsKv -> tsKv.getTs() == tsKvEntry.getTs()) + .findFirst(); + return foundEntry.isPresent(); }); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java index af34e8bf0e..c637ce5bb1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java @@ -78,7 +78,7 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); }); - Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); + Assert.assertTrue("Actual tsKvEntries is not containsAll Expected tsKvEntries", containsIgnoreVersion(finalFuture.get().get(), listTsKvEntry)); } protected void processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple() throws Exception { @@ -107,7 +107,20 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); }); - Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); + Assert.assertTrue("Actual tsKvEntries is not containsAll Expected tsKvEntries", containsIgnoreVersion(finalFuture.get().get(), listTsKvEntry)); } + private static boolean containsIgnoreVersion(List expected, List actual) { + for (TsKvEntry actualEntry : actual) { + var found = expected.stream() + .filter(tsKv -> tsKv.getKey().equals(actualEntry.getKey())) + .filter(tsKv -> tsKv.getValue().equals(actualEntry.getValue())) + .filter(tsKv -> tsKv.getTs() == actualEntry.getTs()) + .findFirst(); + if (found.isEmpty()) { + return false; + } + } + return true; + } } From 0805ee5de2acf14c216d10ed5dad917512bbaff6 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 10 Jul 2024 14:59:29 +0300 Subject: [PATCH 047/136] Added 'With Response' to MQTT templates and removed Response Time when With Response = false --- ...teway-service-rpc-connector.component.html | 6 ++--- ...gateway-service-rpc-connector.component.ts | 23 ++++++++++++------- .../shared/pipe/key-value-not-empty.pipe.ts | 2 +- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html index 3b590e3872..eb428bebdc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html @@ -31,15 +31,15 @@
- + {{ 'gateway.rpc.withResponse' | translate }} - + {{ 'gateway.rpc.responseTopicExpression' | translate }} - + {{ 'gateway.rpc.responseTimeout' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts index c515cbc201..e35061a611 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts @@ -53,7 +53,7 @@ import { } from '@shared/components/dialog/json-object-edit-dialog.component'; import { jsonRequired } from '@shared/components/json-object-edit.component'; import { deepClone } from '@core/utils'; -import { takeUntil, tap } from "rxjs/operators"; +import { filter, takeUntil, tap } from "rxjs/operators"; import { Subject } from "rxjs"; @Component({ @@ -80,7 +80,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C saveTemplate: EventEmitter = new EventEmitter(); commandForm: FormGroup; - isMQTTWithResponse: FormControl; codesArray: Array = [1, 2, 3, 4, 5, 6, 15, 16]; ConnectorType = ConnectorType; modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[]; @@ -135,7 +134,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C this.propagateChange({...this.commandForm.value, ...value}); } }); - this.isMQTTWithResponse = this.fb.control(false); this.observeMQTTWithResponse(); } @@ -153,9 +151,10 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], responseTopicExpression: [{ value: null, disabled: true }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - responseTimeout: [null, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]], + responseTimeout: [{ value: null, disabled: true }, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]], valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - }) + withResponse: [false, []], + }); break; case ConnectorType.MODBUS: formGroup = this.fb.group({ @@ -407,10 +406,18 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C } private observeMQTTWithResponse(): void { - this.isMQTTWithResponse.valueChanges.pipe( + this.commandForm.get('withResponse').valueChanges.pipe( + filter(() => this.connectorType === ConnectorType.MQTT), tap((isActive: boolean) => { - const responseControl = this.commandForm.get('responseTopicExpression'); - isActive ? responseControl.enable() : responseControl.disable(); + const responseTopicControl = this.commandForm.get('responseTopicExpression'); + const responseTimeoutControl = this.commandForm.get('responseTimeout'); + if (isActive) { + responseTopicControl.enable(); + responseTimeoutControl.enable(); + } else { + responseTopicControl.disable(); + responseTimeoutControl.disable(); + } }), takeUntil(this.destroy$), ).subscribe(); diff --git a/ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts b/ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts index 94eeced50e..08afba9d81 100644 --- a/ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts @@ -62,6 +62,6 @@ export class KeyValueIsNotEmptyPipe implements PipeTransform { } private makeKeyValuePair(key: string, value: unknown): KeyValue { - return {key: key, value: value}; + return {key, value}; } } From 78136e2734da9754580654677d0d30d80efb93f6 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 10 Jul 2024 15:04:55 +0300 Subject: [PATCH 048/136] refactoring --- ...gateway-service-rpc-connector.component.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts index e35061a611..3027cafa8d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts @@ -406,20 +406,21 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C } private observeMQTTWithResponse(): void { - this.commandForm.get('withResponse').valueChanges.pipe( - filter(() => this.connectorType === ConnectorType.MQTT), - tap((isActive: boolean) => { - const responseTopicControl = this.commandForm.get('responseTopicExpression'); - const responseTimeoutControl = this.commandForm.get('responseTimeout'); - if (isActive) { - responseTopicControl.enable(); - responseTimeoutControl.enable(); - } else { - responseTopicControl.disable(); - responseTimeoutControl.disable(); - } - }), - takeUntil(this.destroy$), - ).subscribe(); + if (this.connectorType === ConnectorType.MQTT) { + this.commandForm.get('withResponse').valueChanges.pipe( + tap((isActive: boolean) => { + const responseTopicControl = this.commandForm.get('responseTopicExpression'); + const responseTimeoutControl = this.commandForm.get('responseTimeout'); + if (isActive) { + responseTopicControl.enable(); + responseTimeoutControl.enable(); + } else { + responseTopicControl.disable(); + responseTimeoutControl.disable(); + } + }), + takeUntil(this.destroy$), + ).subscribe(); + } } } From 728042680fcbf5d1a0e472ba0a831513754d66c4 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 10 Jul 2024 15:45:26 +0300 Subject: [PATCH 049/136] Fixed spacing in OPC-UA server tab --- .../server-config.component.html | 168 +++++++++--------- 1 file changed, 83 insertions(+), 85 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html index 43b15de06c..bb77cb4a65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html @@ -16,95 +16,93 @@ -->
-
-
-
gateway.server-url
-
- - - - warning - - -
+
+
gateway.server-url
+
+ + + + warning + +
-
-
- gateway.timeout -
-
- - - - warning - - -
+
+
+
+ gateway.timeout +
+
+ + + + warning + +
-
-
gateway.security
-
- - - {{ version.name }} - - -
+
+
+
gateway.security
+
+ + + {{ version.name }} + +
-
-
- gateway.scan-period -
-
- - - - warning - - -
+
+
+
+ gateway.scan-period +
+
+ + + + warning + + +
+
+
+
+ gateway.sub-check-period
-
-
- gateway.sub-check-period -
-
- - - - warning - - -
+
+ + + + warning + +
From 1804cb18e62c1ca4b3e3c17bcbe2c8efb7274ba7 Mon Sep 17 00:00:00 2001 From: ThingsBoard Date: Wed, 10 Jul 2024 16:50:19 +0300 Subject: [PATCH 050/136] test --- .../gateway/gateway-statistics.component.html | 16 +++++++++++----- .../gateway/gateway-statistics.component.scss | 5 +++++ .../lib/gateway/gateway-statistics.component.ts | 6 ++++++ .../src/assets/locale/locale.constant-en_US.json | 1 + ui-ngx/src/styles.scss | 3 +++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html index 4abf67a2d1..7866e8a1c5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html @@ -30,8 +30,11 @@ - {{'gateway.statistics.statistic-commands-empty' | translate }} + {{ 'gateway.statistics.statistic-commands-empty' | translate }} + {{ 'gateway.statistics.command' | translate }} @@ -42,14 +45,17 @@
- - {{ 'widgets.gateway.created-time' | translate }} + test + {{ 'widgets.gateway.created-time' | translate }} + - {{row[0]| date:'yyyy-MM-dd HH:mm:ss' }} + {{ row[0]| date:'yyyy-MM-dd HH:mm:ss' }} - {{ 'widgets.gateway.message' | translate }} + {{ 'widgets.gateway.message' | translate }} + {{ row[1] }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss index cd7722d12d..f4b06433b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss @@ -36,6 +36,11 @@ } } + .mat-mdc-icon-button { + height: 40px; + margin-top: 22px; + } + .chart-box, .chart-container { height: 100%; flex-grow: 1; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts index a3cb79e125..1caab7a462 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts @@ -32,6 +32,7 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { MatTableDataSource } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import {deepClone} from "@core/utils"; @Component({ selector: 'tb-gateway-statistics', @@ -135,6 +136,11 @@ export class GatewayStatisticsComponent implements AfterViewInit { } } + public navigateToStatistics() { + const params = deepClone(this.ctx.stateController.getStateParams()); + this.ctx.stateController.openState('configuration', params); + } + public sortData() { this.dataSource.sortData(this.dataSource.data, this.sort); } 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 33c8d25652..d968219862 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3152,6 +3152,7 @@ "statistic": "Statistic", "statistics": "Statistics", "statistic-commands-empty": "No configured statistic keys found. You can configure them in \"Statistics\" tab in general configuration.", + "statistics-button": "Go to configuration", "commands": "Commands", "send-period": "Statistic send period (in sec)", "send-period-required": "Statistic send period is required", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 4d5ae09a15..1e12eb4936 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -941,6 +941,9 @@ mat-icon { &.tb-mat-96 { @include tb-mat-icon-button-size(96); } + &.tb-mat-185 { + @include tb-mat-icon-button-size(185); + } } .mat-mdc-snack-bar-container { From 71a474bbf202626c367cc0b02ffb27812ccc0bd6 Mon Sep 17 00:00:00 2001 From: Anna Bondar <111653147+Aniutikm@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:58:16 +0300 Subject: [PATCH 051/136] refactoring --- .../widget/lib/gateway/gateway-statistics.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html index 7866e8a1c5..896f94463c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html @@ -45,7 +45,7 @@
- test + {{ 'widgets.gateway.created-time' | translate }} From e9967306b67a31778312059354457bb6adf92021 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 10 Jul 2024 18:14:38 +0300 Subject: [PATCH 052/136] Added emmition of default values on Connector with basic config creation --- .../broker-config-control.component.html | 2 +- .../broker-config-control.component.ts | 18 +++++++++++++----- .../mqtt-basic-config.component.ts | 10 ++++++---- .../security-config.component.ts | 11 ++++++++++- .../server-config/server-config.component.ts | 12 ++++++++++-- .../workers-config-control.component.ts | 15 +++++++++++++-- .../gateway/gateway-connectors.component.ts | 5 ----- 7 files changed, 53 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html index f70ed366e1..7e25033e84 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html @@ -74,7 +74,7 @@ aria-label="Generate" matTooltip="{{ 'gateway.generate-client-id' | translate }}" matTooltipPosition="above" - (click)="generate('clientId')" + (click)="generate()" *ngIf="!brokerConfigFormGroup.get('clientId').value"> autorenew diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index 13f5c7c77e..449d2ad1a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -61,7 +61,7 @@ import { Subject } from 'rxjs'; } ] }) -export class BrokerConfigControlComponent implements ControlValueAccessor, Validator, OnDestroy { +export class BrokerConfigControlComponent implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy { brokerConfigFormGroup: UntypedFormGroup; mqttVersions = MqttVersions; portLimits = PortLimits; @@ -78,7 +78,7 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], version: [5, []], - clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + clientId: ['tb_gw_' + generateSecret(5), [Validators.pattern(noLeadTrailSpacesRegex)]], security: [] }); @@ -101,13 +101,17 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid return ''; } + ngAfterViewInit(): void { + this.emitDefaultValue(); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } - generate(formControlName: string): void { - this.brokerConfigFormGroup.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5)); + generate(): void { + this.brokerConfigFormGroup.get('clientId').patchValue('tb_gw_' + generateSecret(5)); } registerOnChange(fn: (value: string) => void): void { @@ -127,4 +131,8 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid brokerConfigFormGroup: {valid: false} }; } + + private emitDefaultValue(): void { + this.onChange(this.brokerConfigFormGroup.value); + }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index 650ecef68a..57436daafd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -114,10 +114,12 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator writeValue(basicConfig: ConnectorBaseConfig): void { const editedBase = { - workers: { - maxNumberOfWorkers: basicConfig.broker?.maxNumberOfWorkers, - maxMessageNumberPerWorker: basicConfig.broker?.maxMessageNumberPerWorker, - }, + workers: basicConfig.broker + ? { + maxNumberOfWorkers: basicConfig.broker.maxNumberOfWorkers, + maxMessageNumberPerWorker: basicConfig.broker.maxMessageNumberPerWorker, + } + : {}, dataMapping: basicConfig.dataMapping || [], broker: basicConfig.broker || {}, requestsMapping: Array.isArray(basicConfig.requestsMapping) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts index 38a66daafc..a6c147a3e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts @@ -15,6 +15,7 @@ /// import { + AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, @@ -67,7 +68,7 @@ import { CommonModule } from '@angular/common'; SharedModule, ] }) -export class SecurityConfigComponent implements ControlValueAccessor, OnInit, OnDestroy { +export class SecurityConfigComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { @Input() title = 'gateway.security'; @@ -112,6 +113,10 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On ).subscribe((type) => this.updateValidators(type)); } + ngAfterViewInit(): void { + this.emitDefaultValue(); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -143,6 +148,10 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On this.onTouched = fn; } + private emitDefaultValue(): void { + this.onChange(this.securityFormGroup.value); + }; + private updateValidators(type: SecurityType): void { if (type) { this.securityFormGroup.get('username').disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts index 98a36182a6..4f8e563d86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -61,7 +61,7 @@ import { takeUntil } from 'rxjs/operators'; SecurityConfigComponent, ] }) -export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { +export class ServerConfigComponent implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy { securityPolicyTypes = SecurityPolicyTypes; serverConfigFormGroup: UntypedFormGroup; @@ -92,6 +92,10 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, O }); } + ngAfterViewInit(): void { + this.emitDefaultValue(); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -114,4 +118,8 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, O writeValue(serverConfig: ServerConfig): void { this.serverConfigFormGroup.patchValue(serverConfig, {emitEvent: false}); } + + private emitDefaultValue(): void { + this.onChange(this.serverConfigFormGroup.value); + }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index 9a37bdb07c..be6542963f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -15,6 +15,7 @@ /// import { + AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, @@ -25,7 +26,9 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormGroup, ValidationErrors, Validator, + UntypedFormGroup, + ValidationErrors, + Validator, Validators } from '@angular/forms'; import { SharedModule } from '@shared/shared.module'; @@ -56,7 +59,7 @@ import { takeUntil } from 'rxjs/operators'; } ] }) -export class WorkersConfigControlComponent implements OnDestroy, ControlValueAccessor, Validator { +export class WorkersConfigControlComponent implements AfterViewInit, OnDestroy, ControlValueAccessor, Validator { workersConfigFormGroup: UntypedFormGroup; @@ -77,6 +80,10 @@ export class WorkersConfigControlComponent implements OnDestroy, ControlValueAcc }); } + ngAfterViewInit(): void { + this.emitDefaultValue(); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -99,4 +106,8 @@ export class WorkersConfigControlComponent implements OnDestroy, ControlValueAcc workersConfigFormGroup: {valid: false} }; } + + private emitDefaultValue(): void { + this.onChange(this.workersConfigFormGroup.value); + }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 14a8c15c00..5eeffd62cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -551,7 +551,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } value.basicConfig = value.configurationJson; this.updateConnector(value); - this.generate('basicConfig.broker.clientId'); setTimeout(() => this.saveConnector()); } }); @@ -559,10 +558,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); } - generate(formControlName: string): void { - this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5)); - } - uniqNameRequired(): ValidatorFn { return (c: UntypedFormControl) => { const newName = c.value.trim().toLowerCase(); From 01fb50d73ac6592b8ff5d26c8f2d6b862511bd20 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 11 Jul 2024 11:57:41 +0300 Subject: [PATCH 053/136] Revert "Added emmition of default values on Connector with basic config creation" This reverts commit e9967306b67a31778312059354457bb6adf92021. --- .../broker-config-control.component.html | 2 +- .../broker-config-control.component.ts | 18 +++++------------- .../mqtt-basic-config.component.ts | 10 ++++------ .../security-config.component.ts | 11 +---------- .../server-config/server-config.component.ts | 12 ++---------- .../workers-config-control.component.ts | 15 ++------------- .../gateway/gateway-connectors.component.ts | 5 +++++ 7 files changed, 20 insertions(+), 53 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html index 7e25033e84..f70ed366e1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html @@ -74,7 +74,7 @@ aria-label="Generate" matTooltip="{{ 'gateway.generate-client-id' | translate }}" matTooltipPosition="above" - (click)="generate()" + (click)="generate('clientId')" *ngIf="!brokerConfigFormGroup.get('clientId').value"> autorenew diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index 449d2ad1a5..13f5c7c77e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -61,7 +61,7 @@ import { Subject } from 'rxjs'; } ] }) -export class BrokerConfigControlComponent implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy { +export class BrokerConfigControlComponent implements ControlValueAccessor, Validator, OnDestroy { brokerConfigFormGroup: UntypedFormGroup; mqttVersions = MqttVersions; portLimits = PortLimits; @@ -78,7 +78,7 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], version: [5, []], - clientId: ['tb_gw_' + generateSecret(5), [Validators.pattern(noLeadTrailSpacesRegex)]], + clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], security: [] }); @@ -101,17 +101,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid return ''; } - ngAfterViewInit(): void { - this.emitDefaultValue(); - } - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } - generate(): void { - this.brokerConfigFormGroup.get('clientId').patchValue('tb_gw_' + generateSecret(5)); + generate(formControlName: string): void { + this.brokerConfigFormGroup.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5)); } registerOnChange(fn: (value: string) => void): void { @@ -131,8 +127,4 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid brokerConfigFormGroup: {valid: false} }; } - - private emitDefaultValue(): void { - this.onChange(this.brokerConfigFormGroup.value); - }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index 57436daafd..650ecef68a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -114,12 +114,10 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator writeValue(basicConfig: ConnectorBaseConfig): void { const editedBase = { - workers: basicConfig.broker - ? { - maxNumberOfWorkers: basicConfig.broker.maxNumberOfWorkers, - maxMessageNumberPerWorker: basicConfig.broker.maxMessageNumberPerWorker, - } - : {}, + workers: { + maxNumberOfWorkers: basicConfig.broker?.maxNumberOfWorkers, + maxMessageNumberPerWorker: basicConfig.broker?.maxMessageNumberPerWorker, + }, dataMapping: basicConfig.dataMapping || [], broker: basicConfig.broker || {}, requestsMapping: Array.isArray(basicConfig.requestsMapping) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts index a6c147a3e7..38a66daafc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts @@ -15,7 +15,6 @@ /// import { - AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, @@ -68,7 +67,7 @@ import { CommonModule } from '@angular/common'; SharedModule, ] }) -export class SecurityConfigComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { +export class SecurityConfigComponent implements ControlValueAccessor, OnInit, OnDestroy { @Input() title = 'gateway.security'; @@ -113,10 +112,6 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, Af ).subscribe((type) => this.updateValidators(type)); } - ngAfterViewInit(): void { - this.emitDefaultValue(); - } - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -148,10 +143,6 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, Af this.onTouched = fn; } - private emitDefaultValue(): void { - this.onChange(this.securityFormGroup.value); - }; - private updateValidators(type: SecurityType): void { if (type) { this.securityFormGroup.get('username').disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts index 4f8e563d86..98a36182a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -61,7 +61,7 @@ import { takeUntil } from 'rxjs/operators'; SecurityConfigComponent, ] }) -export class ServerConfigComponent implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy { +export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { securityPolicyTypes = SecurityPolicyTypes; serverConfigFormGroup: UntypedFormGroup; @@ -92,10 +92,6 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, A }); } - ngAfterViewInit(): void { - this.emitDefaultValue(); - } - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -118,8 +114,4 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, A writeValue(serverConfig: ServerConfig): void { this.serverConfigFormGroup.patchValue(serverConfig, {emitEvent: false}); } - - private emitDefaultValue(): void { - this.onChange(this.serverConfigFormGroup.value); - }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index be6542963f..9a37bdb07c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -15,7 +15,6 @@ /// import { - AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, @@ -26,9 +25,7 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormGroup, - ValidationErrors, - Validator, + UntypedFormGroup, ValidationErrors, Validator, Validators } from '@angular/forms'; import { SharedModule } from '@shared/shared.module'; @@ -59,7 +56,7 @@ import { takeUntil } from 'rxjs/operators'; } ] }) -export class WorkersConfigControlComponent implements AfterViewInit, OnDestroy, ControlValueAccessor, Validator { +export class WorkersConfigControlComponent implements OnDestroy, ControlValueAccessor, Validator { workersConfigFormGroup: UntypedFormGroup; @@ -80,10 +77,6 @@ export class WorkersConfigControlComponent implements AfterViewInit, OnDestroy, }); } - ngAfterViewInit(): void { - this.emitDefaultValue(); - } - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -106,8 +99,4 @@ export class WorkersConfigControlComponent implements AfterViewInit, OnDestroy, workersConfigFormGroup: {valid: false} }; } - - private emitDefaultValue(): void { - this.onChange(this.workersConfigFormGroup.value); - }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 5eeffd62cf..14a8c15c00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -551,6 +551,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } value.basicConfig = value.configurationJson; this.updateConnector(value); + this.generate('basicConfig.broker.clientId'); setTimeout(() => this.saveConnector()); } }); @@ -558,6 +559,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); } + generate(formControlName: string): void { + this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5)); + } + uniqNameRequired(): ValidatorFn { return (c: UntypedFormControl) => { const newName = c.value.trim().toLowerCase(); From 4d69299ff619afc14493fffadb097076374714a7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 11 Jul 2024 11:58:13 +0300 Subject: [PATCH 054/136] refactoring --- .../broker-config-control.component.ts | 2 +- .../mqtt-basic-config.component.ts | 20 ++++++++++--------- .../workers-config-control.component.ts | 4 +++- .../dialog/add-connector-dialog.component.ts | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index 13f5c7c77e..83ece09cde 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -78,7 +78,7 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], version: [5, []], - clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + clientId: ['tb_gw_' + generateSecret(5), [Validators.pattern(noLeadTrailSpacesRegex)]], security: [] }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index 650ecef68a..557d218581 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -113,16 +113,18 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator } writeValue(basicConfig: ConnectorBaseConfig): void { + const { broker, dataMapping = [], requestsMapping } = basicConfig; + const editedBase = { - workers: { - maxNumberOfWorkers: basicConfig.broker?.maxNumberOfWorkers, - maxMessageNumberPerWorker: basicConfig.broker?.maxMessageNumberPerWorker, - }, - dataMapping: basicConfig.dataMapping || [], - broker: basicConfig.broker || {}, - requestsMapping: Array.isArray(basicConfig.requestsMapping) - ? basicConfig.requestsMapping - : this.getRequestDataArray(basicConfig.requestsMapping), + workers: broker ? { + maxNumberOfWorkers: broker.maxNumberOfWorkers, + maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker, + } : {}, + dataMapping: dataMapping || [], + broker: broker || {}, + requestsMapping: Array.isArray(requestsMapping) + ? requestsMapping + : this.getRequestDataArray(requestsMapping), }; this.basicFormGroup.setValue(editedBase, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index 9a37bdb07c..988851f4d8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -25,7 +25,9 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormGroup, ValidationErrors, Validator, + UntypedFormGroup, + ValidationErrors, + Validator, Validators } from '@angular/forms'; import { SharedModule } from '@shared/shared.module'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index 7cdfbf5808..5a1b5d2a1b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -33,7 +33,7 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { Subject } from 'rxjs'; import { ResourcesService } from '@core/services/resources.service'; -import { takeUntil, tap } from "rxjs/operators"; +import { takeUntil, tap } from 'rxjs/operators'; @Component({ selector: 'tb-add-connector-dialog', From 82a6364c8be894e35d3d9bbc2426b82921fad50d Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 11 Jul 2024 12:04:08 +0300 Subject: [PATCH 055/136] refactoring --- .../mqtt-basic-config/mqtt-basic-config.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index 557d218581..1b37ff5bf7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -116,7 +116,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator const { broker, dataMapping = [], requestsMapping } = basicConfig; const editedBase = { - workers: broker ? { + workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? { maxNumberOfWorkers: broker.maxNumberOfWorkers, maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker, } : {}, From 94ee43a3da95880449536e3bd5c5cf297010d20d Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 11 Jul 2024 16:09:26 +0300 Subject: [PATCH 056/136] Added view update for Connectors on switching active connector --- .../broker-config-control.component.ts | 11 +++++++++-- .../security-config/security-config.component.ts | 4 +++- .../server-config/server-config.component.ts | 12 +++++++++++- .../workers-config-control.component.ts | 6 +++++- .../lib/gateway/gateway-connectors.component.ts | 1 + 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index 13f5c7c77e..e06409c116 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -72,6 +72,7 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid private destroy$ = new Subject(); constructor(private fb: FormBuilder, + private cdr: ChangeDetectorRef, private translate: TranslateService) { this.brokerConfigFormGroup = this.fb.group({ name: ['', []], @@ -119,7 +120,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid } writeValue(brokerConfig: BrokerConfig): void { - this.brokerConfigFormGroup.patchValue(brokerConfig, {emitEvent: false}); + const brokerConfigState = { + ...brokerConfig, + version: brokerConfig.version || 5, + clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5), + }; + this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false}); + this.cdr.markForCheck(); } validate(): ValidationErrors | null { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts index 38a66daafc..18d1a96e38 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts @@ -16,6 +16,7 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, forwardRef, Input, @@ -87,7 +88,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On private destroy$ = new Subject(); - constructor(private fb: FormBuilder) {} + constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {} ngOnInit(): void { this.securityFormGroup = this.fb.group({ @@ -127,6 +128,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On } this.securityFormGroup.reset(securityInfo, {emitEvent: false}); } + this.cdr.markForCheck(); } validate(): ValidationErrors | null { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts index 98a36182a6..41e72d3256 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts @@ -112,6 +112,16 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, O } writeValue(serverConfig: ServerConfig): void { - this.serverConfigFormGroup.patchValue(serverConfig, {emitEvent: false}); + const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig; + const serverConfigState = { + ...serverConfig, + timeoutInMillis: timeoutInMillis || 1000, + scanPeriodInMillis: scanPeriodInMillis || 1000, + enableSubscriptions: enableSubscriptions || true, + subCheckPeriodInMillis: subCheckPeriodInMillis || 10, + showMap: showMap || false, + security: security || SecurityPolicy.BASIC128, + }; + this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index 9a37bdb07c..45235ef8ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -91,7 +91,11 @@ export class WorkersConfigControlComponent implements OnDestroy, ControlValueAcc } writeValue(workersConfig: WorkersConfig): void { - this.workersConfigFormGroup.patchValue(workersConfig, {emitEvent: false}); + const { maxNumberOfWorkers, maxMessageNumberPerWorker } = workersConfig; + this.workersConfigFormGroup.reset({ + maxNumberOfWorkers: maxNumberOfWorkers || 100, + maxMessageNumberPerWorker: maxMessageNumberPerWorker || 10, + }, {emitEvent: false}); } validate(): ValidationErrors | null { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 14a8c15c00..e6a83aeed5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -733,6 +733,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie case ConnectorType.MQTT: case ConnectorType.OPCUA: this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true}); + this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false}); setTimeout(() => { this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC}); From cd62960ec392d55494f63c692baecede10b445da Mon Sep 17 00:00:00 2001 From: ThingsBoard Date: Thu, 11 Jul 2024 17:18:54 +0300 Subject: [PATCH 057/136] button improvements --- .../widget/lib/gateway/gateway-statistics.component.html | 2 +- .../widget/lib/gateway/gateway-statistics.component.scss | 4 ++-- ui-ngx/src/styles.scss | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html index 7866e8a1c5..42332f65b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html @@ -32,7 +32,7 @@ *ngIf="!statisticsKeys.length && !commands.length"> {{ 'gateway.statistics.statistic-commands-empty' | translate }} - diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss index f4b06433b2..c0c8e084b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss @@ -36,8 +36,8 @@ } } - .mat-mdc-icon-button { - height: 40px; + .mat-mdc-button { + width: 185px; margin-top: 22px; } diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 1e12eb4936..4d5ae09a15 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -941,9 +941,6 @@ mat-icon { &.tb-mat-96 { @include tb-mat-icon-button-size(96); } - &.tb-mat-185 { - @include tb-mat-icon-button-size(185); - } } .mat-mdc-snack-bar-container { From 573367e90822c7943313508ccfafb7e78636109a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 11 Jul 2024 17:39:32 +0300 Subject: [PATCH 058/136] Made title read only on LwM2M resorces --- .../resource/resources-library.component.html | 2 +- .../resource/resources-library.component.ts | 38 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html index 76625326b4..c42d94d51c 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html @@ -58,7 +58,7 @@ resource.title - + {{ 'resource.title-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts index 54dfc9340e..049e53caf8 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts @@ -59,22 +59,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme ngOnInit() { super.ngOnInit(); - this.entityForm.get('resourceType').valueChanges.pipe( - startWith(ResourceType.JS_MODULE), - filter(() => this.isAdd), - takeUntil(this.destroy$) - ).subscribe((type) => { - if (type === this.resourceType.LWM2M_MODEL) { - this.entityForm.get('title').disable({emitEvent: false}); - this.entityForm.patchValue({title: ''}, {emitEvent: false}); - } else { - this.entityForm.get('title').enable({emitEvent: false}); - } - this.entityForm.patchValue({ - data: null, - fileName: null - }, {emitEvent: false}); - }); + this.observeResourceTypeChange(); } ngOnDestroy() { @@ -153,4 +138,25 @@ export class ResourcesLibraryComponent extends EntityComponent impleme horizontalPosition: 'right' })); } + + private observeResourceTypeChange(): void { + this.entityForm.get('resourceType').valueChanges.pipe( + startWith(ResourceType.JS_MODULE), + filter(() => this.isAdd), + takeUntil(this.destroy$) + ).subscribe((type: ResourceType) => this.onResourceTypeChange(type)); + } + + private onResourceTypeChange(type: ResourceType): void { + if (type === this.resourceType.LWM2M_MODEL) { + this.entityForm.get('title').disable({emitEvent: false}); + this.entityForm.patchValue({title: ''}, {emitEvent: false}); + } else { + this.entityForm.get('title').enable({emitEvent: false}); + } + this.entityForm.patchValue({ + data: null, + fileName: null + }, {emitEvent: false}); + } } From a2ec97bf194f50897fee544941ca8d9d9ab574e3 Mon Sep 17 00:00:00 2001 From: ThingsBoard Date: Thu, 11 Jul 2024 17:50:50 +0300 Subject: [PATCH 059/136] final improvements --- .../lib/gateway/gateway-statistics.component.html | 10 ++++++---- .../lib/gateway/gateway-statistics.component.scss | 6 +----- .../widget/lib/gateway/gateway-statistics.component.ts | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html index 96770f9c12..5dbc0aee00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html @@ -17,7 +17,7 @@ -->
- + {{ 'gateway.statistics.statistic' | translate }} @@ -32,9 +32,11 @@ *ngIf="!statisticsKeys.length && !commands.length"> {{ 'gateway.statistics.statistic-commands-empty' | translate }} - +
+ +
{{ 'gateway.statistics.command' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss index c0c8e084b1..9f719807ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss @@ -28,6 +28,7 @@ height: 100%; margin-right: 35px; padding: 15px; + gap: 22px; } @media only screen and (max-width: 750px) { @@ -36,11 +37,6 @@ } } - .mat-mdc-button { - width: 185px; - margin-top: 22px; - } - .chart-box, .chart-container { height: 100%; flex-grow: 1; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts index 1caab7a462..b17b8ef28a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts @@ -32,7 +32,7 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { MatTableDataSource } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; import { NULL_UUID } from '@shared/models/id/has-uuid'; -import {deepClone} from "@core/utils"; +import { deepClone } from '@core/utils'; @Component({ selector: 'tb-gateway-statistics', From 078c5be6d436ac36c6397f6cc344de4e434e3dd5 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 15 Jul 2024 10:51:45 +0300 Subject: [PATCH 060/136] UI: power layouts for power button widget --- .../lib/rpc/power-button-widget.models.ts | 164 +++++++++++++++--- .../assets/locale/locale.constant-en_US.json | 3 + .../power-button/default-power-layout.svg | 51 ++++++ .../power-button/outlined-power-layout.svg | 38 ++++ .../power-button/simplified-power-layout.svg | 34 ++++ 5 files changed, 267 insertions(+), 23 deletions(-) create mode 100644 ui-ngx/src/assets/widget/power-button/default-power-layout.svg create mode 100644 ui-ngx/src/assets/widget/power-button/outlined-power-layout.svg create mode 100644 ui-ngx/src/assets/widget/power-button/simplified-power-layout.svg diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts index 00cc3f24f9..ee94f5f05f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -24,7 +24,7 @@ import { SetValueSettings, ValueToDataType } from '@shared/models/action-widget-settings.models'; -import { Circle, Effect, Element, G, Gradient, Runner, Svg, Text, Timeline } from '@svgdotjs/svg.js'; +import { Circle, Effect, Element, G, Gradient, Path, Runner, Svg, Text, Timeline } from '@svgdotjs/svg.js'; import '@svgdotjs/svg.filter.js'; import tinycolor from 'tinycolor2'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -35,7 +35,10 @@ export enum PowerButtonLayout { outlined = 'outlined', default_volume = 'default_volume', simplified_volume = 'simplified_volume', - outlined_volume = 'outlined_volume' + outlined_volume = 'outlined_volume', + default_power = 'default_power', + simplified_power = 'simplified_power', + outlined_power = 'outlined_power' } export const powerButtonLayouts = Object.keys(PowerButtonLayout) as PowerButtonLayout[]; @@ -47,7 +50,10 @@ export const powerButtonLayoutTranslations = new Map( [PowerButtonLayout.outlined, 'widgets.power-button.layout-outlined'], [PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-volume'], [PowerButtonLayout.simplified_volume, 'widgets.power-button.layout-simplified-volume'], - [PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume'] + [PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume'], + [PowerButtonLayout.default_power, 'widgets.power-button.layout-default-power'], + [PowerButtonLayout.simplified_power, 'widgets.power-button.layout-simplified-power'], + [PowerButtonLayout.outlined_power, 'widgets.power-button.layout-outlined-power'] ] ); @@ -58,7 +64,10 @@ export const powerButtonLayoutImages = new Map( [PowerButtonLayout.outlined, 'assets/widget/power-button/outlined-layout.svg'], [PowerButtonLayout.default_volume, 'assets/widget/power-button/default-volume-layout.svg'], [PowerButtonLayout.simplified_volume, 'assets/widget/power-button/simplified-volume-layout.svg'], - [PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg'] + [PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg'], + [PowerButtonLayout.default_power, 'assets/widget/power-button/default-power-layout.svg'], + [PowerButtonLayout.simplified_power, 'assets/widget/power-button/simplified-power-layout.svg'], + [PowerButtonLayout.outlined_power, 'assets/widget/power-button/outlined-power-layout.svg'] ] ); @@ -242,6 +251,12 @@ export abstract class PowerButtonShape { return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); case PowerButtonLayout.outlined_volume: return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.default_power: + return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick, true); + case PowerButtonLayout.simplified_power: + return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick, true); + case PowerButtonLayout.outlined_power: + return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick, true); } } @@ -249,6 +264,13 @@ export abstract class PowerButtonShape { protected readonly onLabel: string; protected readonly offLabel: string; + protected powerCircle = 'M13 4.67063C13 3.65748 12.0377 2.91866 11.0946 3.28889C4.59815 5.83928 0 12.1545 0 19.5412C0 29.1835 ' + + '7.83502 37.0001 17.5 37.0001C27.165 37.0001 35 29.1835 35 19.5412C35 12.1545 30.4019 5.83928 23.9054 3.28889C22.9623 2.91866 22 ' + + '3.65748 22 4.67063C22 5.33931 22.434 5.92434 23.0519 6.17991C28.3077 8.35375 32 13.5209 32 19.5412C32 27.52 25.5148 34.0001 17.5 ' + + '34.0001C9.48521 34.0001 3 27.52 3 19.5412C3 13.5209 6.69234 8.35374 11.9481 6.17991C12.566 5.92434 13 5.33931 13 4.67063Z'; + protected powerLine = 'M16.5 1C16.5 0.447716 16.9477 0 17.5 0C18.0523 0 18.5 0.447715 18.5 1V17C18.5 ' + + '17.5523 18.0523 18 17.5 18C16.9477 18 16.5 17.5523 16.5 17V1Z'; + protected backgroundShape: Circle; protected hoverShape: Circle; protected hovered = false; @@ -260,7 +282,8 @@ export abstract class PowerButtonShape { protected settings: PowerButtonWidgetSettings, protected value: boolean, protected disabled: boolean, - protected onClick: () => void) { + protected onClick: () => void, + protected iconMode: boolean = false) { this.colors = createPowerButtonShapeColors(this.settings); this.onLabel = this.widgetContext.translate.instant('widgets.power-button.on-label').toUpperCase(); this.offLabel = this.widgetContext.translate.instant('widgets.power-button.off-label').toUpperCase(); @@ -702,6 +725,10 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { private offLabelShape: Text; private onCircleShape: Circle; private onLabelShape: Text; + private offPowerSymbolCircle: Path; + private offPowerSymbolLine: Path; + private onPowerSymbolCircle: Path; + private onPowerSymbolLine: Path; private pressedTimeline: Timeline; private centerGroup: G; @@ -723,15 +750,30 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { add.stop(1, '#FFFFFF', 1); }).from(0.832, 0.1188).to(0.268, 0.92); this.centerGroup = this.svgShape.group(); - this.offLabelShape = this.createOffLabel('400').addTo(this.centerGroup); + if (this.iconMode) { + this.offPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.centerGroup); + this.offPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.centerGroup); + } else { + this.offLabelShape = this.createOffLabel('400').addTo(this.centerGroup); + } this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); - this.onLabelShape = this.createOnLabel('400'); - this.createMask(this.onCircleShape, [this.onLabelShape]); + if (this.iconMode) { + this.onPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy); + this.onPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12); + } else { + this.onLabelShape = this.createOnLabel('400'); + } + this.createMask(this.onCircleShape, this.iconMode ? [this.onPowerSymbolCircle, this.onPowerSymbolLine] : [this.onLabelShape]); this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); - this.onLabelShape.timeline(this.pressedTimeline); + if (this.iconMode) { + this.onPowerSymbolCircle.timeline(this.pressedTimeline); + this.onPowerSymbolLine.timeline(this.pressedTimeline); + } else { + this.onLabelShape.timeline(this.pressedTimeline); + } this.innerShadow.timeline(this.pressedTimeline); } @@ -751,7 +793,12 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.innerBorder.fill(this.innerBorderGradient); this.innerBorder.attr({ 'fill-opacity': 1 }); } - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + if (this.iconMode) { + this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -776,14 +823,24 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.innerShadow.show(); const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); - powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}}); + if (this.iconMode) { + powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: pressedScale, origin: {x: cx, y: cy}}); + powerButtonAnimation(this.onPowerSymbolLine).transform({scale: pressedScale, origin: {x: cx, y: cy}}); + } else { + powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}}); + } this.innerShadow.animate(6, 0.6); } protected onPressEnd() { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); - powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); + if (this.iconMode) { + powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: 1, origin: {x: cx, y: cy}}); + powerButtonAnimation(this.onPowerSymbolLine).transform({scale: 1, origin: {x: cx, y: cy}}); + } else { + powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); + } this.innerShadow.animateRestore().after(() => { if (this.disabled) { this.innerShadow.hide(); @@ -799,6 +856,10 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { private outerBorderMask: Circle; private offLabelShape: Text; private onLabelShape: Text; + private offPowerSymbolCircle: Path; + private offPowerSymbolLine: Path; + private onPowerSymbolCircle: Path; + private onPowerSymbolLine: Path; private innerShadow: InnerShadowCircle; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; @@ -812,9 +873,17 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy); this.createMask(this.outerBorder, [this.outerBorderMask]); this.centerGroup = this.svgShape.group(); - this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); this.onCenterGroup = this.svgShape.group(); - this.onLabelShape = this.createOnLabel().addTo(this.onCenterGroup); + + if (this.iconMode) { + this.offPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.centerGroup); + this.offPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.centerGroup); + this.onPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.onCenterGroup); + this.onPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.onCenterGroup); + } else { + this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); + this.onLabelShape = this.createOnLabel().addTo(this.onCenterGroup); + } this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 3, 0.3); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0); this.pressedTimeline = new Timeline(); @@ -824,8 +893,15 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { } protected drawColorState(mainColor: PowerButtonColor){ - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + if (this.iconMode) { + this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } } protected drawOff() { @@ -876,6 +952,10 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { private offLabelShape: Text; private onCircleShape: Circle; private onLabelShape: Text; + private offPowerSymbolCircle: Path; + private offPowerSymbolLine: Path; + private onPowerSymbolCircle: Path; + private onPowerSymbolLine: Path; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; private centerGroup: G; @@ -895,19 +975,42 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy); this.createMask(this.innerBorder, [this.innerBorderMask]); this.centerGroup = this.svgShape.group(); - this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup); + if (this.iconMode) { + this.powerCircle = 'M12 2.95698C12 1.45236 10.4775 0.438524 9.19424 1.22416C3.68417 4.59764 0 10.7283 0 17.7316C0 ' + + '28.3732 8.50659 37 19 37C29.4934 37 38 28.3732 38 17.7316C38 10.7283 34.3158 4.59764 28.8058 1.22416C27.5225 ' + + '0.438524 26 1.45236 26 2.95698C26 3.73878 26.4365 4.44718 27.0911 4.87461C31.2354 7.58066 34 12.3083 34 ' + + '17.7316C34 26.2172 27.2316 33 19 33C10.7684 33 4 26.2172 4 17.7316C4 12.3084 6.76462 7.58066 10.9089 ' + + '4.87461C11.5635 4.44718 12 3.73878 12 2.95698Z'; + this.powerLine = 'M0 2.5C0 1.11929 1.11929 0 2.5 0C3.88071 0 5 1.11929 5 2.5V15.5C5 16.8807 3.88071 18 ' + + '2.5 18C1.11929 18 0 16.8807 0 15.5V2.5Z'; + + this.offPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.centerGroup); + this.offPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.centerGroup); + } else { + this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup); + } this.onCenterGroup = this.svgShape.group(); this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy) .addTo(this.onCenterGroup); - this.onLabelShape = this.createOnLabel('800'); - this.createMask(this.onCircleShape, [this.onLabelShape]); + if (this.iconMode) { + this.onPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy); + this.onPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12); + } else { + this.onLabelShape = this.createOnLabel('800'); + } + this.createMask(this.onCircleShape, this.iconMode ? [this.onPowerSymbolCircle, this.onPowerSymbolLine] : [this.onLabelShape]); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0); this.backgroundShape.addClass('tb-small-shadow'); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); this.onCenterGroup.timeline(this.pressedTimeline); - this.onLabelShape.timeline(this.pressedTimeline); + if (this.iconMode) { + this.onPowerSymbolCircle.timeline(this.pressedTimeline); + this.onPowerSymbolLine.timeline(this.pressedTimeline); + } else { + this.onLabelShape.timeline(this.pressedTimeline); + } this.pressedShadow.timeline(this.pressedTimeline); } @@ -919,7 +1022,12 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.outerBorder.attr({ 'fill-opacity': 1 }); } this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + if (this.iconMode) { + this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -940,7 +1048,12 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98}); - powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); + if (this.iconMode) { + powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); + powerButtonAnimation(this.onPowerSymbolLine).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); + } else { + powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); + } this.pressedShadow.animate(6, 0.6); } @@ -948,7 +1061,12 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); - powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); + if (this.iconMode) { + powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: 1, origin: {x: cx, y: cy}}); + powerButtonAnimation(this.onPowerSymbolLine).transform({scale: 1, origin: {x: cx, y: cy}}); + } else { + powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); + } this.pressedShadow.animateRestore(); } 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 b1b17ca3aa..f4a0286aa5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5621,6 +5621,9 @@ "layout-default-volume": "Default.Volume", "layout-simplified-volume": "Simplified.Volume", "layout-outlined-volume": "Outlined.Volume", + "layout-default-power": "Default.Power", + "layout-simplified-power": "Simplified.Power", + "layout-outlined-power": "Outlined.Power", "main": "Main", "background": "Background", "power-on-colors": "Power 'On' colors", diff --git a/ui-ngx/src/assets/widget/power-button/default-power-layout.svg b/ui-ngx/src/assets/widget/power-button/default-power-layout.svg new file mode 100644 index 0000000000..9da9ba8535 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/default-power-layout.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/outlined-power-layout.svg b/ui-ngx/src/assets/widget/power-button/outlined-power-layout.svg new file mode 100644 index 0000000000..b884cd06b0 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/outlined-power-layout.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/power-button/simplified-power-layout.svg b/ui-ngx/src/assets/widget/power-button/simplified-power-layout.svg new file mode 100644 index 0000000000..be3596da29 --- /dev/null +++ b/ui-ngx/src/assets/widget/power-button/simplified-power-layout.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c044d8ea5da758a14335b3e73d46ea1722c0c7f9 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 15 Jul 2024 12:56:18 +0300 Subject: [PATCH 061/136] UI: Refactroing --- .../lib/rpc/power-button-widget.models.ts | 309 +++++++++++------- 1 file changed, 183 insertions(+), 126 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts index ee94f5f05f..d24c92ff16 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -228,6 +228,21 @@ export const powerButtonShapeSize = 110; const cx = powerButtonShapeSize / 2; const cy = powerButtonShapeSize / 2; +const powerCircle = 'M13 4.67063C13 3.65748 12.0377 2.91866 11.0946 3.28889C4.59815 5.83928 0 12.1545 0 19.5412C0 29.1835 ' + + '7.83502 37.0001 17.5 37.0001C27.165 37.0001 35 29.1835 35 19.5412C35 12.1545 30.4019 5.83928 23.9054 3.28889C22.9623 2.91866 22 ' + + '3.65748 22 4.67063C22 5.33931 22.434 5.92434 23.0519 6.17991C28.3077 8.35375 32 13.5209 32 19.5412C32 27.52 25.5148 34.0001 17.5 ' + + '34.0001C9.48521 34.0001 3 27.52 3 19.5412C3 13.5209 6.69234 8.35374 11.9481 6.17991C12.566 5.92434 13 5.33931 13 4.67063Z'; +const powerLine = 'M16.5 1C16.5 0.447716 16.9477 0 17.5 0C18.0523 0 18.5 0.447715 18.5 1V17C18.5 ' + + '17.5523 18.0523 18 17.5 18C16.9477 18 16.5 17.5523 16.5 17V1Z'; + +const powerCircleStroke = 'M12 2.95698C12 1.45236 10.4775 0.438524 9.19424 1.22416C3.68417 4.59764 0 10.7283 0 17.7316C0 ' + + '28.3732 8.50659 37 19 37C29.4934 37 38 28.3732 38 17.7316C38 10.7283 34.3158 4.59764 28.8058 1.22416C27.5225 ' + + '0.438524 26 1.45236 26 2.95698C26 3.73878 26.4365 4.44718 27.0911 4.87461C31.2354 7.58066 34 12.3083 34 ' + + '17.7316C34 26.2172 27.2316 33 19 33C10.7684 33 4 26.2172 4 17.7316C4 12.3084 6.76462 7.58066 10.9089 ' + + '4.87461C11.5635 4.44718 12 3.73878 12 2.95698Z'; +const powerLineStroke = 'M0 2.5C0 1.11929 1.11929 0 2.5 0C3.88071 0 5 1.11929 5 2.5V15.5C5 16.8807 3.88071 18 ' + + '2.5 18C1.11929 18 0 16.8807 0 15.5V2.5Z'; + const powerButtonAnimation = (element: Element): Runner => element.animate(200, 0, 'now'); export abstract class PowerButtonShape { @@ -252,11 +267,11 @@ export abstract class PowerButtonShape { case PowerButtonLayout.outlined_volume: return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); case PowerButtonLayout.default_power: - return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick, true); + return new DefaultPowerPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); case PowerButtonLayout.simplified_power: - return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick, true); + return new SimplifiedPowerPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); case PowerButtonLayout.outlined_power: - return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick, true); + return new OutlinedPowerPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); } } @@ -264,13 +279,6 @@ export abstract class PowerButtonShape { protected readonly onLabel: string; protected readonly offLabel: string; - protected powerCircle = 'M13 4.67063C13 3.65748 12.0377 2.91866 11.0946 3.28889C4.59815 5.83928 0 12.1545 0 19.5412C0 29.1835 ' + - '7.83502 37.0001 17.5 37.0001C27.165 37.0001 35 29.1835 35 19.5412C35 12.1545 30.4019 5.83928 23.9054 3.28889C22.9623 2.91866 22 ' + - '3.65748 22 4.67063C22 5.33931 22.434 5.92434 23.0519 6.17991C28.3077 8.35375 32 13.5209 32 19.5412C32 27.52 25.5148 34.0001 17.5 ' + - '34.0001C9.48521 34.0001 3 27.52 3 19.5412C3 13.5209 6.69234 8.35374 11.9481 6.17991C12.566 5.92434 13 5.33931 13 4.67063Z'; - protected powerLine = 'M16.5 1C16.5 0.447716 16.9477 0 17.5 0C18.0523 0 18.5 0.447715 18.5 1V17C18.5 ' + - '17.5523 18.0523 18 17.5 18C16.9477 18 16.5 17.5523 16.5 17V1Z'; - protected backgroundShape: Circle; protected hoverShape: Circle; protected hovered = false; @@ -282,8 +290,7 @@ export abstract class PowerButtonShape { protected settings: PowerButtonWidgetSettings, protected value: boolean, protected disabled: boolean, - protected onClick: () => void, - protected iconMode: boolean = false) { + protected onClick: () => void) { this.colors = createPowerButtonShapeColors(this.settings); this.onLabel = this.widgetContext.translate.instant('widgets.power-button.on-label').toUpperCase(); this.offLabel = this.widgetContext.translate.instant('widgets.power-button.off-label').toUpperCase(); @@ -725,13 +732,34 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { private offLabelShape: Text; private onCircleShape: Circle; private onLabelShape: Text; - private offPowerSymbolCircle: Path; - private offPowerSymbolLine: Path; - private onPowerSymbolCircle: Path; - private onPowerSymbolLine: Path; private pressedTimeline: Timeline; private centerGroup: G; + + protected drawOffCenter(centerGroup: G) { + this.offLabelShape = this.createOffLabel('400').addTo(centerGroup); + } + + protected drawOnCenter() { + this.onLabelShape = this.createOnLabel('400'); + } + + protected addMask(onCircleShape: Circle) { + this.createMask(onCircleShape,[this.onLabelShape]); + } + + protected addTimeLine(pressedTimeline: Timeline) { + this.onLabelShape.timeline(pressedTimeline); + } + + protected drawColor(mainColor: PowerButtonColor) { + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected animation(scale: number) { + powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); + } + protected drawShape() { this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) .fill({opacity: 0}).stroke({width: 0}); @@ -750,30 +778,15 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { add.stop(1, '#FFFFFF', 1); }).from(0.832, 0.1188).to(0.268, 0.92); this.centerGroup = this.svgShape.group(); - if (this.iconMode) { - this.offPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.centerGroup); - this.offPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.centerGroup); - } else { - this.offLabelShape = this.createOffLabel('400').addTo(this.centerGroup); - } + this.drawOffCenter(this.centerGroup); this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); - if (this.iconMode) { - this.onPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy); - this.onPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12); - } else { - this.onLabelShape = this.createOnLabel('400'); - } - this.createMask(this.onCircleShape, this.iconMode ? [this.onPowerSymbolCircle, this.onPowerSymbolLine] : [this.onLabelShape]); + this.drawOnCenter(); + this.addMask(this.onCircleShape); this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); - if (this.iconMode) { - this.onPowerSymbolCircle.timeline(this.pressedTimeline); - this.onPowerSymbolLine.timeline(this.pressedTimeline); - } else { - this.onLabelShape.timeline(this.pressedTimeline); - } + this.addTimeLine(this.pressedTimeline); this.innerShadow.timeline(this.pressedTimeline); } @@ -793,12 +806,7 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.innerBorder.fill(this.innerBorderGradient); this.innerBorder.attr({ 'fill-opacity': 1 }); } - if (this.iconMode) { - this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - } else { - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - } + this.drawColor(mainColor); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -823,24 +831,14 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.innerShadow.show(); const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); - if (this.iconMode) { - powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: pressedScale, origin: {x: cx, y: cy}}); - powerButtonAnimation(this.onPowerSymbolLine).transform({scale: pressedScale, origin: {x: cx, y: cy}}); - } else { - powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}}); - } + this.animation(pressedScale); this.innerShadow.animate(6, 0.6); } protected onPressEnd() { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); - if (this.iconMode) { - powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: 1, origin: {x: cx, y: cy}}); - powerButtonAnimation(this.onPowerSymbolLine).transform({scale: 1, origin: {x: cx, y: cy}}); - } else { - powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); - } + this.animation(1); this.innerShadow.animateRestore().after(() => { if (this.disabled) { this.innerShadow.hide(); @@ -850,22 +848,58 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { } +class DefaultPowerPowerButtonShape extends DefaultVolumePowerButtonShape { + private offPowerSymbolCircle: Path; + private offPowerSymbolLine: Path; + private onPowerSymbolCircle: Path; + private onPowerSymbolLine: Path; + + protected drawOffCenter(centerGroup: G) { + this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup); + this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup); + } + + protected drawOnCenter() { + this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy); + this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12); + } + + protected addMask(onCircleShape: Circle) { + this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]); + } + + protected addTimeLine(pressedTimeline: Timeline) { + this.onPowerSymbolCircle.timeline(pressedTimeline); + this.onPowerSymbolLine.timeline(pressedTimeline); + } + + protected drawColor(mainColor: PowerButtonColor) { + this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected animation(scale: number) { + powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}}); + powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}}); + } +} + class SimplifiedVolumePowerButtonShape extends PowerButtonShape { private outerBorder: Circle; private outerBorderMask: Circle; private offLabelShape: Text; private onLabelShape: Text; - private offPowerSymbolCircle: Path; - private offPowerSymbolLine: Path; - private onPowerSymbolCircle: Path; - private onPowerSymbolLine: Path; private innerShadow: InnerShadowCircle; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; private centerGroup: G; private onCenterGroup: G; + protected drawCenterGroup(centerGroup: G, onCenterGroup: G) { + this.offLabelShape = this.createOffLabel().addTo(centerGroup); + this.onLabelShape = this.createOnLabel().addTo(onCenterGroup); + } protected drawShape() { this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) @@ -874,16 +908,7 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { this.createMask(this.outerBorder, [this.outerBorderMask]); this.centerGroup = this.svgShape.group(); this.onCenterGroup = this.svgShape.group(); - - if (this.iconMode) { - this.offPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.centerGroup); - this.offPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.centerGroup); - this.onPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.onCenterGroup); - this.onPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.onCenterGroup); - } else { - this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); - this.onLabelShape = this.createOnLabel().addTo(this.onCenterGroup); - } + this.drawCenterGroup(this.centerGroup, this.onCenterGroup); this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 3, 0.3); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0); this.pressedTimeline = new Timeline(); @@ -893,15 +918,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { } protected drawColorState(mainColor: PowerButtonColor){ - if (this.iconMode) { - this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - } else { - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - } + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } protected drawOff() { @@ -943,7 +961,28 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { } } -class OutlinedVolumePowerButtonShape extends PowerButtonShape { +class SimplifiedPowerPowerButtonShape extends SimplifiedVolumePowerButtonShape { + private offPowerSymbolCircle: Path; + private offPowerSymbolLine: Path; + private onPowerSymbolCircle: Path; + private onPowerSymbolLine: Path; + + protected drawCenterGroup(centerGroup: G, onCenterGroup: G) { + this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup); + this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup); + this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(onCenterGroup); + this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(onCenterGroup); + } + + protected drawColorState(mainColor: PowerButtonColor) { + this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } +} + + class OutlinedVolumePowerButtonShape extends PowerButtonShape { private outerBorder: Circle; private outerBorderMask: Circle; private outerBorderGradient: Gradient; @@ -952,15 +991,35 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { private offLabelShape: Text; private onCircleShape: Circle; private onLabelShape: Text; - private offPowerSymbolCircle: Path; - private offPowerSymbolLine: Path; - private onPowerSymbolCircle: Path; - private onPowerSymbolLine: Path; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; private centerGroup: G; private onCenterGroup: G; + protected drawOffCenter(centerGroup: G) { + this.offLabelShape = this.createOffLabel('800').addTo(centerGroup); + } + + protected drawOnCenter() { + this.onLabelShape = this.createOnLabel('800'); + } + + protected addMask(onCircleShape: Circle) { + this.createMask(onCircleShape,[this.onLabelShape]); + } + + protected addTimeLine(pressedTimeline: Timeline) { + this.onLabelShape.timeline(pressedTimeline); + } + + protected drawColor(mainColor: PowerButtonColor) { + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected animation(scale: number) { + powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); + } + protected drawShape() { this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) .fill({opacity: 0}).stroke({width: 0}); @@ -975,42 +1034,19 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy); this.createMask(this.innerBorder, [this.innerBorderMask]); this.centerGroup = this.svgShape.group(); - if (this.iconMode) { - this.powerCircle = 'M12 2.95698C12 1.45236 10.4775 0.438524 9.19424 1.22416C3.68417 4.59764 0 10.7283 0 17.7316C0 ' + - '28.3732 8.50659 37 19 37C29.4934 37 38 28.3732 38 17.7316C38 10.7283 34.3158 4.59764 28.8058 1.22416C27.5225 ' + - '0.438524 26 1.45236 26 2.95698C26 3.73878 26.4365 4.44718 27.0911 4.87461C31.2354 7.58066 34 12.3083 34 ' + - '17.7316C34 26.2172 27.2316 33 19 33C10.7684 33 4 26.2172 4 17.7316C4 12.3084 6.76462 7.58066 10.9089 ' + - '4.87461C11.5635 4.44718 12 3.73878 12 2.95698Z'; - this.powerLine = 'M0 2.5C0 1.11929 1.11929 0 2.5 0C3.88071 0 5 1.11929 5 2.5V15.5C5 16.8807 3.88071 18 ' + - '2.5 18C1.11929 18 0 16.8807 0 15.5V2.5Z'; - - this.offPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy).addTo(this.centerGroup); - this.offPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12).addTo(this.centerGroup); - } else { - this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup); - } + this.drawOffCenter(this.centerGroup); this.onCenterGroup = this.svgShape.group(); this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy) .addTo(this.onCenterGroup); - if (this.iconMode) { - this.onPowerSymbolCircle = this.svgShape.path(this.powerCircle).center(cx, cy); - this.onPowerSymbolLine = this.svgShape.path(this.powerLine).center(cx, cy-12); - } else { - this.onLabelShape = this.createOnLabel('800'); - } - this.createMask(this.onCircleShape, this.iconMode ? [this.onPowerSymbolCircle, this.onPowerSymbolLine] : [this.onLabelShape]); + this.drawOnCenter(); + this.addMask(this.onCircleShape); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0); this.backgroundShape.addClass('tb-small-shadow'); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); this.onCenterGroup.timeline(this.pressedTimeline); - if (this.iconMode) { - this.onPowerSymbolCircle.timeline(this.pressedTimeline); - this.onPowerSymbolLine.timeline(this.pressedTimeline); - } else { - this.onLabelShape.timeline(this.pressedTimeline); - } + this.addTimeLine(this.pressedTimeline); this.pressedShadow.timeline(this.pressedTimeline); } @@ -1022,12 +1058,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.outerBorder.attr({ 'fill-opacity': 1 }); } this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - if (this.iconMode) { - this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - } else { - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - } + this.drawColor(mainColor); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -1048,12 +1079,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98}); - if (this.iconMode) { - powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); - powerButtonAnimation(this.onPowerSymbolLine).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); - } else { - powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); - } + this.animation(pressedScale / 0.98); this.pressedShadow.animate(6, 0.6); } @@ -1061,13 +1087,44 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); - if (this.iconMode) { - powerButtonAnimation(this.onPowerSymbolCircle).transform({scale: 1, origin: {x: cx, y: cy}}); - powerButtonAnimation(this.onPowerSymbolLine).transform({scale: 1, origin: {x: cx, y: cy}}); - } else { - powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); - } + this.animation(1); this.pressedShadow.animateRestore(); } } + +class OutlinedPowerPowerButtonShape extends OutlinedVolumePowerButtonShape { + private offPowerSymbolCircle: Path; + private offPowerSymbolLine: Path; + private onPowerSymbolCircle: Path; + private onPowerSymbolLine: Path; + + protected drawOffCenter(centerGroup: G) { + this.offPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy).addTo(centerGroup); + this.offPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12).addTo(centerGroup); + } + + protected drawOnCenter() { + this.onPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy); + this.onPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12); + } + + protected addMask(onCircleShape: Circle) { + this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]); + } + + protected addTimeLine(pressedTimeline: Timeline) { + this.onPowerSymbolCircle.timeline(pressedTimeline); + this.onPowerSymbolLine.timeline(pressedTimeline); + } + + protected drawColor(mainColor: PowerButtonColor) { + this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + + protected animation(scale: number) { + powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}}); + powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}}); + } +} From 5bb078beb27edd458c52152c0ff5d43d0d206581 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 15 Jul 2024 14:28:45 +0300 Subject: [PATCH 062/136] UI: Refactoring --- .../components/widget/lib/rpc/power-button-widget.models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts index d24c92ff16..664ebe5a56 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -982,7 +982,7 @@ class SimplifiedPowerPowerButtonShape extends SimplifiedVolumePowerButtonShape { } } - class OutlinedVolumePowerButtonShape extends PowerButtonShape { +class OutlinedVolumePowerButtonShape extends PowerButtonShape { private outerBorder: Circle; private outerBorderMask: Circle; private outerBorderGradient: Gradient; From 828ac3e197c16a6b939c1c01fe611e8b5bcba3a3 Mon Sep 17 00:00:00 2001 From: ThingsBoard Date: Mon, 15 Jul 2024 15:24:54 +0300 Subject: [PATCH 063/136] UI: remove persisted data in connector for gateway dashboard --- .../widget/lib/gateway/gateway-service-rpc.component.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts index dadd1a38d2..1f3327ec06 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts @@ -206,9 +206,6 @@ export class GatewayServiceRPCComponent implements OnInit { private updateTemplates() { this.templates = this.subscription.data[0].data[0][1].length ? JSON.parse(this.subscription.data[0].data[0][1]) : []; - if (this.templates.length && this.commandForm.get('params').value == "{}") { - this.commandForm.get('params').patchValue(this.templates[0].config); - } this.cd.detectChanges(); } From 31d2d14f6069735efb3455608b1822216f162c58 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 16 Jul 2024 10:29:58 +0300 Subject: [PATCH 064/136] Cache transaction support for versioned entities --- .../main/data/upgrade/3.7.0/schema_update.sql | 4 +- .../cache/CaffeineTbTransactionalCache.java | 5 +++ .../server/cache/RedisTbCacheTransaction.java | 2 +- .../cache/RedisTbTransactionalCache.java | 13 ++++-- .../server/cache/TbTransactionalCache.java | 6 ++- .../server/cache/VersionedRedisTbCache.java | 40 +++++++++++-------- .../server/cache/VersionedTbCache.java | 8 +++- 7 files changed, 52 insertions(+), 26 deletions(-) diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql index 0e10667545..f964387359 100644 --- a/application/src/main/data/upgrade/3.7.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -14,10 +14,12 @@ -- limitations under the License. -- --- UPDATE PUBLIC CUSTOMERS START +-- KV VERSIONING UPDATE START CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1000; CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; ALTER TABLE attribute_kv ADD COLUMN version bigint default 0; ALTER TABLE ts_kv_latest ADD COLUMN version bigint default 0; + +-- KV VERSIONING UPDATE END diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java index d2ea960e68..4ce6571f1c 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineTbTransactionalCache.java @@ -54,6 +54,11 @@ public abstract class CaffeineTbTransactionalCache get(K key, boolean transactionMode) { + return get(key); + } + @Override public void put(K key, V value) { lock.lock(); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java index fb852493ce..3dcb6e878f 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbCacheTransaction.java @@ -31,7 +31,7 @@ public class RedisTbCacheTransaction get(K key) { + return get(key, false); + } + + @Override + public TbCacheValueWrapper get(K key, boolean transactionMode) { try (var connection = connectionFactory.getConnection()) { byte[] rawKey = getRawKey(key); - byte[] rawValue = doGet(connection, rawKey); + byte[] rawValue = doGet(connection, rawKey, transactionMode); if (rawValue == null || rawValue.length == 0) { return null; } else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) { @@ -96,18 +101,18 @@ public abstract class RedisTbTransactionalCache get(K key); + TbCacheValueWrapper get(K key, boolean transactionMode); + void put(K key, V value); void putIfAbsent(K key, V value); @@ -60,7 +62,7 @@ public interface TbTransactionalCache dbCall, boolean cacheNullValue) { - TbCacheValueWrapper cacheValueWrapper = get(key); + TbCacheValueWrapper cacheValueWrapper = get(key, true); if (cacheValueWrapper != null) { return cacheValueWrapper.get(); } @@ -95,7 +97,7 @@ public interface TbTransactionalCache R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { - TbCacheValueWrapper cacheValueWrapper = get(key); + TbCacheValueWrapper cacheValueWrapper = get(key, true); if (cacheValueWrapper != null) { var cacheValue = cacheValueWrapper.get(); return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java index 3ddcc53f38..67b6c86f96 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/VersionedRedisTbCache.java @@ -88,31 +88,30 @@ public abstract class VersionedRedisTbCache get(K key); default V get(K key, Supplier supplier) { + return get(key, supplier, true); + } + + default V get(K key, Supplier supplier, boolean putToCache) { return Optional.ofNullable(get(key)) .map(TbCacheValueWrapper::get) .orElseGet(() -> { V value = supplier.get(); - put(key, value); + if (putToCache) { + put(key, value); + } return value; }); } From ece8132c9001393ac78885eda8d99c6f18ce8d2a Mon Sep 17 00:00:00 2001 From: ThingsBoard Date: Wed, 17 Jul 2024 10:39:16 +0300 Subject: [PATCH 065/136] fixed updating credentials --- .../widget/lib/gateway/gateway-configuration.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts index 1c65920179..3b2e0e9fc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts @@ -619,6 +619,9 @@ export class GatewayConfigurationComponent implements OnInit { updateCredentials = true; } else { updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken; + if (updateCredentials) { + this.initialCredentials.credentialsId = securityConfig.accessToken; + } } if (updateCredentials) { newCredentials = { From b6cfc32b6be6c29dfc504bed3c264d0e8a3d3fcc Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 16 Jul 2024 11:22:54 +0300 Subject: [PATCH 066/136] TbHttpClient - fixed proxy ssl configuration --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 26de3ad5e5..b6a7234cac 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -118,8 +118,8 @@ public class TbHttpClient { o.username(proxyUser).password(u -> proxyPassword); } }); - SslContext sslContext = SslContextBuilder.forClient().build(); - httpClient.secure(t -> t.sslContext(sslContext)); + SslContext sslContext = config.getCredentials().initSslContext(); + httpClient = httpClient.secure(t -> t.sslContext(sslContext)); } } else if (!config.isUseSimpleClientHttpFactory()) { if (CredentialsType.CERT_PEM == config.getCredentials().getType()) { From ed8a20b89d86361137f8288933d7e39cbe1db7c2 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 16 Jul 2024 11:23:23 +0300 Subject: [PATCH 067/136] EdgeEventSourcing - ignore EntityAlarm save events. Alarms pushed to edge using push_to_edge node --- .../server/service/edge/EdgeEventSourcingListener.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 11974d2ebc..3de4c95cc3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; @@ -194,7 +195,7 @@ public class EdgeEventSourcingListener { } break; case ALARM: - if (entity instanceof AlarmApiCallResult || entity instanceof Alarm) { + if (entity instanceof AlarmApiCallResult || entity instanceof Alarm || entity instanceof EntityAlarm) { return false; } break; From a2e4cb9a7d7b27706a546ec4e5e28d3830133de5 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 27 Jun 2024 19:06:38 +0300 Subject: [PATCH 068/136] Handle properly DELETE cases for ASSET/DEVICE to push ENTITY_DELETED msg into correct rule chain of the profile --- .../queue/DefaultTbClusterService.java | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index eabec663a3..7538e498eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetId; @@ -45,6 +47,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -205,7 +208,7 @@ public class DefaultTbClusterService implements TbClusterService { return; } } else { - HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId); + HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId, tbMsg); tbMsg = transformMsg(tbMsg, ruleEngineProfile); } @@ -213,13 +216,39 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); } - private HasRuleEngineProfile getRuleEngineProfileForEntityOrElseNull(TenantId tenantId, EntityId entityId) { + private HasRuleEngineProfile getRuleEngineProfileForEntityOrElseNull(TenantId tenantId, EntityId entityId, TbMsg tbMsg) { if (entityId.getEntityType().equals(EntityType.DEVICE)) { - return deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + if (TbMsgType.ENTITY_DELETED.equals(tbMsg.getInternalType())) { + try { + Device deletedDevice = JacksonUtil.fromString(tbMsg.getData(), Device.class); + if (deletedDevice == null) { + return null; + } + return deviceProfileCache.get(tenantId, deletedDevice.getDeviceProfileId()); + } catch (Exception e) { + log.warn("[{}][{}] Failed to deserialize device: {}", tenantId, entityId, tbMsg, e); + return null; + } + } else { + return deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + } } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { return deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId())); } else if (entityId.getEntityType().equals(EntityType.ASSET)) { - return assetProfileCache.get(tenantId, new AssetId(entityId.getId())); + if (TbMsgType.ENTITY_DELETED.equals(tbMsg.getInternalType())) { + try { + Asset deletedAsset = JacksonUtil.fromString(tbMsg.getData(), Asset.class); + if (deletedAsset == null) { + return null; + } + return assetProfileCache.get(tenantId, deletedAsset.getAssetProfileId()); + } catch (Exception e) { + log.warn("[{}][{}] Failed to deserialize asset: {}", tenantId, entityId, tbMsg, e); + return null; + } + } else { + return assetProfileCache.get(tenantId, new AssetId(entityId.getId())); + } } else if (entityId.getEntityType().equals(EntityType.ASSET_PROFILE)) { return assetProfileCache.get(tenantId, new AssetProfileId(entityId.getId())); } From b7c505efcd1911d953e4d0077d7c3245b98c6ee8 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 17 Jul 2024 11:59:20 +0300 Subject: [PATCH 069/136] DefaultTbClusterService.getRuleEngineProfileForEntityOrElseNull - Added unit tests --- .../queue/DefaultTbClusterService.java | 2 +- .../queue/DefaultTbClusterServiceTest.java | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 7538e498eb..49ec72d27a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -216,7 +216,7 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); } - private HasRuleEngineProfile getRuleEngineProfileForEntityOrElseNull(TenantId tenantId, EntityId entityId, TbMsg tbMsg) { + HasRuleEngineProfile getRuleEngineProfileForEntityOrElseNull(TenantId tenantId, EntityId entityId, TbMsg tbMsg) { if (entityId.getEntityType().equals(EntityType.DEVICE)) { if (TbMsgType.ENTITY_DELETED.equals(tbMsg.getInternalType())) { try { diff --git a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java index 22f3620d8c..78cca2de62 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/DefaultTbClusterServiceTest.java @@ -23,11 +23,20 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.gen.transport.TransportProtos; @@ -249,4 +258,44 @@ public class DefaultTbClusterServiceTest { queue.setPartitions(10); return queue; } + + @Test + public void testGetRuleEngineProfileForUpdatedAndDeletedDevice() { + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TenantId tenantId = new TenantId(UUID.randomUUID()); + DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); + + Device device = new Device(deviceId); + device.setDeviceProfileId(deviceProfileId); + + // device updated + TbMsg tbMsg = TbMsg.builder().internalType(TbMsgType.ENTITY_UPDATED).build(); + ((DefaultTbClusterService) clusterService).getRuleEngineProfileForEntityOrElseNull(tenantId, deviceId, tbMsg); + verify(deviceProfileCache, times(1)).get(tenantId, deviceId); + + // device deleted + tbMsg = TbMsg.builder().internalType(TbMsgType.ENTITY_DELETED).data(JacksonUtil.toString(device)).build(); + ((DefaultTbClusterService) clusterService).getRuleEngineProfileForEntityOrElseNull(tenantId, deviceId, tbMsg); + verify(deviceProfileCache, times(1)).get(tenantId, deviceProfileId); + } + + @Test + public void testGetRuleEngineProfileForUpdatedAndDeletedAsset() { + AssetId assetId = new AssetId(UUID.randomUUID()); + TenantId tenantId = new TenantId(UUID.randomUUID()); + AssetProfileId assetProfileId = new AssetProfileId(UUID.randomUUID()); + + Asset asset = new Asset(assetId); + asset.setAssetProfileId(assetProfileId); + + // asset updated + TbMsg tbMsg = TbMsg.builder().internalType(TbMsgType.ENTITY_UPDATED).build(); + ((DefaultTbClusterService) clusterService).getRuleEngineProfileForEntityOrElseNull(tenantId, assetId, tbMsg); + verify(assetProfileCache, times(1)).get(tenantId, assetId); + + // asset deleted + tbMsg = TbMsg.builder().internalType(TbMsgType.ENTITY_DELETED).data(JacksonUtil.toString(asset)).build(); + ((DefaultTbClusterService) clusterService).getRuleEngineProfileForEntityOrElseNull(tenantId, assetId, tbMsg); + verify(assetProfileCache, times(1)).get(tenantId, assetProfileId); + } } \ No newline at end of file From 7addeaf96c39d066ce77953edadc9b54cfee061c Mon Sep 17 00:00:00 2001 From: rusikv Date: Wed, 17 Jul 2024 18:12:58 +0300 Subject: [PATCH 070/136] UI: fixed device profile create/update form validation --- .../device/device-profile-transport-configuration.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts index 7d1ab27ebd..f0a58a6ded 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts @@ -121,7 +121,8 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu } public validate(c: UntypedFormControl): ValidationErrors | null { - return (this.deviceProfileTransportConfigurationFormGroup.valid) ? null : { + return (this.transportType === DeviceTransportType.DEFAULT || + this.deviceProfileTransportConfigurationFormGroup.valid) ? null : { configuration: { valid: false, }, From f48fbc61b4228a838e5a9876699ddc462248ebbe Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 18 Jul 2024 18:18:31 +0300 Subject: [PATCH 071/136] Added extended functionality for building Modbus connector using UI --- .../broker-config-control.component.html | 2 +- .../broker-config-control.component.ts | 15 +- .../mapping-table/mapping-table.component.ts | 39 +-- .../connectors-configuration/modbus/index.ts | 7 + .../modbus-basic-config.component.html | 28 ++ .../modbus-basic-config.component.ts | 130 ++++++++ .../modbus-data-keys-panel.component.html | 147 +++++++++ .../modbus-data-keys-panel.component.scss | 44 +++ .../modbus-data-keys-panel.component.ts | 164 ++++++++++ .../modbus-master-table.component.html | 131 ++++++++ .../modbus-master-table.component.scss | 101 ++++++ .../modbus-master-table.component.ts | 231 +++++++++++++ .../modbus-security-config.component.html | 44 +++ .../modbus-security-config.component.ts | 126 +++++++ .../modbus-slave-config.component.html | 217 +++++++++++++ .../modbus-slave-config.component.ts | 185 +++++++++++ .../modbus-slave-dialog.component.html | 307 ++++++++++++++++++ .../modbus-slave-dialog.component.ts | 198 +++++++++++ .../modbus-values.component.html | 117 +++++++ .../modbus-values/modbus-values.component.ts | 204 ++++++++++++ .../modbus/public-api.ts | 7 + .../opc-server-config.component.html} | 0 .../opc-server-config.component.scss} | 0 .../opc-server-config.component.ts} | 12 +- .../opc-ua-basic-config.component.html | 2 +- .../opc-ua-basic-config.component.ts | 4 +- .../connectors-configuration/public-api.ts | 3 +- .../gateway/gateway-connectors.component.html | 4 + .../gateway/gateway-connectors.component.ts | 4 +- .../lib/gateway/gateway-widget.models.ts | 243 ++++++++++++++ .../widget/widget-components.module.ts | 42 +-- .../gateway-port-tooltip.pipe.ts | 42 +++ .../src/app/modules/home/pipes/public-api.ts | 1 + .../datasource/datasource.abstract.ts | 32 ++ ui-ngx/src/app/shared/abstract/public-api.ts | 1 + .../ellipsis-chip-list.directive.ts | 22 +- .../src/app/shared/directives/public-api.ts | 1 + .../assets/locale/locale.constant-en_US.json | 62 ++++ .../connector-default-configs/modbus.json | 111 +++---- 39 files changed, 2892 insertions(+), 138 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/index.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/{server-config/server-config.component.html => opc-server-config/opc-server-config.component.html} (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/{server-config/server-config.component.scss => opc-server-config/opc-server-config.component.scss} (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/{server-config/server-config.component.ts => opc-server-config/opc-server-config.component.ts} (89%) create mode 100644 ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts create mode 100644 ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts create mode 100644 ui-ngx/src/app/shared/abstract/public-api.ts rename ui-ngx/src/app/{modules/home/components/widget/lib/gateway/connectors-configuration => shared/directives/ellipsis-chip-list}/ellipsis-chip-list.directive.ts (89%) create mode 100644 ui-ngx/src/app/shared/directives/public-api.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html index f70ed366e1..b5ef346300 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html @@ -42,7 +42,7 @@ + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts new file mode 100644 index 0000000000..d61d63edb8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -0,0 +1,130 @@ +/// +/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, TemplateRef } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + ConnectorBaseConfig, + ConnectorType, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { + ModbusMasterTableComponent, + ModbusSlaveConfigComponent, +} from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; +import { EllipsisChipListDirective } from '@shared/directives/public-api'; + +@Component({ + selector: 'tb-modbus-basic-config', + templateUrl: './modbus-basic-config.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusBasicConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusBasicConfigComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ModbusSlaveConfigComponent, + ModbusMasterTableComponent, + EllipsisChipListDirective, + ], + styles: [` + :host { + height: 100%; + } + :host ::ng-deep { + .mat-mdc-tab-group, .mat-mdc-tab-body-wrapper { + height: 100%; + } + } + `] +}) + +export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy { + + @Input() generalTabContent: TemplateRef; + + basicFormGroup: FormGroup; + + onChange: (value: string) => void; + onTouched: () => void; + + protected readonly connectorType = ConnectorType; + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.basicFormGroup = this.fb.group({ + master: [], + slave: [], + }); + + this.basicFormGroup.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.onChange(value); + this.onTouched(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + writeValue(basicConfig: ConnectorBaseConfig): void { + const editedBase = { + slave: basicConfig.slave || {}, + master: basicConfig.master || {}, + }; + + this.basicFormGroup.setValue(editedBase, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.basicFormGroup.valid ? null : { + basicFormGroup: {valid: false} + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html new file mode 100644 index 0000000000..08ef83eeb5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -0,0 +1,147 @@ + +
+
+
{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}
+
+
+
+ + + + +
+ {{ keyControl.get('tag').value }} +
+
+
+ +
+
+ gateway.key +
+
+ + + + warning + + +
+
+
+
+ gateway.type +
+
+ + + {{ type }} + + +
+
+
+
gateway.function-code
+
+ + + {{ ModbusFunctionCodeTranslationsMap.get(code) | translate }} + + +
+
+
+
gateway.objects-count
+
+ + + +
+
+
+
gateway.address
+
+ + + +
+
+
+
gateway.value
+
+ + + +
+
+
+
+
+
+ +
+
+
+ +
+
+ +
+ {{ noKeysText }} +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss new file mode 100644 index 0000000000..0e9f9a432f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .tb-modbus-keys-panel { + width: 77vw; + max-width: 700px; + + .title-container { + max-width: 11vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap + } + + .key-panel { + height: 500px; + overflow: auto; + } + + .tb-form-panel { + .mat-mdc-icon-button { + width: 56px; + height: 56px; + padding: 16px; + color: rgba(0, 0, 0, 0.54); + } + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts new file mode 100644 index 0000000000..a63b2e488e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -0,0 +1,164 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { + ModbusDataType, + ModbusFunctionCodeTranslationsMap, + ModbusObjectCountByDataType, + ModbusRegisterType, + ModbusValue, + ModbusValueKey, + noLeadTrailSpacesRegex, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; + +@Component({ + selector: 'tb-modbus-data-keys-panel', + templateUrl: './modbus-data-keys-panel.component.html', + styleUrls: ['./modbus-data-keys-panel.component.scss'], + standalone: true, + imports: [ + CommonModule, + SharedModule, + GatewayHelpLinkPipe, + ] +}) +export class ModbusDataKeysPanelComponent implements OnInit { + + @Input() isMaster = false; + @Input() panelTitle: string; + @Input() addKeyTitle: string; + @Input() deleteKeyTitle: string; + @Input() noKeysText: string; + @Input() register: ModbusRegisterType; + @Input() keysType: ModbusValueKey; + @Input() values: ModbusValue[]; + @Input() popover: TbPopoverComponent; + + @Output() keysDataApplied = new EventEmitter>(); + + keysListFormArray: UntypedFormArray; + errorText = ''; + modbusDataTypes = Object.values(ModbusDataType); + withFunctionCode = true; + functionCodesMap = new Map(); + defaultFunctionCodes = []; + + readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; + readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; + readonly defaultReadFunctionCodes = [3, 4]; + readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; + + constructor(private fb: UntypedFormBuilder) {} + + ngOnInit(): void { + this.keysListFormArray = this.prepareKeysFormArray(this.values); + this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TELEMENTRY); + this.defaultFunctionCodes = this.getDefaultFunctionCodes(); + } + + trackByKey(_: number, keyControl: AbstractControl): AbstractControl { + return keyControl; + } + + addKey(): void { + const dataKeyFormGroup = this.fb.group({ + tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + type: [ModbusDataType.STRING, [Validators.required]], + address: [0, [Validators.required]], + objectsCount: [1, [Validators.required]], + functionCode: [this.getDefaultFunctionCodes()[0]] + }); + this.observeKeyDataType(dataKeyFormGroup); + + this.keysListFormArray.push(dataKeyFormGroup); + } + + deleteKey($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + } + this.keysListFormArray.removeAt(index); + this.keysListFormArray.markAsDirty(); + } + + cancel(): void { + this.popover?.hide(); + } + + applyKeysData(): void { + this.keysDataApplied.emit(this.keysListFormArray.value); + } + + private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray { + const keysControlGroups: Array = []; + if (values) { + values.forEach(keyData => { + const { tag, value, type, address, objectsCount, functionCode } = keyData; + const dataKeyFormGroup = this.fb.group({ + tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + type: [type, [Validators.required]], + address: [address, [Validators.required]], + objectsCount: [objectsCount, [Validators.required]], + functionCode: [functionCode, []], + }); + this.observeKeyDataType(dataKeyFormGroup); + this.functionCodesMap.set(tag+address, this.getFunctionCodes(type)); + + keysControlGroups.push(dataKeyFormGroup); + }); + } + return this.fb.array(keysControlGroups); + } + + private observeKeyDataType(keyFormGroup: FormGroup): void { + keyFormGroup.get('type').valueChanges.subscribe(dataType => { + const objectsCountControl = keyFormGroup.get('objectsCount'); + if (!this.editableDataTypes.includes(dataType)) { + objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]); + } + const keyId = keyFormGroup.get('tag').value + keyFormGroup.get('address').value; + this.functionCodesMap.set(keyId, this.getFunctionCodes(dataType)); + }); + } + + private getFunctionCodes(dataType: ModbusDataType): number[] { + if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { + return this.defaultWriteFunctionCodes; + } + const functionCodes = this.defaultReadFunctionCodes; + if (dataType === ModbusDataType.BITS) { + const bitsFunctionCodes = [1, 2]; + bitsFunctionCodes.forEach(code => functionCodes.push(code)); + functionCodes.sort(); + } + if (this.keysType === ModbusValueKey.RPC_REQUESTS) { + this.defaultWriteFunctionCodes.forEach(code => functionCodes.push(code)); + } + return functionCodes; + } + + private getDefaultFunctionCodes(): number[] { + return this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES ? this.defaultWriteFunctionCodes : this.defaultReadFunctionCodes; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html new file mode 100644 index 0000000000..ac5e6f3108 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -0,0 +1,131 @@ + +
+
+ +
+
+ {{ 'gateway.servers-slaves' | translate}} +
+ + + +
+
+ +
+ + +   + + + +
+
+
+
+ + + {{ 'gateway.name' | translate }} + + + {{ mapping['name'] }} + + + + + {{ 'gateway.client-communication-type' | translate }} + + + {{ ModbusClientTypeLabelsMap.get(mapping['type']) }} + + + + + + +
+ + +
+
+ + + + + +
+
+
+ + +
+
+ +
+
+ + widget.no-data-found + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss new file mode 100644 index 0000000000..82cead07a4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../scss/constants'; + +:host { + width: 100%; + height: 100%; + display: block; + .tb-master-table { + .tb-master-table-content { + width: 100%; + height: 100%; + background: #fff; + overflow: hidden; + + &.tb-outlined-border { + box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); + border: solid 1px #e0e0e0; + border-radius: 4px; + } + + .mat-toolbar-tools{ + min-height: auto; + } + + .title-container{ + overflow: hidden; + } + + .tb-master-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + .mat-mdc-table { + table-layout: fixed; + min-width: 450px; + + .table-value-column { + padding: 0 12px; + width: 23%; + + &.request-column { + width: 38%; + } + } + } + } + + .ellipsis { + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .no-data-found { + height: calc(100% - 120px); + } + + @media #{$mat-xs} { + .mat-toolbar { + height: auto; + min-height: 100px; + + .tb-master-table-title{ + padding-bottom: 5px; + width: 100%; + } + } + } +} + +:host ::ng-deep { + mat-cell.tb-value-cell { + cursor: pointer; + .mat-icon { + height: 24px; + width: 24px; + font-size: 24px; + color: #757575 + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts new file mode 100644 index 0000000000..b1f2c09689 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -0,0 +1,231 @@ +/// +/// Copyright © 2016-2024 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 { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + forwardRef, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators'; +import { + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + ModbusClientTypeLabelsMap, ModbusMasterConfig, SlaveConfig, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { TbDatasource } from '@shared/abstract/datasource/datasource.abstract'; +import { ModbusSlaveDialogComponent } from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; + +@Component({ + selector: 'tb-modbus-master-table', + templateUrl: './modbus-master-table.component.html', + styleUrls: ['./modbus-master-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusMasterTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusMasterTableComponent), + multi: true + } + ], + standalone: true, + imports: [CommonModule, SharedModule] +}) +export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { + + @ViewChild('searchInput') searchInputField: ElementRef; + + textSearchMode = false; + dataSource: SlavesDatasource; + hidePageSize = false; + activeValue = false; + dirtyValue = false; + masterFormGroup: UntypedFormGroup; + textSearch = this.fb.control('', {nonNullable: true}); + + readonly ModbusClientTypeLabelsMap = ModbusClientTypeLabelsMap; + + private onChange: (value: string) => void = () => {}; + private onTouched: () => void = () => {}; + + private destroy$ = new Subject(); + + constructor( + public translate: TranslateService, + public dialog: MatDialog, + private dialogService: DialogService, + private fb: FormBuilder + ) { + this.masterFormGroup = this.fb.group({ slaves: this.fb.array([])}); + this.dirtyValue = !this.activeValue; + this.dataSource = new SlavesDatasource(); + } + + get slaves(): FormArray { + return this.masterFormGroup.get('slaves') as FormArray; + } + + ngOnInit(): void { + this.masterFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateTableData(value.slaves); + this.onChange(value); + this.onTouched(); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit(): void { + this.textSearch.valueChanges.pipe( + debounceTime(150), + distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), + takeUntil(this.destroy$) + ).subscribe((text) => { + const searchText = text.trim(); + this.updateTableData(this.slaves.value, searchText.trim()); + }); + } + + registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + writeValue(master: ModbusMasterConfig): void { + this.slaves.clear(); + this.pushDataAsFormArrays(master.slaves); + } + + validate(): ValidationErrors | null { + return this.slaves.controls.length ? null : { + slavesFormGroup: {valid: false} + }; + } + + enterFilterMode(): void { + this.textSearchMode = true; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode(): void { + this.updateTableData(this.slaves.value); + this.textSearchMode = false; + this.textSearch.reset(); + } + + manageSlave($event: Event, index?: number): void { + if ($event) { + $event.stopPropagation(); + } + const value = isDefinedAndNotNull(index) ? this.slaves.at(index).value : {}; + this.dialog.open(ModbusSlaveDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + value, + buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' + } + }).afterClosed() + .pipe(take(1), takeUntil(this.destroy$)) + .subscribe(res => { + if (res) { + if (isDefinedAndNotNull(index)) { + this.slaves.at(index).patchValue(res); + } else { + this.slaves.push(this.fb.control(res)); + } + this.masterFormGroup.markAsDirty(); + } + }); + } + + deleteMapping($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('gateway.delete-slave-title'), + '', + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.slaves.removeAt(index); + this.masterFormGroup.markAsDirty(); + } + }); + } + + private updateTableData(data: SlaveConfig[], textSearch?: string): void { + let tableValue = data; + if (textSearch) { + tableValue = tableValue.filter(value => + Object.values(value).some(val => + val.toString().toLowerCase().includes(textSearch.toLowerCase()) + ) + ); + } + this.dataSource.loadData(tableValue); + } + + private pushDataAsFormArrays(slaves: SlaveConfig[]): void { + if (slaves?.length) { + slaves.forEach((slave: SlaveConfig) => this.slaves.push(this.fb.control(slave))); + } + } +} + +export class SlavesDatasource extends TbDatasource { + constructor() { + super(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html new file mode 100644 index 0000000000..900973d327 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html @@ -0,0 +1,44 @@ +
+
+
gateway.client-cert-path
+
+ + + +
+
+
+
gateway.private-key-path
+
+ + + +
+
+
+
gateway.password
+
+ + +
+ +
+
+
+
+
+
gateway.server-hostname
+
+ + + +
+
+
+ + + {{ 'gateway.request-client-certificate' | translate }} + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts new file mode 100644 index 0000000000..2f5786882b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -0,0 +1,126 @@ +/// +/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, Input, OnChanges, OnDestroy } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + ModbusSecurity, + noLeadTrailSpacesRegex, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'tb-modbus-security-config', + templateUrl: './modbus-security-config.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusSecurityConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusSecurityConfigComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + SecurityConfigComponent, + ] +}) +export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { + + @Input() isMaster = false; + + securityConfigFormGroup: UntypedFormGroup; + + private onChange: (value: ModbusSecurity) => void; + private onTouched: () => void; + + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.securityConfigFormGroup = this.fb.group({ + certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + }); + + this.observeValueChanges(); + } + + ngOnChanges(): void { + if (this.isMaster) { + this.securityConfigFormGroup = this.fb.group({ + certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + reqclicert: [false, []], + }); + this.observeValueChanges(); + } + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: ModbusSecurity) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + validate(): ValidationErrors | null { + return this.securityConfigFormGroup.valid ? null : { + serverConfigFormGroup: { valid: false } + }; + } + + writeValue(securityConfig: ModbusSecurity): void { + this.securityConfigFormGroup.patchValue(securityConfig, {emitEvent: false}); + } + + private observeValueChanges(): void { + this.securityConfigFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value: ModbusSecurity) => { + this.onChange(value); + this.onTouched(); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html new file mode 100644 index 0000000000..43c71958bf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -0,0 +1,217 @@ + +
+
+
gateway.server-slave-config
+ + {{ ModbusProtocolLabelsMap.get(type) }} + +
+
+
+
gateway.host
+
+ + + + warning + + +
+
+
+
gateway.port
+
+ + + + warning + + +
+
+ +
+
gateway.port
+
+ + + +
+
+
+
+
+ gateway.method +
+
+ + + {{ ModbusMethodLabelsMap.get(method) }} + + +
+
+
+
+
gateway.unit-id
+
+ + + +
+
+
+
gateway.device-name
+
+ + + +
+
+
+
gateway.device-profile
+
+ + + +
+
+
+
gateway.poll-period
+
+ + + +
+
+
+
gateway.baudrate
+
+ + + +
+
+
+ + + {{ 'gateway.send-data-TB' | translate }} + + +
+ + + +
gateway.advanced-connection-settings
+
+
+
+
+
gateway.byte-order
+
+ + + {{ order }} + + +
+
+ + + + + + {{ 'gateway.tls-connection' | translate }} + + + + + + + +
+
gateway.vendor-name
+
+ + + +
+
+
+
gateway.product-code
+
+ + + +
+
+
+
gateway.vendor-url
+
+ + + +
+
+
+
gateway.product-name
+
+ + + +
+
+
+
gateway.model-name
+
+ + + +
+
+
+
+
+
+
gateway.values
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts new file mode 100644 index 0000000000..3bb21cd9d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -0,0 +1,185 @@ +/// +/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors, + Validator, Validators, +} from '@angular/forms'; +import { + ModbusMethodLabelsMap, + ModbusMethodType, + ModbusOrderType, + ModbusProtocolLabelsMap, + ModbusProtocolType, + noLeadTrailSpacesRegex, + PortLimits, SlaveConfig, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { + ModbusValuesComponent, + ModbusSecurityConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; +import { GatewayPortTooltipPipe } from '@home/pipes/public-api'; + +@Component({ + selector: 'tb-modbus-server-config', + templateUrl: './modbus-slave-config.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusSlaveConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusSlaveConfigComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ModbusValuesComponent, + ModbusSecurityConfigComponent, + GatewayPortTooltipPipe, + ], + styles: [` + :host { + .nested-expansion-header { + .mat-content { + height: 100%; + } + } + } + `], +}) +export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { + + slaveConfigFormGroup: UntypedFormGroup; + showSecurityControl: UntypedFormControl; + ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; + ModbusMethodLabelsMap = ModbusMethodLabelsMap; + portLimits = PortLimits; + + readonly modbusProtocolTypes = Object.values(ModbusProtocolType); + readonly modbusMethodTypes = Object.values(ModbusMethodType); + readonly modbusOrderType = Object.values(ModbusOrderType); + readonly ModbusProtocolType = ModbusProtocolType; + readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; + readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; + + private onChange: (value: SlaveConfig) => void; + private onTouched: () => void; + + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.showSecurityControl = this.fb.control(false); + this.slaveConfigFormGroup = this.fb.group({ + type: [ModbusProtocolType.TCP, []], + host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], + serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + method: [ModbusMethodType.SOCKET, []], + unitId: [null, []], + baudrate: [null, []], + deviceName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + deviceType: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + pollPeriod: [null, []], + sendDataToThingsBoard: [false, []], + byteOrder:[ModbusOrderType.BIG, []], + security: [], + identity: this.fb.group({ + vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + productCode: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + vendorUrl: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + productName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + modelName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + }), + values: [], + }); + + this.slaveConfigFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value: SlaveConfig) => { + if (value.type === ModbusProtocolType.Serial) { + value.port = value.serialPort; + delete value.serialPort; + } + this.onChange(value); + this.onTouched(); + }); + + this.observeTypeChange(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: SlaveConfig) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + validate(): ValidationErrors | null { + return this.slaveConfigFormGroup.valid ? null : { + serverConfigFormGroup: { valid: false } + }; + } + + writeValue(slaveConfig: SlaveConfig): void { + if (slaveConfig.type === ModbusProtocolType.Serial) { + slaveConfig.serialPort = slaveConfig.port as string; + delete slaveConfig.port; + } + this.slaveConfigFormGroup.patchValue(slaveConfig, {emitEvent: false}); + this.showSecurityControl.patchValue(!!slaveConfig.security); + this.updateControlsEnabling(slaveConfig.type); + } + + private observeTypeChange(): void { + this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => { + this.updateControlsEnabling(type); + }); + } + + private updateControlsEnabling(type: ModbusProtocolType): void { + if (type === ModbusProtocolType.Serial) { + this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); + this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); + } else { + this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); + this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); + } + }; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html new file mode 100644 index 0000000000..5c8fb4c397 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -0,0 +1,307 @@ + +
+ +

{{ 'gateway.server-slave' | translate }}

+ +
+ +
+
+
+
gateway.name
+
+ + + +
+
+
+
+
+
gateway.server-connection
+ + {{ ModbusProtocolLabelsMap.get(type) }} + +
+
+
+
gateway.host
+
+ + + + warning + + +
+
+
+
gateway.port
+
+ + + + warning + + +
+
+ +
+
gateway.port
+
+ + + +
+
+
+
+
+ gateway.method +
+
+ + + {{ ModbusMethodLabelsMap.get(method) }} + + +
+
+
+ +
+
gateway.baudrate
+
+ + + +
+
+
+
gateway.bytesize
+
+ + + +
+
+
+
gateway.stopbits
+
+ + + +
+
+
+
gateway.parity
+
+ + + +
+
+
+ + + {{ 'gateway.strict' | translate }} + + +
+
+
+
gateway.unit-id
+
+ + + +
+
+
+
gateway.device-name
+
+ + + +
+
+
+
gateway.device-profile
+
+ + + +
+
+
+ + + {{ 'gateway.send-data-on-change' | translate }} + + +
+ + + +
gateway.advanced-connection-settings
+
+
+
+
+
gateway.connection-timeout
+
+ + + +
+
+
+
gateway.byte-order
+
+ + + {{ order }} + + +
+
+
+
gateway.word-order
+
+ + + {{ order }} + + +
+
+ + + + + + {{ 'gateway.tls-connection' | translate }} + + + + + + +
+ + + {{ 'gateway.retries' | translate }} + + +
+
+ + + {{ 'gateway.retries-on-empty' | translate }} + + +
+
+ + + {{ 'gateway.retries-on-invalid' | translate }} + + +
+
+
gateway.poll-period
+
+ + + +
+
+
+
gateway.connect-attempt-time
+
+ + + +
+
+
+
gateway.connect-attempt-count
+
+ + + +
+
+
+
gateway.wait-after-failed-attempts
+
+ + + +
+
+
+
+
+
gateway.values
+ +
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts new file mode 100644 index 0000000000..202ef260c7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -0,0 +1,198 @@ +/// +/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, Inject, OnDestroy } from '@angular/core'; +import { + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { + MappingInfo, + ModbusMethodLabelsMap, + ModbusMethodType, + ModbusOrderType, + ModbusProtocolLabelsMap, + ModbusProtocolType, + noLeadTrailSpacesRegex, + PortLimits, SlaveConfig, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { Subject } from 'rxjs'; +import { + ModbusValuesComponent, + ModbusSecurityConfigComponent, +} from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'tb-modbus-slave-dialog', + templateUrl: './modbus-slave-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusSlaveDialogComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusSlaveDialogComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ModbusValuesComponent, + ModbusSecurityConfigComponent, + GatewayPortTooltipPipe, + ], + styles: [` + :host { + .slaves-config-container { + width: 900px; + } + .nested-expansion-header { + .mat-content { + height: 100%; + overflow: hidden; + } + } + } + `], +}) +export class ModbusSlaveDialogComponent extends DialogComponent implements OnDestroy { + + slaveConfigFormGroup: UntypedFormGroup; + showSecurityControl: UntypedFormControl; + portLimits = PortLimits; + + readonly modbusProtocolTypes = Object.values(ModbusProtocolType); + readonly modbusMethodTypes = Object.values(ModbusMethodType); + readonly modbusOrderType = Object.values(ModbusOrderType); + readonly ModbusProtocolType = ModbusProtocolType; + readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; + readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap; + readonly modbusHelpLink = + 'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters'; + readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict']; + readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; + + private destroy$ = new Subject(); + + constructor( + private fb: FormBuilder, + protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: MappingInfo, + public dialogRef: MatDialogRef, + ) { + super(store, router, dialogRef); + + this.showSecurityControl = this.fb.control(false); + this.slaveConfigFormGroup = this.fb.group({ + name: ['', [Validators.required]], + type: [ModbusProtocolType.TCP, [Validators.required]], + host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], + serialPort: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + method: [ModbusMethodType.SOCKET, []], + baudrate: [null, []], + stopbits: [null, []], + bytesize: [null, []], + parity: [null, []], + strict: [false, []], + unitId: [null, []], + deviceName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + deviceType: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + sendDataOnlyOnChange: [false, []], + timeout: [], + byteOrder: [ModbusOrderType.BIG, []], + wordOrder: [ModbusOrderType.BIG, []], + retries: [false, []], + retryOnEmpty: [false, []], + retryOnInvalid: [false, []], + pollPeriod: [null, []], + connectAttemptTimeMs: [null, []], + connectAttemptCount: [null, []], + waitAfterFailedAttemptsMs: [null, []], + values: [{}, []], + security: [], + }); + + this.slaveConfigFormGroup.patchValue({ + ...this.data.value, + values: { + attributes: this.data.value.attributes ?? [], + timeseries: this.data.value.timeseries ?? [], + attributeUpdates: this.data.value.attributeUpdates ?? [], + rpc: this.data.value.rpc ?? [], + } + }); + this.showSecurityControl.patchValue(!!this.data.value.security); + this.updateControlsEnabling(this.data.value.type); + this.observeTypeChange(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + if (this.slaveConfigFormGroup.valid) { + const slaveResult = {...this.slaveConfigFormGroup.value, ...this.slaveConfigFormGroup.value.values}; + delete slaveResult.values; + if (slaveResult.type === ModbusProtocolType.Serial) { + slaveResult.port = slaveResult.serialPort; + delete slaveResult.serialPort; + } + this.dialogRef.close(slaveResult); + } + } + + private observeTypeChange(): void { + this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => { + this.updateControlsEnabling(type); + }); + } + + private updateControlsEnabling(type: ModbusProtocolType): void { + if (type === ModbusProtocolType.Serial) { + this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); + this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); + } else { + this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); + this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); + } + }; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html new file mode 100644 index 0000000000..5b8077d77c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html @@ -0,0 +1,117 @@ + + + +
+ +
+
+ + + + +
+ +
+
+
+
+ + +
+
gateway.attributes
+
+ + + {{ attribute.tag }} + + + + + + +
+
+
+
gateway.telementry
+
+ + + {{ telemetry.tag }} + + + + + + +
+
+
+
gateway.attribute-updates
+
+ + + {{ attributeUpdate.tag }} + + + + + + +
+
+
+
gateway.rpc-requests
+
+ + + {{ rpcRequest.tag }} + + + + + + +
+
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts new file mode 100644 index 0000000000..76b376fb1b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts @@ -0,0 +1,204 @@ +/// +/// Copyright © 2016-2024 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 { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnChanges, + OnDestroy, + Renderer2, + ViewContainerRef +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + ModbusKeysAddKeyTranslationsMap, + ModbusKeysDeleteKeyTranslationsMap, + ModbusKeysNoKeysTextTranslationsMap, + ModbusKeysPanelTitleTranslationsMap, + ModbusRegisterTranslationsMap, + ModbusRegisterType, + ModbusRegisterValues, + ModbusValueKey, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { ModbusDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { EllipsisChipListDirective } from '@shared/directives/public-api'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; + +@Component({ + selector: 'tb-modbus-values', + templateUrl: './modbus-values.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusValuesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusValuesComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + EllipsisChipListDirective, + ], + styles: [` + :host { + .mat-mdc-tab-body-wrapper { + min-height: 320px; + } + } + `] +}) + +export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { + + @Input() singleMode = false; + + modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType); + modbusValueKeys = Object.values(ModbusValueKey); + ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap; + ModbusValueKey = ModbusValueKey; + valuesFormGroup: FormGroup; + + private onChange: (value: string) => void; + private onTouched: () => void; + + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private cdr: ChangeDetectorRef, + ) { + this.valuesFormGroup = this.fb.group(this.modbusRegisterTypes.reduce((registersAcc, register) => { + return { + ...registersAcc, + [register]: this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})), + }; + }, {})); + + this.observeValuesChanges(); + } + + ngOnChanges(): void { + if (this.singleMode) { + this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})); + this.observeValuesChanges(); + } + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + writeValue(values: ModbusRegisterValues): void { + this.valuesFormGroup.patchValue(values, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.valuesFormGroup.valid ? null : { + valuesFormGroup: {valid: false} + }; + } + + getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType) { + return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value; + } + + manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const group = this.valuesFormGroup; + + const keysControl = register ? group.get(register).get(keysType) : group.get(keysType); + const ctx = { + values: keysControl.value, + isMaster: !this.singleMode, + keysType, + panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType), + addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType), + deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType), + noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType) + }; + const dataKeysPanelPopover = this.popoverService.displayPopover( + trigger, + this.renderer, + this.viewContainerRef, + ModbusDataKeysPanelComponent, + 'leftBottom', + false, + null, + ctx, + {}, + {}, + {}, + true + ); + dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; + dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => { + dataKeysPanelPopover.hide(); + keysControl.patchValue(keysData); + keysControl.markAsDirty(); + this.cdr.markForCheck(); + }); + } + } + + private observeValuesChanges(): void { + this.valuesFormGroup.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => { + this.onChange(value); + this.onTouched(); + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts new file mode 100644 index 0000000000..803a8ff8dd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts @@ -0,0 +1,7 @@ +export * from './modbus-basic-config/modbus-basic-config.component'; +export * from './modbus-values/modbus-values.component'; +export * from './modbus-data-keys-panel/modbus-data-keys-panel.component'; +export * from './modbus-slave-config/modbus-slave-config.component'; +export * from './modbus-master-table/modbus-master-table.component'; +export * from './modbus-slave-dialog/modbus-slave-dialog.component'; +export * from './modbus-security-config/modbus-security-config.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts similarity index 89% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index 98a36182a6..955e7e8145 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -38,19 +38,19 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'tb-server-config', - templateUrl: './server-config.component.html', - styleUrls: ['./server-config.component.scss'], + selector: 'tb-opc-server-config', + templateUrl: './opc-server-config.component.html', + styleUrls: ['./opc-server-config.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ServerConfigComponent), + useExisting: forwardRef(() => OpcServerConfigComponent), multi: true }, { provide: NG_VALIDATORS, - useExisting: forwardRef(() => ServerConfigComponent), + useExisting: forwardRef(() => OpcServerConfigComponent), multi: true } ], @@ -61,7 +61,7 @@ import { takeUntil } from 'rxjs/operators'; SecurityConfigComponent, ] }) -export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { +export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { securityPolicyTypes = SecurityPolicyTypes; serverConfigFormGroup: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html index 1089fd114c..5f8b25af9d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html @@ -20,7 +20,7 @@ - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts index 2c1dd7dd2b..67aa4d9bd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts @@ -35,7 +35,7 @@ import { BrokerConfigControlComponent, MappingTableComponent, SecurityConfigComponent, - ServerConfigComponent, + OpcServerConfigComponent, WorkersConfigControlComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { takeUntil } from 'rxjs/operators'; @@ -65,7 +65,7 @@ import { Subject } from 'rxjs'; WorkersConfigControlComponent, BrokerConfigControlComponent, MappingTableComponent, - ServerConfigComponent, + OpcServerConfigComponent, ], styleUrls: ['./opc-ua-basic-config.component.scss'] }) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts index 5e185ddf0a..f98e6c569e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts @@ -17,10 +17,11 @@ export * from './mapping-table/mapping-table.component'; export * from './device-info-table/device-info-table.component'; export * from './security-config/security-config.component'; -export * from './server-config/server-config.component'; +export * from './opc-server-config/opc-server-config.component'; export * from './mapping-data-keys-panel/mapping-data-keys-panel.component'; export * from './type-value-panel/type-value-panel.component'; export * from './broker-config-control/broker-config-control.component'; export * from './workers-config-control/workers-config-control.component'; export * from './opc-ua-basic-config/opc-ua-basic-config.component'; export * from './mqtt-basic-config/mqtt-basic-config.component'; +export * from './modbus/public-api'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index 7896ac2516..5fc5693bec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -182,6 +182,10 @@ formControlName="basicConfig" [generalTabContent]="generalTabContent"> + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 14a8c15c00..a3ededb498 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -92,7 +92,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie allowBasicConfig = new Set([ ConnectorType.MQTT, - ConnectorType.OPCUA + ConnectorType.OPCUA, + ConnectorType.MODBUS, ]); gatewayLogLevel = Object.values(GatewayLogLevel); @@ -732,6 +733,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie switch (connector.type) { case ConnectorType.MQTT: case ConnectorType.OPCUA: + case ConnectorType.MODBUS: this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true}); setTimeout(() => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 9805008503..3e6471c0b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -181,6 +181,8 @@ export interface ConnectorBaseConfig { server?: ServerConfig; broker?: BrokerConfig; workers?: WorkersConfig; + master?: ModbusMasterConfig; + slave?: ModbusSlave; } export interface WorkersConfig { @@ -739,3 +741,244 @@ export const SecurityPolicyTypes = [ { value: SecurityPolicy.BASIC256, name: 'Basic256' }, { value: SecurityPolicy.BASIC256SHA, name: 'Basic256SHA256' } ]; + +export enum ModbusProtocolType { + TCP = 'tcp', + UDP = 'udp', + Serial = 'serial', +} + +export const ModbusProtocolLabelsMap = new Map( + [ + [ModbusProtocolType.TCP, 'TCP'], + [ModbusProtocolType.UDP, 'UDP'], + [ModbusProtocolType.Serial, 'Serial'], + ] +); + +export enum ModbusMethodType { + SOCKET = 'socket', + RTU = 'rtu', +} + +export const ModbusMethodLabelsMap = new Map( + [ + [ModbusMethodType.SOCKET, 'Socket'], + [ModbusMethodType.RTU, 'RTU'], + ] +); + +export enum ModbusOrderType { + BIG = 'BIG', + LITTLE = 'LITTLE', +} + +export enum ModbusRegisterType { + HoldingRegister = 'holding_registers', + CoilsInitializer = 'coils_initializer', + InputRegister = 'input_registers', + DiscreteInputs = 'discrete_inputs' +} + +export const ModbusRegisterTranslationsMap = new Map( + [ + [ModbusRegisterType.HoldingRegister, 'gateway.holding_registers'], + [ModbusRegisterType.CoilsInitializer, 'gateway.coils_initializer'], + [ModbusRegisterType.InputRegister, 'gateway.input_registers'], + [ModbusRegisterType.DiscreteInputs, 'gateway.discrete_inputs'] + ] +); + +export enum ModbusDataType { + STRING = 'string', + BYTES = 'bytes', + BITS = 'bits', + INT16 = '16int', + UINT16 = '16uint', + FLOAT16 = '16float', + INT32 = '32int', + UINT32 = '32uint', + FLOAT32 = '32float', + INT64 = '64int', + UINT64 = '64uint', + FLOAT64 = '64float' +} + +export enum ModbusObjectCountByDataType { + '16int' = 1, + '16uint' = 1, + '16float' = 1, + '32int' = 2, + '32uint' = 2, + '32float' = 2, + '64int' = 4, + '64uint' = 4, + '64float' = 4, +} + +export enum ModbusValueField { + Tag = 'tag', + Type = 'type', + ObjectsCount = 'objectsCount', + Address = 'address', + Value = 'value', +} + +export const ModbusFieldsTranslationsMap = new Map( + [ + [ModbusValueField.Tag, 'gateway.tag'], + [ModbusValueField.Type, 'gateway.type'], + [ModbusValueField.ObjectsCount, 'gateway.objects_count'], + [ModbusValueField.Address, 'gateway.address'], + [ModbusValueField.Value, 'gateway.value'] + ] +); + +export enum ModbusValueKey { + ATTRIBUTES = 'attributes', + TELEMENTRY = 'timeseries', + ATTRIBUTES_UPDATES = 'attributeUpdates', + RPC_REQUESTS = 'rpc', +} + +export const ModbusKeysPanelTitleTranslationsMap = new Map( + [ + [ModbusValueKey.ATTRIBUTES, 'gateway.attributes'], + [ModbusValueKey.TELEMENTRY, 'gateway.telementry'], + [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.attribute-updates'], + [ModbusValueKey.RPC_REQUESTS, 'gateway.rpc-requests'] + ] +); + +export const ModbusKeysAddKeyTranslationsMap = new Map( + [ + [ModbusValueKey.ATTRIBUTES, 'gateway.add-attribute'], + [ModbusValueKey.TELEMENTRY, 'gateway.add-telementry'], + [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.add-attribute-update'], + [ModbusValueKey.RPC_REQUESTS, 'gateway.add-rpc-request'] + ] +); + +export const ModbusKeysDeleteKeyTranslationsMap = new Map( + [ + [ModbusValueKey.ATTRIBUTES, 'gateway.delete-attribute'], + [ModbusValueKey.TELEMENTRY, 'gateway.delete-telementry'], + [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.delete-attribute-update'], + [ModbusValueKey.RPC_REQUESTS, 'gateway.delete-rpc-requests'] + ] +); + +export const ModbusKeysNoKeysTextTranslationsMap = new Map( + [ + [ModbusValueKey.ATTRIBUTES, 'gateway.no-attributes'], + [ModbusValueKey.TELEMENTRY, 'gateway.no-telementry'], + [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.no-attribute-updates'], + [ModbusValueKey.RPC_REQUESTS, 'gateway.no-rpc-requests'] + ] +); + +export const ModbusClientTypeLabelsMap = new Map( + [ + [ModbusProtocolType.TCP, 'TCP/UDP'], + [ModbusProtocolType.UDP, 'TCP/UDP'], + [ModbusProtocolType.Serial, 'Serial'], + ] +); + +export const ModbusFunctionCodeTranslationsMap = new Map( + [ + [1, 'gateway.read-coils'], + [2, 'gateway.read-discrete-inputs'], + [3, 'gateway.read-multiple-holding-registers'], + [4, 'gateway.read-input-registers'], + [5, 'gateway.write-coil'], + [6, 'gateway.write-register'], + [15, 'gateway.write-coils'], + [16, 'gateway.write-registers'], + ] +); + +export interface ModbusMasterConfig { + slaves: SlaveConfig[]; +} + +export interface SlaveConfig { + name: string; + host?: string; + port: string | number; + serialPort?: string; + type: ModbusProtocolType; + method: ModbusMethodType; + timeout: number; + byteOrder: ModbusOrderType; + wordOrder: ModbusOrderType; + retries: boolean; + retryOnEmpty: boolean; + retryOnInvalid: boolean; + pollPeriod: number; + unitId: number; + deviceName: string; + deviceType?: string; + sendDataOnlyOnChange: boolean; + connectAttemptTimeMs: number; + connectAttemptCount: number; + waitAfterFailedAttemptsMs: number; + attributes: ModbusValue[]; + timeseries: ModbusValue[]; + attributeUpdates: ModbusValue[]; + rpc: ModbusValue[]; + security?: ModbusSecurity; + baudrate?: number; + stopbits?: number; + bytesize?: number; + parity?: string; + strict?: boolean; +} + +export interface ModbusValue { + tag: string; + type: ModbusDataType; + functionCode?: number; + objectsCount: number; + address: number; + value?: string; +} + +export interface ModbusSecurity { + certfile?: string; + keyfile?: string; + password?: string; + server_hostname?: string; +} + +export interface ModbusSlave { + host?: string; + type: ModbusProtocolType; + method: ModbusMethodType; + unitId: number; + baudrate?: number; + deviceName: string; + deviceType: string; + pollPeriod: number; + sendDataToThingsBoard: boolean; + byteOrder: ModbusOrderType; + identity: ModbusIdentity; + values: ModbusRegisterValues; + port: string | number; + security: ModbusSecurity; +} + +export interface ModbusRegisterValues { + attributes: ModbusValue[]; + timeseries: ModbusValue[]; + attributeUpdates: ModbusValue[]; + rpc: ModbusValue[]; +} + +export interface ModbusIdentity { + vendorName?: string; + productCode?: string; + vendorUrl?: string; + productName?: string; + modelName?: string; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 1d28f139c0..3c2b7affc1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -90,9 +90,6 @@ import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/ import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; -import { - EllipsisChipListDirective -} from '@home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive'; import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component'; import { LatestChartComponent } from '@home/components/widget/lib/chart/latest-chart.component'; import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component'; @@ -120,10 +117,12 @@ import { MappingTableComponent, MqttBasicConfigComponent, OpcUaBasicConfigComponent, - ServerConfigComponent, + OpcServerConfigComponent, TypeValuePanelComponent, WorkersConfigControlComponent, + ModbusBasicConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; +import { EllipsisChipListDirective } from '@shared/directives/public-api'; @NgModule({ declarations: [ @@ -163,7 +162,6 @@ import { GatewayConfigurationComponent, GatewayRemoteConfigurationDialogComponent, GatewayServiceRPCConnectorTemplateDialogComponent, - EllipsisChipListDirective, ValueCardWidgetComponent, AggregatedValueCardWidgetComponent, CountWidgetComponent, @@ -193,22 +191,24 @@ import { LabelValueCardWidgetComponent, UnreadNotificationWidgetComponent, NotificationTypeFilterPanelComponent], - imports: [ - CommonModule, - SharedModule, - RpcWidgetsModule, - HomePageWidgetsModule, - SharedHomeComponentsModule, - RestConnectorSecurityComponent, - GatewayHelpLinkPipe, - BrokerConfigControlComponent, - WorkersConfigControlComponent, - ServerConfigComponent, - MqttBasicConfigComponent, - MappingTableComponent, - OpcUaBasicConfigComponent, - KeyValueIsNotEmptyPipe - ], + imports: [ + CommonModule, + SharedModule, + RpcWidgetsModule, + HomePageWidgetsModule, + SharedHomeComponentsModule, + RestConnectorSecurityComponent, + GatewayHelpLinkPipe, + BrokerConfigControlComponent, + WorkersConfigControlComponent, + OpcServerConfigComponent, + MqttBasicConfigComponent, + MappingTableComponent, + OpcUaBasicConfigComponent, + KeyValueIsNotEmptyPipe, + ModbusBasicConfigComponent, + EllipsisChipListDirective, + ], exports: [ EntitiesTableWidgetComponent, AlarmsTableWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts b/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts new file mode 100644 index 0000000000..9ff3e134c6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2024 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 { Pipe, PipeTransform } from '@angular/core'; +import { PortLimits, } from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { AbstractControl } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; + +@Pipe({ + name: 'getGatewayPortTooltip', + standalone: true, +}) +export class GatewayPortTooltipPipe implements PipeTransform { + + constructor(private translate: TranslateService) {} + + transform(portControl: AbstractControl): string { + if (portControl.hasError('required')) { + return this.translate.instant('gateway.port-required'); + } else if ( + portControl.hasError('min') || + portControl.hasError('max') + ) { + return this.translate.instant('gateway.port-limits-error', + {min: PortLimits.MIN, max: PortLimits.MAX}); + } + return ''; + } +} diff --git a/ui-ngx/src/app/modules/home/pipes/public-api.ts b/ui-ngx/src/app/modules/home/pipes/public-api.ts index 1cb3ce312a..9db71f9a06 100644 --- a/ui-ngx/src/app/modules/home/pipes/public-api.ts +++ b/ui-ngx/src/app/modules/home/pipes/public-api.ts @@ -15,3 +15,4 @@ /// export * from './gateway-help-link/gateway-help-link.pipe'; +export * from './gateway-port-tooltip/gateway-port-tooltip.pipe'; diff --git a/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts b/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts new file mode 100644 index 0000000000..fd8808c9cf --- /dev/null +++ b/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts @@ -0,0 +1,32 @@ +import { DataSource } from '@angular/cdk/collections'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export abstract class TbDatasource implements DataSource { + + protected dataSubject = new BehaviorSubject>([]); + + connect(): Observable> { + return this.dataSubject.asObservable(); + } + + disconnect(): void { + this.dataSubject.complete(); + } + + loadData(data: Array): void { + this.dataSubject.next(data); + } + + isEmpty(): Observable { + return this.dataSubject.pipe( + map((data: DataType[]) => !data.length) + ); + } + + total(): Observable { + return this.dataSubject.pipe( + map((data: DataType[]) => data.length) + ); + } +} diff --git a/ui-ngx/src/app/shared/abstract/public-api.ts b/ui-ngx/src/app/shared/abstract/public-api.ts new file mode 100644 index 0000000000..4345e3f4f2 --- /dev/null +++ b/ui-ngx/src/app/shared/abstract/public-api.ts @@ -0,0 +1 @@ +export * from './datasource/datasource.abstract'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts similarity index 89% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts rename to ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts index aa4738d707..4c44203b64 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts +++ b/ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts @@ -20,7 +20,8 @@ import { Inject, Input, OnDestroy, - Renderer2 + Renderer2, + AfterViewInit, } from '@angular/core'; import { isEqual } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; @@ -30,13 +31,15 @@ import { takeUntil } from 'rxjs/operators'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector - selector: '[tb-ellipsis-chip-list]' + selector: '[tb-ellipsis-chip-list]', + standalone: true, }) -export class EllipsisChipListDirective implements OnDestroy { +export class EllipsisChipListDirective implements OnDestroy, AfterViewInit { chipsValue: string[]; private destroy$ = new Subject(); + private intersectionObserver: IntersectionObserver; @Input('tb-ellipsis-chip-list') set chips(value: string[]) { @@ -59,6 +62,18 @@ export class EllipsisChipListDirective implements OnDestroy { ).subscribe(() => { this.adjustChips(); }); + + this.intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.adjustChips(); + } + }); + }); + } + + ngAfterViewInit(): void { + this.intersectionObserver.observe(this.el.nativeElement); } private adjustChips(): void { @@ -124,5 +139,6 @@ export class EllipsisChipListDirective implements OnDestroy { ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); + this.intersectionObserver.disconnect(); } } diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts new file mode 100644 index 0000000000..0e94de2a78 --- /dev/null +++ b/ui-ngx/src/app/shared/directives/public-api.ts @@ -0,0 +1 @@ +export * from './ellipsis-chip-list/ellipsis-chip-list.directive'; 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 33c8d25652..6517f2214f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2755,19 +2755,27 @@ "function": "Function" }, "gateway": { + "address": "Address", "add-entry": "Add configuration", "add-attribute": "Add attribute", "add-attribute-update": "Add attribute update", "add-key": "Add key", "add-timeseries": "Add time series", + "add-telementry": "Add telementry", "add-mapping": "Add mapping", + "add-slave": "Add Slave", "arguments": "Arguments", "add-rpc-method": "Add method", + "add-rpc-request": "Add request", "add-value": "Add argument", + "baudrate": "Baudrate", + "bytesize": "Bytesize", "delete-value": "Delete value", "delete-rpc-method": "Delete method", + "delete-rpc-request": "Delete request", "delete-attribute-update": "Add attribute update", "advanced": "Advanced", + "advanced-connection-settings": "Advanced connection settings", "attributes": "Attributes", "attribute-updates": "Attribute updates", "attribute-filter": "Attribute filter", @@ -2777,6 +2785,8 @@ "attribute-name-expression-required": "Attribute name expression required.", "attribute-name-expression-hint": "Hint for Attribute name expression", "basic": "Basic", + "byte-order": "Byte order", + "word-order": "Word order", "broker": { "connection": "Connection to broker", "name": "Broker name", @@ -2811,6 +2821,9 @@ "connectors-table-actions": "Actions", "connectors-table-key": "Key", "connectors-table-class": "Class", + "connection-timeout": "Connection timeout (s)", + "connect-attempt-time": "Connect attempt time (s)", + "connect-attempt-count": "Connect attempt count", "copy-username": "Copy username", "copy-password": "Copy password", "copy-client-id": "Copy client ID", @@ -2840,6 +2853,7 @@ "device-name-filter-required": "Device name filter is required.", "details": "Details", "delete-mapping-title": "Delete mapping ?", + "delete-slave-title": "Delete slave ?", "download-configuration-file": "Download configuration file", "download-docker-compose": "Download docker-compose.yml for your gateway", "enable-remote-logging": "Enable remote logging", @@ -2858,6 +2872,7 @@ "configuration-delete-dialog-confirm": "Turn Off", "connector-duplicate-name": "Connector with such name already exists.", "connector-side": "Connector side", + "client-communication-type": "Client communication type", "payload-type": "Payload type", "platform-side": "Platform side", "JSON": "JSON", @@ -2877,6 +2892,7 @@ "delete-attribute": "Delete attribute", "delete-key": "Delete key", "delete-timeseries": "Delete time series", + "delete-telementry": "Delete telementry", "default": "Default", "device-node": "Device node", "device-node-required": "Device node required.", @@ -2895,6 +2911,7 @@ "fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.", "from-device-request-settings": "Input request parsing", "from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.", + "function-code": "Function code", "to-device-response-settings": "Output request processing", "to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}", "gateway": "Gateway", @@ -2933,8 +2950,13 @@ "inactivity-timeout-seconds-required": "Inactivity timeout is required", "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1", "inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid", + "unit-id": "Unit ID", "host": "Host", "host-required": "Host is required.", + "holding_registers": "Holding register", + "coils_initializer": "Coils initializer", + "input_registers": "Input register", + "discrete_inputs": "Discrete input", "json-parse": "Not valid JSON.", "json-required": "Field cannot be empty.", "JSONPath-hint": "This field supports constants and JSONPath expressions.", @@ -2968,12 +2990,14 @@ "max-messages-queue-for-worker": "Max messages queue per worker", "max-messages-queue-for-worker-hint": "Maximal messages count that will be in the queue \nfor each converter worker.", "max-messages-queue-for-worker-required": "Max messages queue per worker is required.", + "method": "Method", "method-name": "Method name", "method-required": "Method name is required.", "min-pack-send-delay": "Min pack send delay (in ms)", "min-pack-send-delay-required": "Min pack send delay is required", "min-pack-send-delay-min": "Min pack send delay can not be less then 0", "mode": "Mode", + "model-name": "Model name", "mqtt-version": "MQTT version", "name": "Name", "name-required": "Name is required.", @@ -2984,20 +3008,26 @@ "no-gateway-found": "No gateway found.", "no-gateway-matching": " '{{item}}' not found.", "no-timeseries": "No time series", + "no-telementry": "No telementry", "no-keys": "No keys", "no-value": "No arguments", "no-rpc-methods": "No RPC methods", + "no-rpc-requests": "No RPC requests", "path-hint": "The path is local to the gateway file system", "path-logs": "Path to log files", "path-logs-required": "Path is required.", "password": "Password", "password-required": "Password is required.", "permit-without-calls": "Keep alive permit without calls", + "poll-period": "Poll period (s)", "port": "Port", "port-required": "Port is required.", "port-limits-error": "Port should be number from {{min}} to {{max}}.", "private-key-path": "Path to private key file", "path-to-private-key-required": "Path to private key file is required.", + "parity": "Parity", + "product-code": "Product code", + "product-name": "Product name", "raw": "Raw", "retain": "Retain", "retain-hint": "This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.", @@ -3006,6 +3036,9 @@ "remove-entry": "Remove configuration", "remote-shell": "Remote shell", "remote-configuration": "Remote Configuration", + "retries": "Retries", + "retries-on-empty": "Retries on empty", + "retries-on-invalid": "Retries on invalid", "rpc": { "title": "{{type}} Connector RPC parameters", "templates-title": "Connector RPC Templates", @@ -3097,6 +3130,7 @@ "json-value-invalid": "JSON value has an invalid format" }, "rpc-methods": "RPC methods", + "rpc-requests": "RPC requests", "request" : { "connect-request": "Connect request", "disconnect-request": "Disconnect request", @@ -3108,6 +3142,7 @@ "requests-mapping": "Requests mapping", "requests-mapping-hint": "MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.", "request-topic-expression": "Request topic expression", + "request-client-certificate": "Request client certificate", "request-topic-expression-required": "Request topic expression is required.", "response-timeout": "Response timeout (ms)", "response-timeout-required": "Response timeout is required.", @@ -3118,7 +3153,10 @@ "response-topic-expression-required": "Response topic expression is required.", "response-value-expression": "Response value expression", "response-value-expression-required": "Response value expression is required.", + "vendor-name": "Vendor name", + "vendor-url": "Vendor URL", "value": "Value", + "values": "Values", "value-required": "Value is required.", "value-expression": "Value expression", "value-expression-required": "Value expression is required.", @@ -3141,11 +3179,21 @@ }, "select-connector": "Select connector to display config", "send-change-data": "Send data only on change", + "send-data-TB": "Send data to ThingsBoard", + "send-data-on-change": "Send data only on change", "send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.", "server": "Server", + "server-hostname": "Server hostname", + "server-slave": "Server (Slave)", + "servers-slaves": "Servers (Slaves)", "server-port": "Server port", "server-url": "Server endpoint url", + "server-connection": "Server Connection", + "server-config": "Server configuration", + "server-slave-config": "Server (Slave) configuration", "server-url-required": "Server endpoint url is required.", + "stopbits": "Stopbits", + "strict": "Strict", "set": "Set", "show-map": "Show map", "statistics": { @@ -3217,6 +3265,7 @@ "thingsboard": "ThingsBoard", "general": "General", "timeseries": "Time series", + "telementry": "Telementry", "key": "Key", "keys": "Keys", "key-required": "Key is required.", @@ -3237,6 +3286,8 @@ "topic-required": "Topic filter is required.", "tls-path-ca-certificate": "Path to CA certificate on gateway", "tls-path-client-certificate": "Path to client certificate on gateway", + "tls-connection": "TLS Connection", + "master-connections": "Master Connections", "method-filter": "Method filter", "method-filter-hint": "Regular expression to filter incoming RPC method from platform.", "method-filter-required": "Method filter is required.", @@ -3256,12 +3307,22 @@ "at-least-once": "1 - At least once", "exactly-once": "2 - Exactly once" }, + "objects-count": "Objects count", + "wait-after-failed-attempts": "Wait after failed attempts (s)", "tls-path-private-key": "Path to private key on gateway", "toggle-fullscreen": "Toggle fullscreen", "transformer-json-config": "Configuration JSON*", "update-config": "Add/update configuration JSON", "username": "Username", "username-required": "Username is required.", + "read-coils": "Read Coils", + "read-discrete-inputs": "Read Discrete Inputs", + "read-multiple-holding-registers": "Read Multiple Holding Register", + "read-input-registers": "Read Input Registers", + "write-coil": "Write Coil", + "write-coils": "Write Coils", + "write-register": "Write Register", + "write-registers": "Write Registers", "hints": { "remote-configuration": "Enables remote configuration and management of the gateway", "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", @@ -3302,6 +3363,7 @@ "grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.", "permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.", "memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.", + "framer-type": "Type of framer.", "file": "Your data will be stored in separated files and will be saved even after the gateway restart.", "sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.", "opcua-timeout": "Timeout in seconds for connecting to OPC-UA server.", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json index 437a6abd95..fee4e745d6 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json @@ -2,6 +2,7 @@ "master": { "slaves": [ { + "name": "Slave 1", "host": "127.0.0.1", "port": 5021, "type": "tcp", @@ -185,64 +186,60 @@ "wordOrder": "LITTLE", "unitId": 0, "values": { - "holding_registers": [ - { - "attributes": [ - { - "address": 1, - "type": "string", - "tag": "sm", - "objectsCount": 1, - "value": "ON" - } - ], - "timeseries": [ - { - "address": 2, - "type": "int", - "tag": "smm", - "objectsCount": 1, - "value": "12334" - } - ], - "attributeUpdates": [ - { - "tag": "shared_attribute_write", - "type": "32int", - "functionCode": 6, - "objectsCount": 2, - "address": 29, - "value": 1243 - } - ], - "rpc": [ - { - "tag": "setValue", - "type": "bits", - "functionCode": 5, - "objectsCount": 1, - "address": 31, - "value": 22 - } - ] + "holding_registers": { + "attributes": [ + { + "address": 1, + "type": "string", + "tag": "sm", + "objectsCount": 1, + "value": "ON" + } + ], + "timeseries": [ + { + "address": 2, + "type": "int", + "tag": "smm", + "objectsCount": 1, + "value": "12334" + } + ], + "attributeUpdates": [ + { + "tag": "shared_attribute_write", + "type": "32int", + "functionCode": 6, + "objectsCount": 2, + "address": 29, + "value": 1243 + } + ], + "rpc": [ + { + "tag": "setValue", + "type": "bits", + "functionCode": 5, + "objectsCount": 1, + "address": 31, + "value": 22 + } + ] + }, + "coils_initializer": { + "attributes": [ + { + "address": 5, + "type": "string", + "tag": "sm", + "objectsCount": 1, + "value": "12" + } + ], + "timeseries": [], + "attributeUpdates": [], + "rpc": [] } - ], - "coils_initializer": [ - { - "attributes": [ - { - "address": 5, - "type": "string", - "tag": "sm", - "objectsCount": 1, - "value": "12" - } - ], - "timeseries": [], - "attributeUpdates": [], - "rpc": [] - } - ] } } } From 7ade2b20ed1e61bcb9bdfe6ec098cf24efcc814d Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 18 Jul 2024 18:41:05 +0300 Subject: [PATCH 072/136] refactoring --- .../connectors-configuration/modbus/index.ts | 7 ------- .../modbus-basic-config.component.ts | 7 +++---- .../modbus-master-table.component.ts | 2 +- .../modbus-security-config.component.html | 17 +++++++++++++++++ .../modbus-slave-config.component.ts | 6 ++---- .../modbus-slave-dialog.component.ts | 6 ++---- .../modbus-values/modbus-values.component.ts | 2 +- .../modbus/public-api.ts | 16 ++++++++++++++++ .../abstract/datasource/datasource.abstract.ts | 16 ++++++++++++++++ ui-ngx/src/app/shared/abstract/public-api.ts | 16 ++++++++++++++++ ui-ngx/src/app/shared/directives/public-api.ts | 16 ++++++++++++++++ 11 files changed, 90 insertions(+), 21 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/index.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/index.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/index.ts deleted file mode 100644 index 803a8ff8dd..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './modbus-basic-config/modbus-basic-config.component'; -export * from './modbus-values/modbus-values.component'; -export * from './modbus-data-keys-panel/modbus-data-keys-panel.component'; -export * from './modbus-slave-config/modbus-slave-config.component'; -export * from './modbus-master-table/modbus-master-table.component'; -export * from './modbus-slave-dialog/modbus-slave-dialog.component'; -export * from './modbus-security-config/modbus-security-config.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index d61d63edb8..8499aa2512 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -32,11 +32,10 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; -import { - ModbusMasterTableComponent, - ModbusSlaveConfigComponent, -} from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; + import { EllipsisChipListDirective } from '@shared/directives/public-api'; +import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; +import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; @Component({ selector: 'tb-modbus-basic-config', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index b1f2c09689..69613b9207 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -46,7 +46,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { TbDatasource } from '@shared/abstract/datasource/datasource.abstract'; -import { ModbusSlaveDialogComponent } from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; +import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; @Component({ selector: 'tb-modbus-master-table', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html index 900973d327..0f30dc4314 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html @@ -1,3 +1,20 @@ +
gateway.client-cert-path
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 3bb21cd9d4..903e449d86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -38,11 +38,9 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { - ModbusValuesComponent, - ModbusSecurityConfigComponent -} from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; import { GatewayPortTooltipPipe } from '@home/pipes/public-api'; +import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; +import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; @Component({ selector: 'tb-modbus-server-config', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 202ef260c7..b4b95d8b72 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -36,10 +36,8 @@ import { import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; -import { - ModbusValuesComponent, - ModbusSecurityConfigComponent, -} from '@home/components/widget/lib/gateway/connectors-configuration/modbus'; +import { ModbusValuesComponent } from '../modbus-values/modbus-values.component'; +import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { DialogComponent } from '@shared/components/dialog.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts index 76b376fb1b..57acf4a21c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts @@ -46,12 +46,12 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { ModbusDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { EllipsisChipListDirective } from '@shared/directives/public-api'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; +import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component'; @Component({ selector: 'tb-modbus-values', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts index 803a8ff8dd..87a3ba5c2f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2024 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. +/// + export * from './modbus-basic-config/modbus-basic-config.component'; export * from './modbus-values/modbus-values.component'; export * from './modbus-data-keys-panel/modbus-data-keys-panel.component'; diff --git a/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts b/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts index fd8808c9cf..2f1d9b1d78 100644 --- a/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts +++ b/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts @@ -1,3 +1,19 @@ +/// +/// Copyright © 2016-2024 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 { DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; diff --git a/ui-ngx/src/app/shared/abstract/public-api.ts b/ui-ngx/src/app/shared/abstract/public-api.ts index 4345e3f4f2..562011065a 100644 --- a/ui-ngx/src/app/shared/abstract/public-api.ts +++ b/ui-ngx/src/app/shared/abstract/public-api.ts @@ -1 +1,17 @@ +/// +/// Copyright © 2016-2024 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. +/// + export * from './datasource/datasource.abstract'; diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts index 0e94de2a78..f1fae708d0 100644 --- a/ui-ngx/src/app/shared/directives/public-api.ts +++ b/ui-ngx/src/app/shared/directives/public-api.ts @@ -1 +1,17 @@ +/// +/// Copyright © 2016-2024 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. +/// + export * from './ellipsis-chip-list/ellipsis-chip-list.directive'; From e87f00957f8ec2f56947edd57c9c7e04f6e0fc07 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 19 Jul 2024 11:39:02 +0300 Subject: [PATCH 073/136] review comments adjustments --- .../modbus-basic-config.component.ts | 7 ++--- .../modbus-data-keys-panel.component.html | 13 ++++++++-- .../modbus-slave-config.component.html | 13 +++++++++- .../modbus-slave-dialog.component.html | 24 +++++++++++++++-- .../modbus-slave-dialog.component.ts | 2 +- .../mqtt-basic-config.component.ts | 4 +-- .../opc-ua-basic-config.component.ts | 4 +-- .../gateway/gateway-connectors.component.ts | 3 ++- .../lib/gateway/gateway-widget.models.ts | 26 ++++++++++++------- .../datasource/datasource.abstract.ts | 12 ++++----- .../ellipsis-chip-list.directive.ts | 5 +--- .../assets/locale/locale.constant-en_US.json | 4 +-- 12 files changed, 80 insertions(+), 37 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 8499aa2512..6f2d9aff02 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -24,10 +24,7 @@ import { ValidationErrors, Validator, } from '@angular/forms'; -import { - ConnectorBaseConfig, - ConnectorType, -} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { takeUntil } from 'rxjs/operators'; @@ -112,7 +109,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.onTouched = fn; } - writeValue(basicConfig: ConnectorBaseConfig): void { + writeValue(basicConfig: ModbusBasicConfig): void { const editedBase = { slave: basicConfig.slave || {}, master: basicConfig.master || {}, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index 08ef83eeb5..1101565275 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -33,7 +33,7 @@
-
+
gateway.key
@@ -98,10 +98,19 @@
-
gateway.value
+
gateway.value
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 43c71958bf..bfc45325e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -68,10 +68,21 @@
-
gateway.port
+
gateway.port
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 5c8fb4c397..80d5d6115b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -28,10 +28,19 @@
-
gateway.name
+
gateway.name
+ + warning +
@@ -89,10 +98,21 @@
-
gateway.port
+
gateway.port
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index b4b95d8b72..3f12692dcb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -118,7 +118,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent | RequestMappingData[]; - server?: ServerConfig; - broker?: BrokerConfig; - workers?: WorkersConfig; - master?: ModbusMasterConfig; - slave?: ModbusSlave; +export type ConnectorBaseConfig = MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig; + +export interface MQTTBasicConfig { + dataMapping: ConverterConnectorMapping[]; + requestsMapping: Record | RequestMappingData[]; + broker: BrokerConfig; + workers: WorkersConfig; +} + +export interface OPCBasicConfig { + mapping: DeviceConnectorMapping[]; + server: ServerConfig; +} + +export interface ModbusBasicConfig { + master: ModbusMasterConfig; + slave: ModbusSlave; } export interface WorkersConfig { diff --git a/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts b/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts index 2f1d9b1d78..43de3789a0 100644 --- a/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts +++ b/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts @@ -18,11 +18,11 @@ import { DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -export abstract class TbDatasource implements DataSource { +export abstract class TbDatasource implements DataSource { - protected dataSubject = new BehaviorSubject>([]); + protected dataSubject = new BehaviorSubject>([]); - connect(): Observable> { + connect(): Observable> { return this.dataSubject.asObservable(); } @@ -30,19 +30,19 @@ export abstract class TbDatasource implements DataSource { this.dataSubject.complete(); } - loadData(data: Array): void { + loadData(data: Array): void { this.dataSubject.next(data); } isEmpty(): Observable { return this.dataSubject.pipe( - map((data: DataType[]) => !data.length) + map((data: T[]) => !data.length) ); } total(): Observable { return this.dataSubject.pipe( - map((data: DataType[]) => data.length) + map((data: T[]) => data.length) ); } } diff --git a/ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts index 4c44203b64..bb2847aeee 100644 --- a/ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts +++ b/ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts @@ -21,7 +21,6 @@ import { Input, OnDestroy, Renderer2, - AfterViewInit, } from '@angular/core'; import { isEqual } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; @@ -34,7 +33,7 @@ import { takeUntil } from 'rxjs/operators'; selector: '[tb-ellipsis-chip-list]', standalone: true, }) -export class EllipsisChipListDirective implements OnDestroy, AfterViewInit { +export class EllipsisChipListDirective implements OnDestroy { chipsValue: string[]; @@ -70,9 +69,7 @@ export class EllipsisChipListDirective implements OnDestroy, AfterViewInit { } }); }); - } - ngAfterViewInit(): void { this.intersectionObserver.observe(this.el.nativeElement); } 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 6517f2214f..2b89fd45ee 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2852,8 +2852,8 @@ "device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.", "device-name-filter-required": "Device name filter is required.", "details": "Details", - "delete-mapping-title": "Delete mapping ?", - "delete-slave-title": "Delete slave ?", + "delete-mapping-title": "Delete mapping?", + "delete-slave-title": "Delete slave?", "download-configuration-file": "Download configuration file", "download-docker-compose": "Download docker-compose.yml for your gateway", "enable-remote-logging": "Enable remote logging", From 8b0a2012eec75ae009df3429d94c1bc3f9fe2417 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 19 Jul 2024 12:32:48 +0300 Subject: [PATCH 074/136] minor adjustments --- .../modbus-data-keys-panel.component.html | 152 +++++++++--------- .../modbus-data-keys-panel.component.ts | 3 +- .../modbus-slave-config.component.html | 22 ++- .../modbus-slave-config.component.ts | 4 +- .../modbus-slave-dialog.component.html | 22 ++- .../modbus-slave-dialog.component.ts | 6 +- .../modbus-values.component.html | 8 +- .../lib/gateway/gateway-widget.models.ts | 10 +- .../assets/locale/locale.constant-en_US.json | 6 +- 9 files changed, 137 insertions(+), 96 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index 1101565275..efa8e11019 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -27,91 +27,97 @@
- {{ keyControl.get('tag').value }} + {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
-
-
- gateway.key -
-
- - - +
gateway.platform-side
+
+
+ gateway.key +
+
+ + + - warning - - + class="tb-error"> + warning + + +
-
-
- gateway.type -
-
- - - {{ type }} - - +
+
gateway.connector-side
+
+
+ gateway.type +
+
+ + + {{ type }} + + +
-
-
-
gateway.function-code
-
- - - {{ ModbusFunctionCodeTranslationsMap.get(code) | translate }} - - +
+
gateway.function-code
+
+ + + {{ ModbusFunctionCodeTranslationsMap.get(code) | translate }} + + +
-
-
-
gateway.objects-count
-
- - - +
+
gateway.objects-count
+
+ + + +
-
-
-
gateway.address
-
- - - +
+
gateway.address
+
+ + + +
-
-
-
gateway.value
-
- - - +
gateway.value
+
+ + + - warning - - + class="tb-error"> + warning + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index a63b2e488e..81e275b07c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -18,6 +18,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { + MappingKeysType, ModbusDataType, ModbusFunctionCodeTranslationsMap, ModbusObjectCountByDataType, @@ -71,7 +72,7 @@ export class ModbusDataKeysPanelComponent implements OnInit { ngOnInit(): void { this.keysListFormArray = this.prepareKeysFormArray(this.values); - this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TELEMENTRY); + this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES); this.defaultFunctionCodes = this.getDefaultFunctionCodes(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index bfc45325e4..5587cd09ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -109,18 +109,36 @@
-
gateway.device-name
+
gateway.device-name
+ + warning +
-
gateway.device-profile
+
gateway.device-profile
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 903e449d86..4211b2b09c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -106,8 +106,8 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat method: [ModbusMethodType.SOCKET, []], unitId: [null, []], baudrate: [null, []], - deviceName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - deviceType: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], pollPeriod: [null, []], sendDataToThingsBoard: [false, []], byteOrder:[ModbusOrderType.BIG, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 80d5d6115b..95e7700886 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -180,18 +180,36 @@
-
gateway.device-name
+
gateway.device-name
+ + warning +
-
gateway.device-profile
+
gateway.device-profile
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 3f12692dcb..991c71c16b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -114,7 +114,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent
-
gateway.telementry
+
gateway.timeseries
- - + + {{ telemetry.tag }} @@ -68,7 +68,7 @@ mat-icon-button color="primary" #telemetryButton - (click)="manageKeys($event, telemetryButton, ModbusValueKey.TELEMENTRY, register)"> + (click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)"> edit
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 865f819315..42a58b784e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -844,7 +844,7 @@ export const ModbusFieldsTranslationsMap = new Map( export enum ModbusValueKey { ATTRIBUTES = 'attributes', - TELEMENTRY = 'timeseries', + TIMESERIES = 'timeseries', ATTRIBUTES_UPDATES = 'attributeUpdates', RPC_REQUESTS = 'rpc', } @@ -852,7 +852,7 @@ export enum ModbusValueKey { export const ModbusKeysPanelTitleTranslationsMap = new Map( [ [ModbusValueKey.ATTRIBUTES, 'gateway.attributes'], - [ModbusValueKey.TELEMENTRY, 'gateway.telementry'], + [ModbusValueKey.TIMESERIES, 'gateway.timeseries'], [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.attribute-updates'], [ModbusValueKey.RPC_REQUESTS, 'gateway.rpc-requests'] ] @@ -861,7 +861,7 @@ export const ModbusKeysPanelTitleTranslationsMap = new Map( [ [ModbusValueKey.ATTRIBUTES, 'gateway.add-attribute'], - [ModbusValueKey.TELEMENTRY, 'gateway.add-telementry'], + [ModbusValueKey.TIMESERIES, 'gateway.add-timeseries'], [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.add-attribute-update'], [ModbusValueKey.RPC_REQUESTS, 'gateway.add-rpc-request'] ] @@ -870,7 +870,7 @@ export const ModbusKeysAddKeyTranslationsMap = new Map( export const ModbusKeysDeleteKeyTranslationsMap = new Map( [ [ModbusValueKey.ATTRIBUTES, 'gateway.delete-attribute'], - [ModbusValueKey.TELEMENTRY, 'gateway.delete-telementry'], + [ModbusValueKey.TIMESERIES, 'gateway.delete-timeseries'], [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.delete-attribute-update'], [ModbusValueKey.RPC_REQUESTS, 'gateway.delete-rpc-requests'] ] @@ -879,7 +879,7 @@ export const ModbusKeysDeleteKeyTranslationsMap = new Map( [ [ModbusValueKey.ATTRIBUTES, 'gateway.no-attributes'], - [ModbusValueKey.TELEMENTRY, 'gateway.no-telementry'], + [ModbusValueKey.TIMESERIES, 'gateway.no-timeseries'], [ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.no-attribute-updates'], [ModbusValueKey.RPC_REQUESTS, 'gateway.no-rpc-requests'] ] 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 2b89fd45ee..0475c771e6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2761,7 +2761,6 @@ "add-attribute-update": "Add attribute update", "add-key": "Add key", "add-timeseries": "Add time series", - "add-telementry": "Add telementry", "add-mapping": "Add mapping", "add-slave": "Add Slave", "arguments": "Arguments", @@ -2892,13 +2891,14 @@ "delete-attribute": "Delete attribute", "delete-key": "Delete key", "delete-timeseries": "Delete time series", - "delete-telementry": "Delete telementry", "default": "Default", "device-node": "Device node", "device-node-required": "Device node required.", "device-node-hint": "Path or identifier for device node on OPC UA server. Relative paths from it for attributes and time series can be used.", "device-name": "Device name", "device-profile": "Device profile", + "device-name-required": "Device name required", + "device-profile-required": "Device profile required", "download-tip": "Download configuration file", "drop-file": "Drop file here or", "enable-subscription": "Enable subscription", @@ -3008,7 +3008,6 @@ "no-gateway-found": "No gateway found.", "no-gateway-matching": " '{{item}}' not found.", "no-timeseries": "No time series", - "no-telementry": "No telementry", "no-keys": "No keys", "no-value": "No arguments", "no-rpc-methods": "No RPC methods", @@ -3265,7 +3264,6 @@ "thingsboard": "ThingsBoard", "general": "General", "timeseries": "Time series", - "telementry": "Telementry", "key": "Key", "keys": "Keys", "key-required": "Key is required.", From 80d2b6abfa71508fccae0d3fe2304d1c783a118e Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 19 Jul 2024 14:47:42 +0300 Subject: [PATCH 075/136] UI: device profile transport config form fix validation, minor improvements --- .../add-device-profile-dialog.component.html | 3 +- .../profile/device-profile.component.html | 6 +- ...ofile-transport-configuration.component.ts | 11 ---- ...-device-profile-configuration.component.ts | 10 --- ...ofile-transport-configuration.component.ts | 62 +++++++++++-------- ...evice-profile-configuration.component.html | 1 - .../device-profile-configuration.component.ts | 10 --- ...ile-transport-configuration.component.html | 5 -- ...ofile-transport-configuration.component.ts | 20 ++---- ...ofile-transport-configuration.component.ts | 13 +--- ...ofile-transport-configuration.component.ts | 11 ---- ...ofile-transport-configuration.component.ts | 11 ---- .../device-profile-tabs.component.html | 6 +- 13 files changed, 45 insertions(+), 124 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index 9d5458f942..3b45e4dcfb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -108,8 +108,7 @@ + isAdd="true"> diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index aed11c185b..10c1349b40 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -137,8 +137,7 @@ + formControlName="configuration"> @@ -151,8 +150,7 @@ + [isAdd] = "isTransportTypeChanged"> diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts index fc44a35eda..d772cffc8b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts @@ -28,7 +28,6 @@ import { } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { CoapDeviceProfileTransportConfiguration, coapDeviceTypeTranslationMap, @@ -74,7 +73,6 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control coapTransportConfigurationFormGroup: UntypedFormGroup; private destroy$ = new Subject(); - private requiredValue: boolean; private transportPayloadTypeConfiguration = this.fb.group({ transportPayloadType: [TransportPayloadType.JSON, Validators.required], @@ -84,15 +82,6 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required] }); - get required(): boolean { - return this.requiredValue; - } - - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts index f24be2a996..ff8e947ba3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts @@ -18,7 +18,6 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DefaultDeviceProfileConfiguration, DeviceProfileConfiguration, @@ -39,15 +38,6 @@ export class DefaultDeviceProfileConfigurationComponent implements ControlValueA defaultDeviceProfileConfigurationFormGroup: UntypedFormGroup; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts index d2031e55f7..1289d83745 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts @@ -15,39 +15,42 @@ /// import { Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { - DefaultDeviceProfileTransportConfiguration, - DeviceProfileTransportConfiguration, - DeviceTransportType -} from '@shared/models/device.models'; +import { DefaultDeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models'; @Component({ selector: 'tb-default-device-profile-transport-configuration', templateUrl: './default-device-profile-transport-configuration.component.html', styleUrls: [], - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), - multi: true - }] + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), + multi: true + } + ] }) -export class DefaultDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { +export class DefaultDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator { defaultDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - @Input() disabled: boolean; @@ -86,12 +89,17 @@ export class DefaultDeviceProfileTransportConfigurationComponent implements Cont this.defaultDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); } - private updateModel() { - let configuration: DeviceProfileTransportConfiguration = null; - if (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) { - configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; - configuration.type = DeviceTransportType.DEFAULT; + validate(c: UntypedFormControl): ValidationErrors | null { + return (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) ? null : { + configuration: { + valid: false + } } + } + + private updateModel() { + const configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.DEFAULT; this.propagateChange(configuration); } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html index 8cba244590..0ede71e49b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html @@ -19,7 +19,6 @@
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts index 8b246ce7e0..9f51562c72 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts @@ -18,7 +18,6 @@ import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models'; import { deepClone } from '@core/utils'; import { Subject } from 'rxjs'; @@ -42,15 +41,6 @@ export class DeviceProfileConfigurationComponent implements ControlValueAccessor private destroy$ = new Subject(); - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html index e03278f844..24b21f9ff0 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html @@ -19,32 +19,27 @@
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts index f0a58a6ded..998235aefd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts @@ -23,12 +23,10 @@ import { UntypedFormControl, UntypedFormGroup, ValidationErrors, - Validator, - Validators + Validator } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models'; import { deepClone } from '@core/utils'; @@ -55,15 +53,6 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu deviceProfileTransportConfigurationFormGroup: UntypedFormGroup; - private requiredValue: boolean; - get required(): boolean { - return this.requiredValue; - } - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - @Input() disabled: boolean; @@ -87,7 +76,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu ngOnInit() { this.deviceProfileTransportConfigurationFormGroup = this.fb.group({ - configuration: [null, Validators.required] + configuration: [null] }); this.deviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { this.updateModel(); @@ -110,7 +99,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu delete configuration.type; } setTimeout(() => { - this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}); }, 0); } @@ -121,8 +110,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu } public validate(c: UntypedFormControl): ValidationErrors | null { - return (this.transportType === DeviceTransportType.DEFAULT || - this.deviceProfileTransportConfigurationFormGroup.valid) ? null : { + return (this.deviceProfileTransportConfigurationFormGroup.valid) ? null : { configuration: { valid: false, }, diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index a6f6e3a467..0bda4c08c2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -25,7 +25,6 @@ import { Validator, Validators } from '@angular/forms'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ATTRIBUTE, DEFAULT_EDRX_CYCLE, @@ -47,7 +46,7 @@ import { ObjectIDVerTranslationMap } from './lwm2m-profile-config.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; -import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils'; +import { deepClone, isDefinedAndNotNull, isEmpty } from '@core/utils'; import { Direction } from '@shared/models/page/sort-order'; import _ from 'lodash'; import { Subject } from 'rxjs'; @@ -77,7 +76,6 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro public disabled = false; public isTransportWasRunWithBootstrap = true; public isBootstrapServerUpdateEnable: boolean; - private requiredValue: boolean; private destroy$ = new Subject(); lwm2mDeviceProfileFormGroup: UntypedFormGroup; @@ -88,15 +86,6 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro sortFunction: (key: string, value: object) => object; - get required(): boolean { - return this.requiredValue; - } - - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } - @Input() isAdd: boolean; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index 1566904258..3cabb2a541 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -29,7 +29,6 @@ import { } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { defaultAttributesSchema, defaultRpcRequestSchema, @@ -70,16 +69,6 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control mqttDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup; private destroy$ = new Subject(); - private requiredValue: boolean; - - get required(): boolean { - return this.requiredValue; - } - - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts index f81422943a..202fdca49b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts @@ -25,7 +25,6 @@ import { Validator, Validators } from '@angular/forms'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DeviceTransportType, SnmpDeviceProfileTransportConfiguration @@ -63,16 +62,6 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit, snmpDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup; private destroy$ = new Subject(); - private requiredValue: boolean; - - get required(): boolean { - return this.requiredValue; - } - - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } @Input() disabled: boolean; diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index fc71dbedcb..aa928687ad 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -35,8 +35,7 @@
+ [isAdd]="isTransportTypeChanged">
@@ -65,8 +64,7 @@
+ formControlName="configuration">
From a246b0454c3c75b9f34e270641537233067a6f3a Mon Sep 17 00:00:00 2001 From: kalytka Date: Fri, 19 Jul 2024 14:48:47 +0300 Subject: [PATCH 076/136] Fixed wrong behavior under customer level --- .../recent-dashboards-widget.component.html | 5 ++-- .../recent-dashboards-widget.component.ts | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html index 7ec7562d31..72b5d8a7f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html @@ -48,7 +48,7 @@ {{ 'widgets.recent-dashboards.name' | translate }} - {{ lastVisitedDashboard.title }} + {{ lastVisitedDashboard.title }} @@ -78,7 +78,7 @@ class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}
@@ -87,6 +87,7 @@ subscriptSizing="dynamic" appearance="outline" [useIdValue]="false" + [customerId]="customerId" label="" placeholder="{{ 'dashboard.select-dashboard' | translate }}" [(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts index 69b2230e25..cff11c349a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts @@ -14,16 +14,7 @@ /// limitations under the License. /// -import { - AfterViewInit, - ChangeDetectorRef, - Component, - Input, - OnDestroy, - OnInit, - QueryList, ViewChild, - ViewChildren -} from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -33,7 +24,8 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { WidgetContext } from '@home/models/widget-component.models'; import { AbstractUserDashboardInfo, - LastVisitedDashboardInfo, StarredDashboardInfo, + LastVisitedDashboardInfo, + StarredDashboardInfo, UserDashboardAction, UserDashboardsInfo } from '@shared/models/user-settings.models'; @@ -46,6 +38,8 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { MatSort } from '@angular/material/sort'; import { DashboardInfo } from '@shared/models/dashboard.models'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; +import { UserService } from '@core/http/user.service'; +import { User } from '@shared/models/user.model'; @Component({ selector: 'tb-recent-dashboards-widget', @@ -77,14 +71,23 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On hasDashboardsAccess = true; dirty = false; + public customerId: string; + private isFullscreenMode = false; constructor(protected store: Store, private cd: ChangeDetectorRef, + private userService: UserService, private userSettingService: UserSettingsService) { super(store); } ngOnInit() { + this.userService.getUser(this.authUser.userId).subscribe((userInfo: User) => { + this.isFullscreenMode = userInfo.additionalInfo.defaultDashboardFullscreen; + }); + if (this.authUser.authority === Authority.CUSTOMER_USER) { + this.customerId = this.authUser.customerId; + } this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority); if (this.hasDashboardsAccess) { this.reload(); @@ -110,6 +113,11 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On ); } + public createDashboardUrl(id: string): string { + const baseUrl = this.isFullscreenMode ? '/dashboard/' : '/dashboards/'; + return baseUrl + id; + } + toggleValueChange(value: 'last' | 'starred') { this.toggleValue = value; if (this.dirty) { From 5229efb6ceed7e46c6a045b98a623ef7d2dff7e5 Mon Sep 17 00:00:00 2001 From: kalytka Date: Fri, 19 Jul 2024 15:49:16 +0300 Subject: [PATCH 077/136] Refactoring --- .../home-page/recent-dashboards-widget.component.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts index cff11c349a..76eef3f9b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + QueryList, + ViewChild, + ViewChildren +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; From 62e2c8a6ad687f0ce033502aa317c27bb69590a5 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 19 Jul 2024 17:39:25 +0300 Subject: [PATCH 078/136] functionality adjustments --- .../modbus-data-keys-panel.component.html | 9 ++- .../modbus-data-keys-panel.component.ts | 25 +++++--- .../modbus-security-config.component.html | 3 +- .../modbus-security-config.component.ts | 15 ++++- .../modbus-slave-config.component.html | 10 +++ .../modbus-slave-config.component.ts | 64 ++++++++++++++++--- .../modbus-slave-dialog.component.html | 9 +++ .../modbus-slave-dialog.component.ts | 4 +- .../lib/gateway/gateway-widget.models.ts | 2 + .../assets/locale/locale.constant-en_US.json | 3 + 10 files changed, 119 insertions(+), 25 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index efa8e11019..a38b7e4dda 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -27,7 +27,10 @@
- {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }} + + {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }} + + {{ keyControl.get('tag').value }}
@@ -73,7 +76,7 @@
- {{ ModbusFunctionCodeTranslationsMap.get(code) | translate }} + {{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}
@@ -102,7 +105,7 @@
-
+
gateway.value
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index 81e275b07c..ac448f703e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -18,7 +18,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { - MappingKeysType, ModbusDataType, ModbusFunctionCodeTranslationsMap, ModbusObjectCountByDataType, @@ -30,6 +29,7 @@ import { import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; +import { generateSecret } from '@core/utils'; @Component({ selector: 'tb-modbus-data-keys-panel', @@ -83,11 +83,12 @@ export class ModbusDataKeysPanelComponent implements OnInit { addKey(): void { const dataKeyFormGroup = this.fb.group({ tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], type: [ModbusDataType.STRING, [Validators.required]], address: [0, [Validators.required]], objectsCount: [1, [Validators.required]], - functionCode: [this.getDefaultFunctionCodes()[0]] + functionCode: [this.getDefaultFunctionCodes()[0]], + id: [{value: generateSecret(5), disabled: true}], }); this.observeKeyDataType(dataKeyFormGroup); @@ -117,14 +118,15 @@ export class ModbusDataKeysPanelComponent implements OnInit { const { tag, value, type, address, objectsCount, functionCode } = keyData; const dataKeyFormGroup = this.fb.group({ tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + value: [{value, disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], type: [type, [Validators.required]], address: [address, [Validators.required]], objectsCount: [objectsCount, [Validators.required]], functionCode: [functionCode, []], + id: [{value: generateSecret(5), disabled: true}], }); this.observeKeyDataType(dataKeyFormGroup); - this.functionCodesMap.set(tag+address, this.getFunctionCodes(type)); + this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(type)); keysControlGroups.push(dataKeyFormGroup); }); @@ -138,8 +140,7 @@ export class ModbusDataKeysPanelComponent implements OnInit { if (!this.editableDataTypes.includes(dataType)) { objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]); } - const keyId = keyFormGroup.get('tag').value + keyFormGroup.get('address').value; - this.functionCodesMap.set(keyId, this.getFunctionCodes(dataType)); + this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType)); }); } @@ -147,7 +148,7 @@ export class ModbusDataKeysPanelComponent implements OnInit { if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { return this.defaultWriteFunctionCodes; } - const functionCodes = this.defaultReadFunctionCodes; + const functionCodes = [...this.defaultReadFunctionCodes]; if (dataType === ModbusDataType.BITS) { const bitsFunctionCodes = [1, 2]; bitsFunctionCodes.forEach(code => functionCodes.push(code)); @@ -160,6 +161,12 @@ export class ModbusDataKeysPanelComponent implements OnInit { } private getDefaultFunctionCodes(): number[] { - return this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES ? this.defaultWriteFunctionCodes : this.defaultReadFunctionCodes; + if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { + return this.defaultWriteFunctionCodes; + } + if (this.keysType === ModbusValueKey.RPC_REQUESTS) { + return [...this.defaultReadFunctionCodes, ...this.defaultWriteFunctionCodes]; + } + return this.defaultReadFunctionCodes; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html index 0f30dc4314..66db8c5018 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html @@ -15,7 +15,8 @@ limitations under the License. --> -
+
+
{{ 'gateway.hints.path-in-os' | translate }}
gateway.client-cert-path
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index 2f5786882b..bf926d3454 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -112,7 +112,20 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali } writeValue(securityConfig: ModbusSecurity): void { - this.securityConfigFormGroup.patchValue(securityConfig, {emitEvent: false}); + const { certfile, password, keyfile, server_hostname } = securityConfig; + let securityState = { + certfile: certfile ?? '', + password: password ?? '', + keyfile: keyfile ?? '', + server_hostname: server_hostname?? '', + reqclicert: !!securityConfig.reqclicert, + }; + if (this.isMaster) { + securityState = { ...securityState, reqclicert: !!securityConfig.reqclicert }; + } else { + securityState = { ...securityState, server_hostname: server_hostname ?? '' }; + } + this.securityConfigFormGroup.reset(securityState, {emitEvent: false}); } private observeValueChanges(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 5587cd09ee..13f31bdda5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -16,6 +16,7 @@ -->
+
{{ 'gateway.hints.modbus-server' | translate }}
gateway.server-slave-config
@@ -105,6 +106,15 @@
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 4211b2b09c..27de908e11 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -23,7 +23,8 @@ import { UntypedFormControl, UntypedFormGroup, ValidationErrors, - Validator, Validators, + Validator, + Validators, } from '@angular/forms'; import { ModbusMethodLabelsMap, @@ -31,8 +32,11 @@ import { ModbusOrderType, ModbusProtocolLabelsMap, ModbusProtocolType, + ModbusRegisterValues, + ModbusSlave, noLeadTrailSpacesRegex, - PortLimits, SlaveConfig, + PortLimits, + SlaveConfig, } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; @@ -104,7 +108,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], method: [ModbusMethodType.SOCKET, []], - unitId: [null, []], + unitId: [null, [Validators.required]], baudrate: [null, []], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], @@ -155,13 +159,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }; } - writeValue(slaveConfig: SlaveConfig): void { - if (slaveConfig.type === ModbusProtocolType.Serial) { - slaveConfig.serialPort = slaveConfig.port as string; - delete slaveConfig.port; - } - this.slaveConfigFormGroup.patchValue(slaveConfig, {emitEvent: false}); + writeValue(slaveConfig: ModbusSlave): void { this.showSecurityControl.patchValue(!!slaveConfig.security); + this.updateSlaveConfig(slaveConfig); this.updateControlsEnabling(slaveConfig.type); } @@ -180,4 +180,50 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); } }; + + private updateSlaveConfig(slaveConfig: ModbusSlave): void { + const { + type, + method, + unitId, + deviceName, + deviceType, + pollPeriod, + sendDataToThingsBoard, + byteOrder, + security, + identity, + values, + baudrate, + host, + port, + } = slaveConfig; + let slaveState: ModbusSlave = { + host: host ?? '', + type: type ?? ModbusProtocolType.TCP, + method: method ?? ModbusMethodType.SOCKET, + unitId: unitId ?? null, + deviceName: deviceName ?? '', + deviceType: deviceType ?? '', + pollPeriod: pollPeriod ?? null, + sendDataToThingsBoard: !!sendDataToThingsBoard, + byteOrder: byteOrder ?? ModbusOrderType.BIG, + security: security ?? {}, + identity: identity ?? { + vendorName: '', + productCode: '', + vendorUrl: '', + productName: '', + modelName: '', + }, + values: values ?? {} as ModbusRegisterValues, + port: port ?? null, + }; + if (slaveConfig.type === ModbusProtocolType.Serial) { + slaveState = { ...slaveState, baudrate, serialPort: port, host: '', port: null } as ModbusSlave; + } else { + slaveState = { ...slaveState, serialPort: '', baudrate: null } as ModbusSlave; + } + this.slaveConfigFormGroup.setValue(slaveState, {emitEvent: false}); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 95e7700886..3ac441447c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -176,6 +176,15 @@
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 991c71c16b..d989478e98 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -125,7 +125,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent Date: Fri, 19 Jul 2024 18:19:45 +0300 Subject: [PATCH 079/136] Removed table Add Connector button from page on no connectors displayed --- .../widget/lib/gateway/gateway-connectors.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index 7896ac2516..2dab86fb46 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -20,7 +20,8 @@

{{ 'gateway.connectors' | translate }}

-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index f056c9c92f..1e58cb4a40 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -138,7 +138,7 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali this.securityConfigFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value: ModbusSecurity) => { - this.onChange(value); + this.onChange(this.disabled ? {} : value); this.onTouched(); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 4b0789fb4c..41a7d2aee7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -102,7 +102,8 @@
- {{ ModbusMethodLabelsMap.get(method) }} + {{ ModbusMethodLabelsMap.get(method) }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index f1234ccfb1..d328a97baf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -33,6 +33,7 @@ import { ModbusProtocolLabelsMap, ModbusProtocolType, ModbusRegisterValues, + ModbusSerialMethodType, ModbusSlave, noLeadTrailSpacesRegex, PortLimits, @@ -90,6 +91,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat readonly modbusProtocolTypes = Object.values(ModbusProtocolType); readonly modbusMethodTypes = Object.values(ModbusMethodType); + readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType); readonly modbusOrderType = Object.values(ModbusOrderType); readonly ModbusProtocolType = ModbusProtocolType; readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; @@ -107,7 +109,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - method: [ModbusMethodType.SOCKET, []], + method: [ModbusMethodType.RTU, []], unitId: [null, [Validators.required]], baudrate: [null, []], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], @@ -133,7 +135,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat value.port = value.serialPort; delete value.serialPort; } - this.onChange(value); + this.onChange(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value ? value : {} as SlaveConfig); this.onTouched(); }); @@ -226,7 +228,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat let slaveState: ModbusSlave = { host: host ?? '', type: type ?? ModbusProtocolType.TCP, - method: method ?? ModbusMethodType.SOCKET, + method: method ?? ModbusMethodType.RTU, unitId: unitId ?? null, deviceName: deviceName ?? '', deviceType: deviceType ?? '', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 3ac441447c..d3c06aeb4b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -124,7 +124,8 @@
- {{ ModbusMethodLabelsMap.get(method) }} + {{ ModbusMethodLabelsMap.get(method) }}
@@ -143,7 +144,9 @@
gateway.bytesize
- + + {{ size }} +
@@ -159,7 +162,9 @@
gateway.parity
- + + {{ ModbusParityLabelsMap.get(parity) }} +
@@ -274,7 +279,7 @@ - +
@@ -332,7 +337,6 @@
-
gateway.values
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 006ae7a482..f0c8c2bd45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -25,11 +25,15 @@ import { } from '@angular/forms'; import { MappingInfo, + ModbusByteSizes, ModbusMethodLabelsMap, ModbusMethodType, ModbusOrderType, + ModbusParity, + ModbusParityLabelsMap, ModbusProtocolLabelsMap, ModbusProtocolType, + ModbusSerialMethodType, noLeadTrailSpacesRegex, PortLimits, SlaveConfig, } from '@home/components/widget/lib/gateway/gateway-widget.models'; @@ -92,8 +96,12 @@ export class ModbusSlaveDialogComponent extends DialogComponent( +export enum ModbusSerialMethodType { + RTU = 'rtu', + ASCII = 'ascii', +} + +export const ModbusMethodLabelsMap = new Map( [ [ModbusMethodType.SOCKET, 'Socket'], [ModbusMethodType.RTU, 'RTU'], + [ModbusSerialMethodType.ASCII, 'ASCII'], + ] +); + +export const ModbusByteSizes = [5, 6, 7 ,8]; + +export enum ModbusParity { + Even = 'E', + Odd = 'O', + None = 'N' +} + +export const ModbusParityLabelsMap = new Map( + [ + [ModbusParity.Even, 'Even'], + [ModbusParity.Odd, 'Odd'], + [ModbusParity.None, 'None'], ] ); @@ -872,7 +894,7 @@ export const ModbusKeysDeleteKeyTranslationsMap = new Map Date: Mon, 22 Jul 2024 19:16:27 +0300 Subject: [PATCH 085/136] refactoring --- .../table/table-datasource.abstract.ts} | 2 +- .../broker-config-control.component.ts | 6 ++- .../mapping-table/mapping-table.component.ts | 4 +- .../modbus-basic-config.component.ts | 2 +- .../modbus-data-keys-panel.component.ts | 2 +- .../modbus-master-table.component.ts | 4 +- .../modbus-security-config.component.ts | 4 +- .../modbus-slave-config.component.ts | 2 +- .../modbus-slave-dialog.component.ts | 2 +- .../modbus-values/modbus-values.component.ts | 2 +- .../modbus/public-api.ts | 23 ---------- .../mqtt-basic-config.component.ts | 18 +++++--- .../opc-server-config.component.ts | 4 +- .../opc-ua-basic-config.component.ts | 22 ++++++---- .../connectors-configuration/public-api.ts | 27 ------------ .../dialog/mapping-dialog.component.ts | 4 +- .../widget/widget-components.module.ts | 42 +++++++++++++------ .../gateway-help-link.pipe.ts | 0 .../gateway-port-tooltip.pipe.ts | 2 +- .../src/app/modules/home/pipes/public-api.ts | 18 -------- ui-ngx/src/app/shared/abstract/public-api.ts | 17 -------- .../ellipsis-chip-list.directive.ts | 0 .../src/app/shared/directives/public-api.ts | 17 -------- 23 files changed, 81 insertions(+), 143 deletions(-) rename ui-ngx/src/app/{shared/abstract/datasource/datasource.abstract.ts => modules/home/components/table/table-datasource.abstract.ts} (94%) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts rename ui-ngx/src/app/modules/home/pipes/{gateway-help-link => }/gateway-help-link.pipe.ts (100%) rename ui-ngx/src/app/modules/home/pipes/{gateway-port-tooltip => }/gateway-port-tooltip.pipe.ts (93%) delete mode 100644 ui-ngx/src/app/modules/home/pipes/public-api.ts delete mode 100644 ui-ngx/src/app/shared/abstract/public-api.ts rename ui-ngx/src/app/shared/directives/{ellipsis-chip-list => }/ellipsis-chip-list.directive.ts (100%) delete mode 100644 ui-ngx/src/app/shared/directives/public-api.ts diff --git a/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts b/ui-ngx/src/app/modules/home/components/table/table-datasource.abstract.ts similarity index 94% rename from ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts rename to ui-ngx/src/app/modules/home/components/table/table-datasource.abstract.ts index 43de3789a0..6146b5ebec 100644 --- a/ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts +++ b/ui-ngx/src/app/modules/home/components/table/table-datasource.abstract.ts @@ -18,7 +18,7 @@ import { DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -export abstract class TbDatasource implements DataSource { +export abstract class TbTableDatasource implements DataSource { protected dataSubject = new BehaviorSubject>([]); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index 6de9c8b310..031388b502 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -35,9 +35,11 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; import { generateSecret } from '@core/utils'; -import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { Subject } from 'rxjs'; -import { GatewayPortTooltipPipe } from '@home/pipes/public-api'; +import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; +import { + SecurityConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; @Component({ selector: 'tb-broker-config-control', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index d0df9bc30d..292cb43b6e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts @@ -57,7 +57,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TbDatasource } from '@shared/abstract/public-api'; +import { TbTableDatasource } from '@home/components/table/table-datasource.abstract'; @Component({ selector: 'tb-mapping-table', @@ -310,7 +310,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A } } -export class MappingDatasource extends TbDatasource { +export class MappingDatasource extends TbTableDatasource { constructor() { super(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 6f2d9aff02..445cb39983 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -30,7 +30,7 @@ import { CommonModule } from '@angular/common'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; -import { EllipsisChipListDirective } from '@shared/directives/public-api'; +import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index 3f57658443..1a696dd75f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -28,7 +28,7 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; -import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; +import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe'; import { generateSecret } from '@core/utils'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 69613b9207..6644fbfd5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -45,8 +45,8 @@ import { import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TbDatasource } from '@shared/abstract/datasource/datasource.abstract'; import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; +import { TbTableDatasource } from '@home/components/table/table-datasource.abstract'; @Component({ selector: 'tb-modbus-master-table', @@ -224,7 +224,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat } } -export class SlavesDatasource extends TbDatasource { +export class SlavesDatasource extends TbTableDatasource { constructor() { super(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index 1e58cb4a40..7efcf095ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -31,9 +31,11 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { + SecurityConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; @Component({ selector: 'tb-modbus-security-config', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index d328a97baf..13a4eefb8c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -43,7 +43,7 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; import { startWith, takeUntil } from 'rxjs/operators'; -import { GatewayPortTooltipPipe } from '@home/pipes/public-api'; +import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index f0c8c2bd45..5ac897b91a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -47,7 +47,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe'; +import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; import { takeUntil } from 'rxjs/operators'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts index f1df094e8b..f3e9a28bce 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts @@ -50,7 +50,7 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; -import { EllipsisChipListDirective } from '@shared/directives/public-api'; +import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts deleted file mode 100644 index 87a3ba5c2f..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts +++ /dev/null @@ -1,23 +0,0 @@ -/// -/// Copyright © 2016-2024 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. -/// - -export * from './modbus-basic-config/modbus-basic-config.component'; -export * from './modbus-values/modbus-values.component'; -export * from './modbus-data-keys-panel/modbus-data-keys-panel.component'; -export * from './modbus-slave-config/modbus-slave-config.component'; -export * from './modbus-master-table/modbus-master-table.component'; -export * from './modbus-slave-dialog/modbus-slave-dialog.component'; -export * from './modbus-security-config/modbus-security-config.component'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts index 62613dc28c..f4296b5303 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts @@ -32,15 +32,21 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { - BrokerConfigControlComponent, - MappingTableComponent, - SecurityConfigComponent, - WorkersConfigControlComponent -} from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { isObject } from 'lodash'; +import { + SecurityConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; +import { + WorkersConfigControlComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component'; +import { + BrokerConfigControlComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component'; +import { + MappingTableComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component'; @Component({ selector: 'tb-mqtt-basic-config', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index 955e7e8145..258f295bcb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -33,9 +33,11 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { + SecurityConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; @Component({ selector: 'tb-opc-server-config', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts index dfe5c5e51e..39198c9696 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts @@ -31,15 +31,23 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { - BrokerConfigControlComponent, - MappingTableComponent, - SecurityConfigComponent, - OpcServerConfigComponent, - WorkersConfigControlComponent -} from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { + SecurityConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; +import { + WorkersConfigControlComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component'; +import { + BrokerConfigControlComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component'; +import { + MappingTableComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component'; +import { + OpcServerConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component'; @Component({ selector: 'tb-opc-ua-basic-config', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts deleted file mode 100644 index f98e6c569e..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts +++ /dev/null @@ -1,27 +0,0 @@ -/// -/// Copyright © 2016-2024 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. -/// - -export * from './mapping-table/mapping-table.component'; -export * from './device-info-table/device-info-table.component'; -export * from './security-config/security-config.component'; -export * from './opc-server-config/opc-server-config.component'; -export * from './mapping-data-keys-panel/mapping-data-keys-panel.component'; -export * from './type-value-panel/type-value-panel.component'; -export * from './broker-config-control/broker-config-control.component'; -export * from './workers-config-control/workers-config-control.component'; -export * from './opc-ua-basic-config/opc-ua-basic-config.component'; -export * from './mqtt-basic-config/mqtt-basic-config.component'; -export * from './modbus/public-api'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index 3812fa161e..40bff51fb4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -55,7 +55,9 @@ import { startWith, takeUntil } from 'rxjs/operators'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { TranslateService } from '@ngx-translate/core'; -import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; +import { + MappingDataKeysPanelComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component'; @Component({ selector: 'tb-mapping-dialog', diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 3c2b7affc1..1e4093c22c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -109,20 +109,38 @@ import { import { NotificationTypeFilterPanelComponent } from '@home/components/widget/lib/cards/notification-type-filter-panel.component'; -import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; +import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe'; +import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; +import { + BrokerConfigControlComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component'; +import { + WorkersConfigControlComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component'; +import { + OpcServerConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component'; +import { + MqttBasicConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component'; +import { + MappingTableComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component'; +import { + OpcUaBasicConfigComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component'; import { - BrokerConfigControlComponent, - DeviceInfoTableComponent, - MappingDataKeysPanelComponent, - MappingTableComponent, - MqttBasicConfigComponent, - OpcUaBasicConfigComponent, - OpcServerConfigComponent, - TypeValuePanelComponent, - WorkersConfigControlComponent, ModbusBasicConfigComponent -} from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; -import { EllipsisChipListDirective } from '@shared/directives/public-api'; +} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component'; +import { + DeviceInfoTableComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component'; +import { + MappingDataKeysPanelComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component'; +import { + TypeValuePanelComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component'; @NgModule({ declarations: [ diff --git a/ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts b/ui-ngx/src/app/modules/home/pipes/gateway-help-link.pipe.ts similarity index 100% rename from ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts rename to ui-ngx/src/app/modules/home/pipes/gateway-help-link.pipe.ts diff --git a/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts b/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts similarity index 93% rename from ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts rename to ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts index 9ff3e134c6..3981499a26 100644 --- a/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts +++ b/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts @@ -15,7 +15,7 @@ /// import { Pipe, PipeTransform } from '@angular/core'; -import { PortLimits, } from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { PortLimits } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { AbstractControl } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; diff --git a/ui-ngx/src/app/modules/home/pipes/public-api.ts b/ui-ngx/src/app/modules/home/pipes/public-api.ts deleted file mode 100644 index 9db71f9a06..0000000000 --- a/ui-ngx/src/app/modules/home/pipes/public-api.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// -/// Copyright © 2016-2024 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. -/// - -export * from './gateway-help-link/gateway-help-link.pipe'; -export * from './gateway-port-tooltip/gateway-port-tooltip.pipe'; diff --git a/ui-ngx/src/app/shared/abstract/public-api.ts b/ui-ngx/src/app/shared/abstract/public-api.ts deleted file mode 100644 index 562011065a..0000000000 --- a/ui-ngx/src/app/shared/abstract/public-api.ts +++ /dev/null @@ -1,17 +0,0 @@ -/// -/// Copyright © 2016-2024 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. -/// - -export * from './datasource/datasource.abstract'; diff --git a/ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts similarity index 100% rename from ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts rename to ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts deleted file mode 100644 index f1fae708d0..0000000000 --- a/ui-ngx/src/app/shared/directives/public-api.ts +++ /dev/null @@ -1,17 +0,0 @@ -/// -/// Copyright © 2016-2024 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. -/// - -export * from './ellipsis-chip-list/ellipsis-chip-list.directive'; From 6cc1d28f25f11e491082c51c54af535cce224bab Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 22 Jul 2024 19:25:56 +0300 Subject: [PATCH 086/136] refactoring --- .../modbus-slave-dialog/modbus-slave-dialog.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 5ac897b91a..941f6d49ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -35,7 +35,8 @@ import { ModbusProtocolType, ModbusSerialMethodType, noLeadTrailSpacesRegex, - PortLimits, SlaveConfig, + PortLimits, + SlaveConfig, } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; From 67f6e483e3f9e369b4b99cd24db25094892b3438 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 22 Jul 2024 19:32:42 +0300 Subject: [PATCH 087/136] refactoring --- .../mapping-table/mapping-table.component.ts | 4 ++-- .../server-config/server-config.component.ts | 4 ++-- .../workers-config-control.component.ts | 4 ++-- ui-ngx/src/app/shared/directives/public-api.ts | 17 ----------------- ...rective.ts => truncate-tooltip.directive.ts} | 2 +- 5 files changed, 7 insertions(+), 24 deletions(-) delete mode 100644 ui-ngx/src/app/shared/directives/public-api.ts rename ui-ngx/src/app/shared/directives/{tooltip/tooltip.directive.ts => truncate-tooltip.directive.ts} (97%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index 3e4ce09031..7df3c77be4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts @@ -58,7 +58,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TooltipDirective } from '@shared/directives/public-api'; +import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive'; @Component({ selector: 'tb-mapping-table', @@ -78,7 +78,7 @@ import { TooltipDirective } from '@shared/directives/public-api'; } ], standalone: true, - imports: [CommonModule, SharedModule, TooltipDirective] + imports: [CommonModule, SharedModule, TruncateTooltipDirective] }) export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts index 66af025267..40b9a10fc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts @@ -36,7 +36,7 @@ import { CommonModule } from '@angular/common'; import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { TooltipDirective } from '@shared/directives/public-api'; +import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive'; @Component({ selector: 'tb-server-config', @@ -60,7 +60,7 @@ import { TooltipDirective } from '@shared/directives/public-api'; CommonModule, SharedModule, SecurityConfigComponent, - TooltipDirective, + TruncateTooltipDirective, ] }) export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index c05b7c32b6..8430c29ecd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -33,7 +33,7 @@ import { CommonModule } from '@angular/common'; import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { TooltipDirective } from '@shared/directives/tooltip/tooltip.directive'; +import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive'; @Component({ selector: 'tb-workers-config-control', @@ -43,7 +43,7 @@ import { TooltipDirective } from '@shared/directives/tooltip/tooltip.directive'; imports: [ CommonModule, SharedModule, - TooltipDirective, + TruncateTooltipDirective, ], providers: [ { diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts deleted file mode 100644 index a82428fc19..0000000000 --- a/ui-ngx/src/app/shared/directives/public-api.ts +++ /dev/null @@ -1,17 +0,0 @@ -/// -/// Copyright © 2016-2024 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. -/// - -export * from './tooltip/tooltip.directive'; diff --git a/ui-ngx/src/app/shared/directives/tooltip/tooltip.directive.ts b/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts similarity index 97% rename from ui-ngx/src/app/shared/directives/tooltip/tooltip.directive.ts rename to ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts index 448b7e52de..bf08780e11 100644 --- a/ui-ngx/src/app/shared/directives/tooltip/tooltip.directive.ts +++ b/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts @@ -33,7 +33,7 @@ import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; providers: [MatTooltip], }) -export class TooltipDirective implements OnInit, AfterViewInit, OnDestroy { +export class TruncateTooltipDirective implements OnInit, AfterViewInit, OnDestroy { @Input('tbTruncateTooltip') text: string; @Input() tooltipEnabled = true; @Input() position: TooltipPosition = 'above'; From a0a24b17a8b341373583269f6876e06b62f00cd7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 22 Jul 2024 19:35:02 +0300 Subject: [PATCH 088/136] refactoring --- .../workers-config-control.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index 8430c29ecd..a5fff95a86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -25,7 +25,9 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormGroup, ValidationErrors, Validator, + UntypedFormGroup, + ValidationErrors, + Validator, Validators } from '@angular/forms'; import { SharedModule } from '@shared/shared.module'; From a8f45560d0c0efb545e2bdb45a06df18ba605d06 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 22 Jul 2024 19:38:10 +0300 Subject: [PATCH 089/136] refactoring --- .../button/action-button-basic-config.component.html | 6 +++--- .../button/command-button-basic-config.component.html | 4 ++-- .../basic/button/power-button-basic-config.component.html | 8 ++++---- .../button/toggle-button-basic-config.component.html | 8 ++++---- .../indicator/status-widget-basic-config.component.html | 4 ++-- .../basic/rpc/single-switch-basic-config.component.html | 8 ++++---- .../config/basic/rpc/slider-basic-config.component.html | 6 +++--- .../home/components/widget/widget-config.component.html | 4 ++-- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html index a3030dec79..6940cffc1f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html @@ -27,7 +27,7 @@
widgets.action-button.behavior
-
{{ 'widgets.action-button.on-click' | translate}}
+
widgets.action-button.on-click
-
{{ 'widgets.button-state.activated-state' | translate }}
+
widgets.button-state.activated-state
-
{{ 'widgets.button-state.disabled-state' | translate }}
+
widgets.button-state.disabled-state
widgets.command-button.behavior
-
{{ 'widgets.command-button.on-click' | translate }}
+
widgets.command-button.on-click
-
{{ 'widgets.button-state.disabled-state' | translate }}
+
widgets.button-state.disabled-state
widgets.power-button.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.power-button.power-on' | translate }}
+
widgets.power-button.power-on
-
{{ 'widgets.power-button.power-off' | translate }}
+
widgets.power-button.power-off
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widgets.toggle-button.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.toggle-button.check' | translate }}
+
widgets.toggle-button.check
-
{{ 'widgets.toggle-button.uncheck' | translate }}
+
widgets.toggle-button.uncheck
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widgets.status-widget.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widgets.single-switch.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.rpc-state.turn-on' | translate }}
+
widgets.rpc-state.turn-on
-
{{ 'widgets.rpc-state.turn-off' | translate }}
+
widgets.rpc-state.turn-off
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widgets.slider.behavior
-
{{ 'widgets.slider.initial-value' | translate }}
+
widgets.slider.initial-value
-
{{ 'widgets.slider.on-value-change' | translate }}
+
widgets.slider.on-value-change
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widget-config.data-settings
-
{{ 'widget-config.units-by-default' | translate }}
+
widget-config.units-by-default
-
{{ 'widget-config.decimals-by-default' | translate }}
+
widget-config.decimals-by-default
From 7a472f54f88b3952f64a9af8dbfbad28100c8468 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Mon, 22 Jul 2024 19:41:45 +0300 Subject: [PATCH 090/136] refactoring --- .../button/action-button-widget-settings.component.html | 4 ++-- .../button/command-button-widget-settings.component.html | 4 ++-- .../button/power-button-widget-settings.component.html | 8 ++++---- .../button/toggle-button-widget-settings.component.html | 8 ++++---- .../action/get-value-action-settings-panel.component.html | 2 +- .../indicator/status-widget-state-settings.component.html | 8 ++++---- .../control/single-switch-widget-settings.component.html | 8 ++++---- .../control/slider-widget-settings.component.html | 6 +++--- .../indicator/status-widget-settings.component.html | 4 ++-- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html index a8cdf55efc..265a9c2501 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html @@ -19,7 +19,7 @@
widgets.action-button.behavior
-
{{ 'widgets.button-state.activated-state' | translate }}
+
widgets.button-state.activated-state
-
{{ 'widgets.button-state.disabled-state' | translate }}
+
widgets.button-state.disabled-state
widgets.command-button.behavior
-
{{ 'widgets.command-button.on-click' | translate }}
+
widgets.command-button.on-click
-
{{ 'widgets.button-state.disabled-state' | translate }}
+
widgets.button-state.disabled-state
widgets.power-button.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.power-button.power-on' | translate }}
+
widgets.power-button.power-on
-
{{ 'widgets.power-button.power-off' | translate }}
+
widgets.power-button.power-off
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widgets.toggle-button.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.toggle-button.check' | translate }}
+
widgets.toggle-button.check
-
{{ 'widgets.toggle-button.uncheck' | translate }}
+
widgets.toggle-button.uncheck
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
-
{{ 'widgets.value-action.value' | translate }}
+
widgets.value-action.value
-
{{ 'widgets.status-widget.primary' | translate }}
+
widgets.status-widget.primary
-
{{ 'widgets.status-widget.secondary' | translate }}
+
widgets.status-widget.secondary
@@ -88,14 +88,14 @@ [fxLayoutAlign.lt-md]="layout !== StatusWidgetLayout.icon ? 'space-between center': 'start center'" style="gap: 12px;">
-
{{ 'widgets.status-widget.primary' | translate }}
+
widgets.status-widget.primary
-
{{ 'widgets.status-widget.secondary' | translate }}
+
widgets.status-widget.secondary
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html index d5b732b6bb..9208efd68a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html @@ -19,7 +19,7 @@
widgets.single-switch.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.rpc-state.turn-on' | translate }}
+
widgets.rpc-state.turn-on
-
{{ 'widgets.rpc-state.turn-off' | translate }}
+
widgets.rpc-state.turn-off
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widgets.slider.behavior
-
{{ 'widgets.slider.initial-value' | translate }}
+
widgets.slider.initial-value
-
{{ 'widgets.slider.on-value-change' | translate }}
+
widgets.slider.on-value-change
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
widgets.status-widget.behavior
-
{{ 'widgets.rpc-state.initial-state' | translate }}
+
widgets.rpc-state.initial-state
-
{{ 'widgets.rpc-state.disabled-state' | translate }}
+
widgets.rpc-state.disabled-state
Date: Tue, 23 Jul 2024 11:41:01 +0300 Subject: [PATCH 091/136] added suggestions --- .../modbus-data-keys-panel.component.ts | 2 ++ .../modbus-security-config.component.ts | 16 ++++++++++++++-- .../modbus-slave-config.component.html | 4 ++-- .../modbus-slave-config.component.ts | 13 ++++++++++++- .../modbus-values/modbus-values.component.ts | 9 ++++++++- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index 1a696dd75f..d128e3c246 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -30,6 +30,7 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe'; import { generateSecret } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-modbus-data-keys-panel', @@ -44,6 +45,7 @@ import { generateSecret } from '@core/utils'; }) export class ModbusDataKeysPanelComponent implements OnInit { + @coerceBoolean() @Input() isMaster = false; @Input() panelTitle: string; @Input() addKeyTitle: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index 7efcf095ee..caaff5ea40 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -36,6 +36,7 @@ import { takeUntil } from 'rxjs/operators'; import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-modbus-security-config', @@ -62,8 +63,10 @@ import { }) export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { + @coerceBoolean() @Input() isMaster = false; - @Input() disabled = false; + + disabled = false; securityConfigFormGroup: UntypedFormGroup; @@ -113,8 +116,17 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali this.onTouched = fn; } + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.securityConfigFormGroup.disable({emitEvent: false}); + } else { + this.securityConfigFormGroup.enable({emitEvent: false}); + } + } + validate(): ValidationErrors | null { - return this.securityConfigFormGroup.valid || this.disabled ? null : { + return this.securityConfigFormGroup.valid ? null : { securityConfigFormGroup: { valid: false } }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 41a7d2aee7..0ccbc734bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -204,7 +204,7 @@ - +
@@ -252,6 +252,6 @@
gateway.values
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 13a4eefb8c..1d34914b8b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -135,12 +135,13 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat value.port = value.serialPort; delete value.serialPort; } - this.onChange(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value ? value : {} as SlaveConfig); + this.onChange(value); this.onTouched(); }); this.observeTypeChange(); this.observeFormEnable(); + this.observeShowSecurity(); } ngOnDestroy(): void { @@ -194,6 +195,16 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.updateEnablingByProtocol(this.slaveConfigFormGroup.get('type').value); } + private observeShowSecurity(): void { + this.showSecurityControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { + if (value && this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) { + this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); + } else { + this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); + } + }); + } + private updateEnablingByProtocol(type: ModbusProtocolType): void { if (type === ModbusProtocolType.Serial) { if (this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts index f3e9a28bce..fee7e72616 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts @@ -54,6 +54,7 @@ import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-modbus-values', @@ -88,9 +89,10 @@ import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-d export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { + @coerceBoolean() @Input() singleMode = false; - @Input() disabled = false; + disabled = false; modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType); modbusValueKeys = Object.values(ModbusValueKey); ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap; @@ -159,6 +161,11 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O }; } + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.cdr.markForCheck(); + } + getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType) { return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value; } From 30f57ecd5a12fbc8faeaad5f8d208ed915e59672 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 23 Jul 2024 11:43:45 +0300 Subject: [PATCH 092/136] added suggestions --- .../modbus-security-config.component.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index caaff5ea40..9c7cad5f83 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -96,11 +96,6 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali }); this.observeValueChanges(); } - if (this.disabled) { - this.securityConfigFormGroup.disable({emitEvent:false}); - } else { - this.securityConfigFormGroup.enable({emitEvent:false}); - } } ngOnDestroy(): void { @@ -152,7 +147,7 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali this.securityConfigFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value: ModbusSecurity) => { - this.onChange(this.disabled ? {} : value); + this.onChange(value); this.onTouched(); }); } From 1587fdc216f43a36f4167b8ee09af8341c1a0313 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 23 Jul 2024 11:59:29 +0300 Subject: [PATCH 093/136] edited due to suggestions --- .../pages/admin/resource/resources-library.component.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts index 049e53caf8..66aa0bbab6 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts @@ -29,7 +29,7 @@ import { ResourceTypeMIMETypes, ResourceTypeTranslationMap } from '@shared/models/resource.models'; -import { filter, startWith, takeUntil } from 'rxjs/operators'; +import { startWith, takeUntil } from 'rxjs/operators'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { isDefinedAndNotNull } from '@core/utils'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; @@ -59,7 +59,9 @@ export class ResourcesLibraryComponent extends EntityComponent impleme ngOnInit() { super.ngOnInit(); - this.observeResourceTypeChange(); + if (this.isAdd) { + this.observeResourceTypeChange(); + } } ngOnDestroy() { @@ -142,7 +144,6 @@ export class ResourcesLibraryComponent extends EntityComponent impleme private observeResourceTypeChange(): void { this.entityForm.get('resourceType').valueChanges.pipe( startWith(ResourceType.JS_MODULE), - filter(() => this.isAdd), takeUntil(this.destroy$) ).subscribe((type: ResourceType) => this.onResourceTypeChange(type)); } From c806b06c9e1c0e0b67b77554f420f55a82400740 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 23 Jul 2024 15:25:31 +0300 Subject: [PATCH 094/136] UI: Add validation for min max value --- ...l-simple-gauge-basic-config.component.html | 8 +++++ ...tal-simple-gauge-basic-config.component.ts | 31 ++++++++++++++++-- ...gital-gauge-widget-settings.component.html | 8 +++++ ...digital-gauge-widget-settings.component.ts | 32 ++++++++++++++++--- .../assets/locale/locale.constant-en_US.json | 3 +- 5 files changed, 74 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html index 9f170dff8c..11e0d6abc2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html @@ -76,6 +76,14 @@
widgets.gauge.max-value-short
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts index c3850db5ee..f6a74c496e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts @@ -14,7 +14,14 @@ /// limitations under the License. /// -import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { + AbstractControl, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; @@ -109,7 +116,7 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom showMinMax: [settings.showMinMax, []], minValue: [settings.minValue, []], - maxValue: [settings.maxValue, []], + maxValue: [settings.maxValue, [this.maxValueValidation()]], minMaxFont: [settings.minMaxFont, []], minMaxColor: [settings.minMaxFont?.color, []], @@ -182,11 +189,29 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom return this.widgetConfig; } + private maxValueValidation(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value: string = control.value; + if (value) { + if (value < control.parent?.get('minValue').value) { + return {maxValue: true}; + } + } + return null; + }; + } + protected validatorTriggers(): string[] { - return ['gaugeType', 'showValue', 'showTitle', 'showMinMax']; + return ['gaugeType', 'showValue', 'showTitle', 'showMinMax', 'minValue']; } protected updateValidators(emitEvent: boolean, trigger?: string) { + if (trigger === 'minValue') { + this.simpleGaugeWidgetConfigForm.get('maxValue').updateValueAndValidity({emitEvent: true}); + this.simpleGaugeWidgetConfigForm.get('maxValue').markAsTouched({onlySelf: true}); + return; + } + const isDonut = this.simpleGaugeWidgetConfigForm.get('gaugeType').value === this.digitalGaugeType.donut; if (isDonut) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html index 62cacf0730..173d2102d6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html @@ -65,6 +65,14 @@
widgets.gauge.max-value-short
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts index 5a4289a8d2..5de1b77b18 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts @@ -16,7 +16,14 @@ import { Datasource, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { Component } from '@angular/core'; -import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { + AbstractControl, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; @@ -167,7 +174,7 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent donutStartAngle: [settings.donutStartAngle, []], showMinMax: [settings.showMinMax, []], minValue: [settings.minValue, []], - maxValue: [settings.maxValue, []], + maxValue: [settings.maxValue, [this.maxValueValidation()]], minMaxFont: [settings.minMaxFont, []], minMaxColor: [settings.minMaxFont.color, []], @@ -206,6 +213,18 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent }); } + private maxValueValidation(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value: string = control.value; + if (value) { + if (value < control.parent?.get('minValue').value) { + return {maxValue: true}; + } + } + return null; + }; + } + protected prepareOutputSettings(settings) { const barColor: ColorSettings = this.digitalGaugeWidgetSettingsForm.get('barColor').value; @@ -230,10 +249,15 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent } protected validatorTriggers(): string[] { - return ['gaugeType', 'showTitle', 'showUnitTitle', 'showValue', 'showMinMax', 'showTimestamp', 'showTicks', 'animation']; + return ['gaugeType', 'showTitle', 'showUnitTitle', 'showValue', 'showMinMax', 'showTimestamp', 'showTicks', 'animation', 'minValue']; } - protected updateValidators(emitEvent: boolean) { + protected updateValidators(emitEvent: boolean, trigger: string) { + if (trigger === 'minValue') { + this.digitalGaugeWidgetSettingsForm.get('maxValue').updateValueAndValidity({emitEvent: true}); + this.digitalGaugeWidgetSettingsForm.get('maxValue').markAsTouched({onlySelf: true}); + return; + } const gaugeType: GaugeType = this.digitalGaugeWidgetSettingsForm.get('gaugeType').value; const showTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showTitle').value; const showUnitTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showUnitTitle').value; 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 33c8d25652..fe6d8b356f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6279,7 +6279,8 @@ "min-and-max-value": "Min and max value", "min-and-max-label": "Min and max label", "font": "Font", - "tick-width-and-color": "Tick width and color" + "tick-width-and-color": "Tick width and color", + "min-max-validation-text": "Max value must be bigger than min value" }, "gpio": { "pin": "Pin", From be482b8b3004cd68b28f80347ff7e51a06dfad8c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 23 Jul 2024 15:30:00 +0300 Subject: [PATCH 095/136] UI: optimize import --- .../gauge/digital-simple-gauge-basic-config.component.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts index f6a74c496e..bb0eae4faf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts @@ -14,14 +14,7 @@ /// limitations under the License. /// -import { - AbstractControl, - UntypedFormBuilder, - UntypedFormGroup, - ValidationErrors, - ValidatorFn, - Validators -} from '@angular/forms'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; From 3a9a654fd298c77c7445c47edc35892d968caabb Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 23 Jul 2024 17:59:18 +0300 Subject: [PATCH 096/136] functionality adjustments --- .../modbus-basic-config.component.ts | 7 + .../modbus-slave-config.component.html | 408 +++++++++--------- .../modbus-slave-config.component.scss | 18 + .../modbus-slave-config.component.ts | 27 +- .../modbus-slave-dialog.component.html | 6 +- .../modbus-slave-dialog.component.ts | 22 +- .../modbus-values/modbus-values.component.ts | 4 + .../lib/gateway/gateway-widget.models.ts | 10 +- .../assets/locale/locale.constant-en_US.json | 14 +- 9 files changed, 277 insertions(+), 239 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 445cb39983..35e0ee6e61 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -62,6 +62,13 @@ import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master :host { height: 100%; } + + :host ::ng-deep { + .mat-mdc-tab-body-content { + overflow: hidden !important; + } + } + :host ::ng-deep { .mat-mdc-tab-group, .mat-mdc-tab-body-wrapper { height: 100%; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 0ccbc734bf..1c1b55e6c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -15,243 +15,251 @@ limitations under the License. --> -
-
{{ 'gateway.hints.modbus-server' | translate }}
-
- - - {{ 'gateway.enable' | translate }} - - -
-
-
gateway.server-slave-config
- - {{ ModbusProtocolLabelsMap.get(type) }} - -
+
-
-
gateway.host
+
{{ 'gateway.hints.modbus-server' | translate }}
+
+ + + {{ 'gateway.enable' | translate }} + + +
+
+
+
+
gateway.server-slave-config
+ + {{ ModbusProtocolLabelsMap.get(type) }} + +
+
+
+
gateway.host
+
+ + + + warning + + +
+
+
+
gateway.port
+
+ + + + warning + + +
+
+ +
+
gateway.port
+
+ + + + warning + + +
+
+
+
+
+ gateway.method +
+
+ + + {{ ModbusMethodLabelsMap.get(method) }} + + +
+
+
+
+
gateway.unit-id
- + warning
-
-
gateway.port
+
+
gateway.device-name
- + warning
- -
-
gateway.port
-
- - - - warning - - -
+
+
gateway.device-profile
+
+ + + + warning + +
- +
-
- gateway.method +
gateway.poll-period
+
+ + +
+
+
+
gateway.baudrate
- - {{ ModbusMethodLabelsMap.get(method) }} + + {{ rate }}
-
-
-
gateway.unit-id
-
- - - - warning - - -
-
-
-
gateway.device-name
-
- - - - warning - - -
-
-
-
gateway.device-profile
-
- - - - warning - - -
-
-
-
gateway.poll-period
-
- - - -
-
-
-
gateway.baudrate
-
- - - -
-
- - - -
gateway.advanced-connection-settings
-
-
-
-
-
gateway.byte-order
-
- - - {{ order }} - - -
-
- - +
+ + - - - {{ 'gateway.tls-connection' | translate }} - - +
gateway.advanced-connection-settings
- -
- -
-
gateway.vendor-name
-
- - - -
-
-
-
gateway.product-code
-
- - - -
-
-
-
gateway.vendor-url
-
- - - -
-
-
-
gateway.product-name
-
- - - +
+
+
gateway.byte-order
+
+ + + {{ order }} + + +
+ + + + + + {{ 'gateway.tls-connection' | translate }} + + + + + + + +
+
gateway.vendor-name
+
+ + + +
+
+
+
gateway.product-code
+
+ + + +
+
+
+
gateway.vendor-url
+
+ + + +
+
+
+
gateway.product-name
+
+ + + +
+
+
+
gateway.model-name
+
+ + + +
+
+
-
-
gateway.model-name
-
- - - -
-
- + +
+
+
gateway.values
+
- -
-
gateway.values
-
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss new file mode 100644 index 0000000000..f41241f63a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss @@ -0,0 +1,18 @@ +$server-config-header-height: 132px; + +:host { + .nested-expansion-header { + ::ng-deep .mat-content { + height: 100%; + } + } + + .slave-content { + height: calc(100% - #{$server-config-header-height}); + overflow: auto; + } + + .slave-container { + display: inherit; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 1d34914b8b..33a18ebbb7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -27,6 +27,7 @@ import { Validators, } from '@angular/forms'; import { + ModbusBaudrates, ModbusMethodLabelsMap, ModbusMethodType, ModbusOrderType, @@ -71,15 +72,7 @@ import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component ModbusSecurityConfigComponent, GatewayPortTooltipPipe, ], - styles: [` - :host { - .nested-expansion-header { - .mat-content { - height: 100%; - } - } - } - `], + styleUrls: ['modbus-slave-config.component.scss'], }) export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { @@ -94,6 +87,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType); readonly modbusOrderType = Object.values(ModbusOrderType); readonly ModbusProtocolType = ModbusProtocolType; + readonly modbusBaudrates = ModbusBaudrates; readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; @@ -110,11 +104,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], method: [ModbusMethodType.RTU, []], - unitId: [null, [Validators.required]], - baudrate: [null, []], + unitId: [0, [Validators.required]], + baudrate: [this.ModbusProtocolType[0], []], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - pollPeriod: [null, []], + pollPeriod: [5000, []], sendDataToThingsBoard: [false, []], byteOrder:[ModbusOrderType.BIG, []], security: [], @@ -240,10 +234,10 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat host: host ?? '', type: type ?? ModbusProtocolType.TCP, method: method ?? ModbusMethodType.RTU, - unitId: unitId ?? null, + unitId: unitId ?? 0, deviceName: deviceName ?? '', deviceType: deviceType ?? '', - pollPeriod: pollPeriod ?? null, + pollPeriod: pollPeriod ?? 5000, sendDataToThingsBoard: !!sendDataToThingsBoard, byteOrder: byteOrder ?? ModbusOrderType.BIG, security: security ?? {}, @@ -256,11 +250,12 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat }, values: values ?? {} as ModbusRegisterValues, port: port ?? null, + baudrate: baudrate ?? this.modbusBaudrates[0], }; if (slaveConfig.type === ModbusProtocolType.Serial) { - slaveState = { ...slaveState, baudrate, serialPort: port, host: '', port: null } as ModbusSlave; + slaveState = { ...slaveState, serialPort: port, host: '', port: null } as ModbusSlave; } else { - slaveState = { ...slaveState, serialPort: '', baudrate: null } as ModbusSlave; + slaveState = { ...slaveState, serialPort: '' } as ModbusSlave; } this.slaveConfigFormGroup.setValue(slaveState, {emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index d3c06aeb4b..026d1eef32 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -136,7 +136,9 @@
gateway.baudrate
- + + {{ rate }} +
@@ -162,7 +164,7 @@
gateway.parity
- + {{ ModbusParityLabelsMap.get(parity) }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 941f6d49ee..8f78372e0f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -25,6 +25,7 @@ import { } from '@angular/forms'; import { MappingInfo, + ModbusBaudrates, ModbusByteSizes, ModbusMethodLabelsMap, ModbusMethodType, @@ -100,6 +101,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent( [ - [ModbusRegisterType.HoldingRegister, 'gateway.holding_registers'], + [ModbusRegisterType.HoldingRegisters, 'gateway.holding_registers'], [ModbusRegisterType.CoilsInitializer, 'gateway.coils_initializer'], - [ModbusRegisterType.InputRegister, 'gateway.input_registers'], + [ModbusRegisterType.InputRegisters, 'gateway.input_registers'], [ModbusRegisterType.DiscreteInputs, 'gateway.discrete_inputs'] ] ); @@ -1023,3 +1023,5 @@ export interface ModbusIdentity { productName?: string; modelName?: string; } + +export const ModbusBaudrates = [4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]; 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 15583665b7..fb8f695619 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2772,7 +2772,7 @@ "delete-value": "Delete value", "delete-rpc-method": "Delete method", "delete-rpc-request": "Delete request", - "delete-attribute-update": "Add attribute update", + "delete-attribute-update": "Delete attribute update", "advanced": "Advanced", "advanced-connection-settings": "Advanced connection settings", "attributes": "Attributes", @@ -2821,7 +2821,7 @@ "connectors-table-key": "Key", "connectors-table-class": "Class", "connection-timeout": "Connection timeout (s)", - "connect-attempt-time": "Connect attempt time (s)", + "connect-attempt-time": "Connect attempt time (ms)", "connect-attempt-count": "Connect attempt count", "copy-username": "Copy username", "copy-password": "Copy password", @@ -2954,10 +2954,10 @@ "unit-id": "Unit ID", "host": "Host", "host-required": "Host is required.", - "holding_registers": "Holding register", + "holding_registers": "Holding registers", "coils_initializer": "Coils initializer", - "input_registers": "Input register", - "discrete_inputs": "Discrete input", + "input_registers": "Input registers", + "discrete_inputs": "Discrete inputs", "json-parse": "Not valid JSON.", "json-required": "Field cannot be empty.", "JSONPath-hint": "This field supports constants and JSONPath expressions.", @@ -3019,7 +3019,7 @@ "password": "Password", "password-required": "Password is required.", "permit-without-calls": "Keep alive permit without calls", - "poll-period": "Poll period (s)", + "poll-period": "Poll period (ms)", "port": "Port", "port-required": "Port is required.", "port-limits-error": "Port should be number from {{min}} to {{max}}.", @@ -3307,7 +3307,7 @@ "exactly-once": "2 - Exactly once" }, "objects-count": "Objects count", - "wait-after-failed-attempts": "Wait after failed attempts (s)", + "wait-after-failed-attempts": "Wait after failed attempts (ms)", "tls-path-private-key": "Path to private key on gateway", "toggle-fullscreen": "Toggle fullscreen", "transformer-json-config": "Configuration JSON*", From 2382badc121751f0cff695079e1fc87ea4f812a7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 23 Jul 2024 18:03:32 +0300 Subject: [PATCH 097/136] added license --- .../modbus-slave-config.component.scss | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss index f41241f63a..f3b39e3508 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2024 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. + */ $server-config-header-height: 132px; :host { From 4d264d30a24833974658415dc52af2329f5a352d Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 23 Jul 2024 19:33:56 +0300 Subject: [PATCH 098/136] security for slaves disable state fix --- .../modbus-security-config.component.ts | 13 +++++++++++-- .../modbus-slave-dialog.component.html | 2 +- .../modbus-slave-dialog.component.ts | 11 +++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index 9c7cad5f83..362edbee29 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -14,7 +14,15 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, forwardRef, Input, OnChanges, OnDestroy } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnChanges, + OnDestroy +} from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -75,7 +83,7 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali private destroy$ = new Subject(); - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { this.securityConfigFormGroup = this.fb.group({ certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], @@ -118,6 +126,7 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali } else { this.securityConfigFormGroup.enable({emitEvent: false}); } + this.cdr.markForCheck(); } validate(): ValidationErrors | null { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 026d1eef32..caa1af4029 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -281,7 +281,7 @@ - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 8f78372e0f..40a4459221 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -172,6 +172,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); } }; + + private observeShowSecurity(): void { + this.showSecurityControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { + if (value) { + this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); + } else { + this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); + } + }); + } } From d663e7b1c63acec927fdb388bf3e04487b89776c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 24 Jul 2024 10:35:32 +0300 Subject: [PATCH 099/136] UI: Refactoring rename --- .../lib/rpc/power-button-widget.models.ts | 88 +++++++++---------- .../assets/locale/locale.constant-en_US.json | 6 +- ...wer-layout.svg => default-icon-layout.svg} | 0 ...er-layout.svg => outlined-icon-layout.svg} | 0 ...-layout.svg => simplified-icon-layout.svg} | 0 5 files changed, 47 insertions(+), 47 deletions(-) rename ui-ngx/src/assets/widget/power-button/{default-power-layout.svg => default-icon-layout.svg} (100%) rename ui-ngx/src/assets/widget/power-button/{outlined-power-layout.svg => outlined-icon-layout.svg} (100%) rename ui-ngx/src/assets/widget/power-button/{simplified-power-layout.svg => simplified-icon-layout.svg} (100%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts index 664ebe5a56..12e2a1065b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -36,9 +36,9 @@ export enum PowerButtonLayout { default_volume = 'default_volume', simplified_volume = 'simplified_volume', outlined_volume = 'outlined_volume', - default_power = 'default_power', - simplified_power = 'simplified_power', - outlined_power = 'outlined_power' + default_icon = 'default_icon', + simplified_icon = 'simplified_icon', + outlined_icon = 'outlined_icon' } export const powerButtonLayouts = Object.keys(PowerButtonLayout) as PowerButtonLayout[]; @@ -51,9 +51,9 @@ export const powerButtonLayoutTranslations = new Map( [PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-volume'], [PowerButtonLayout.simplified_volume, 'widgets.power-button.layout-simplified-volume'], [PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume'], - [PowerButtonLayout.default_power, 'widgets.power-button.layout-default-power'], - [PowerButtonLayout.simplified_power, 'widgets.power-button.layout-simplified-power'], - [PowerButtonLayout.outlined_power, 'widgets.power-button.layout-outlined-power'] + [PowerButtonLayout.default_icon, 'widgets.power-button.layout-default-icon'], + [PowerButtonLayout.simplified_icon, 'widgets.power-button.layout-simplified-icon'], + [PowerButtonLayout.outlined_icon, 'widgets.power-button.layout-outlined-icon'] ] ); @@ -65,9 +65,9 @@ export const powerButtonLayoutImages = new Map( [PowerButtonLayout.default_volume, 'assets/widget/power-button/default-volume-layout.svg'], [PowerButtonLayout.simplified_volume, 'assets/widget/power-button/simplified-volume-layout.svg'], [PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg'], - [PowerButtonLayout.default_power, 'assets/widget/power-button/default-power-layout.svg'], - [PowerButtonLayout.simplified_power, 'assets/widget/power-button/simplified-power-layout.svg'], - [PowerButtonLayout.outlined_power, 'assets/widget/power-button/outlined-power-layout.svg'] + [PowerButtonLayout.default_icon, 'assets/widget/power-button/default-icon-layout.svg'], + [PowerButtonLayout.simplified_icon, 'assets/widget/power-button/simplified-icon-layout.svg'], + [PowerButtonLayout.outlined_icon, 'assets/widget/power-button/outlined-icon-layout.svg'] ] ); @@ -266,12 +266,12 @@ export abstract class PowerButtonShape { return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); case PowerButtonLayout.outlined_volume: return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); - case PowerButtonLayout.default_power: - return new DefaultPowerPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); - case PowerButtonLayout.simplified_power: - return new SimplifiedPowerPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); - case PowerButtonLayout.outlined_power: - return new OutlinedPowerPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.default_icon: + return new DefaultIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.simplified_icon: + return new SimplifiedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + case PowerButtonLayout.outlined_icon: + return new OutlinedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); } } @@ -744,19 +744,19 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.onLabelShape = this.createOnLabel('400'); } - protected addMask(onCircleShape: Circle) { + protected addOnCenterToMask(onCircleShape: Circle) { this.createMask(onCircleShape,[this.onLabelShape]); } - protected addTimeLine(pressedTimeline: Timeline) { + protected addOnCenterTimeLine(pressedTimeline: Timeline) { this.onLabelShape.timeline(pressedTimeline); } - protected drawColor(mainColor: PowerButtonColor) { + protected drawOffCenterColor(mainColor: PowerButtonColor) { this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } - protected animation(scale: number) { + protected onCenterAnimation(scale: number) { powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); } @@ -781,12 +781,12 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.drawOffCenter(this.centerGroup); this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); this.drawOnCenter(); - this.addMask(this.onCircleShape); + this.addOnCenterToMask(this.onCircleShape); this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); - this.addTimeLine(this.pressedTimeline); + this.addOnCenterTimeLine(this.pressedTimeline); this.innerShadow.timeline(this.pressedTimeline); } @@ -806,7 +806,7 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.innerBorder.fill(this.innerBorderGradient); this.innerBorder.attr({ 'fill-opacity': 1 }); } - this.drawColor(mainColor); + this.drawOffCenterColor(mainColor); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -831,14 +831,14 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.innerShadow.show(); const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); - this.animation(pressedScale); + this.onCenterAnimation(pressedScale); this.innerShadow.animate(6, 0.6); } protected onPressEnd() { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); - this.animation(1); + this.onCenterAnimation(1); this.innerShadow.animateRestore().after(() => { if (this.disabled) { this.innerShadow.hide(); @@ -848,7 +848,7 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { } -class DefaultPowerPowerButtonShape extends DefaultVolumePowerButtonShape { +class DefaultIconPowerButtonShape extends DefaultVolumePowerButtonShape { private offPowerSymbolCircle: Path; private offPowerSymbolLine: Path; private onPowerSymbolCircle: Path; @@ -864,21 +864,21 @@ class DefaultPowerPowerButtonShape extends DefaultVolumePowerButtonShape { this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12); } - protected addMask(onCircleShape: Circle) { + protected addOnCenterToMask(onCircleShape: Circle) { this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]); } - protected addTimeLine(pressedTimeline: Timeline) { + protected addOnCenterTimeLine(pressedTimeline: Timeline) { this.onPowerSymbolCircle.timeline(pressedTimeline); this.onPowerSymbolLine.timeline(pressedTimeline); } - protected drawColor(mainColor: PowerButtonColor) { + protected drawOffCenterColor(mainColor: PowerButtonColor) { this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } - protected animation(scale: number) { + protected onCenterAnimation(scale: number) { powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}}); powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}}); } @@ -961,7 +961,7 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { } } -class SimplifiedPowerPowerButtonShape extends SimplifiedVolumePowerButtonShape { +class SimplifiedIconPowerButtonShape extends SimplifiedVolumePowerButtonShape { private offPowerSymbolCircle: Path; private offPowerSymbolLine: Path; private onPowerSymbolCircle: Path; @@ -1004,19 +1004,19 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.onLabelShape = this.createOnLabel('800'); } - protected addMask(onCircleShape: Circle) { + protected addOnCenterToMask(onCircleShape: Circle) { this.createMask(onCircleShape,[this.onLabelShape]); } - protected addTimeLine(pressedTimeline: Timeline) { + protected addOnCenterTimeLine(pressedTimeline: Timeline) { this.onLabelShape.timeline(pressedTimeline); } - protected drawColor(mainColor: PowerButtonColor) { + protected drawOffCenterColor(mainColor: PowerButtonColor) { this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } - protected animation(scale: number) { + protected onCenterAnimation(scale: number) { powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); } @@ -1039,14 +1039,14 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy) .addTo(this.onCenterGroup); this.drawOnCenter(); - this.addMask(this.onCircleShape); + this.addOnCenterToMask(this.onCircleShape); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0); this.backgroundShape.addClass('tb-small-shadow'); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); this.onCenterGroup.timeline(this.pressedTimeline); - this.addTimeLine(this.pressedTimeline); + this.addOnCenterTimeLine(this.pressedTimeline); this.pressedShadow.timeline(this.pressedTimeline); } @@ -1058,7 +1058,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.outerBorder.attr({ 'fill-opacity': 1 }); } this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.drawColor(mainColor); + this.drawOffCenterColor(mainColor); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -1079,7 +1079,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98}); - this.animation(pressedScale / 0.98); + this.onCenterAnimation(pressedScale / 0.98); this.pressedShadow.animate(6, 0.6); } @@ -1087,13 +1087,13 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); - this.animation(1); + this.onCenterAnimation(1); this.pressedShadow.animateRestore(); } } -class OutlinedPowerPowerButtonShape extends OutlinedVolumePowerButtonShape { +class OutlinedIconPowerButtonShape extends OutlinedVolumePowerButtonShape { private offPowerSymbolCircle: Path; private offPowerSymbolLine: Path; private onPowerSymbolCircle: Path; @@ -1109,21 +1109,21 @@ class OutlinedPowerPowerButtonShape extends OutlinedVolumePowerButtonShape { this.onPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12); } - protected addMask(onCircleShape: Circle) { + protected addOnCenterToMask(onCircleShape: Circle) { this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]); } - protected addTimeLine(pressedTimeline: Timeline) { + protected addOnCenterTimeLine(pressedTimeline: Timeline) { this.onPowerSymbolCircle.timeline(pressedTimeline); this.onPowerSymbolLine.timeline(pressedTimeline); } - protected drawColor(mainColor: PowerButtonColor) { + protected drawOffCenterColor(mainColor: PowerButtonColor) { this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } - protected animation(scale: number) { + protected onCenterAnimation(scale: number) { powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}}); powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}}); } 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 f4a0286aa5..d1dd82ad91 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5621,9 +5621,9 @@ "layout-default-volume": "Default.Volume", "layout-simplified-volume": "Simplified.Volume", "layout-outlined-volume": "Outlined.Volume", - "layout-default-power": "Default.Power", - "layout-simplified-power": "Simplified.Power", - "layout-outlined-power": "Outlined.Power", + "layout-default-icon": "Default.Icon", + "layout-simplified-icon": "Simplified.Icon", + "layout-outlined-icon": "Outlined.Icon", "main": "Main", "background": "Background", "power-on-colors": "Power 'On' colors", diff --git a/ui-ngx/src/assets/widget/power-button/default-power-layout.svg b/ui-ngx/src/assets/widget/power-button/default-icon-layout.svg similarity index 100% rename from ui-ngx/src/assets/widget/power-button/default-power-layout.svg rename to ui-ngx/src/assets/widget/power-button/default-icon-layout.svg diff --git a/ui-ngx/src/assets/widget/power-button/outlined-power-layout.svg b/ui-ngx/src/assets/widget/power-button/outlined-icon-layout.svg similarity index 100% rename from ui-ngx/src/assets/widget/power-button/outlined-power-layout.svg rename to ui-ngx/src/assets/widget/power-button/outlined-icon-layout.svg diff --git a/ui-ngx/src/assets/widget/power-button/simplified-power-layout.svg b/ui-ngx/src/assets/widget/power-button/simplified-icon-layout.svg similarity index 100% rename from ui-ngx/src/assets/widget/power-button/simplified-power-layout.svg rename to ui-ngx/src/assets/widget/power-button/simplified-icon-layout.svg From 5fe6fcce9571495513be708e729618a4e36f414a Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 24 Jul 2024 13:28:28 +0300 Subject: [PATCH 100/136] UI: Fixed overflow list on button click --- .../profile/asset-profile-autocomplete.component.ts | 4 ++-- .../profile/device-profile-autocomplete.component.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts index e96f4224ec..f65a44d019 100644 --- a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts @@ -327,7 +327,7 @@ export class AssetProfileAutocompleteComponent implements ControlValueAccessor, } createAssetProfile($event: Event, profileName: string) { - $event.preventDefault(); + $event.stopPropagation(); const assetProfile: AssetProfile = { name: profileName } as AssetProfile; @@ -337,7 +337,7 @@ export class AssetProfileAutocompleteComponent implements ControlValueAccessor, } editAssetProfile($event: Event) { - $event.preventDefault(); + $event.stopPropagation(); this.assetProfileService.getAssetProfile(this.modelValue.id).subscribe( (assetProfile) => { this.openAssetProfileDialog(assetProfile, false); diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts index 3e4501736a..f7538c6524 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts @@ -349,7 +349,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, } createDeviceProfile($event: Event, profileName: string) { - $event.preventDefault(); + $event.stopPropagation(); const deviceProfile: DeviceProfile = { name: profileName, transportType: this.transportType @@ -360,7 +360,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, } editDeviceProfile($event: Event) { - $event.preventDefault(); + $event.stopPropagation(); this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe( (deviceProfile) => { this.openDeviceProfileDialog(deviceProfile, false); From 358925cffe4a18822fe23f016c0bc46d1b9218d0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 24 Jul 2024 13:35:29 +0200 Subject: [PATCH 101/136] added version for relations --- .../main/data/upgrade/3.7.0/schema_update.sql | 7 + .../server/dao/relation/RelationService.java | 2 +- .../common/data/relation/EntityRelation.java | 45 +---- .../server/dao/model/sql/RelationEntity.java | 5 +- .../dao/relation/BaseRelationService.java | 112 +++++-------- .../server/dao/relation/RelationDao.java | 24 ++- .../dao/sql/relation/JpaRelationDao.java | 157 +++++++++++------- .../relation/RelationInsertRepository.java | 2 +- .../dao/sql/relation/RelationRepository.java | 3 - .../relation/SqlRelationInsertRepository.java | 56 +++++-- .../main/resources/sql/schema-entities.sql | 3 + .../server/dao/service/AlarmServiceTest.java | 4 +- .../dao/service/RelationServiceTest.java | 92 +++++----- .../resources/sql/psql/drop-all-tables.sql | 1 + 14 files changed, 266 insertions(+), 247 deletions(-) diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql index f964387359..ad8b76a4f3 100644 --- a/application/src/main/data/upgrade/3.7.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -23,3 +23,10 @@ ALTER TABLE attribute_kv ADD COLUMN version bigint default 0; ALTER TABLE ts_kv_latest ADD COLUMN version bigint default 0; -- KV VERSIONING UPDATE END + +-- RELATION VERSIONING UPDATE START + +CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1000; +ALTER TABLE relation ADD COLUMN version bigint default 0; + +-- RELATION VERSIONING UPDATE END diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index 05dc01a039..013b27275e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -37,7 +37,7 @@ public interface RelationService { EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); - boolean saveRelation(TenantId tenantId, EntityRelation relation); + EntityRelation saveRelation(TenantId tenantId, EntityRelation relation); void saveRelations(TenantId tenantId, List relations); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 1b32cc632e..3c1bc83932 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -18,8 +18,10 @@ package org.thingsboard.server.common.data.relation; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.validation.Length; @@ -27,7 +29,8 @@ import java.io.Serializable; @Slf4j @Schema -public class EntityRelation implements Serializable { +@Data +public class EntityRelation implements HasVersion, Serializable { private static final long serialVersionUID = 2807343040519543363L; @@ -40,6 +43,7 @@ public class EntityRelation implements Serializable { @Length(fieldName = "type") private String type; private RelationTypeGroup typeGroup; + private Long version; private transient JsonNode additionalInfo; @JsonIgnore private byte[] additionalInfoBytes; @@ -70,6 +74,7 @@ public class EntityRelation implements Serializable { this.type = entityRelation.getType(); this.typeGroup = entityRelation.getTypeGroup(); this.additionalInfo = entityRelation.getAdditionalInfo(); + this.version = entityRelation.getVersion(); } @Schema(description = "JSON object with [from] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY) @@ -77,35 +82,24 @@ public class EntityRelation implements Serializable { return from; } - public void setFrom(EntityId from) { - this.from = from; - } - @Schema(description = "JSON object with [to] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY) public EntityId getTo() { return to; } - public void setTo(EntityId to) { - this.to = to; - } - @Schema(description = "String value of relation type.", example = "Contains") public String getType() { return type; } - public void setType(String type) { - this.type = type; - } - @Schema(description = "Represents the type group of the relation.", example = "COMMON") public RelationTypeGroup getTypeGroup() { return typeGroup; } - public void setTypeGroup(RelationTypeGroup typeGroup) { - this.typeGroup = typeGroup; + @Override + public Long getVersion() { + return version; } @Schema(description = "Additional parameters of the relation",implementation = com.fasterxml.jackson.databind.JsonNode.class) @@ -117,25 +111,4 @@ public class EntityRelation implements Serializable { BaseDataWithAdditionalInfo.setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - EntityRelation that = (EntityRelation) o; - - if (from != null ? !from.equals(that.from) : that.from != null) return false; - if (to != null ? !to.equals(that.to) : that.to != null) return false; - if (type != null ? !type.equals(that.type) : that.type != null) return false; - return typeGroup == that.typeGroup; - } - - @Override - public int hashCode() { - int result = from != null ? from.hashCode() : 0; - result = 31 * result + (to != null ? to.hashCode() : 0); - result = 31 * result + (type != null ? type.hashCode() : 0); - result = 31 * result + (typeGroup != null ? typeGroup.hashCode() : 0); - return result; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java index d7d74d4e8d..2aa5c3e573 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RelationEntity.java @@ -23,6 +23,7 @@ import jakarta.persistence.Id; import jakarta.persistence.IdClass; import jakarta.persistence.Table; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -40,11 +41,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_P import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY; +@EqualsAndHashCode(callSuper = true) @Data @Entity @Table(name = RELATION_TABLE_NAME) @IdClass(RelationCompositeKey.class) -public final class RelationEntity implements ToData { +public final class RelationEntity extends VersionedEntity implements ToData { @Id @Column(name = RELATION_FROM_ID_PROPERTY, columnDefinition = "uuid") @@ -103,6 +105,7 @@ public final class RelationEntity implements ToData { } relation.setType(relationType); relation.setTypeGroup(RelationTypeGroup.valueOf(relationTypeGroup)); + relation.setVersion(version); relation.setAdditionalInfo(additionalInfo); return relation; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 5ac1d7ad3b..da383ae1bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -21,12 +21,13 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; -import org.springframework.dao.ConcurrencyFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; @@ -52,8 +53,6 @@ import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -149,11 +148,11 @@ public class BaseRelationService implements RelationService { return relationDao.getRelation(tenantId, from, to, relationType, typeGroup); }, RelationCacheValue::getRelation, - relations -> RelationCacheValue.builder().relation(relations).build(), false); + relation -> RelationCacheValue.builder().relation(relation).build(), false); } @Override - public boolean saveRelation(TenantId tenantId, EntityRelation relation) { + public EntityRelation saveRelation(TenantId tenantId, EntityRelation relation) { log.trace("Executing saveRelation [{}]", relation); validate(relation); var result = relationDao.saveRelation(tenantId, relation); @@ -182,11 +181,11 @@ public class BaseRelationService implements RelationService { log.trace("Executing saveRelationAsync [{}]", relation); validate(relation); var future = relationDao.saveRelationAsync(tenantId, relation); - future.addListener(() -> { + return Futures.transform(future, savedRelation -> { handleEvictEvent(EntityRelationEvent.from(relation)); eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); + return savedRelation != null; }, MoreExecutors.directExecutor()); - return future; } @Override @@ -194,10 +193,11 @@ public class BaseRelationService implements RelationService { log.trace("Executing DeleteRelation [{}]", relation); validate(relation); var result = relationDao.deleteRelation(tenantId, relation); - //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. - publishEvictEvent(EntityRelationEvent.from(relation)); - eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); - return result; + if (result != null) { + publishEvictEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); + } + return result != null; } @Override @@ -205,11 +205,13 @@ public class BaseRelationService implements RelationService { log.trace("Executing deleteRelationAsync [{}]", relation); validate(relation); var future = relationDao.deleteRelationAsync(tenantId, relation); - future.addListener(() -> { - handleEvictEvent(EntityRelationEvent.from(relation)); - eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); + return Futures.transform(future, deletedRelation -> { + if (deletedRelation != null) { + handleEvictEvent(EntityRelationEvent.from(relation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); + } + return deletedRelation != null; }, MoreExecutors.directExecutor()); - return future; } @Override @@ -217,11 +219,11 @@ public class BaseRelationService implements RelationService { log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup); var result = relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup); - //TODO: evict cache only if the relation was deleted. Note: relationDao.deleteRelation requires improvement. - EntityRelation entityRelation = new EntityRelation(from, to, relationType, typeGroup); - publishEvictEvent(EntityRelationEvent.from(entityRelation)); - eventPublisher.publishEvent(new RelationActionEvent(tenantId, entityRelation, ActionType.RELATION_DELETED)); - return result; + if (result != null) { + publishEvictEvent(EntityRelationEvent.from(result)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, result, ActionType.RELATION_DELETED)); + } + return result != null; } @Override @@ -229,9 +231,12 @@ public class BaseRelationService implements RelationService { log.trace("Executing deleteRelationAsync [{}][{}][{}][{}]", from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup); var future = relationDao.deleteRelationAsync(tenantId, from, to, relationType, typeGroup); - EntityRelationEvent event = new EntityRelationEvent(from, to, relationType, typeGroup); - future.addListener(() -> handleEvictEvent(event), MoreExecutors.directExecutor()); - return future; + return Futures.transform(future, deletedEvent -> { + if (deletedEvent != null) { + handleEvictEvent(EntityRelationEvent.from(deletedEvent)); + } + return deletedEvent != null; + }, MoreExecutors.directExecutor()); } @Transactional @@ -250,60 +255,27 @@ public class BaseRelationService implements RelationService { public void deleteEntityRelations(TenantId tenantId, EntityId entityId, RelationTypeGroup relationTypeGroup) { log.trace("Executing deleteEntityRelations [{}]", entityId); validate(entityId); - List inboundRelations = relationTypeGroup == null - ? relationDao.findAllByTo(tenantId, entityId) - : relationDao.findAllByTo(tenantId, entityId, relationTypeGroup); - List outboundRelations = relationTypeGroup == null - ? relationDao.findAllByFrom(tenantId, entityId) - : relationDao.findAllByFrom(tenantId, entityId, relationTypeGroup); - - if (!inboundRelations.isEmpty()) { - try { - if (relationTypeGroup == null) { - relationDao.deleteInboundRelations(tenantId, entityId); - } else { - relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup); - } - } catch (ConcurrencyFailureException e) { - log.debug("Concurrency exception while deleting relations [{}]", inboundRelations, e); - } - for (EntityRelation relation : inboundRelations) { - eventPublisher.publishEvent(EntityRelationEvent.from(relation)); - } + List inboundRelations; + if (relationTypeGroup == null) { + inboundRelations = relationDao.deleteInboundRelations(tenantId, entityId); + } else { + inboundRelations = relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup); } - if (!outboundRelations.isEmpty()) { - if (relationTypeGroup == null) { - relationDao.deleteOutboundRelations(tenantId, entityId); - } else { - relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup); - } - - for (EntityRelation relation : outboundRelations) { - eventPublisher.publishEvent(EntityRelationEvent.from(relation)); - } + for (EntityRelation relation : inboundRelations) { + eventPublisher.publishEvent(EntityRelationEvent.from(relation)); } - } - private List> deleteRelationGroupsAsync(TenantId tenantId, List> relations, boolean deleteFromDb) { - List> results = new ArrayList<>(); - for (List relationList : relations) { - relationList.forEach(relation -> results.add(deleteAsync(tenantId, relation, deleteFromDb))); + List outboundRelations; + if (relationTypeGroup == null) { + outboundRelations = relationDao.deleteOutboundRelations(tenantId, entityId); + } else { + outboundRelations = relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup); } - return results; - } - private ListenableFuture deleteAsync(TenantId tenantId, EntityRelation relation, boolean deleteFromDb) { - if (deleteFromDb) { - return Futures.transform(relationDao.deleteRelationAsync(tenantId, relation), - bool -> { - handleEvictEvent(EntityRelationEvent.from(relation)); - return bool; - }, MoreExecutors.directExecutor()); - } else { - handleEvictEvent(EntityRelationEvent.from(relation)); - return Futures.immediateFuture(false); + for (EntityRelation relation : outboundRelations) { + eventPublisher.publishEvent(EntityRelationEvent.from(relation)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index da7b17a62d..2302dd0abf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -48,29 +48,27 @@ public interface RelationDao { EntityRelation getRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); - boolean saveRelation(TenantId tenantId, EntityRelation relation); + EntityRelation saveRelation(TenantId tenantId, EntityRelation relation); - void saveRelations(TenantId tenantId, Collection relations); + List saveRelations(TenantId tenantId, List relations); - ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation); + ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation); - boolean deleteRelation(TenantId tenantId, EntityRelation relation); + EntityRelation deleteRelation(TenantId tenantId, EntityRelation relation); - ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation); + ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation); - boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); + EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); - ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); + ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); - void deleteOutboundRelations(TenantId tenantId, EntityId entity); + List deleteOutboundRelations(TenantId tenantId, EntityId entity); - void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); + List deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); - void deleteInboundRelations(TenantId tenantId, EntityId entity); + List deleteInboundRelations(TenantId tenantId, EntityId entity); - void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); - - ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity); + List deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 96cef3e909..0e726f8917 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -18,11 +18,11 @@ package org.thingsboard.server.dao.sql.relation; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.ConcurrencyFailureException; -import org.springframework.dao.DataAccessException; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -36,11 +36,19 @@ import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; + /** * Created by Valerii Sosliuk on 5/29/2017. */ @@ -50,6 +58,8 @@ import java.util.stream.Collectors; public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService implements RelationDao { private static final List ALL_TYPE_GROUP_NAMES = new ArrayList<>(); + private static final String RETURNING = "RETURNING from_id, from_type, to_id, to_type, relation_type, relation_type_group, nextval('relation_version_seq') as version"; + private static final String DELETE_QUERY = "DELETE FROM relation WHERE from_id = ? AND from_type = ? AND to_id = ? AND to_type = ? AND relation_type = ? AND relation_type_group = ? " + RETURNING; static { Arrays.stream(RelationTypeGroup.values()).map(RelationTypeGroup::name).forEach(ALL_TYPE_GROUP_NAMES::add); @@ -144,107 +154,138 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } @Override - public boolean saveRelation(TenantId tenantId, EntityRelation relation) { - return relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null; + public EntityRelation saveRelation(TenantId tenantId, EntityRelation relation) { + return DaoUtil.getData(relationInsertRepository.saveOrUpdate(new RelationEntity(relation))); } @Override - public void saveRelations(TenantId tenantId, Collection relations) { + public List saveRelations(TenantId tenantId, List relations) { List entities = relations.stream().map(RelationEntity::new).collect(Collectors.toList()); - relationInsertRepository.saveOrUpdate(entities); + return DaoUtil.convertDataList(relationInsertRepository.saveOrUpdate(entities)); } @Override - public ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation) { - return service.submit(() -> relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null); + public ListenableFuture saveRelationAsync(TenantId tenantId, EntityRelation relation) { + return service.submit(() -> DaoUtil.getData(relationInsertRepository.saveOrUpdate(new RelationEntity(relation)))); } @Override - public boolean deleteRelation(TenantId tenantId, EntityRelation relation) { + public EntityRelation deleteRelation(TenantId tenantId, EntityRelation relation) { RelationCompositeKey key = new RelationCompositeKey(relation); return deleteRelationIfExists(key); } @Override - public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation) { + public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation) { RelationCompositeKey key = new RelationCompositeKey(relation); return service.submit( () -> deleteRelationIfExists(key)); } @Override - public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { + public EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup); return deleteRelationIfExists(key); } @Override - public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { + public ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { RelationCompositeKey key = getRelationCompositeKey(from, to, relationType, typeGroup); return service.submit( () -> deleteRelationIfExists(key)); } - private boolean deleteRelationIfExists(RelationCompositeKey key) { - boolean relationExistsBeforeDelete = relationRepository.existsById(key); - if (relationExistsBeforeDelete) { - try { - relationRepository.deleteById(key); - } catch (DataAccessException e) { - log.debug("[{}] Concurrency exception while deleting relation", key, e); + private EntityRelation deleteRelationIfExists(RelationCompositeKey key) { + return jdbcTemplate.query(DELETE_QUERY, rs -> { + if (!rs.next()) { + return null; } - } - return relationExistsBeforeDelete; + EntityRelation relation = new EntityRelation(); + + var fromId = rs.getObject(RELATION_FROM_ID_PROPERTY, UUID.class); + var fromType = rs.getString(RELATION_FROM_TYPE_PROPERTY); + var toId = rs.getObject(RELATION_TO_ID_PROPERTY, UUID.class); + var toType = rs.getString(RELATION_TO_TYPE_PROPERTY); + var relationTypeGroup = rs.getString(RELATION_TYPE_GROUP_PROPERTY); + var relationType = rs.getString(RELATION_TYPE_PROPERTY); + var version = rs.getLong(VERSION_COLUMN); + + //additionalInfo ignored (no need to send extra data for delete events) + + relation.setTo(EntityIdFactory.getByTypeAndUuid(toType, toId)); + relation.setFrom(EntityIdFactory.getByTypeAndUuid(fromType, fromId)); + relation.setType(relationType); + relation.setTypeGroup(RelationTypeGroup.valueOf(relationTypeGroup)); + relation.setVersion(version); + return relation; + }, key.getFromId(), key.getFromType(), key.getToId(), key.getToType(), key.getRelationType(), key.getRelationTypeGroup()); } @Override - public void deleteOutboundRelations(TenantId tenantId, EntityId entity) { - try { - relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name()); - } catch (ConcurrencyFailureException e) { - log.debug("Concurrency exception while deleting relations [{}]", entity, e); - } + public List deleteOutboundRelations(TenantId tenantId, EntityId entity) { + return deleteRelations(entity, null, false); } @Override - public void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { - try { - relationRepository.deleteByFromIdAndFromTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name())); - } catch (ConcurrencyFailureException e) { - log.debug("Concurrency exception while deleting relations [{}]", entity, e); - } + public List deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { + return deleteRelations(entity, Collections.singletonList(relationTypeGroup.name()), false); } @Override - public void deleteInboundRelations(TenantId tenantId, EntityId entity) { - try { - relationRepository.deleteByToIdAndToTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), ALL_TYPE_GROUP_NAMES); - } catch (ConcurrencyFailureException e) { - log.debug("Concurrency exception while deleting relations [{}]", entity, e); - } + public List deleteInboundRelations(TenantId tenantId, EntityId entity) { + return deleteRelations(entity, ALL_TYPE_GROUP_NAMES, true); } @Override - public void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { - try { - relationRepository.deleteByToIdAndToTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name())); - } catch (ConcurrencyFailureException e) { - log.debug("Concurrency exception while deleting relations [{}]", entity, e); - } + public List deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { + return deleteRelations(entity, Collections.singletonList(relationTypeGroup.name()), true); } - @Override - public ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) { - return service.submit( - () -> { - boolean relationExistsBeforeDelete = relationRepository - .findAllByFromIdAndFromType(entity.getId(), entity.getEntityType().name()) - .size() > 0; - if (relationExistsBeforeDelete) { - relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name()); - } - return relationExistsBeforeDelete; - }); + private List deleteRelations(EntityId entityId, List relationTypeGroups, boolean inbound) { + List params = new ArrayList<>(); + params.add(entityId.getId()); + params.add(entityId.getEntityType().name()); + + StringBuilder sqlBuilder = new StringBuilder("DELETE FROM relation WHERE "); + if (inbound) { + sqlBuilder.append("to_id = ? AND to_type = ? "); + } else { + sqlBuilder.append("from_id = ? AND from_type = ? "); + } + + if (!CollectionUtils.isEmpty(relationTypeGroups)) { + sqlBuilder.append("AND relation_type_group IN (?"); + for (int i = 1; i < relationTypeGroups.size(); i++) { + sqlBuilder.append(", ?"); + } + sqlBuilder.append(")"); + params.addAll(relationTypeGroups); + } + + sqlBuilder.append(RETURNING); + + return jdbcTemplate.queryForList(sqlBuilder.toString(), params.toArray()).stream() + .map(row -> { + EntityRelation relation = new EntityRelation(); + + var fromId = row.get(RELATION_FROM_ID_PROPERTY); + var fromType = row.get(RELATION_FROM_TYPE_PROPERTY); + var toId = row.get(RELATION_TO_ID_PROPERTY); + var toType = row.get(RELATION_TO_TYPE_PROPERTY); + var relationTypeGroup = row.get(RELATION_TYPE_GROUP_PROPERTY); + var relationType = row.get(RELATION_TYPE_PROPERTY); + var version = row.get(VERSION_COLUMN); + + //additionalInfo ignored (no need to send extra data for delete events) + + relation.setTo(EntityIdFactory.getByTypeAndUuid((String) toType, (UUID) toId)); + relation.setFrom(EntityIdFactory.getByTypeAndUuid((String) fromType, (UUID) fromId)); + relation.setType((String) relationType); + relation.setTypeGroup(RelationTypeGroup.valueOf((String) relationTypeGroup)); + relation.setVersion((Long) version); + return relation; + }) + .collect(Collectors.toList()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java index 188d8afa0f..56cb5718d5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java @@ -23,6 +23,6 @@ public interface RelationInsertRepository { RelationEntity saveOrUpdate(RelationEntity entity); - void saveOrUpdate(List entities); + List saveOrUpdate(List entities); } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index c812788ed0..0f56a413df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -58,9 +58,6 @@ public interface RelationRepository String relationType, String relationTypeGroup); - List findAllByFromIdAndFromType(UUID fromId, - String fromType); - @Query("SELECT r FROM RelationEntity r WHERE " + "r.relationTypeGroup = 'RULE_NODE' AND r.toType = 'RULE_CHAIN' " + "AND r.toId in (SELECT id from RuleChainEntity where type = :ruleChainType )") diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/SqlRelationInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/SqlRelationInsertRepository.java index 7655ff5539..f2d374641e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/SqlRelationInsertRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/SqlRelationInsertRepository.java @@ -15,33 +15,39 @@ */ package org.thingsboard.server.dao.sql.relation; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.SqlProvider; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.dao.model.sql.RelationEntity; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; +import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; + @Repository @Transactional public class SqlRelationInsertRepository implements RelationInsertRepository { - private static final String INSERT_ON_CONFLICT_DO_UPDATE_JPA = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" + - " VALUES (:fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, :additionalInfo) " + - "ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = :additionalInfo returning *"; - - private static final String INSERT_ON_CONFLICT_DO_UPDATE_JDBC = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" + - " VALUES (?, ?, ?, ?, ?, ?, ?) " + - "ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = ?"; + private static final String INSERT_ON_CONFLICT_DO_UPDATE_JPA = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, version, additional_info)" + + " VALUES (:fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, nextval('relation_version_seq'), :additionalInfo) " + + "ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = :additionalInfo, version = nextval('relation_version_seq') returning *"; + private static final String INSERT_ON_CONFLICT_DO_UPDATE_JDBC = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, version, additional_info)" + + " VALUES (?, ?, ?, ?, ?, ?, nextval('relation_version_seq'), ?) " + + "ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = ?, version = nextval('relation_version_seq')"; @PersistenceContext protected EntityManager entityManager; @@ -71,8 +77,9 @@ public class SqlRelationInsertRepository implements RelationInsertRepository { } @Override - public void saveOrUpdate(List entities) { - jdbcTemplate.batchUpdate(INSERT_ON_CONFLICT_DO_UPDATE_JDBC, new BatchPreparedStatementSetter() { + public List saveOrUpdate(List entities) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.batchUpdate(new SequencePreparedStatementCreator(INSERT_ON_CONFLICT_DO_UPDATE_JDBC), new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { RelationEntity relation = entities.get(i); @@ -98,7 +105,30 @@ public class SqlRelationInsertRepository implements RelationInsertRepository { public int getBatchSize() { return entities.size(); } - }); + }, keyHolder); + + var seqNumbers = keyHolder.getKeyList(); + + for (int i = 0; i < entities.size(); i++) { + entities.get(i).setVersion((Long) seqNumbers.get(i).get(VERSION_COLUMN)); + } + + return entities; + } + + private record SequencePreparedStatementCreator(String sql) implements PreparedStatementCreator, SqlProvider { + + private static final String[] COLUMNS = {VERSION_COLUMN}; + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + return con.prepareStatement(sql, COLUMNS); + } + + @Override + public String getSql() { + return this.sql; + } } } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9e6ab22248..28b842076a 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -420,6 +420,8 @@ CREATE TABLE IF NOT EXISTS error_event ( e_error varchar ) PARTITION BY RANGE (ts); +CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1000; + CREATE TABLE IF NOT EXISTS relation ( from_id uuid, from_type varchar(255), @@ -428,6 +430,7 @@ CREATE TABLE IF NOT EXISTS relation ( relation_type_group varchar(255), relation_type varchar(255), additional_info varchar, + version bigint default 0, CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type) ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java index 6c3a359f73..aa1c96d84b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java @@ -340,7 +340,7 @@ public class AlarmServiceTest extends AbstractServiceTest { EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(relationService.saveRelation(tenantId, relation)); + Assert.assertNotNull(relationService.saveRelation(tenantId, relation)); long ts = System.currentTimeMillis(); AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() @@ -877,7 +877,7 @@ public class AlarmServiceTest extends AbstractServiceTest { EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(relationService.saveRelation(tenantId, relation)); + Assert.assertNotNull(relationService.saveRelation(tenantId, relation)); long ts = System.currentTimeMillis(); AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java index 4e20744ef7..710a69fac9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java @@ -57,13 +57,13 @@ public class RelationServiceTest extends AbstractServiceTest { } @Test - public void testSaveRelation() throws ExecutionException, InterruptedException { + public void testSaveRelation() { AssetId parentId = new AssetId(Uuids.timeBased()); AssetId childId = new AssetId(Uuids.timeBased()); EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(saveRelation(relation)); + Assert.assertNotNull(saveRelation(relation)); Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)); @@ -204,8 +204,8 @@ public class RelationServiceTest extends AbstractServiceTest { Assert.assertEquals(0, relations.size()); } - private Boolean saveRelation(EntityRelation relationA1) { - return relationService.saveRelation(SYSTEM_TENANT_ID, relationA1); + private EntityRelation saveRelation(EntityRelation relation) { + return relationService.saveRelation(SYSTEM_TENANT_ID, relation); } @Test @@ -265,9 +265,9 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); EntityRelation relationC = new EntityRelation(assetC, assetA, EntityRelation.CONTAINS_TYPE); - saveRelation(relationA); - saveRelation(relationB); - saveRelation(relationC); + relationA = saveRelation(relationA); + relationB = saveRelation(relationB); + relationC = saveRelation(relationC); EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); @@ -299,8 +299,8 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationBD = new EntityRelation(assetB, deviceD, EntityRelation.CONTAINS_TYPE); - saveRelation(relationAB); - saveRelation(relationBC); + relationAB = saveRelation(relationAB); + relationBC = saveRelation(relationBC); saveRelation(relationBD); EntityRelationsQuery query = new EntityRelationsQuery(); @@ -329,26 +329,20 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationAB = new EntityRelation(root, left, EntityRelation.CONTAINS_TYPE); EntityRelation relationBC = new EntityRelation(root, right, EntityRelation.CONTAINS_TYPE); - saveRelation(relationAB); - expected.add(relationAB); - - saveRelation(relationBC); - expected.add(relationBC); + expected.add(saveRelation(relationAB)); + expected.add(saveRelation(relationBC)); for (int i = 0; i < maxLevel; i++) { var newLeft = new AssetId(Uuids.timeBased()); var newRight = new AssetId(Uuids.timeBased()); EntityRelation relationLeft = new EntityRelation(left, newLeft, EntityRelation.CONTAINS_TYPE); EntityRelation relationRight = new EntityRelation(right, newRight, EntityRelation.CONTAINS_TYPE); - saveRelation(relationLeft); - expected.add(relationLeft); - saveRelation(relationRight); - expected.add(relationRight); + expected.add(saveRelation(relationLeft)); + expected.add(saveRelation(relationRight)); left = newLeft; right = newRight; } - EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(root, EntitySearchDirection.FROM, -1, false)); query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); @@ -372,7 +366,7 @@ public class RelationServiceTest extends AbstractServiceTest { relation.setTo(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); Assertions.assertThrows(DataValidationException.class, () -> { - Assert.assertTrue(saveRelation(relation)); + Assert.assertNotNull(saveRelation(relation)); }); } @@ -382,7 +376,7 @@ public class RelationServiceTest extends AbstractServiceTest { relation.setFrom(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); Assertions.assertThrows(DataValidationException.class, () -> { - Assert.assertTrue(saveRelation(relation)); + Assert.assertNotNull(saveRelation(relation)); }); } @@ -392,7 +386,7 @@ public class RelationServiceTest extends AbstractServiceTest { relation.setFrom(new AssetId(Uuids.timeBased())); relation.setTo(new AssetId(Uuids.timeBased())); Assertions.assertThrows(DataValidationException.class, () -> { - Assert.assertTrue(saveRelation(relation)); + Assert.assertNotNull(saveRelation(relation)); }); } @@ -414,10 +408,10 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE); EntityRelation relationD = new EntityRelation(assetC, assetE, EntityRelation.CONTAINS_TYPE); - saveRelation(relationA); - saveRelation(relationB); - saveRelation(relationC); - saveRelation(relationD); + relationA = saveRelation(relationA); + relationB = saveRelation(relationB); + relationC = saveRelation(relationC); + relationD = saveRelation(relationD); EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true)); @@ -450,9 +444,9 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE); EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE); - saveRelation(relationA); - saveRelation(relationB); - saveRelation(relationC); + relationA = saveRelation(relationA); + relationB = saveRelation(relationB); + relationC = saveRelation(relationC); EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true)); @@ -494,12 +488,12 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE); EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE); - saveRelation(relationA); - saveRelation(relationB); - saveRelation(relationC); - saveRelation(relationD); - saveRelation(relationE); - saveRelation(relationF); + relationA = saveRelation(relationA); + relationB = saveRelation(relationB); + relationC = saveRelation(relationC); + relationD = saveRelation(relationD); + relationE = saveRelation(relationE); + relationF = saveRelation(relationF); EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, true)); @@ -547,12 +541,12 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE); EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE); - saveRelation(relationA); - saveRelation(relationB); - saveRelation(relationC); - saveRelation(relationD); - saveRelation(relationE); - saveRelation(relationF); + relationA = saveRelation(relationA); + relationB = saveRelation(relationB); + relationC = saveRelation(relationC); + relationD = saveRelation(relationD); + relationE = saveRelation(relationE); + relationF = saveRelation(relationF); EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, false)); @@ -600,12 +594,12 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE); EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE); - saveRelation(relationA); - saveRelation(relationB); - saveRelation(relationC); - saveRelation(relationD); - saveRelation(relationE); - saveRelation(relationF); + relationA = saveRelation(relationA); + relationB = saveRelation(relationB); + relationC = saveRelation(relationC); + relationD = saveRelation(relationD); + relationE = saveRelation(relationE); + relationF = saveRelation(relationF); EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); @@ -670,8 +664,8 @@ public class RelationServiceTest extends AbstractServiceTest { EntityRelation firstRelation = new EntityRelation(rootAsset, firstAsset, EntityRelation.CONTAINS_TYPE); EntityRelation secondRelation = new EntityRelation(rootAsset, secondAsset, EntityRelation.CONTAINS_TYPE); - saveRelation(firstRelation); - saveRelation(secondRelation); + firstRelation = saveRelation(firstRelation); + secondRelation = saveRelation(secondRelation); if (!lastLvlOnly || lvl == 1) { entityRelations.add(firstRelation); diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index e1c35d7a21..da6eca161b 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -34,6 +34,7 @@ DROP TABLE IF EXISTS stats_event; DROP TABLE IF EXISTS lc_event; DROP TABLE IF EXISTS error_event; DROP TABLE IF EXISTS relation; +DROP SEQUENCE IF EXISTS relation_version_seq; DROP TABLE IF EXISTS tenant; DROP TABLE IF EXISTS ts_kv; DROP TABLE IF EXISTS ts_kv_latest; From 38f07984c41d55a1f31a09183638531a3211a55a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 24 Jul 2024 15:53:22 +0300 Subject: [PATCH 102/136] functionality fixes --- .../broker-config-control.component.ts | 4 +- .../modbus-master-table.component.html | 2 +- .../modbus-master-table.component.ts | 6 +- .../modbus-security-config.component.ts | 40 ++-- .../modbus-slave-config.component.html | 31 +-- .../modbus-slave-config.component.ts | 20 +- .../modbus-slave-dialog.component.html | 192 +++++++++--------- .../modbus-slave-dialog.component.ts | 10 +- .../modbus-values/modbus-values.component.ts | 43 ++-- .../lib/gateway/gateway-widget.models.ts | 14 +- 10 files changed, 187 insertions(+), 175 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index 031388b502..da67296869 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -37,9 +37,7 @@ import { TranslateService } from '@ngx-translate/core'; import { generateSecret } from '@core/utils'; import { Subject } from 'rxjs'; import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; -import { - SecurityConfigComponent -} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; +import { SecurityConfigComponent } from '../security-config/security-config.component'; @Component({ selector: 'tb-broker-config-control', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html index ac5e6f3108..2e92d80c9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -72,7 +72,7 @@ {{ 'gateway.client-communication-type' | translate }} - {{ ModbusClientTypeLabelsMap.get(mapping['type']) }} + {{ ModbusProtocolLabelsMap.get(mapping['type']) }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 6644fbfd5e..10b1e5bbbe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -40,7 +40,9 @@ import { Validator, } from '@angular/forms'; import { - ModbusClientTypeLabelsMap, ModbusMasterConfig, SlaveConfig, + ModbusMasterConfig, + ModbusProtocolLabelsMap, + SlaveConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { SharedModule } from '@shared/shared.module'; @@ -80,7 +82,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat masterFormGroup: UntypedFormGroup; textSearch = this.fb.control('', {nonNullable: true}); - readonly ModbusClientTypeLabelsMap = ModbusClientTypeLabelsMap; + readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; private onChange: (value: string) => void = () => {}; private onTouched: () => void = () => {}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index 362edbee29..75328d1264 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -41,9 +41,6 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { - SecurityConfigComponent -} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ @@ -66,7 +63,6 @@ import { coerceBoolean } from '@shared/decorators/coercion'; imports: [ CommonModule, SharedModule, - SecurityConfigComponent, ] }) export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { @@ -74,10 +70,10 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali @coerceBoolean() @Input() isMaster = false; - disabled = false; - securityConfigFormGroup: UntypedFormGroup; + private disabled = false; + private onChange: (value: ModbusSecurity) => void; private onTouched: () => void; @@ -89,21 +85,14 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], + reqclicert: [{value: false, disabled: true}, []], }); this.observeValueChanges(); } ngOnChanges(): void { - if (this.isMaster) { - this.securityConfigFormGroup = this.fb.group({ - certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - reqclicert: [false, []], - }); - this.observeValueChanges(); - } + this.updateMasterEnabling(); } ngOnDestroy(): void { @@ -126,6 +115,7 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali } else { this.securityConfigFormGroup.enable({emitEvent: false}); } + this.updateMasterEnabling(); this.cdr.markForCheck(); } @@ -137,19 +127,29 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali writeValue(securityConfig: ModbusSecurity): void { const { certfile, password, keyfile, server_hostname } = securityConfig; - let securityState = { + const securityState = { certfile: certfile ?? '', password: password ?? '', keyfile: keyfile ?? '', - server_hostname: server_hostname?? '', + server_hostname: server_hostname ?? '', reqclicert: !!securityConfig.reqclicert, }; + + this.securityConfigFormGroup.reset(securityState, {emitEvent: false}); + } + + private updateMasterEnabling(): void { if (this.isMaster) { - securityState = { ...securityState, reqclicert: !!securityConfig.reqclicert }; + if (!this.disabled) { + this.securityConfigFormGroup.get('reqclicert').enable({emitEvent: false}); + } + this.securityConfigFormGroup.get('server_hostname').disable({emitEvent: false}); } else { - securityState = { ...securityState, server_hostname: server_hostname ?? '' }; + if (!this.disabled) { + this.securityConfigFormGroup.get('server_hostname').enable({emitEvent: false}); + } + this.securityConfigFormGroup.get('reqclicert').disable({emitEvent: false}); } - this.securityConfigFormGroup.reset(securityState, {emitEvent: false}); } private observeValueChanges(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 1c1b55e6c2..25a07ccfa5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -181,8 +181,8 @@ -
- +
+
gateway.advanced-connection-settings
@@ -199,19 +199,20 @@
- - - - - - {{ 'gateway.tls-connection' | translate }} - - - - - - +
+ + + + + + {{ 'gateway.tls-connection' | translate }} + + + + + + +
gateway.vendor-name
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 33a18ebbb7..189ca75762 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -47,6 +47,7 @@ import { startWith, takeUntil } from 'rxjs/operators'; import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; +import { isEqual } from '@core/utils'; @Component({ selector: 'tb-modbus-slave-config', @@ -158,7 +159,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } writeValue(slaveConfig: ModbusSlave): void { - this.showSecurityControl.patchValue(!!slaveConfig.security); + this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); this.updateSlaveConfig(slaveConfig); this.updateFormEnableState(slaveConfig.sendDataToThingsBoard); } @@ -187,16 +188,19 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false}); } this.updateEnablingByProtocol(this.slaveConfigFormGroup.get('type').value); + this.updateSecurityEnable(this.showSecurityControl.value); } private observeShowSecurity(): void { - this.showSecurityControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { - if (value && this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) { - this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); - } else { - this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); - } - }); + this.showSecurityControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => this.updateSecurityEnable(value)); + } + + private updateSecurityEnable(isEnabled: boolean): void { + if (isEnabled && this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) { + this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); + } else { + this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); + } } private updateEnablingByProtocol(type: ModbusProtocolType): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index caa1af4029..a8aa70c878 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -236,108 +236,112 @@
- - - -
gateway.advanced-connection-settings
-
-
-
-
-
gateway.connection-timeout
-
- - - +
+ + + +
gateway.advanced-connection-settings
+
+
+
+
+
gateway.connection-timeout
+
+ + + +
-
-
-
gateway.byte-order
-
- - - {{ order }} - - +
+
gateway.byte-order
+
+ + + {{ order }} + + +
-
-
-
gateway.word-order
-
- - - {{ order }} - - +
+
gateway.word-order
+
+ + + {{ order }} + + +
-
- - - - - - {{ 'gateway.tls-connection' | translate }} - - - - - - -
- - - {{ 'gateway.retries' | translate }} - - -
-
- - - {{ 'gateway.retries-on-empty' | translate }} - - -
-
- - - {{ 'gateway.retries-on-invalid' | translate }} - - -
-
-
gateway.poll-period
-
- - - +
+ + + + + + {{ 'gateway.tls-connection' | translate }} + + + + + +
-
-
-
gateway.connect-attempt-time
-
- - - +
+ + + {{ 'gateway.retries' | translate }} + +
-
-
-
gateway.connect-attempt-count
-
- - - +
+ + + {{ 'gateway.retries-on-empty' | translate }} + +
-
-
-
gateway.wait-after-failed-attempts
-
- - - +
+ + + {{ 'gateway.retries-on-invalid' | translate }} + + +
+
+
gateway.poll-period
+
+ + + +
+
+
+
gateway.connect-attempt-time
+
+ + + +
+
+
+
gateway.connect-attempt-count
+
+ + + +
+
+
+
gateway.wait-after-failed-attempts
+
+ + + +
-
- + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 40a4459221..f01bbf7618 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -51,6 +51,7 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; import { takeUntil } from 'rxjs/operators'; +import { isEqual } from '@core/utils'; @Component({ selector: 'tb-modbus-slave-dialog', @@ -81,12 +82,6 @@ import { takeUntil } from 'rxjs/operators'; .slaves-config-container { width: 900px; } - .nested-expansion-header { - .mat-content { - height: 100%; - overflow: hidden; - } - } } `], }) @@ -169,10 +164,10 @@ export class ModbusSlaveDialogComponent extends DialogComponent { - return { - ...registersAcc, - [register]: this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})), - }; - }, {})); + ) {} + ngOnInit() { + this.initializeValuesFormGroup(); this.observeValuesChanges(); } - ngOnChanges(): void { - if (this.singleMode) { - this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})); - this.observeValuesChanges(); - } - } - ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -218,6 +207,28 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O } } + private initializeValuesFormGroup(): void { + if (this.singleMode) { + this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => { + acc[key] = this.fb.control([[], []]); + return acc; + }, {})); + } else { + this.valuesFormGroup = this.fb.group( + this.modbusRegisterTypes.reduce((registersAcc, register) => { + + registersAcc[register] = this.fb.group(this.modbusValueKeys.reduce((acc, key) => { + acc[key] = this.fb.control([[], []]); + return acc; + }, {})); + + return registersAcc; + }, {}) + ); + } + } + + private observeValuesChanges(): void { this.valuesFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 3c041cd005..2db6d9d2fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -823,6 +823,9 @@ export enum ModbusDataType { STRING = 'string', BYTES = 'bytes', BITS = 'bits', + INT8 = '8int', + UINT8 = '8uint', + FLOAT8 = '8float', INT16 = '16int', UINT16 = '16uint', FLOAT16 = '16float', @@ -835,6 +838,9 @@ export enum ModbusDataType { } export enum ModbusObjectCountByDataType { + '8int' = 1, + '8uint' = 1, + '8float' = 1, '16int' = 1, '16uint' = 1, '16float' = 1, @@ -907,14 +913,6 @@ export const ModbusKeysNoKeysTextTranslationsMap = new Map( - [ - [ModbusProtocolType.TCP, 'TCP/UDP'], - [ModbusProtocolType.UDP, 'TCP/UDP'], - [ModbusProtocolType.Serial, 'Serial'], - ] -); - export const ModbusFunctionCodeTranslationsMap = new Map( [ [1, 'gateway.read-coils'], From e9d58525bc26ddca5a4de4061c5b0bd09fa1d359 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 24 Jul 2024 16:26:51 +0300 Subject: [PATCH 103/136] dialog word order for serial fix --- .../modbus-slave-dialog/modbus-slave-dialog.component.html | 2 +- .../modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index a8aa70c878..60ef25f875 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -262,7 +262,7 @@
-
+
gateway.word-order
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index f01bbf7618..8c6fb87e11 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -105,7 +105,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent(); From 093e57374ea6a85846b667615377b121de5ae2af Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 24 Jul 2024 17:25:50 +0300 Subject: [PATCH 104/136] UI: Improvements for notification widget --- .../widget_types/unread_notifications.json | 2 +- ...d-notification-basic-config.component.html | 2 +- ...ead-notification-basic-config.component.ts | 40 ++++++++++++++----- .../unread-notification-widget.component.html | 1 + .../unread-notification-widget.component.scss | 2 +- .../unread-notification-widget.component.ts | 8 ++-- .../unread-notification-widget.models.ts | 2 +- ...otification-widget-settings.component.html | 6 ++- ...-notification-widget-settings.component.ts | 6 +++ .../assets/locale/locale.constant-en_US.json | 1 + 10 files changed, 51 insertions(+), 19 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/unread_notifications.json b/application/src/main/data/json/system/widget_types/unread_notifications.json index 7e3e6b0477..ed7a8492ad 100644 --- a/application/src/main/data/json/system/widget_types/unread_notifications.json +++ b/application/src/main/data/json/system/widget_types/unread_notifications.json @@ -17,7 +17,7 @@ "settingsDirective": "tb-unread-notification-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-unread-notification-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\",\"maxNotificationDisplay\":6,\"showCounter\":true,\"counterValueFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"\"},\"counterValueColor\":\"#fff\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"enableViewAll\":true,\"enableFilter\":true,\"enableMarkAsRead\":true},\"title\":\"Unread notification\",\"dropShadow\":true,\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"#000000\",\"showTitleIcon\":true,\"iconSize\":\"22px\",\"titleIcon\":\"notifications\",\"iconColor\":\"#000000\",\"actions\":{},\"enableFullscreen\":false,\"borderRadius\":\"4px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\",\"maxNotificationDisplay\":6,\"showCounter\":true,\"counterValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"\"},\"counterValueColor\":\"#fff\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"enableViewAll\":true,\"enableFilter\":true,\"enableMarkAsRead\":true},\"title\":\"Unread notification\",\"dropShadow\":true,\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"#000000\",\"showTitleIcon\":true,\"iconSize\":\"22px\",\"titleIcon\":\"notifications\",\"iconColor\":\"#000000\",\"actions\":{},\"enableFullscreen\":false,\"borderRadius\":\"4px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" }, "tags": null } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html index 22bfc17787..7fc472e6cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html @@ -77,7 +77,7 @@
, protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { @@ -87,16 +89,6 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom const showTitle: boolean = this.unreadNotificationWidgetConfigForm.get('showTitle').value; const showIcon: boolean = this.unreadNotificationWidgetConfigForm.get('showIcon').value; - if (showTitle) { - this.unreadNotificationWidgetConfigForm.get('title').enable({emitEvent}); - this.unreadNotificationWidgetConfigForm.get('titleFont').enable({emitEvent}); - this.unreadNotificationWidgetConfigForm.get('titleColor').enable({emitEvent}); - } else { - this.unreadNotificationWidgetConfigForm.get('title').disable({emitEvent}); - this.unreadNotificationWidgetConfigForm.get('titleFont').disable({emitEvent}); - this.unreadNotificationWidgetConfigForm.get('titleColor').disable({emitEvent}); - } - if (showIcon) { this.unreadNotificationWidgetConfigForm.get('iconSize').enable({emitEvent}); this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').enable({emitEvent}); @@ -118,6 +110,30 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent}); this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent}); } + + if (showTitle) { + this.unreadNotificationWidgetConfigForm.get('title').enable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('titleFont').enable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('titleColor').enable({emitEvent}); + + this.unreadNotificationWidgetConfigForm.get('showCounter').enable({emitEvent: false}); + this.unreadNotificationWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + } else { + this.unreadNotificationWidgetConfigForm.get('title').disable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('titleFont').disable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('titleColor').disable({emitEvent}); + + this.unreadNotificationWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.unreadNotificationWidgetConfigForm.get('iconSize').disable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').disable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('icon').disable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('iconColor').disable({emitEvent}); + + this.unreadNotificationWidgetConfigForm.get('showCounter').disable({emitEvent: false}); + this.unreadNotificationWidgetConfigForm.get('counterValueFont').disable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent}); + this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent}); + } } protected prepareOutputConfig(config: any): WidgetConfigComponentData { @@ -174,4 +190,8 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom config.enableFullscreen = buttons.includes('fullscreen'); } + private _countPreviewFn(): string { + return this.unreadNotificationWidgetConfigForm.get('maxNotificationDisplay').value?.toString() || '6'; + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html index 3537c8bb1d..7da0738e59 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html @@ -32,6 +32,7 @@ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss index 5fb3ad8950..456660c332 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss @@ -60,6 +60,6 @@ width: 24px; height: 22px; background-color: green; - border-radius: 7px; + border-radius: 5px; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts index fcad768e66..2c778862fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts @@ -153,9 +153,11 @@ export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy { this.ctx.widgetActions = [this.viewAllAction, this.filterAction, this.markAsReadAction]; this.viewAllAction.show = isDefined(this.settings.enableViewAll) ? this.settings.enableViewAll : true; - this.store.pipe(select(selectUserDetails), take(1)).subscribe( - user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen - ); + if (this.viewAllAction.show) { + this.store.pipe(select(selectUserDetails), take(1)).subscribe( + user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen + ); + } this.filterAction.show = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true; this.markAsReadAction.show = isDefined(this.settings.enableMarkAsRead) ? this.settings.enableMarkAsRead : true; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts index 008c1d9a9d..507ed9d6a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts @@ -35,7 +35,7 @@ export const unreadNotificationDefaultSettings: UnreadNotificationWidgetSettings showCounter: true, counterValueFont: { family: 'Roboto', - size: 14, + size: 12, sizeUnit: 'px', style: 'normal', weight: '600', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html index 5f452f3508..f59c85b3ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html @@ -41,7 +41,9 @@
- {{ 'widgets.notification.counter' | translate }} +
+ {{ 'widgets.notification.counter' | translate }} +
@@ -49,7 +51,7 @@
, private fb: UntypedFormBuilder) { super(store); @@ -78,4 +80,8 @@ export class UnreadNotificationWidgetSettingsComponent extends WidgetSettingsCom } } + private _countPreviewFn(): string { + return this.unreadNotificationWidgetSettingsForm.get('maxNotificationDisplay').value?.toString() || '6'; + } + } 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 33c8d25652..21ffe9e4e4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7046,6 +7046,7 @@ "notification": { "max-notification-display": "Maximum notifications to display", "counter": "Counter", + "counter-hint": "Counter will be displayed if \"Widget title\" is enabled", "icon": "Icon", "counter-value": "Value", "counter-color": "Color", From a15ec624d769284bf6340055d96bbae9ccc72e9d Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 24 Jul 2024 16:52:35 +0200 Subject: [PATCH 105/136] fixed RelationCacheTest --- .../thingsboard/server/dao/service/RelationCacheTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationCacheTest.java index 495f3c24e0..e86517930f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationCacheTest.java @@ -85,8 +85,12 @@ public class RelationCacheTest extends AbstractServiceTest { @Test public void testDeleteRelations_EvictsCache() { + EntityRelation relation = new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE); when(relationDao.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON)) - .thenReturn(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE)); + .thenReturn(relation); + + when(relationDao.deleteRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON)) + .thenReturn(relation); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); relationService.getRelation(SYSTEM_TENANT_ID, ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); From 9e83035e0448c590633de8c230a11f9ffd91ea15 Mon Sep 17 00:00:00 2001 From: kalytka Date: Thu, 25 Jul 2024 11:10:03 +0300 Subject: [PATCH 106/136] Added "_blank" to the documentation link --- .../help/en_US/rulenode/common_node_fields_templatization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md index 6779128c89..0542e592d3 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md @@ -1,4 +1,4 @@ Fields templatization feature allows you to process the incoming messages with dynamic configuration by substitution of templates specified in the configuration fields with values from message or message metadata. -For more detailed information, please refer to the ThingsBoard [documentation](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/templatization/) +For more detailed information, please refer to the ThingsBoard [documentation{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/templatization/) From 7b9d8e0a5528e07344cd829267e55c3821b144a7 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 25 Jul 2024 12:09:26 +0300 Subject: [PATCH 107/136] major refactoring --- .../modbus-basic-config.component.ts | 15 +- .../modbus-data-keys-panel.component.html | 13 +- .../modbus-data-keys-panel.component.ts | 92 +++++++---- .../modbus-master-table.component.html | 10 +- .../modbus-master-table.component.scss | 21 +-- .../modbus-master-table.component.ts | 45 +++--- .../modbus-security-config.component.ts | 2 +- .../modbus-slave-config.component.html | 9 +- .../modbus-slave-config.component.scss | 5 - .../modbus-slave-config.component.ts | 151 +++++++++--------- .../modbus-slave-dialog.component.html | 6 +- .../modbus-slave-dialog.component.ts | 122 +++++++------- .../modbus-values/modbus-values.component.ts | 109 ++++++------- .../lib/gateway/gateway-widget.models.ts | 27 +--- .../widget/widget-components.module.ts | 39 ++--- .../home/pipes/gateway-port-tooltip.pipe.ts | 12 +- .../ellipsis-chip-list.directive.ts | 5 +- .../assets/locale/locale.constant-en_US.json | 1 + .../connector-default-configs/modbus.json | 1 + 19 files changed, 340 insertions(+), 345 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 35e0ee6e61..12143fb6db 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -68,12 +68,6 @@ import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master overflow: hidden !important; } } - - :host ::ng-deep { - .mat-mdc-tab-group, .mat-mdc-tab-body-wrapper { - height: 100%; - } - } `] }) @@ -83,10 +77,9 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat basicFormGroup: FormGroup; - onChange: (value: string) => void; + onChange: (value: ModbusBasicConfig) => void; onTouched: () => void; - protected readonly connectorType = ConnectorType; private destroy$ = new Subject(); constructor(private fb: FormBuilder) { @@ -108,7 +101,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat this.destroy$.complete(); } - registerOnChange(fn: (value: string) => void): void { + registerOnChange(fn: (value: ModbusBasicConfig) => void): void { this.onChange = fn; } @@ -118,8 +111,8 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat writeValue(basicConfig: ModbusBasicConfig): void { const editedBase = { - slave: basicConfig.slave || {}, - master: basicConfig.master || {}, + slave: basicConfig.slave ?? {}, + master: basicConfig.master ?? {}, }; this.basicFormGroup.setValue(editedBase, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index 048ee94553..5285422bd6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -20,7 +20,7 @@
{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}
+ *ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByControlId; let $index = index; let last = last;">
@@ -99,10 +99,19 @@
-
gateway.address
+
gateway.address
+ + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index d128e3c246..d0bfcf1e76 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -14,14 +14,21 @@ /// limitations under the License. /// -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { + AbstractControl, + FormArray, + FormGroup, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { ModbusDataType, ModbusFunctionCodeTranslationsMap, ModbusObjectCountByDataType, - ModbusRegisterType, ModbusValue, ModbusValueKey, noLeadTrailSpacesRegex, @@ -31,6 +38,8 @@ import { SharedModule } from '@shared/shared.module'; import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe'; import { generateSecret } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; @Component({ selector: 'tb-modbus-data-keys-panel', @@ -43,7 +52,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; GatewayHelpLinkPipe, ] }) -export class ModbusDataKeysPanelComponent implements OnInit { +export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { @coerceBoolean() @Input() isMaster = false; @@ -51,15 +60,13 @@ export class ModbusDataKeysPanelComponent implements OnInit { @Input() addKeyTitle: string; @Input() deleteKeyTitle: string; @Input() noKeysText: string; - @Input() register: ModbusRegisterType; @Input() keysType: ModbusValueKey; @Input() values: ModbusValue[]; @Input() popover: TbPopoverComponent; @Output() keysDataApplied = new EventEmitter>(); - keysListFormArray: UntypedFormArray; - errorText = ''; + keysListFormArray: FormArray; modbusDataTypes = Object.values(ModbusDataType); withFunctionCode = true; functionCodesMap = new Map(); @@ -67,9 +74,12 @@ export class ModbusDataKeysPanelComponent implements OnInit { readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; - readonly defaultReadFunctionCodes = [3, 4]; - readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; - readonly stringAttrUpdatesWriteFunctionCodes = [6, 16]; + + private destroy$ = new Subject(); + + private readonly defaultReadFunctionCodes = [3, 4]; + private readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; + private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16]; constructor(private fb: UntypedFormBuilder) {} @@ -79,8 +89,13 @@ export class ModbusDataKeysPanelComponent implements OnInit { this.defaultFunctionCodes = this.getDefaultFunctionCodes(); } - trackByKey(_: number, keyControl: AbstractControl): AbstractControl { - return keyControl; + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + trackByControlId(_: number, keyControl: AbstractControl): string { + return keyControl.value.id; } addKey(): void { @@ -88,7 +103,7 @@ export class ModbusDataKeysPanelComponent implements OnInit { tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], type: [ModbusDataType.BYTES, [Validators.required]], - address: [0, [Validators.required]], + address: [null, [Validators.required]], objectsCount: [1, [Validators.required]], functionCode: [this.getDefaultFunctionCodes()[0]], id: [{value: generateSecret(5), disabled: true}], @@ -107,7 +122,7 @@ export class ModbusDataKeysPanelComponent implements OnInit { } cancel(): void { - this.popover?.hide(); + this.popover.hide(); } applyKeysData(): void { @@ -116,32 +131,38 @@ export class ModbusDataKeysPanelComponent implements OnInit { private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray { const keysControlGroups: Array = []; + if (values) { - values.forEach(keyData => { - const { tag, value, type, address, objectsCount, functionCode } = keyData; - const dataKeyFormGroup = this.fb.group({ - tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - value: [{value, disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - type: [type, [Validators.required]], - address: [address, [Validators.required]], - objectsCount: [objectsCount, [Validators.required]], - functionCode: [functionCode, []], - id: [{value: generateSecret(5), disabled: true}], - }); + values.forEach(value => { + const dataKeyFormGroup = this.createDataKeyFormGroup(value); this.observeKeyDataType(dataKeyFormGroup); - this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(type)); + this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type)); keysControlGroups.push(dataKeyFormGroup); }); } + return this.fb.array(keysControlGroups); } + private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup { + const { tag, value, type, address, objectsCount, functionCode } = modbusValue; + + return this.fb.group({ + tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + value: [{ value, disabled: !this.isMaster }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + type: [type, [Validators.required]], + address: [address, [Validators.required]], + objectsCount: [objectsCount, [Validators.required]], + functionCode: [functionCode, [Validators.required]], + id: [{ value: generateSecret(5), disabled: true }], + }); + } + private observeKeyDataType(keyFormGroup: FormGroup): void { - keyFormGroup.get('type').valueChanges.subscribe(dataType => { - const objectsCountControl = keyFormGroup.get('objectsCount'); + keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { if (!this.editableDataTypes.includes(dataType)) { - objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]); + keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); } this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType)); }); @@ -149,20 +170,21 @@ export class ModbusDataKeysPanelComponent implements OnInit { private getFunctionCodes(dataType: ModbusDataType): number[] { if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { - if (dataType === ModbusDataType.STRING) { - return this.stringAttrUpdatesWriteFunctionCodes; - } - return this.defaultWriteFunctionCodes; + return dataType === ModbusDataType.STRING + ? this.stringAttrUpdatesWriteFunctionCodes + : this.defaultWriteFunctionCodes; } + const functionCodes = [...this.defaultReadFunctionCodes]; if (dataType === ModbusDataType.BITS) { const bitsFunctionCodes = [1, 2]; - bitsFunctionCodes.forEach(code => functionCodes.push(code)); + functionCodes.push(...bitsFunctionCodes); functionCodes.sort(); } if (this.keysType === ModbusValueKey.RPC_REQUESTS) { - this.defaultWriteFunctionCodes.forEach(code => functionCodes.push(code)); + functionCodes.push(...this.defaultWriteFunctionCodes); } + return functionCodes; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html index 2e92d80c9f..e16aa0c51f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -60,18 +60,18 @@
- + {{ 'gateway.name' | translate }} - + {{ mapping['name'] }} - + {{ 'gateway.client-communication-type' | translate }} - + {{ ModbusProtocolLabelsMap.get(mapping['type']) }} @@ -111,7 +111,7 @@ - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss index 82cead07a4..e9a5d3ebcd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss @@ -19,19 +19,15 @@ width: 100%; height: 100%; display: block; + .tb-master-table { + .tb-master-table-content { width: 100%; height: 100%; background: #fff; overflow: hidden; - &.tb-outlined-border { - box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); - border: solid 1px #e0e0e0; - border-radius: 4px; - } - .mat-toolbar-tools{ min-height: auto; } @@ -49,25 +45,17 @@ .table-container { overflow: auto; + .mat-mdc-table { table-layout: fixed; min-width: 450px; .table-value-column { padding: 0 12px; - width: 23%; - - &.request-column { - width: 38%; - } + width: 38%; } } } - - .ellipsis { - overflow: hidden; - text-overflow: ellipsis; - } } } @@ -91,6 +79,7 @@ :host ::ng-deep { mat-cell.tb-value-cell { cursor: pointer; + .mat-icon { height: 24px; width: 24px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 10b1e5bbbe..7ca5c8a7fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -17,6 +17,7 @@ import { AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, forwardRef, @@ -76,15 +77,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat textSearchMode = false; dataSource: SlavesDatasource; - hidePageSize = false; - activeValue = false; - dirtyValue = false; masterFormGroup: UntypedFormGroup; textSearch = this.fb.control('', {nonNullable: true}); readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; - private onChange: (value: string) => void = () => {}; + private onChange: (value: ModbusMasterConfig) => void = () => {}; private onTouched: () => void = () => {}; private destroy$ = new Subject(); @@ -93,10 +91,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat public translate: TranslateService, public dialog: MatDialog, private dialogService: DialogService, - private fb: FormBuilder + private fb: FormBuilder, + private cdr: ChangeDetectorRef, ) { - this.masterFormGroup = this.fb.group({ slaves: this.fb.array([])}); - this.dirtyValue = !this.activeValue; + this.masterFormGroup = this.fb.group({ slaves: this.fb.array([]) }); this.dataSource = new SlavesDatasource(); } @@ -124,13 +122,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat debounceTime(150), distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), takeUntil(this.destroy$) - ).subscribe((text) => { - const searchText = text.trim(); - this.updateTableData(this.slaves.value, searchText.trim()); - }); + ).subscribe(text => this.updateTableData(this.slaves.value, text.trim())); } - registerOnChange(fn: (value: string) => void): void { + registerOnChange(fn: (value: ModbusMasterConfig) => void): void { this.onChange = fn; } @@ -151,10 +146,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat enterFilterMode(): void { this.textSearchMode = true; - setTimeout(() => { - this.searchInputField.nativeElement.focus(); - this.searchInputField.nativeElement.setSelectionRange(0, 0); - }, 10); + this.cdr.detectChanges(); + const searchInput = this.searchInputField.nativeElement; + searchInput.focus(); + searchInput.setSelectionRange(0, 0); } exitFilterMode(): void { @@ -167,19 +162,20 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat if ($event) { $event.stopPropagation(); } - const value = isDefinedAndNotNull(index) ? this.slaves.at(index).value : {}; + const withIndex = isDefinedAndNotNull(index); + const value = withIndex ? this.slaves.at(index).value : {}; this.dialog.open(ModbusSlaveDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, - buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' + buttonTitle: withIndex ? 'action.add' : 'action.apply' } }).afterClosed() .pipe(take(1), takeUntil(this.destroy$)) .subscribe(res => { if (res) { - if (isDefinedAndNotNull(index)) { + if (withIndex) { this.slaves.at(index).patchValue(res); } else { this.slaves.push(this.fb.control(res)); @@ -199,7 +195,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat this.translate.instant('action.no'), this.translate.instant('action.yes'), true - ).subscribe((result) => { + ).pipe(take(1), takeUntil(this.destroy$)).subscribe((result) => { if (result) { this.slaves.removeAt(index); this.masterFormGroup.markAsDirty(); @@ -208,15 +204,14 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat } private updateTableData(data: SlaveConfig[], textSearch?: string): void { - let tableValue = data; if (textSearch) { - tableValue = tableValue.filter(value => - Object.values(value).some(val => - val.toString().toLowerCase().includes(textSearch.toLowerCase()) + data = data.filter(item => + Object.values(item).some(value => + value.toString().toLowerCase().includes(textSearch.toLowerCase()) ) ); } - this.dataSource.loadData(tableValue); + this.dataSource.loadData(data); } private pushDataAsFormArrays(slaves: SlaveConfig[]): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts index 75328d1264..bc40727b55 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts @@ -85,7 +85,7 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - reqclicert: [{value: false, disabled: true}, []], + reqclicert: [{value: false, disabled: true}], }); this.observeValueChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 25a07ccfa5..56fc579361 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -86,11 +86,8 @@ warning @@ -188,7 +185,7 @@
gateway.advanced-connection-settings
-
+
gateway.byte-order
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss index f3b39e3508..0c2f9240be 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss @@ -16,11 +16,6 @@ $server-config-header-height: 132px; :host { - .nested-expansion-header { - ::ng-deep .mat-content { - height: 100%; - } - } .slave-content { height: calc(100% - #{$server-config-header-height}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 189ca75762..d44272734d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -18,9 +18,9 @@ import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angu import { ControlValueAccessor, FormBuilder, + FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, UntypedFormGroup, ValidationErrors, Validator, @@ -78,7 +78,7 @@ import { isEqual } from '@core/utils'; export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { slaveConfigFormGroup: UntypedFormGroup; - showSecurityControl: UntypedFormControl; + showSecurityControl: FormControl; ModbusProtocolLabelsMap = ModbusProtocolLabelsMap; ModbusMethodLabelsMap = ModbusMethodLabelsMap; portLimits = PortLimits; @@ -89,8 +89,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat readonly modbusOrderType = Object.values(ModbusOrderType); readonly ModbusProtocolType = ModbusProtocolType; readonly modbusBaudrates = ModbusBaudrates; - readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; - readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; + + private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; + private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; private onChange: (value: SlaveConfig) => void; private onTouched: () => void; @@ -100,18 +101,18 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat constructor(private fb: FormBuilder) { this.showSecurityControl = this.fb.control(false); this.slaveConfigFormGroup = this.fb.group({ - type: [ModbusProtocolType.TCP, []], + type: [ModbusProtocolType.TCP], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - method: [ModbusMethodType.RTU, []], + method: [ModbusMethodType.SOCKET], unitId: [0, [Validators.required]], - baudrate: [this.ModbusProtocolType[0], []], + baudrate: [this.modbusBaudrates[0]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - pollPeriod: [5000, []], - sendDataToThingsBoard: [false, []], - byteOrder:[ModbusOrderType.BIG, []], + pollPeriod: [5000], + sendDataToThingsBoard: [false], + byteOrder:[ModbusOrderType.BIG], security: [], identity: this.fb.group({ vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], @@ -123,22 +124,16 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat values: [], }); - this.slaveConfigFormGroup.valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value: SlaveConfig) => { - if (value.type === ModbusProtocolType.Serial) { - value.port = value.serialPort; - delete value.serialPort; - } - this.onChange(value); - this.onTouched(); - }); - + this.observeValueChanges(); this.observeTypeChange(); this.observeFormEnable(); this.observeShowSecurity(); } + get isSlaveEnabled(): boolean { + return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value; + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -154,7 +149,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat validate(): ValidationErrors | null { return this.slaveConfigFormGroup.valid ? null : { - serverConfigFormGroup: { valid: false } + slaveConfigFormGroup: { valid: false } }; } @@ -164,18 +159,29 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.updateFormEnableState(slaveConfig.sendDataToThingsBoard); } - private observeTypeChange(): void { - this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => { - this.updateFormEnableState(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value); + private observeValueChanges(): void { + this.slaveConfigFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value: SlaveConfig) => { + if (value.type === ModbusProtocolType.Serial) { + value.port = value.serialPort; + delete value.serialPort; + } + this.onChange(value); + this.onTouched(); }); } + private observeTypeChange(): void { + this.slaveConfigFormGroup.get('type').valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.updateFormEnableState(this.isSlaveEnabled)); + } + private observeFormEnable(): void { this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges - .pipe(startWith(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value), takeUntil(this.destroy$)) - .subscribe(value => { - this.updateFormEnableState(value); - }); + .pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$)) + .subscribe(value => this.updateFormEnableState(value)); } private updateFormEnableState(enabled: boolean): void { @@ -192,11 +198,13 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } private observeShowSecurity(): void { - this.showSecurityControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => this.updateSecurityEnable(value)); + this.showSecurityControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => this.updateSecurityEnable(value)); } - private updateSecurityEnable(isEnabled: boolean): void { - if (isEnabled && this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) { + private updateSecurityEnable(securityEnabled: boolean): void { + if (securityEnabled && this.isSlaveEnabled) { this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); } else { this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); @@ -204,63 +212,58 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } private updateEnablingByProtocol(type: ModbusProtocolType): void { - if (type === ModbusProtocolType.Serial) { - if (this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) { - this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); - } - this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); - } else { - this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); - if (this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) { - this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); - } + const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; + const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; + + if (this.isSlaveEnabled) { + enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); } - }; + + disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false })); + } private updateSlaveConfig(slaveConfig: ModbusSlave): void { const { + type = ModbusProtocolType.TCP, + method = ModbusMethodType.RTU, + unitId = 0, + deviceName = '', + deviceType = '', + pollPeriod = 5000, + sendDataToThingsBoard = false, + byteOrder = ModbusOrderType.BIG, + security = {}, + identity = { + vendorName: '', + productCode: '', + vendorUrl: '', + productName: '', + modelName: '', + }, + values = {} as ModbusRegisterValues, + baudrate = this.modbusBaudrates[0], + host = '', + port = null, + } = slaveConfig; + + const slaveState: ModbusSlave = { type, method, unitId, deviceName, deviceType, pollPeriod, - sendDataToThingsBoard, + sendDataToThingsBoard: !!sendDataToThingsBoard, byteOrder, security, identity, values, baudrate, - host, - port, - } = slaveConfig; - let slaveState: ModbusSlave = { - host: host ?? '', - type: type ?? ModbusProtocolType.TCP, - method: method ?? ModbusMethodType.RTU, - unitId: unitId ?? 0, - deviceName: deviceName ?? '', - deviceType: deviceType ?? '', - pollPeriod: pollPeriod ?? 5000, - sendDataToThingsBoard: !!sendDataToThingsBoard, - byteOrder: byteOrder ?? ModbusOrderType.BIG, - security: security ?? {}, - identity: identity ?? { - vendorName: '', - productCode: '', - vendorUrl: '', - productName: '', - modelName: '', - }, - values: values ?? {} as ModbusRegisterValues, - port: port ?? null, - baudrate: baudrate ?? this.modbusBaudrates[0], + host: type === ModbusProtocolType.Serial ? '' : host, + port: type === ModbusProtocolType.Serial ? null : port, + serialPort: (type === ModbusProtocolType.Serial ? port : '') as string, }; - if (slaveConfig.type === ModbusProtocolType.Serial) { - slaveState = { ...slaveState, serialPort: port, host: '', port: null } as ModbusSlave; - } else { - slaveState = { ...slaveState, serialPort: '' } as ModbusSlave; - } - this.slaveConfigFormGroup.setValue(slaveState, {emitEvent: false}); + + this.slaveConfigFormGroup.setValue(slaveState, { emitEvent: false }); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 60ef25f875..31d2202fcb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -105,10 +105,8 @@ warning diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 8c6fb87e11..aae327ee6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -17,14 +17,13 @@ import { ChangeDetectionStrategy, Component, forwardRef, Inject, OnDestroy } from '@angular/core'; import { FormBuilder, + FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormControl, UntypedFormGroup, Validators, } from '@angular/forms'; import { - MappingInfo, ModbusBaudrates, ModbusByteSizes, ModbusMethodLabelsMap, @@ -35,6 +34,7 @@ import { ModbusProtocolLabelsMap, ModbusProtocolType, ModbusSerialMethodType, + ModbusSlaveInfo, noLeadTrailSpacesRegex, PortLimits, SlaveConfig, @@ -80,15 +80,15 @@ import { isEqual } from '@core/utils'; styles: [` :host { .slaves-config-container { - width: 900px; - } + width: 80vw; + max-width: 900px; } } `], }) export class ModbusSlaveDialogComponent extends DialogComponent implements OnDestroy { slaveConfigFormGroup: UntypedFormGroup; - showSecurityControl: UntypedFormControl; + showSecurityControl: FormControl; portLimits = PortLimits; readonly modbusProtocolTypes = Object.values(ModbusProtocolType); @@ -104,8 +104,9 @@ export class ModbusSlaveDialogComponent extends DialogComponent(); @@ -113,7 +114,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent, protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: MappingInfo, + @Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo, public dialogRef: MatDialogRef, ) { super(store, router, dialogRef); @@ -121,42 +122,38 @@ export class ModbusSlaveDialogComponent extends DialogComponent { - this.updateControlsEnabling(type); - }); + this.slaveConfigFormGroup.get('type').valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(type => this.updateControlsEnabling(type)); } private updateControlsEnabling(type: ModbusProtocolType): void { - if (type === ModbusProtocolType.Serial) { - this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); - this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); - } else { - this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false})); - this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false})); - } - }; + const [enableKeys, disableKeys] = type === ModbusProtocolType.Serial + ? [this.serialSpecificControlKeys, this.tcpUdpSpecificControlKeys] + : [this.tcpUdpSpecificControlKeys, this.serialSpecificControlKeys]; + + enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); + disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false })); + + this.updateSecurityEnabling(this.showSecurityControl.value); + } private observeShowSecurity(): void { - this.showSecurityControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => { - if (value) { - this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); - } else { - this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); - } - }); + this.showSecurityControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(value => this.updateSecurityEnabling(value)); + } + + private updateSecurityEnabling(isEnabled: boolean): void { + if (isEnabled) { + this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); + } else { + this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts index 0e57d06d85..e111fee7b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts @@ -42,6 +42,7 @@ import { ModbusRegisterTranslationsMap, ModbusRegisterType, ModbusRegisterValues, + ModbusValue, ModbusValueKey, ModbusValues, ModbusValuesState, @@ -103,7 +104,7 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O ModbusValueKey = ModbusValueKey; valuesFormGroup: FormGroup; - private onChange: (value: string) => void; + private onChange: (value: ModbusValuesState) => void; private onTouched: () => void; private destroy$ = new Subject(); @@ -125,7 +126,7 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O this.destroy$.complete(); } - registerOnChange(fn: (value: string) => void): void { + registerOnChange(fn: (value: ModbusValuesState) => void): void { this.onChange = fn; } @@ -135,15 +136,15 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O writeValue(values: ModbusValuesState): void { if (this.singleMode) { - this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), {emitEvent: false}); + this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), { emitEvent: false }); } else { - const registers = values as ModbusRegisterValues; + const { holding_registers, coils_initializer, input_registers, discrete_inputs } = values as ModbusRegisterValues; this.valuesFormGroup.setValue({ - holding_registers: this.getSingleRegisterState(registers.holding_registers), - coils_initializer: this.getSingleRegisterState(registers.coils_initializer), - input_registers: this.getSingleRegisterState(registers.input_registers), - discrete_inputs: this.getSingleRegisterState(registers.discrete_inputs), - }, {emitEvent: false}); + holding_registers: this.getSingleRegisterState(holding_registers), + coils_initializer: this.getSingleRegisterState(coils_initializer), + input_registers: this.getSingleRegisterState(input_registers), + discrete_inputs: this.getSingleRegisterState(discrete_inputs), + }, { emitEvent: false }); } this.cdr.markForCheck(); } @@ -159,69 +160,63 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O this.cdr.markForCheck(); } - getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType) { + getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType): FormGroup { return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value; } manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void { - if ($event) { - $event.stopPropagation(); - } + $event.stopPropagation(); const trigger = matButton._elementRef.nativeElement; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); - } else { - const group = this.valuesFormGroup; - - const keysControl = register ? group.get(register).get(keysType) : group.get(keysType); - const ctx = { - values: keysControl.value, - isMaster: !this.singleMode, - keysType, - panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType), - addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType), - deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType), - noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType) - }; - const dataKeysPanelPopover = this.popoverService.displayPopover( - trigger, - this.renderer, - this.viewContainerRef, - ModbusDataKeysPanelComponent, - 'leftBottom', - false, - null, - ctx, - {}, - {}, - {}, - true - ); - dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; - dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => { - dataKeysPanelPopover.hide(); - keysControl.patchValue(keysData); - keysControl.markAsDirty(); - this.cdr.markForCheck(); - }); + return; } + + const keysControl = this.getValueGroup(keysType, register); + const ctx = { + values: keysControl.value, + isMaster: !this.singleMode, + keysType, + panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType), + addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType), + deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType), + noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType) + }; + const dataKeysPanelPopover = this.popoverService.displayPopover( + trigger, + this.renderer, + this.viewContainerRef, + ModbusDataKeysPanelComponent, + 'leftBottom', + false, + null, + ctx, + {}, + {}, + {}, + true + ); + dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; + dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData: ModbusValue[]) => { + dataKeysPanelPopover.hide(); + keysControl.patchValue(keysData); + keysControl.markAsDirty(); + this.cdr.markForCheck(); + }); } private initializeValuesFormGroup(): void { + const getValuesFormGroup = () => this.fb.group(this.modbusValueKeys.reduce((acc, key) => { + acc[key] = this.fb.control([[], []]); + return acc; + }, {})); + if (this.singleMode) { - this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => { - acc[key] = this.fb.control([[], []]); - return acc; - }, {})); + this.valuesFormGroup = getValuesFormGroup(); } else { this.valuesFormGroup = this.fb.group( this.modbusRegisterTypes.reduce((registersAcc, register) => { - - registersAcc[register] = this.fb.group(this.modbusValueKeys.reduce((acc, key) => { - acc[key] = this.fb.control([[], []]); - return acc; - }, {})); - + registersAcc[register] = getValuesFormGroup(); return registersAcc; }, {}) ); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 2db6d9d2fe..3bdb67069a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -485,6 +485,11 @@ export interface MappingInfo { buttonTitle: string; } +export interface ModbusSlaveInfo { + value: SlaveConfig; + buttonTitle: string; +} + export enum ConnectorConfigurationModes { BASIC = 'basic', ADVANCED = 'advanced' @@ -852,24 +857,6 @@ export enum ModbusObjectCountByDataType { '64float' = 4, } -export enum ModbusValueField { - Tag = 'tag', - Type = 'type', - ObjectsCount = 'objectsCount', - Address = 'address', - Value = 'value', -} - -export const ModbusFieldsTranslationsMap = new Map( - [ - [ModbusValueField.Tag, 'gateway.tag'], - [ModbusValueField.Type, 'gateway.type'], - [ModbusValueField.ObjectsCount, 'gateway.objects_count'], - [ModbusValueField.Address, 'gateway.address'], - [ModbusValueField.Value, 'gateway.value'] - ] -); - export enum ModbusValueKey { ATTRIBUTES = 'attributes', TIMESERIES = 'timeseries', @@ -946,7 +933,7 @@ export interface SlaveConfig { pollPeriod: number; unitId: number; deviceName: string; - deviceType?: string; + deviceType: string; sendDataOnlyOnChange: boolean; connectAttemptTimeMs: number; connectAttemptCount: number; @@ -959,7 +946,7 @@ export interface SlaveConfig { baudrate?: number; stopbits?: number; bytesize?: number; - parity?: string; + parity?: ModbusParity; strict?: boolean; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 1e4093c22c..504f98420e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -208,25 +208,26 @@ import { LabelCardWidgetComponent, LabelValueCardWidgetComponent, UnreadNotificationWidgetComponent, - NotificationTypeFilterPanelComponent], - imports: [ - CommonModule, - SharedModule, - RpcWidgetsModule, - HomePageWidgetsModule, - SharedHomeComponentsModule, - RestConnectorSecurityComponent, - GatewayHelpLinkPipe, - BrokerConfigControlComponent, - WorkersConfigControlComponent, - OpcServerConfigComponent, - MqttBasicConfigComponent, - MappingTableComponent, - OpcUaBasicConfigComponent, - KeyValueIsNotEmptyPipe, - ModbusBasicConfigComponent, - EllipsisChipListDirective, - ], + NotificationTypeFilterPanelComponent + ], + imports: [ + CommonModule, + SharedModule, + RpcWidgetsModule, + HomePageWidgetsModule, + SharedHomeComponentsModule, + RestConnectorSecurityComponent, + GatewayHelpLinkPipe, + BrokerConfigControlComponent, + WorkersConfigControlComponent, + OpcServerConfigComponent, + MqttBasicConfigComponent, + MappingTableComponent, + OpcUaBasicConfigComponent, + KeyValueIsNotEmptyPipe, + ModbusBasicConfigComponent, + EllipsisChipListDirective, + ], exports: [ EntitiesTableWidgetComponent, AlarmsTableWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts b/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts index 3981499a26..fcf5766c05 100644 --- a/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts +++ b/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts @@ -30,12 +30,12 @@ export class GatewayPortTooltipPipe implements PipeTransform { transform(portControl: AbstractControl): string { if (portControl.hasError('required')) { return this.translate.instant('gateway.port-required'); - } else if ( - portControl.hasError('min') || - portControl.hasError('max') - ) { - return this.translate.instant('gateway.port-limits-error', - {min: PortLimits.MIN, max: PortLimits.MAX}); + } + if (portControl.hasError('min') || portControl.hasError('max')) { + return this.translate.instant('gateway.port-limits-error', { + min: PortLimits.MIN, + max: PortLimits.MAX, + }); } return ''; } diff --git a/ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts index bb2847aeee..c320a2f78a 100644 --- a/ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts +++ b/ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts @@ -61,8 +61,11 @@ export class EllipsisChipListDirective implements OnDestroy { ).subscribe(() => { this.adjustChips(); }); + this.observeIntersection(); + } - this.intersectionObserver = new IntersectionObserver((entries) => { + private observeIntersection(): void { + this.intersectionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { this.adjustChips(); 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 fb8f695619..967e6a0122 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2756,6 +2756,7 @@ }, "gateway": { "address": "Address", + "address-required": "Address required", "add-entry": "Add configuration", "add-attribute": "Add attribute", "add-attribute-update": "Add attribute update", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json index fee4e745d6..c84a2278b9 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json @@ -16,6 +16,7 @@ "pollPeriod": 5000, "unitId": 1, "deviceName": "Temp Sensor", + "deviceType": "default", "sendDataOnlyOnChange": true, "connectAttemptTimeMs": 5000, "connectAttemptCount": 5, From 0b8c122f0ac079a703d827d7fee02f8664896910 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 25 Jul 2024 12:11:19 +0300 Subject: [PATCH 108/136] refactoring --- .../modbus-data-keys-panel.component.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index 5285422bd6..02d2d1491d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -76,7 +76,12 @@
- {{ ModbusFunctionCodeTranslationsMap.get(code) | translate }} + + {{ ModbusFunctionCodeTranslationsMap.get(code) | translate }} +
From 7624517a5e7673ef056712d0d18b7c74c9852985 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 25 Jul 2024 12:17:08 +0300 Subject: [PATCH 109/136] refactoring --- .../modbus-slave-dialog.component.ts | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index aae327ee6f..44f093e06e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -120,6 +120,39 @@ export class ModbusSlaveDialogComponent extends DialogComponent Date: Thu, 25 Jul 2024 12:44:57 +0300 Subject: [PATCH 110/136] minor fix --- .../modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 44f093e06e..0aa4174c25 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -143,7 +143,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent Date: Thu, 25 Jul 2024 12:01:42 +0200 Subject: [PATCH 111/136] fixed tests --- .../controller/EntityRelationController.java | 8 +++--- .../DefaultTbEntityRelationService.java | 16 ++++++----- .../relation/TbEntityRelationService.java | 4 +-- .../EntityRelationControllerTest.java | 14 +++++----- .../server/edge/RelationEdgeTest.java | 11 ++++---- .../service/sync/vc/VersionControlTest.java | 3 +-- .../server/dao/relation/RelationService.java | 2 +- .../common/data/relation/EntityRelation.java | 19 ++++++++----- .../dao/relation/BaseRelationService.java | 27 ++++++++++--------- 9 files changed, 56 insertions(+), 48 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 0cd31a729b..0a471e3a2d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -81,7 +81,7 @@ public class EntityRelationController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relation", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) + public EntityRelation saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true) @RequestBody EntityRelation relation) throws ThingsboardException { checkNotNull(relation); checkCanCreateRelation(relation.getFrom()); @@ -90,7 +90,7 @@ public class EntityRelationController extends BaseController { relation.setTypeGroup(RelationTypeGroup.COMMON); } - tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); + return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } @ApiOperation(value = "Delete Relation (deleteRelation)", @@ -98,7 +98,7 @@ public class EntityRelationController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) @ResponseStatus(value = HttpStatus.OK) - public void deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + public EntityRelation deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, @@ -116,7 +116,7 @@ public class EntityRelationController extends BaseController { RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON); EntityRelation relation = new EntityRelation(fromId, toId, strRelationType, relationTypeGroup); - tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); + return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } @ApiOperation(value = "Delete common relations (deleteCommonRelations)", diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java index 724c077e62..e9cfc109f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java @@ -39,12 +39,13 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl private final RelationService relationService; @Override - public void save(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { + public EntityRelation save(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { ActionType actionType = ActionType.RELATION_ADD_OR_UPDATE; try { - relationService.saveRelation(tenantId, relation); + var savedRelation = relationService.saveRelation(tenantId, relation); logEntityActionService.logEntityRelationAction(tenantId, customerId, - relation, user, actionType, null, relation); + savedRelation, user, actionType, null, savedRelation); + return savedRelation; } catch (Exception e) { logEntityActionService.logEntityRelationAction(tenantId, customerId, relation, user, actionType, e, relation); @@ -53,14 +54,15 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl } @Override - public void delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { + public EntityRelation delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException { ActionType actionType = ActionType.RELATION_DELETED; try { - boolean found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); - if (!found) { + var found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); + if (found == null) { throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); } - logEntityActionService.logEntityRelationAction(tenantId, customerId, relation, user, actionType, null, relation); + logEntityActionService.logEntityRelationAction(tenantId, customerId, found, user, actionType, null, found); + return found; } catch (Exception e) { logEntityActionService.logEntityRelationAction(tenantId, customerId, relation, user, actionType, e, relation); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java index 7b732ff9ee..0ef75d0354 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java @@ -24,9 +24,9 @@ import org.thingsboard.server.common.data.relation.EntityRelation; public interface TbEntityRelationService { - void save(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; + EntityRelation save(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; - void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; + EntityRelation delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException; diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java index 48e0fb6196..f2464f3e47 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java @@ -103,7 +103,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - doPost("/api/relation", relation).andExpect(status().isOk()); + relation = doPost("/api/relation", relation, EntityRelation.class); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", mainDevice.getUuidId(), EntityType.DEVICE, @@ -315,7 +315,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { Device device = buildSimpleDevice("Test device 1"); EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS"); - doPost("/api/relation", relation).andExpect(status().isOk()); + relation = doPost("/api/relation", relation, EntityRelation.class); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", mainDevice.getUuidId(), EntityType.DEVICE, @@ -329,11 +329,11 @@ public class EntityRelationControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - doDelete(url).andExpect(status().isOk()); + var deletedRelation = doDelete(url, EntityRelation.class); - testNotifyEntityAllOneTimeRelation(foundRelation, + testNotifyEntityAllOneTimeRelation(deletedRelation, savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), - ActionType.RELATION_DELETED, foundRelation); + ActionType.RELATION_DELETED, deletedRelation); doGet(url).andExpect(status().is4xxClientError()); } @@ -523,7 +523,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { @Test public void testCreateRelationFromTenantToDevice() throws Exception { EntityRelation relation = new EntityRelation(tenantAdmin.getTenantId(), mainDevice.getId(), "CONTAINS"); - doPost("/api/relation", relation).andExpect(status().isOk()); + relation = doPost("/api/relation", relation, EntityRelation.class); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", tenantAdmin.getTenantId(), EntityType.TENANT, @@ -539,7 +539,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest { @Test public void testCreateRelationFromDeviceToTenant() throws Exception { EntityRelation relation = new EntityRelation(mainDevice.getId(), tenantAdmin.getTenantId(), "CONTAINS"); - doPost("/api/relation", relation).andExpect(status().isOk()); + relation = doPost("/api/relation", relation, EntityRelation.class); String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", mainDevice.getUuidId(), EntityType.DEVICE, diff --git a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java index d89b601866..baeafd3ad2 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java @@ -48,7 +48,7 @@ public class RelationEdgeTest extends AbstractEdgeTest { relation.setTo(asset.getId()); relation.setTypeGroup(RelationTypeGroup.COMMON); edgeImitator.expectMessageAmount(1); - doPost("/api/relation", relation); + relation = doPost("/api/relation", relation, EntityRelation.class); Assert.assertTrue(edgeImitator.waitForMessages()); AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); @@ -60,21 +60,20 @@ public class RelationEdgeTest extends AbstractEdgeTest { // delete relation edgeImitator.expectMessageAmount(1); - doDelete("/api/relation?" + + var deletedRelation = doDelete("/api/relation?" + "fromId=" + relation.getFrom().getId().toString() + "&fromType=" + relation.getFrom().getEntityType().name() + "&relationType=" + relation.getType() + "&relationTypeGroup=" + relation.getTypeGroup().name() + "&toId=" + relation.getTo().getId().toString() + - "&toType=" + relation.getTo().getEntityType().name()) - .andExpect(status().isOk()); + "&toType=" + relation.getTo().getEntityType().name(), EntityRelation.class); Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); relationUpdateMsg = (RelationUpdateMsg) latestMessage; entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true); Assert.assertNotNull(entityRelation); - Assert.assertEquals(relation, entityRelation); + Assert.assertEquals(deletedRelation, entityRelation); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); } @@ -119,7 +118,7 @@ public class RelationEdgeTest extends AbstractEdgeTest { deviceToAssetRelation.setTypeGroup(RelationTypeGroup.COMMON); edgeImitator.expectMessageAmount(1); - doPost("/api/relation", deviceToAssetRelation); + deviceToAssetRelation = doPost("/api/relation", deviceToAssetRelation, EntityRelation.class); Assert.assertTrue(edgeImitator.waitForMessages()); EntityRelation assetToTenantRelation = new EntityRelation(); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 8b8f89fabf..a55bcc849d 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -937,8 +937,7 @@ public class VersionControlTest extends AbstractControllerTest { relation.setType(EntityRelation.MANAGES_TYPE); relation.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b"))); relation.setTypeGroup(RelationTypeGroup.COMMON); - doPost("/api/relation", relation).andExpect(status().isOk()); - return relation; + return doPost("/api/relation", relation, EntityRelation.class); } protected void checkImportedRuleChainData(RuleChain initialRuleChain, RuleChainMetaData initialMetaData, RuleChain importedRuleChain, RuleChainMetaData importedMetaData) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index 013b27275e..fbc79719e0 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -47,7 +47,7 @@ public interface RelationService { ListenableFuture deleteRelationAsync(TenantId tenantId, EntityRelation relation); - boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); + EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); ListenableFuture deleteRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 3c1bc83932..62b5caafd3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -18,7 +18,10 @@ package org.thingsboard.server.common.data.relation; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.HasVersion; @@ -29,7 +32,8 @@ import java.io.Serializable; @Slf4j @Schema -@Data +@EqualsAndHashCode(exclude = "additionalInfoBytes") +@ToString(exclude = {"additionalInfoBytes"}) public class EntityRelation implements HasVersion, Serializable { private static final long serialVersionUID = 2807343040519543363L; @@ -38,11 +42,17 @@ public class EntityRelation implements HasVersion, Serializable { public static final String CONTAINS_TYPE = "Contains"; public static final String MANAGES_TYPE = "Manages"; + @Setter private EntityId from; + @Setter private EntityId to; + @Setter @Length(fieldName = "type") private String type; + @Setter private RelationTypeGroup typeGroup; + @Getter + @Setter private Long version; private transient JsonNode additionalInfo; @JsonIgnore @@ -97,11 +107,6 @@ public class EntityRelation implements HasVersion, Serializable { return typeGroup; } - @Override - public Long getVersion() { - return version; - } - @Schema(description = "Additional parameters of the relation",implementation = com.fasterxml.jackson.databind.JsonNode.class) public JsonNode getAdditionalInfo() { return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index da383ae1bf..0ee34f2b9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -156,8 +156,8 @@ public class BaseRelationService implements RelationService { log.trace("Executing saveRelation [{}]", relation); validate(relation); var result = relationDao.saveRelation(tenantId, relation); - publishEvictEvent(EntityRelationEvent.from(relation)); - eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); + publishEvictEvent(EntityRelationEvent.from(result)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, result, ActionType.RELATION_ADD_OR_UPDATE)); return result; } @@ -167,10 +167,11 @@ public class BaseRelationService implements RelationService { for (EntityRelation relation : relations) { validate(relation); } + List savedRelations = new ArrayList<>(relations.size()); for (List partition : Lists.partition(relations, 1024)) { - relationDao.saveRelations(tenantId, partition); + savedRelations.addAll(relationDao.saveRelations(tenantId, partition)); } - for (EntityRelation relation : relations) { + for (EntityRelation relation : savedRelations) { publishEvictEvent(EntityRelationEvent.from(relation)); eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); } @@ -182,8 +183,10 @@ public class BaseRelationService implements RelationService { validate(relation); var future = relationDao.saveRelationAsync(tenantId, relation); return Futures.transform(future, savedRelation -> { - handleEvictEvent(EntityRelationEvent.from(relation)); - eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_ADD_OR_UPDATE)); + if (savedRelation != null) { + handleEvictEvent(EntityRelationEvent.from(savedRelation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, savedRelation, ActionType.RELATION_ADD_OR_UPDATE)); + } return savedRelation != null; }, MoreExecutors.directExecutor()); } @@ -194,8 +197,8 @@ public class BaseRelationService implements RelationService { validate(relation); var result = relationDao.deleteRelation(tenantId, relation); if (result != null) { - publishEvictEvent(EntityRelationEvent.from(relation)); - eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); + publishEvictEvent(EntityRelationEvent.from(result)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, result, ActionType.RELATION_DELETED)); } return result != null; } @@ -207,15 +210,15 @@ public class BaseRelationService implements RelationService { var future = relationDao.deleteRelationAsync(tenantId, relation); return Futures.transform(future, deletedRelation -> { if (deletedRelation != null) { - handleEvictEvent(EntityRelationEvent.from(relation)); - eventPublisher.publishEvent(new RelationActionEvent(tenantId, relation, ActionType.RELATION_DELETED)); + handleEvictEvent(EntityRelationEvent.from(deletedRelation)); + eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedRelation, ActionType.RELATION_DELETED)); } return deletedRelation != null; }, MoreExecutors.directExecutor()); } @Override - public boolean deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { + public EntityRelation deleteRelation(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); validate(from, to, relationType, typeGroup); var result = relationDao.deleteRelation(tenantId, from, to, relationType, typeGroup); @@ -223,7 +226,7 @@ public class BaseRelationService implements RelationService { publishEvictEvent(EntityRelationEvent.from(result)); eventPublisher.publishEvent(new RelationActionEvent(tenantId, result, ActionType.RELATION_DELETED)); } - return result != null; + return result; } @Override From 7fcb82cea950887e6401e81784482c1e4fa0ca1f Mon Sep 17 00:00:00 2001 From: nick Date: Thu, 25 Jul 2024 13:22:21 +0300 Subject: [PATCH 112/136] tbel: added new version tbel and new test hexToBytes --- .../thingsboard/script/api/tbel/TbUtils.java | 5 +++++ .../script/api/tbel/TbUtilsTest.java | 20 +++++++++++++++++++ pom.xml | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 9286ffec46..d8aecb2f09 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -598,6 +598,11 @@ public class TbUtils { if (len % 2 > 0) { throw new IllegalArgumentException("Hex string must be even-length."); } + int radix = isHexadecimal(value); + if (radix != HEX_RADIX) { + throw new NumberFormatException("Value: \"" + value + "\" is not numeric or hexDecimal format!"); + } + ExecutionArrayList data = new ExecutionArrayList<>(ctx); for (int i = 0; i < hex.length(); i += 2) { // Extract two characters from the hex string diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 52bafe2fd2..a7292d4280 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -668,6 +668,26 @@ public class TbUtilsTest { Assertions.assertEquals(expectedBe, actualBe); } + @Test + public void hexToBytes_Test() { + String input = "0x01752B0367FA000500010488FFFFFFFFFFFFFFFF33"; + byte[] expected = {1, 117, 43, 3, 103, -6, 0, 5, 0, 1, 4, -120, -1, -1, -1, -1, -1, -1, -1, -1, 51}; + List actual = TbUtils.hexToBytes(ctx, input); + Assertions.assertEquals(toList(expected), actual); + try { + input = "0x01752B0367FA000500010488FFFFFFFFFFFFFFFF3"; + actual = TbUtils.hexToBytes(ctx, input); + } catch (IllegalArgumentException e) { + Assertions.assertTrue(e.getMessage().contains("Hex string must be even-length.")); + } + try { + input = "0x01752B0367KA000500010488FFFFFFFFFFFFFFFF33"; + actual = TbUtils.hexToBytes(ctx, input); + } catch (NumberFormatException e) { + Assertions.assertTrue(e.getMessage().contains("Value: \"" + input + "\" is not numeric or hexDecimal format!")); + } + } + @Test public void floatToHex_Test() { Float value = 123456789.00f; diff --git a/pom.xml b/pom.xml index 3364cc1eba..f2154720e2 100755 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 3.9.2 3.25.3 1.63.0 - 1.2.1 + 1.2.2 1.18.32 1.2.5 1.2.5 From d9c5b5efc4341b98d3fb291bce7a5981f7a827d4 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 25 Jul 2024 15:30:07 +0300 Subject: [PATCH 113/136] method type switch fix and data keys crash fix --- .../modbus-data-keys-panel.component.ts | 14 +++++++++++--- .../modbus-slave-config.component.ts | 16 +++++++++++++++- .../modbus-slave-dialog.component.ts | 16 +++++++++++++++- .../modbus-values/modbus-values.component.html | 16 ++++++++-------- .../modbus-values/modbus-values.component.ts | 4 +++- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index d0bfcf1e76..c68bd8086e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -105,7 +105,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { type: [ModbusDataType.BYTES, [Validators.required]], address: [null, [Validators.required]], objectsCount: [1, [Validators.required]], - functionCode: [this.getDefaultFunctionCodes()[0]], + functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]], id: [{value: generateSecret(5), disabled: true}], }); this.observeKeyDataType(dataKeyFormGroup); @@ -154,7 +154,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { type: [type, [Validators.required]], address: [address, [Validators.required]], objectsCount: [objectsCount, [Validators.required]], - functionCode: [functionCode, [Validators.required]], + functionCode: [{ value: functionCode, disabled: !this.withFunctionCode }, [Validators.required]], id: [{ value: generateSecret(5), disabled: true }], }); } @@ -164,10 +164,18 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { if (!this.editableDataTypes.includes(dataType)) { keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); } - this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType)); + this.updateFunctionCodes(keyFormGroup, dataType); }); } + private updateFunctionCodes(keyFormGroup: FormGroup, dataType: ModbusDataType): void { + const functionCodes = this.getFunctionCodes(dataType); + this.functionCodesMap.set(keyFormGroup.get('id').value, functionCodes); + if (!functionCodes.includes(keyFormGroup.get('functionCode').value)) { + keyFormGroup.get('functionCode').patchValue(functionCodes[0], {emitEvent: false}); + } + } + private getFunctionCodes(dataType: ModbusDataType): number[] { if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { return dataType === ModbusDataType.STRING diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index d44272734d..3ff9e929ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -175,7 +175,21 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat private observeTypeChange(): void { this.slaveConfigFormGroup.get('type').valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(() => this.updateFormEnableState(this.isSlaveEnabled)); + .subscribe(type => { + this.updateFormEnableState(this.isSlaveEnabled); + this.updateMethodType(type); + }); + } + + private updateMethodType(type: ModbusProtocolType): void { + if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) { + this.slaveConfigFormGroup.get('method').patchValue( + type === ModbusProtocolType.Serial + ? ModbusSerialMethodType.ASCII + : ModbusMethodType.SOCKET, + {emitEvent: false} + ); + } } private observeFormEnable(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 0aa4174c25..38a77ac587 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -201,7 +201,21 @@ export class ModbusSlaveDialogComponent extends DialogComponent this.updateControlsEnabling(type)); + .subscribe(type => { + this.updateControlsEnabling(type); + this.updateMethodType(type); + }); + } + + private updateMethodType(type: ModbusProtocolType): void { + if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) { + this.slaveConfigFormGroup.get('method').patchValue( + type === ModbusProtocolType.Serial + ? ModbusSerialMethodType.ASCII + : ModbusMethodType.SOCKET, + {emitEvent: false} + ); + } } private updateControlsEnabling(type: ModbusProtocolType): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html index 5e0b4d7649..a6b4382638 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html @@ -36,8 +36,8 @@
gateway.attributes
- - + + {{ attribute.tag }} @@ -57,8 +57,8 @@
gateway.timeseries
- - + + {{ telemetry.tag }} @@ -78,8 +78,8 @@
gateway.attribute-updates
- - + + {{ attributeUpdate.tag }} @@ -99,8 +99,8 @@
gateway.rpc-requests
- - + + {{ rpcRequest.tag }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts index e111fee7b1..caff181b6a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts @@ -161,7 +161,9 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O } getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType): FormGroup { - return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value; + return register + ? this.valuesFormGroup.get(register).get(valueKey) as FormGroup + : this.valuesFormGroup.get(valueKey) as FormGroup; } manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void { From 2ff3d0bafc883e4e650e2b0de4ed436c03ca3737 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 25 Jul 2024 15:58:48 +0300 Subject: [PATCH 114/136] server register values fix --- .../modbus-data-keys-panel/modbus-data-keys-panel.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index c68bd8086e..f1919e7a57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -84,8 +84,8 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { constructor(private fb: UntypedFormBuilder) {} ngOnInit(): void { - this.keysListFormArray = this.prepareKeysFormArray(this.values); this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES); + this.keysListFormArray = this.prepareKeysFormArray(this.values); this.defaultFunctionCodes = this.getDefaultFunctionCodes(); } From 2837edbbd1f6a90059164324c3a8f907c8a9746a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 25 Jul 2024 16:31:58 +0300 Subject: [PATCH 115/136] refactoring --- .../modbus-slave-config.component.html | 10 +++++----- .../modbus-slave-config.component.ts | 8 ++++++-- .../modbus-slave-dialog.component.html | 14 +++++++------- .../modbus-slave-dialog.component.ts | 6 +++++- .../metadata/connector-default-configs/modbus.json | 2 +- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index 56fc579361..d37c8cc9ee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -34,7 +34,7 @@
-
@@ -54,7 +54,7 @@
-
@@ -102,7 +102,7 @@
- {{ ModbusMethodLabelsMap.get(method) }} @@ -168,7 +168,7 @@
-
+
gateway.baudrate
@@ -196,7 +196,7 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 3ff9e929ed..05083a087d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -134,6 +134,10 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value; } + get protocolType(): ModbusProtocolType { + return this.slaveConfigFormGroup.get('type').value; + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); @@ -207,7 +211,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.showSecurityControl.disable({emitEvent: false}); this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false}); } - this.updateEnablingByProtocol(this.slaveConfigFormGroup.get('type').value); + this.updateEnablingByProtocol(this.protocolType); this.updateSecurityEnable(this.showSecurityControl.value); } @@ -218,7 +222,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } private updateSecurityEnable(securityEnabled: boolean): void { - if (securityEnabled && this.isSlaveEnabled) { + if (securityEnabled && this.isSlaveEnabled && this.protocolType !== ModbusProtocolType.Serial) { this.slaveConfigFormGroup.get('security').enable({emitEvent: false}); } else { this.slaveConfigFormGroup.get('security').disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 31d2202fcb..1e47479f03 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -53,7 +53,7 @@
-
@@ -73,7 +73,7 @@
-
@@ -122,14 +122,14 @@
- {{ ModbusMethodLabelsMap.get(method) }}
- +
gateway.baudrate
@@ -260,7 +260,7 @@
-
+
gateway.word-order
@@ -270,7 +270,7 @@
-
+
@@ -356,7 +356,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index 38a77ac587..cbfc7cc8c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -128,6 +128,10 @@ export class ModbusSlaveDialogComponent extends DialogComponent Date: Thu, 25 Jul 2024 19:30:59 +0300 Subject: [PATCH 116/136] Fixed Connector Form state after connector with basic config removal --- .../widget/lib/gateway/gateway-connectors.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 14a8c15c00..802ff6b1d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -397,7 +397,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private clearOutConnectorForm(): void { this.initialConnector = null; - this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false}); this.connectorForm.setValue({ mode: ConnectorConfigurationModes.BASIC, name: '', From 75c8a9ed5e2ea728e1e5c331bce18588230b3562 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 26 Jul 2024 12:15:28 +0300 Subject: [PATCH 117/136] Fixed Editing Resource title for JS module Resource --- .../home/pages/admin/resource/resources-library.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts index 54dfc9340e..cd2d45b817 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts @@ -103,9 +103,9 @@ export class ResourcesLibraryComponent extends EntityComponent impleme updateForm(entity: Resource) { if (this.isEdit) { this.entityForm.get('resourceType').disable({emitEvent: false}); + this.entityForm.get('data').disable({emitEvent: false}); if (entity.resourceType !== ResourceType.JS_MODULE) { this.entityForm.get('fileName').disable({emitEvent: false}); - this.entityForm.get('data').disable({emitEvent: false}); } } this.entityForm.patchValue({ From 6ef1d91832817668a163a3475b9d0ce7f6fc47d9 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 26 Jul 2024 12:56:23 +0300 Subject: [PATCH 118/136] Fixed Device Profile LwM2M Server information collapsed state view --- .../device/lwm2m/lwm2m-device-config-server.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html index d5665191ce..43ff1078af 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html @@ -22,7 +22,7 @@
{{ (serverFormGroup.get('bootstrapServerIs').value ? 'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}
-
+
{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }} {{ serverFormGroup.get('shortServerId').value }}
From 05558d291ad2386e279aa353084d3cf3cdc0bd0a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 26 Jul 2024 13:19:18 +0300 Subject: [PATCH 119/136] tb-datasource to shared --- .../mapping-table/mapping-table.component.ts | 2 +- .../modbus/modbus-master-table/modbus-master-table.component.ts | 2 +- .../components/table/table-datasource.abstract.ts | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename ui-ngx/src/app/{modules/home => shared}/components/table/table-datasource.abstract.ts (100%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index 292cb43b6e..29dd89c59f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts @@ -57,7 +57,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TbTableDatasource } from '@home/components/table/table-datasource.abstract'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; @Component({ selector: 'tb-mapping-table', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 7ca5c8a7fa..2cb66f6062 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -49,7 +49,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; -import { TbTableDatasource } from '@home/components/table/table-datasource.abstract'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; @Component({ selector: 'tb-modbus-master-table', diff --git a/ui-ngx/src/app/modules/home/components/table/table-datasource.abstract.ts b/ui-ngx/src/app/shared/components/table/table-datasource.abstract.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/table/table-datasource.abstract.ts rename to ui-ngx/src/app/shared/components/table/table-datasource.abstract.ts From 922847baf6fac4a00dbbf0d53ec8e4407e249973 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 26 Jul 2024 14:03:49 +0200 Subject: [PATCH 120/136] minor refactoring --- .../dao/model/sql/AttributeKvEntity.java | 18 ++++++----- .../server/dao/model/sql/RelationEntity.java | 8 +++-- .../server/dao/model/sql/VersionedEntity.java | 30 ------------------- 3 files changed, 15 insertions(+), 41 deletions(-) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/VersionedEntity.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java index d7bd23db4d..615932ddde 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AttributeKvEntity.java @@ -15,8 +15,12 @@ */ package org.thingsboard.server.dao.model.sql; +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; import lombok.Data; -import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; @@ -27,11 +31,6 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.dao.model.ToData; -import jakarta.persistence.Column; -import jakarta.persistence.EmbeddedId; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.Transient; import java.io.Serializable; import static org.thingsboard.server.dao.model.ModelConstants.BOOLEAN_VALUE_COLUMN; @@ -40,12 +39,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.JSON_VALUE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.LAST_UPDATE_TS_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; @Data -@EqualsAndHashCode(callSuper = true) @Entity @Table(name = "attribute_kv") -public class AttributeKvEntity extends VersionedEntity implements ToData, Serializable { +public class AttributeKvEntity implements ToData, Serializable { @EmbeddedId private AttributeKvCompositeKey id; @@ -68,6 +67,9 @@ public class AttributeKvEntity extends VersionedEntity implements ToData { +public final class RelationEntity implements ToData { @Id @Column(name = RELATION_FROM_ID_PROPERTY, columnDefinition = "uuid") @@ -72,6 +71,9 @@ public final class RelationEntity extends VersionedEntity implements ToData Date: Fri, 26 Jul 2024 15:46:30 +0300 Subject: [PATCH 121/136] UI: fixed force full screen calculation --- ui-ngx/src/app/core/auth/auth.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 48018e9317..51a70a46de 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -624,6 +624,7 @@ export class AuthService { private userForceFullscreen(authPayload: AuthPayload): boolean { return (authPayload.authUser && authPayload.authUser.isPublic) || (authPayload.userDetails && authPayload.userDetails.additionalInfo && + authPayload.userDetails.additionalInfo.defaultDashboardId && authPayload.userDetails.additionalInfo.defaultDashboardFullscreen && authPayload.userDetails.additionalInfo.defaultDashboardFullscreen === true); } From 4e6dbd8446a4050cc28501c80874fb6eafd6640e Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 26 Jul 2024 16:19:25 +0300 Subject: [PATCH 122/136] refactoring --- .../home/pages/admin/resource/resources-library.component.html | 2 +- .../home/pages/admin/resource/resources-library.component.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html index 76625326b4..441a311021 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html @@ -68,7 +68,7 @@ impleme title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required], fileName: [entity ? entity.fileName : null, Validators.required], - data: [entity ? entity.data : null, Validators.required] + data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []] }); } updateForm(entity: Resource) { if (this.isEdit) { this.entityForm.get('resourceType').disable({emitEvent: false}); - this.entityForm.get('data').disable({emitEvent: false}); if (entity.resourceType !== ResourceType.JS_MODULE) { this.entityForm.get('fileName').disable({emitEvent: false}); } From 2d11bb3af88025a43918a8319a1063ea038853be Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 26 Jul 2024 17:32:22 +0300 Subject: [PATCH 123/136] UI: Refactoring gateway components --- .../broker-config-control.component.ts | 2 +- .../mapping-table.component.html | 2 +- .../modbus-basic-config.component.scss | 24 +++++++++++++++++++ .../modbus-basic-config.component.ts | 12 +--------- .../modbus-data-keys-panel.component.ts | 2 +- .../modbus-slave-config.component.scss | 1 - .../modbus-slave-config.component.ts | 4 ++-- .../modbus-slave-dialog.component.scss | 21 ++++++++++++++++ .../modbus-slave-dialog.component.ts | 10 ++------ .../modbus-values.component.scss | 24 +++++++++++++++++++ .../modbus-values/modbus-values.component.ts | 12 +--------- .../opc-server-config.component.html | 12 +++++----- .../workers-config-control.component.html | 4 ++-- .../gateway}/pipes/gateway-help-link.pipe.ts | 0 .../pipes/gateway-port-tooltip.pipe.ts | 0 .../widget/widget-components.module.ts | 2 +- .../directives/truncate-tooltip.directive.ts | 17 +++++++++---- 17 files changed, 99 insertions(+), 50 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss rename ui-ngx/src/app/modules/home/{ => components/widget/lib/gateway}/pipes/gateway-help-link.pipe.ts (100%) rename ui-ngx/src/app/modules/home/{ => components/widget/lib/gateway}/pipes/gateway-port-tooltip.pipe.ts (100%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index 13cf1f4f13..f7d37b167e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -36,7 +36,7 @@ import { CommonModule } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; import { generateSecret } from '@core/utils'; import { Subject } from 'rxjs'; -import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; +import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { SecurityConfigComponent } from '../security-config/security-config.component'; @Component({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html index 7ebfb700bf..9f0566b566 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html @@ -64,7 +64,7 @@ [class.request-column]="mappingType === mappingTypeEnum.REQUESTS"> {{ column.title | translate }} - {{ mapping[column.def] }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss new file mode 100644 index 0000000000..b70fe42401 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + height: 100%; +} + +:host ::ng-deep { + .mat-mdc-tab-body-content { + overflow: hidden !important; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 12143fb6db..1fca768980 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -58,17 +58,7 @@ import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master ModbusMasterTableComponent, EllipsisChipListDirective, ], - styles: [` - :host { - height: 100%; - } - - :host ::ng-deep { - .mat-mdc-tab-body-content { - overflow: hidden !important; - } - } - `] + styleUrls: ['./modbus-basic-config.component.scss'], }) export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index f1919e7a57..5b312f52c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -35,7 +35,7 @@ import { } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; -import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe'; +import { GatewayHelpLinkPipe } from '@home/components/widget/lib/gateway/pipes/gateway-help-link.pipe'; import { generateSecret } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntil } from 'rxjs/operators'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss index 0c2f9240be..a464832202 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss @@ -16,7 +16,6 @@ $server-config-header-height: 132px; :host { - .slave-content { height: calc(100% - #{$server-config-header-height}); overflow: auto; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 05083a087d..2a8488a2d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -44,7 +44,7 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; import { startWith, takeUntil } from 'rxjs/operators'; -import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; +import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; import { isEqual } from '@core/utils'; @@ -73,7 +73,7 @@ import { isEqual } from '@core/utils'; ModbusSecurityConfigComponent, GatewayPortTooltipPipe, ], - styleUrls: ['modbus-slave-config.component.scss'], + styleUrls: ['./modbus-slave-config.component.scss'], }) export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss new file mode 100644 index 0000000000..8cea184957 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .slaves-config-container { + width: 80vw; + max-width: 900px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index cbfc7cc8c2..a00a488aa0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -49,7 +49,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe'; +import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { takeUntil } from 'rxjs/operators'; import { isEqual } from '@core/utils'; @@ -77,13 +77,7 @@ import { isEqual } from '@core/utils'; ModbusSecurityConfigComponent, GatewayPortTooltipPipe, ], - styles: [` - :host { - .slaves-config-container { - width: 80vw; - max-width: 900px; } - } - `], + styleUrls: ['./modbus-slave-dialog.component.scss'], }) export class ModbusSlaveDialogComponent extends DialogComponent implements OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss new file mode 100644 index 0000000000..2d0782e6a5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-mdc-tab-body-wrapper { + min-height: 320px; + } +} + +::ng-deep .mdc-evolution-chip-set__chips { + align-items: center; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts index caff181b6a..6bfc07c130 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts @@ -79,17 +79,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; SharedModule, EllipsisChipListDirective, ], - styles: [` - :host { - .mat-mdc-tab-body-wrapper { - min-height: 320px; - } - } - - ::ng-deep .mdc-evolution-chip-set__chips { - align-items: center; - } - `] + styleUrls: ['./modbus-values.component.scss'] }) export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html index ddff8f4945..4c992c0e21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html @@ -17,7 +17,7 @@ -->
-
gateway.server-url
+
gateway.server-url
@@ -35,7 +35,7 @@
-
{{ 'gateway.timeout' | translate }}
+
{{ 'gateway.timeout' | translate }}
@@ -54,7 +54,7 @@
-
gateway.security
+
gateway.security
@@ -65,7 +65,7 @@
-
{{ 'gateway.scan-period' | translate }}
+
{{ 'gateway.scan-period' | translate }}
@@ -86,7 +86,7 @@
-
{{ 'gateway.sub-check-period' | translate }}
+
{{ 'gateway.sub-check-period' | translate }}
@@ -108,7 +108,7 @@
-
{{ 'gateway.enable-subscription' | translate }}
+
{{ 'gateway.enable-subscription' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html index b348b49252..0a3bd4e6f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html @@ -19,7 +19,7 @@
-
{{ 'gateway.max-number-of-workers' | translate }}
+
{{ 'gateway.max-number-of-workers' | translate }}
@@ -41,7 +41,7 @@
-
{{ 'gateway.max-messages-queue-for-worker' | translate }}
+
{{ 'gateway.max-messages-queue-for-worker' | translate }}
diff --git a/ui-ngx/src/app/modules/home/pipes/gateway-help-link.pipe.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts similarity index 100% rename from ui-ngx/src/app/modules/home/pipes/gateway-help-link.pipe.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts diff --git a/ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe.ts similarity index 100% rename from ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 504f98420e..51d82dece9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -109,7 +109,7 @@ import { import { NotificationTypeFilterPanelComponent } from '@home/components/widget/lib/cards/notification-type-filter-panel.component'; -import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe'; +import { GatewayHelpLinkPipe } from '@home/components/widget/lib/gateway/pipes/gateway-help-link.pipe'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { BrokerConfigControlComponent diff --git a/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts b/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts index bf08780e11..95ec9fcf63 100644 --- a/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts +++ b/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts @@ -26,17 +26,24 @@ import { import { fromEvent, Subject } from 'rxjs'; import { filter, takeUntil, tap } from 'rxjs/operators'; import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Directive({ standalone: true, - selector: '[tbTruncateTooltip]', + selector: '[tbTruncateWithTooltip]', providers: [MatTooltip], - }) export class TruncateTooltipDirective implements OnInit, AfterViewInit, OnDestroy { - @Input('tbTruncateTooltip') text: string; - @Input() tooltipEnabled = true; - @Input() position: TooltipPosition = 'above'; + + @Input('tbTruncateWithTooltip') + text: string; + + @Input() + @coerceBoolean() + tooltipEnabled = true; + + @Input() + position: TooltipPosition = 'above'; private destroy$ = new Subject(); From 4acc32e6a9100a4d02ba7f598bc8fd5055e299e5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 26 Jul 2024 17:37:34 +0300 Subject: [PATCH 124/136] UI: Change name class from TruncateTooltipDirective to TruncateWithTooltipDirective --- .../mapping-table/mapping-table.component.ts | 4 ++-- .../opc-server-config/opc-server-config.component.ts | 4 ++-- .../workers-config-control.component.ts | 4 ++-- ...ooltip.directive.ts => truncate-with-tooltip.directive.ts} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename ui-ngx/src/app/shared/directives/{truncate-tooltip.directive.ts => truncate-with-tooltip.directive.ts} (97%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index abaeb1a4ab..2652f032b8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts @@ -57,7 +57,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive'; +import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; @Component({ @@ -78,7 +78,7 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs } ], standalone: true, - imports: [CommonModule, SharedModule, TruncateTooltipDirective] + imports: [CommonModule, SharedModule, TruncateWithTooltipDirective] }) export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index 35e144a6c7..eddad255a3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -35,7 +35,7 @@ import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive'; +import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive'; import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; @@ -62,7 +62,7 @@ import { CommonModule, SharedModule, SecurityConfigComponent, - TruncateTooltipDirective, + TruncateWithTooltipDirective, ] }) export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts index db7f74555c..ad13b921bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts @@ -35,7 +35,7 @@ import { CommonModule } from '@angular/common'; import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive'; +import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive'; @Component({ selector: 'tb-workers-config-control', @@ -45,7 +45,7 @@ import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.di imports: [ CommonModule, SharedModule, - TruncateTooltipDirective, + TruncateWithTooltipDirective, ], providers: [ { diff --git a/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts similarity index 97% rename from ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts rename to ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts index 95ec9fcf63..1281b86040 100644 --- a/ui-ngx/src/app/shared/directives/truncate-tooltip.directive.ts +++ b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts @@ -33,7 +33,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; selector: '[tbTruncateWithTooltip]', providers: [MatTooltip], }) -export class TruncateTooltipDirective implements OnInit, AfterViewInit, OnDestroy { +export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDestroy { @Input('tbTruncateWithTooltip') text: string; From c619be45b6542a8d6493d8936016794b79dbe813 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 29 Jul 2024 21:31:14 +0200 Subject: [PATCH 125/136] set version_seq cache to 1 --- application/src/main/data/upgrade/3.7.0/schema_update.sql | 6 +++--- dao/src/main/resources/sql/schema-entities.sql | 6 +++--- dao/src/main/resources/sql/schema-timescale.sql | 2 +- dao/src/main/resources/sql/schema-ts-latest-psql.sql | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql index ad8b76a4f3..a52eb73783 100644 --- a/application/src/main/data/upgrade/3.7.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql @@ -16,8 +16,8 @@ -- KV VERSIONING UPDATE START -CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1000; -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1; +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1; ALTER TABLE attribute_kv ADD COLUMN version bigint default 0; ALTER TABLE ts_kv_latest ADD COLUMN version bigint default 0; @@ -26,7 +26,7 @@ ALTER TABLE ts_kv_latest ADD COLUMN version bigint default 0; -- RELATION VERSIONING UPDATE START -CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1; ALTER TABLE relation ADD COLUMN version bigint default 0; -- RELATION VERSIONING UPDATE END diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 28b842076a..391aaf9e5f 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -102,7 +102,7 @@ CREATE TABLE IF NOT EXISTS audit_log ( action_failure_details varchar(1000000) ) PARTITION BY RANGE (created_time); -CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1; CREATE TABLE IF NOT EXISTS attribute_kv ( entity_id uuid, @@ -420,7 +420,7 @@ CREATE TABLE IF NOT EXISTS error_event ( e_error varchar ) PARTITION BY RANGE (ts); -CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1; CREATE TABLE IF NOT EXISTS relation ( from_id uuid, @@ -544,7 +544,7 @@ CREATE TABLE IF NOT EXISTS entity_view ( CONSTRAINT entity_view_external_id_unq_key UNIQUE (tenant_id, external_id) ); -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1; CREATE TABLE IF NOT EXISTS ts_kv_latest ( diff --git a/dao/src/main/resources/sql/schema-timescale.sql b/dao/src/main/resources/sql/schema-timescale.sql index 1d5b67a95e..6142770c1c 100644 --- a/dao/src/main/resources/sql/schema-timescale.sql +++ b/dao/src/main/resources/sql/schema-timescale.sql @@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS key_dictionary ( CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key) ); -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1; CREATE TABLE IF NOT EXISTS ts_kv_latest ( entity_id uuid NOT NULL, diff --git a/dao/src/main/resources/sql/schema-ts-latest-psql.sql b/dao/src/main/resources/sql/schema-ts-latest-psql.sql index 4892e40176..725c14e9bf 100644 --- a/dao/src/main/resources/sql/schema-ts-latest-psql.sql +++ b/dao/src/main/resources/sql/schema-ts-latest-psql.sql @@ -14,7 +14,7 @@ -- limitations under the License. -- -CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1000; +CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1; CREATE TABLE IF NOT EXISTS ts_kv_latest ( From b48474861ed65f8bfde32c8751de6729fd07ecd5 Mon Sep 17 00:00:00 2001 From: Kulikov <44275303+nickAS21@users.noreply.github.com> Date: Tue, 30 Jul 2024 11:20:30 +0300 Subject: [PATCH 126/136] TBEL: added parseBytesIntToFloat, parseBytesLongToDouble, byteArrayToExecutionArrayList (#11296) * tbel: added parseBytesIntLongToFloat * tbel: added to doubleToHex_Test() String valueHex = "0x000000000000000A"; * tbel: added byteArray to ExecutionArrayList; --- .../thingsboard/script/api/tbel/TbUtils.java | 250 ++++++++++++------ .../script/api/tbel/TbUtilsTest.java | 190 ++++++++++--- 2 files changed, 321 insertions(+), 119 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index d8aecb2f09..2884ba6214 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -194,6 +194,22 @@ public class TbUtils { byte[].class, int.class, int.class))); parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat", byte[].class, int.class, int.class, boolean.class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + List.class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + List.class, int.class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + List.class, int.class, int.class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + List.class, int.class, int.class, boolean.class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + byte[].class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + byte[].class, int.class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + byte[].class, int.class, int.class))); + parserConfig.addImport("parseBytesIntToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesIntToFloat", + byte[].class, int.class, int.class, boolean.class))); parserConfig.addImport("parseLittleEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToDouble", String.class))); parserConfig.addImport("parseBigEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToDouble", @@ -218,6 +234,22 @@ public class TbUtils { byte[].class, int.class, int.class))); parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble", byte[].class, int.class, int.class, boolean.class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + List.class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + List.class, int.class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + List.class, int.class, int.class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + List.class, int.class, int.class, boolean.class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + byte[].class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + byte[].class, int.class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + byte[].class, int.class, int.class))); + parserConfig.addImport("parseBytesLongToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesLongToDouble", + byte[].class, int.class, int.class, boolean.class))); parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", double.class, int.class))); parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", @@ -292,6 +324,9 @@ public class TbUtils { String.class))); parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal", String.class))); + parserConfig.addImport("byteArrayToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("byteArrayToExecutionArrayList", + ExecutionContext.class, byte[].class))); + } public static String btoa(String input) { @@ -753,7 +788,7 @@ public class TbUtils { long bits = Double.doubleToRawLongBits(d); // Format the integer bits as a hexadecimal string - String result = String.format("0x%16X", bits); + String result = String.format("0x%016X", bits); return bigEndian ? result : reverseHexStringByOrder(result); } @@ -770,15 +805,15 @@ public class TbUtils { } public static int parseBytesToInt(List data) { - return parseBytesToInt(Bytes.toArray(data)); + return parseBytesToInt(data, 0); } public static int parseBytesToInt(List data, int offset) { - return parseBytesToInt(Bytes.toArray(data), offset); + return parseBytesToInt(data, offset, validateLength(data.size(), offset, BYTES_LEN_INT_MAX)); } public static int parseBytesToInt(List data, int offset, int length) { - return parseBytesToInt(Bytes.toArray(data), offset, length); + return parseBytesToInt(data, offset, length, true); } public static int parseBytesToInt(List data, int offset, int length, boolean bigEndian) { @@ -798,15 +833,7 @@ public class TbUtils { } public static int parseBytesToInt(byte[] data, int offset, int length, boolean bigEndian) { - if (offset > data.length) { - throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); - } - if (length > BYTES_LEN_INT_MAX) { - throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!"); - } - if (offset + length > data.length) { - throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); - } + validationNumberByLength(data, offset, length, BYTES_LEN_INT_MAX); var bb = ByteBuffer.allocate(4); if (!bigEndian) { bb.order(ByteOrder.LITTLE_ENDIAN); @@ -818,15 +845,15 @@ public class TbUtils { } public static long parseBytesToLong(List data) { - return parseBytesToLong(Bytes.toArray(data)); + return parseBytesToLong(data, 0); } public static long parseBytesToLong(List data, int offset) { - return parseBytesToLong(Bytes.toArray(data), offset); + return parseBytesToLong(data, offset, validateLength(data.size(), offset, BYTES_LEN_LONG_MAX)); } public static long parseBytesToLong(List data, int offset, int length) { - return parseBytesToLong(Bytes.toArray(data), offset, length); + return parseBytesToLong(data, offset, length, true); } public static long parseBytesToLong(List data, int offset, int length, boolean bigEndian) { @@ -846,15 +873,7 @@ public class TbUtils { } public static long parseBytesToLong(byte[] data, int offset, int length, boolean bigEndian) { - if (offset > data.length) { - throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); - } - if (length > BYTES_LEN_LONG_MAX) { - throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_LONG_MAX + " bytes is allowed!"); - } - if (offset + length > data.length) { - throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); - } + validationNumberByLength(data, offset, length, BYTES_LEN_LONG_MAX); var bb = ByteBuffer.allocate(BYTES_LEN_LONG_MAX); if (!bigEndian) { bb.order(ByteOrder.LITTLE_ENDIAN); @@ -866,15 +885,15 @@ public class TbUtils { } public static float parseBytesToFloat(List data) { - return parseBytesToFloat(Bytes.toArray(data), 0); + return parseBytesToFloat(data, 0); } public static float parseBytesToFloat(List data, int offset) { - return parseBytesToFloat(Bytes.toArray(data), offset, BYTES_LEN_INT_MAX); + return parseBytesToFloat(data, offset, validateLength(data.size(), offset, BYTES_LEN_INT_MAX)); } public static float parseBytesToFloat(List data, int offset, int length) { - return parseBytesToFloat(Bytes.toArray(data), offset, length, true); + return parseBytesToFloat(data, offset, length, true); } public static float parseBytesToFloat(List data, int offset, int length, boolean bigEndian) { @@ -886,7 +905,7 @@ public class TbUtils { } public static float parseBytesToFloat(byte[] data, int offset) { - return parseBytesToFloat(data, offset, BYTES_LEN_INT_MAX); + return parseBytesToFloat(data, offset, validateLength(data.length, offset, BYTES_LEN_INT_MAX)); } public static float parseBytesToFloat(byte[] data, int offset, int length) { @@ -894,39 +913,65 @@ public class TbUtils { } public static float parseBytesToFloat(byte[] data, int offset, int length, boolean bigEndian) { - if (length > BYTES_LEN_INT_MAX) { - throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_INT_MAX + " bytes is allowed!"); - } - if (offset + length > data.length) { - throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); - } - byte[] bytesToNumber = prepareBytesToNumber(data, offset, length, bigEndian); - if (bytesToNumber.length < BYTES_LEN_INT_MAX) { - byte[] extendedBytes = new byte[BYTES_LEN_INT_MAX]; - Arrays.fill(extendedBytes, (byte) 0); - System.arraycopy(bytesToNumber, 0, extendedBytes, 0, bytesToNumber.length); - bytesToNumber = extendedBytes; + var bb = ByteBuffer.allocate(BYTES_LEN_INT_MAX); + if (!bigEndian) { + bb.order(ByteOrder.LITTLE_ENDIAN); } - float floatValue = ByteBuffer.wrap(bytesToNumber).getFloat(); - if (!Float.isNaN(floatValue)) { - return floatValue; - } else { - long longValue = parseBytesToLong(bytesToNumber, 0, BYTES_LEN_INT_MAX); - BigDecimal bigDecimalValue = new BigDecimal(longValue); - return bigDecimalValue.floatValue(); + bb.position(bigEndian ? BYTES_LEN_INT_MAX - length : 0); + bb.put(data, offset, length); + bb.position(0); + float floatValue = bb.getFloat(); + if (Float.isNaN(floatValue)) { + throw new NumberFormatException("byte[] 0x" + bytesToHex(data) + " is a Not-a-Number (NaN) value"); } + return floatValue; + } + + public static float parseBytesIntToFloat(List data) { + return parseBytesIntToFloat(data, 0); + } + + public static float parseBytesIntToFloat(List data, int offset) { + return parseBytesIntToFloat(data, offset, validateLength(data.size(), offset, BYTES_LEN_INT_MAX)); + } + + public static float parseBytesIntToFloat(List data, int offset, int length) { + return parseBytesIntToFloat(data, offset, length, true); + } + + public static float parseBytesIntToFloat(List data, int offset, int length, boolean bigEndian) { + return parseBytesIntToFloat(Bytes.toArray(data), offset, length, bigEndian); + } + + public static float parseBytesIntToFloat(byte[] data) { + return parseBytesIntToFloat(data, 0); + } + + public static float parseBytesIntToFloat(byte[] data, int offset) { + return parseBytesIntToFloat(data, offset, validateLength(data.length, offset, BYTES_LEN_INT_MAX)); + } + + public static float parseBytesIntToFloat(byte[] data, int offset, int length) { + return parseBytesIntToFloat(data, offset, length, true); + } + + public static float parseBytesIntToFloat(byte[] data, int offset, int length, boolean bigEndian) { + byte[] bytesToNumber = prepareBytesToNumber(data, offset, length, bigEndian, BYTES_LEN_INT_MAX); + long longValue = parseBytesToLong(bytesToNumber, 0, length); + BigDecimal bigDecimalValue = new BigDecimal(longValue); + return bigDecimalValue.floatValue(); } public static double parseBytesToDouble(List data) { - return parseBytesToDouble(Bytes.toArray(data)); + return parseBytesToDouble(data, 0); } public static double parseBytesToDouble(List data, int offset) { - return parseBytesToDouble(Bytes.toArray(data), offset); + return parseBytesToDouble(data, offset, validateLength(data.size(), offset, BYTES_LEN_LONG_MAX)); } public static double parseBytesToDouble(List data, int offset, int length) { - return parseBytesToDouble(Bytes.toArray(data), offset, length); + return parseBytesToDouble(data, offset, length, true); } public static double parseBytesToDouble(List data, int offset, int length, boolean bigEndian) { @@ -938,7 +983,7 @@ public class TbUtils { } public static double parseBytesToDouble(byte[] data, int offset) { - return parseBytesToDouble(data, offset, BYTES_LEN_LONG_MAX); + return parseBytesToDouble(data, offset, validateLength(data.length, offset, BYTES_LEN_LONG_MAX)); } public static double parseBytesToDouble(byte[] data, int offset, int length) { @@ -946,36 +991,58 @@ public class TbUtils { } public static double parseBytesToDouble(byte[] data, int offset, int length, boolean bigEndian) { - if (length > BYTES_LEN_LONG_MAX) { - throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_LONG_MAX + " bytes is allowed!"); - } - if (offset + length > data.length) { - throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); - } - byte[] bytesToNumber = prepareBytesToNumber(data, offset, length, bigEndian); - if (bytesToNumber.length < BYTES_LEN_LONG_MAX) { - byte[] extendedBytes = new byte[BYTES_LEN_LONG_MAX]; - Arrays.fill(extendedBytes, (byte) 0); - System.arraycopy(bytesToNumber, 0, extendedBytes, 0, bytesToNumber.length); - bytesToNumber = extendedBytes; + var bb = ByteBuffer.allocate(BYTES_LEN_LONG_MAX); + if (!bigEndian) { + bb.order(ByteOrder.LITTLE_ENDIAN); } - double doubleValue = ByteBuffer.wrap(bytesToNumber).getDouble(); - if (!Double.isNaN(doubleValue)) { - return doubleValue; - } else { - BigInteger bigInt = new BigInteger(1, bytesToNumber); - BigDecimal bigDecimalValue = new BigDecimal(bigInt); - return bigDecimalValue.doubleValue(); + bb.position(bigEndian ? BYTES_LEN_LONG_MAX - length : 0); + bb.put(data, offset, length); + bb.position(0); + double doubleValue = bb.getDouble(); + if (Double.isNaN(doubleValue)) { + throw new NumberFormatException("byte[] 0x" + bytesToHex(data) + " is a Not-a-Number (NaN) value"); } + return doubleValue; } - private static byte[] prepareBytesToNumber(byte[] data, int offset, int length, boolean bigEndian) { - if (offset > data.length) { - throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); - } - if ((offset + length) > data.length) { - throw new IllegalArgumentException("Default length is always " + length + " bytes. Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); - } + public static double parseBytesLongToDouble(List data) { + return parseBytesLongToDouble(data, 0); + } + + public static double parseBytesLongToDouble(List data, int offset) { + return parseBytesLongToDouble(data, offset, validateLength(data.size(), offset, BYTES_LEN_LONG_MAX)); + } + + public static double parseBytesLongToDouble(List data, int offset, int length) { + return parseBytesLongToDouble(data, offset, length, true); + } + + public static double parseBytesLongToDouble(List data, int offset, int length, boolean bigEndian) { + return parseBytesLongToDouble(Bytes.toArray(data), offset, length, bigEndian); + } + + public static double parseBytesLongToDouble(byte[] data) { + return parseBytesLongToDouble(data, 0); + } + + public static double parseBytesLongToDouble(byte[] data, int offset) { + return parseBytesLongToDouble(data, offset, validateLength(data.length, offset, BYTES_LEN_LONG_MAX)); + } + + public static double parseBytesLongToDouble(byte[] data, int offset, int length) { + return parseBytesLongToDouble(data, offset, length, true); + } + + public static double parseBytesLongToDouble(byte[] data, int offset, int length, boolean bigEndian) { + byte[] bytesToNumber = prepareBytesToNumber(data, offset, length, bigEndian, BYTES_LEN_LONG_MAX); + BigInteger bigInt = new BigInteger(1, bytesToNumber); + BigDecimal bigDecimalValue = new BigDecimal(bigInt); + return bigDecimalValue.doubleValue(); + } + + private static byte[] prepareBytesToNumber(byte[] data, int offset, int length, boolean bigEndian, + int bytesLenMax) { + validationNumberByLength(data, offset, length, bytesLenMax); byte[] dataBytesArray = Arrays.copyOfRange(data, offset, (offset + length)); if (!bigEndian) { ArrayUtils.reverse(dataBytesArray); @@ -1168,6 +1235,15 @@ public class TbUtils { return str.matches("^-?(0[xX])?[0-9a-fA-F]+$") ? HEX_RADIX : -1; } + public static List byteArrayToExecutionArrayList(ExecutionContext ctx, byte[] byteArray) { + List byteList = new ArrayList<>(); + for (byte b : byteArray) { + byteList.add(b); + } + List list = new ExecutionArrayList(byteList, ctx); + return list; + } + private static byte isValidIntegerToByte(Integer val) { if (val > 255 || val < -128) { throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " + @@ -1194,5 +1270,23 @@ public class TbUtils { String result = reversedHex.toString(); return isHexPref ? "0x" + result : result; } + + private static void validationNumberByLength(byte[] data, int offset, int length, int bytesLenMax) { + if (offset > data.length) { + throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); + } + + if (offset + length > data.length) { + throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); + } + + if (length > bytesLenMax) { + throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + bytesLenMax + " bytes is allowed!"); + } + } + + private static int validateLength(int dataLength, int offset, int bytesLenMax) { + return (dataLength < offset) ? dataLength : Math.min((dataLength - offset), bytesLenMax); + } } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index a7292d4280..c9ee9a977c 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -276,7 +276,10 @@ public class TbUtilsTest { @Test public void parseBytesToFloat() { - byte[] floatValByte = {65, -22, 98, -52}; + byte[] floatValByte = {0x0A}; + Assertions.assertEquals(0, Float.compare(1.4E-44f, TbUtils.parseBytesToFloat(floatValByte))); + + floatValByte = new byte[]{65, -22, 98, -52}; Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0))); Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, 4, false))); @@ -284,40 +287,101 @@ public class TbUtilsTest { Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValList, 0))); Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValList, 0, 4, false))); - // 4 294 967 295L == {0xFF, 0xFF, 0xFF, 0xFF} - floatValByte = new byte[]{-1, -1, -1, -1}; - float floatExpectedBe = 4294.9673f; - float floatExpectedLe = 4.2949673E9f; + + // 1.1803216E8f == 0x4CE120E4 + floatValByte = new byte[]{0x4C, (byte) 0xE1, (byte) 0x20, (byte) 0xE4}; + float floatExpectedBe = 118.03216f; float actualBe = TbUtils.parseBytesToFloat(floatValByte, 0, 4, true); Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000)); - Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValByte, 0, 4, false))); - floatValList = Bytes.asList(floatValByte); actualBe = TbUtils.parseBytesToFloat(floatValList, 0); Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000)); - Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValList, 0, 4, false))); - // 2 143 289 344L == {0x7F, 0xC0, 0x00, 0x00} - floatValByte = new byte[]{0x7F, (byte) 0xC0, (byte) 0xFF, 0x00}; - floatExpectedBe = 2143.3547f; - floatExpectedLe = -3.984375f; - actualBe = TbUtils.parseBytesToFloat(floatValByte, 0, 4, true); - Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000)); + float floatExpectedLe = 8.0821E-41f; Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValByte, 0, 2, false))); + floatExpectedBe = 2.7579E-41f; + Assertions.assertEquals(0, Float.compare(floatExpectedBe, TbUtils.parseBytesToFloat(floatValByte, 0, 2))); - floatValList = Bytes.asList(floatValByte); - floatExpectedLe = 4.2908055E9f; - actualBe = TbUtils.parseBytesToFloat(floatValList, 0); - Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000)); + + floatExpectedLe = 3.019557E-39f; Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValList, 0, 3, false))); + + // 4 294 967 295L == {0xFF, 0xFF, 0xFF, 0xFF} + floatValByte = new byte[]{-1, -1, -1, -1}; + String message = "is a Not-a-Number (NaN) value"; + try { + TbUtils.parseBytesToFloat(floatValByte, 0, 4, true); + Assertions.fail("Should throw NumberFormatException"); + } catch (RuntimeException e) { + Assertions.assertTrue(e.getMessage().contains(message)); + } + // "01752B0367FA000500010488 FFFFFFFF FFFFFFFF 33"; String intToHexBe = "01752B0367FA000500010488FFFFFFFFFFFFFFFF33"; - floatExpectedLe = 4294.9673f; floatValList = TbUtils.hexToBytes(ctx, intToHexBe); - float actualLe = TbUtils.parseBytesToFloat(floatValList, 12, 4, false); - Assertions.assertEquals(0, Float.compare(floatExpectedLe, actualLe / 1000000)); - actualLe = TbUtils.parseBytesToFloat(floatValList, 12 + 4, 4, false); - Assertions.assertEquals(0, Float.compare(floatExpectedLe, actualLe / 1000000)); + try { + TbUtils.parseBytesToFloat(floatValList, 12, 4, false); + Assertions.fail("Should throw NumberFormatException"); + } catch (RuntimeException e) { + Assertions.assertTrue(e.getMessage().contains(message)); + } + } + + @Test + public void parseBytesIntToFloat() { + byte[] intValByte = {0x00, 0x00, 0x00, 0x0A}; + Float valueExpected = 10.0f; + Float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true); + Assertions.assertEquals(valueExpected, valueActual); + valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, false); + Assertions.assertEquals(valueExpected, valueActual); + + valueActual = TbUtils.parseBytesIntToFloat(intValByte, 2, 2, true); + Assertions.assertEquals(valueExpected, valueActual); + valueExpected = 2560.0f; + valueActual = TbUtils.parseBytesIntToFloat(intValByte, 2, 2, false); + Assertions.assertEquals(valueExpected, valueActual); + + valueExpected = 10.0f; + valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, true); + Assertions.assertEquals(valueExpected, valueActual); + valueExpected = 1.6777216E8f; + valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false); + Assertions.assertEquals(valueExpected, valueActual); + + String dataAT101 = "0x01756403671B01048836BF7701F000090722050000"; + List byteAT101 = TbUtils.hexToBytes(ctx, dataAT101); + float latitudeExpected = 24.62495f; + int offset = 9; + valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false); + Assertions.assertEquals(latitudeExpected, valueActual / 1000000); + + float longitudeExpected = 118.030576f; + valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false); + Assertions.assertEquals(longitudeExpected, valueActual / 1000000); + + valueExpected = 9.185175E8f; + valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset); + Assertions.assertEquals(valueExpected, valueActual); + // 0x36BF + valueExpected = 14015.0f; + valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset, 2); + Assertions.assertEquals(valueExpected, valueActual); + // 0xBF36 + valueExpected = 48950.0f; + valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset, 2, false); + Assertions.assertEquals(valueExpected, valueActual); + + valueExpected = 0.0f; + valueActual = TbUtils.parseBytesIntToFloat(byteAT101, byteAT101.size()); + Assertions.assertEquals(valueExpected, valueActual); + + try { + TbUtils.parseBytesIntToFloat(byteAT101, byteAT101.size() + 1); + Assertions.fail("Should throw NumberFormatException"); + } catch (RuntimeException e) { + Assertions.assertTrue(e.getMessage().contains("is out of bounds for array with length:")); + } } @Test @@ -390,7 +454,10 @@ public class TbUtilsTest { @Test public void parseBytesToDouble() { - byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22}; + byte[] doubleValByte = {0x0A}; + Assertions.assertEquals(0, Double.compare(4.9E-323, TbUtils.parseBytesToDouble(doubleValByte))); + + doubleValByte = new byte[]{64, -101, 4, -79, 12, -78, -107, -22}; Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0))); Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, 8, false))); @@ -398,34 +465,61 @@ public class TbUtilsTest { Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValList, 0))); Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValList, 0, 8, false))); - // 4 294 967 295L == {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} - doubleValByte = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1}; - double doubleExpectedBe = 18446.744073709553d; - double doubleExpectedLe = 1.8446744073709552E19d; - double actualBe = TbUtils.parseBytesToDouble(doubleValByte, 0, 8, true); - Assertions.assertEquals(0, Double.compare(doubleExpectedBe, actualBe / 1000000000000000L)); - Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValByte, 0, 8, false))); - - doubleValList = Bytes.asList(doubleValByte); - Assertions.assertEquals(0, Double.compare(doubleExpectedBe, TbUtils.parseBytesToDouble(doubleValList, 0) / 1000000000000000L)); - Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValList, 0, 8, false))); - doubleValByte = new byte[]{0x7F, (byte) 0xC0, (byte) 0xFF, 0x00, 0x7F, (byte) 0xC0, (byte) 0xFF, 0x00}; - doubleExpectedBe = 2387013.651780523d; - doubleExpectedLe = 7.234601680440024E-304d; - actualBe = TbUtils.parseBytesToDouble(doubleValByte, 0, 8, true); + double doubleExpectedBe = 2387013.651780523d; + double doubleExpectedLe = 7.234601680440024E-304d; + double actualBe = TbUtils.parseBytesToDouble(doubleValByte, 0, 8, true); BigDecimal bigDecimal = new BigDecimal(actualBe); // We move the decimal point to the left by 301 positions actualBe = bigDecimal.movePointLeft(301).doubleValue(); Assertions.assertEquals(0, Double.compare(doubleExpectedBe, actualBe)); Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValByte, 0, 8, false))); + doubleValList = Bytes.asList(doubleValByte); - doubleExpectedLe = 5.828674572203954E303d; actualBe = TbUtils.parseBytesToDouble(doubleValList, 0); bigDecimal = new BigDecimal(actualBe); actualBe = bigDecimal.movePointLeft(301).doubleValue(); Assertions.assertEquals(0, Double.compare(doubleExpectedBe, actualBe)); - Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValList, 0, 5, false))); + doubleExpectedLe = 26950.174646662283d; + double actualLe = TbUtils.parseBytesToDouble(doubleValList, 0, 5, false); + bigDecimal = new BigDecimal(actualLe); + actualLe = bigDecimal.movePointRight(316).doubleValue(); + Assertions.assertEquals(0, Double.compare(doubleExpectedLe, actualLe)); + + // 4 294 967 295L == {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + doubleValByte = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1}; + String message = "is a Not-a-Number (NaN) value"; + try { + TbUtils.parseBytesToDouble(doubleValByte, 0, 8, true); + Assertions.fail("Should throw NumberFormatException"); + } catch (RuntimeException e) { + Assertions.assertTrue(e.getMessage().contains(message)); + } + } + + @Test + public void parseBytesLongToDouble() { + byte[] longValByte = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A}; + Double valueExpected = 10.0d; + Double valueActual = TbUtils.parseBytesLongToDouble(longValByte); + Assertions.assertEquals(valueExpected, valueActual); + valueActual = TbUtils.parseBytesLongToDouble(longValByte, 7, 1, true); + Assertions.assertEquals(valueExpected, valueActual); + valueActual = TbUtils.parseBytesLongToDouble(longValByte, 7, 1, false); + Assertions.assertEquals(valueExpected, valueActual); + + valueActual = TbUtils.parseBytesLongToDouble(longValByte, 6, 2, true); + Assertions.assertEquals(valueExpected, valueActual); + valueExpected = 2560.0d; + valueActual = TbUtils.parseBytesLongToDouble(longValByte, 6, 2, false); + Assertions.assertEquals(valueExpected, valueActual); + + valueExpected = 10.0d; + valueActual = TbUtils.parseBytesLongToDouble(longValByte, 0, 8, true); + Assertions.assertEquals(valueExpected, valueActual); + valueExpected = 7.2057594037927936E17d; + valueActual = TbUtils.parseBytesLongToDouble(longValByte, 0, 8, false); + Assertions.assertEquals(valueExpected, valueActual); } @Test @@ -717,6 +811,13 @@ public class TbUtilsTest { Assertions.assertEquals(value, valueActual); valueActual = TbUtils.parseHexToFloat(valueHexRev, false); Assertions.assertEquals(value, valueActual); + + String valueHex = "0x0000000A"; + float expectedValue = 1.4E-44f; + valueActual = TbUtils.parseHexToFloat(valueHex); + Assertions.assertEquals(expectedValue, valueActual); + actual = TbUtils.floatToHex(expectedValue); + Assertions.assertEquals(valueHex, actual); } // If the length is not equal to 8 characters, we process it as an integer (eg "0x0A" for 10.0f). @@ -748,6 +849,13 @@ public class TbUtilsTest { actual = TbUtils.doubleToHex(doubleVal, false); String expectedHexRev = "0xEA95B20CB1049B40"; Assertions.assertEquals(expectedHexRev, actual); + + String valueHex = "0x000000000000000A"; + Double expectedValue = 4.9E-323; + valueActual = TbUtils.parseHexToDouble(valueHex); + Assertions.assertEquals(expectedValue, valueActual); + actual = TbUtils.doubleToHex(expectedValue); + Assertions.assertEquals(valueHex, actual); } @Test From f34361e1eac041bf4de5002e5101ed6a50697b2b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 30 Jul 2024 12:02:01 +0300 Subject: [PATCH 127/136] Fix Nashorn sandbox script compile error (ScriptCPUAbuseException) --- .../script/NashornJsInvokeServiceTest.java | 47 +++++++++++++++++-- pom.xml | 2 +- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 28834a0ab7..33e7ccea23 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -39,12 +39,13 @@ import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @DaoSqlTest @TestPropertySource(properties = { "js.evaluator=local", - "js.max_script_body_size=50", + "js.max_script_body_size=10000", "js.max_total_args_size=50", "js.max_result_size=50", "js.local.max_errors=2", @@ -87,7 +88,7 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { @Test void givenSimpleScriptMultiThreadTestPerformance() throws ExecutionException, InterruptedException, TimeoutException { - int iterations = 1000*4; + int iterations = 1000 * 4; List> futures = new ArrayList<>(iterations); UUID scriptId = evalScript("return msg.temperature > 20 ;"); // warmup @@ -125,7 +126,7 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { @Test void givenTooBigScriptForEval_thenReturnError() { - String hugeScript = "var a = 'qwertyqwertywertyqwabababer'; return {a: a};"; + String hugeScript = "var a = '" + "a".repeat(10000) + "'; return {a: a};"; assertThatThrownBy(() -> { evalScript(hugeScript); @@ -159,6 +160,46 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { assertThatScriptIsBlocked(scriptId); } + @Test + void givenComplexScript_testCompile() { + String script = """ + function(data) { + if (data.get("propertyA") == "a special value 1" || data.get("propertyA") == "a special value 2") { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyC") == "a special value 1" || data.get("propertyJ") == "a special value 1" || data.get("propertyV") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "4" && (data.get("propertyD") == "a special value 1" || data.get("propertyV") == "a special value 1" || data.get("propertyW") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 2" && (data.get("propertyE") == "a special value 1" || data.get("propertyF") == "a special value 1" || data.get("propertyL") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyE") == "a special value 1" || data.get("propertyF") == "a special value 1" || data.get("propertyL") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyM") == "a special value 1" || data.get("propertyY") == "a special value 1" || data.get("propertyH") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyM") == "a special value 1" || data.get("propertyY") == "a special value 1" || data.get("propertyH") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyM") == "a special value 1" || data.get("propertyY") == "a special value 1" || data.get("propertyH") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyM") == "a special value 1" || data.get("propertyY") == "a special value 1" || data.get("propertyH") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyM") == "a special value 1" || data.get("propertyY") == "a special value 1" || data.get("propertyH") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyM") == "a special value 1" || data.get("propertyY") == "a special value 1" || data.get("propertyH") == "a special value 1")) { + return "a special value 1"; + } else if (data.get("propertyB") == "a special value 3" && (data.get("propertyM") == "a special value 1" || data.get("propertyY") == "a special value 1" || data.get("propertyH") == "a special value 1")) { + return "a special value 1"; + } else { + return "0" + }; + } + """; + + // with delight-nashorn-sandbox 0.4.2, this would throw delight.nashornsandbox.exceptions.ScriptCPUAbuseException: Regular expression running for too many iterations. The operation could NOT be gracefully interrupted. + assertDoesNotThrow(() -> { + evalScript(script); + }); + } + private void assertThatScriptIsBlocked(UUID scriptId) { assertThatThrownBy(() -> { invokeScript(scriptId, "{}"); diff --git a/pom.xml b/pom.xml index f75e782bef..9b8943cff3 100755 --- a/pom.xml +++ b/pom.xml @@ -105,7 +105,7 @@ org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 8.13.2 - 0.4.2 + 0.4.5 15.4