diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 1cf107b801..9ab33665e6 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -104,7 +104,7 @@ "settingsDirective": "tb-simple-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-simple-card-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true}" } }, { @@ -143,7 +143,9 @@ "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":true,\"entityNameColumnTitle\":\"\",\"displayEntityLabel\":false,\"displayEntityType\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"useCellContentFunction\":false,\"defaultColumnVisibility\":\"visible\",\"columnSelectionToDisplay\":\"enabled\"},\"_hash\":0.6401141393938932,\"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;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}]}" + "hasBasicMode": true, + "basicModeDirective": "tb-entities-table-basic-config", + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"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;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" } }, { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 6b05df7feb..3916d3e0be 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -502,7 +502,7 @@ cache: spring.data.redis.repositories.enabled: false redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -522,6 +522,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index 9c4c962dc2..36f3dddc3d 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -26,14 +26,19 @@ import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import redis.clients.jedis.JedisPoolConfig; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; @Configuration @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @@ -41,6 +46,9 @@ import java.time.Duration; @Data public abstract class TBRedisCacheConfiguration { + private static final String COMMA = ","; + private static final String COLON = ":"; + @Value("${redis.evictTtlInMs:60000}") private int evictTtlInMs; @@ -126,4 +134,19 @@ public abstract class TBRedisCacheConfiguration { poolConfig.setBlockWhenExhausted(blockWhenExhausted); return poolConfig; } + + protected List getNodes(String nodes) { + List result; + if (StringUtils.isBlank(nodes)) { + result = Collections.emptyList(); + } else { + result = new ArrayList<>(); + for (String hostPort : nodes.split(COMMA)) { + String host = hostPort.split(COLON)[0]; + int port = Integer.parseInt(hostPort.split(COLON)[1]); + result.add(new RedisNode(host, port)); + } + } + return result; + } } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java index dfc6ebd225..0a378103b0 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java @@ -20,22 +20,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; -import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import org.thingsboard.server.common.data.StringUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; @Configuration @ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class) @ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster") public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { - private static final String COMMA = ","; - private static final String COLON = ":"; - @Value("${redis.cluster.nodes:}") private String clusterNodes; @@ -59,19 +50,4 @@ public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration { return new JedisConnectionFactory(clusterConfiguration, buildPoolConfig()); } } - - private List getNodes(String nodes) { - List result; - if (StringUtils.isBlank(nodes)) { - result = Collections.emptyList(); - } else { - result = new ArrayList<>(); - for (String hostPort : nodes.split(COMMA)) { - String host = hostPort.split(COLON)[0]; - Integer port = Integer.valueOf(hostPort.split(COLON)[1]); - result.add(new RedisNode(host, port)); - } - } - return result; - } -} \ No newline at end of file +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java new file mode 100644 index 0000000000..78cb445d82 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2023 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.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + +@Configuration +@ConditionalOnMissingBean(TbCaffeineCacheConfiguration.class) +@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "sentinel") +public class TBRedisSentinelConfiguration extends TBRedisCacheConfiguration { + + @Value("${redis.sentinel.master:}") + private String master; + + @Value("${redis.sentinel.sentinels:}") + private String sentinels; + + @Value("${redis.sentinel.password:}") + private String sentinelPassword; + + @Value("${redis.sentinel.useDefaultPoolConfig:true}") + private boolean useDefaultPoolConfig; + + @Value("${redis.db:}") + private Integer database; + + @Value("${redis.password:}") + private String password; + + public JedisConnectionFactory loadFactory() { + RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); + redisSentinelConfiguration.setMaster(master); + redisSentinelConfiguration.setSentinels(getNodes(sentinels)); + redisSentinelConfiguration.setSentinelPassword(sentinelPassword); + redisSentinelConfiguration.setPassword(password); + redisSentinelConfiguration.setDatabase(database); + if (useDefaultPoolConfig) { + return new JedisConnectionFactory(redisSentinelConfiguration); + } else { + return new JedisConnectionFactory(redisSentinelConfiguration, buildPoolConfig()); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java index 7b11e86c9f..37934e1f50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java index 3696aa0582..c7a65f49d2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java index d9294b7fba..988b92600a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java index 85d26ffde0..435da5e3c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java index 6570d76c57..7e0119c7f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -28,6 +29,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId { super(id); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE") @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java index 4ebc531c4b..866f50f5d9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -34,6 +35,7 @@ public class QueueId extends UUIDBased implements EntityId { return new QueueId(UUID.fromString(queueId)); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "QUEUE", allowableValues = "QUEUE") @Override public EntityType getEntityType() { return EntityType.QUEUE; diff --git a/docker/.env b/docker/.env index f33ed50da5..37c9768296 100644 --- a/docker/.env +++ b/docker/.env @@ -1,6 +1,6 @@ TB_QUEUE_TYPE=kafka -# redis or redis-cluster +# redis or redis-cluster or redis-sentinel CACHE=redis DOCKER_REPO=thingsboard diff --git a/docker/.gitignore b/docker/.gitignore index c9172ae6ce..b6c9fcf18c 100644 --- a/docker/.gitignore +++ b/docker/.gitignore @@ -12,6 +12,9 @@ tb-node/redis-cluster-data-2/** tb-node/redis-cluster-data-3/** tb-node/redis-cluster-data-4/** tb-node/redis-cluster-data-5/** +tb-node/redis-sentinel-data-master/** +tb-node/redis-sentinel-data-slave/** +tb-node/redis-sentinel-data-sentinel/** tb-node/redis-data/** !.env diff --git a/docker/README.md b/docker/README.md index 71ea87f353..1a852c80eb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -21,8 +21,9 @@ In order to set cache type change the value of `CACHE` variable in `.env` file t - `redis` - use Redis standalone cache (1 node - 1 master); - `redis-cluster` - use Redis cluster cache (6 nodes - 3 masters, 3 slaves); +- `redis-sentinel` - use Redis sentinel cache (3 nodes - 1 master, 1 slave, 1 sentinel) -**NOTE**: According to the cache type corresponding docker service will be deployed (see `docker-compose.redis.yml`, `docker-compose.redis-cluster.yml` for details). +**NOTE**: According to the cache type corresponding docker service will be deployed (see `docker-compose.redis.yml`, `docker-compose.redis-cluster.yml`, `docker-compose.redis-sentinel.yml` for details). Execute the following command to create log folders for the services and chown of these folders to the docker container users. To be able to change user, **chown** command is used, which requires sudo permissions (script will request password for a sudo access): diff --git a/docker/cache-redis-sentinel.env b/docker/cache-redis-sentinel.env new file mode 100644 index 0000000000..39a1246d9c --- /dev/null +++ b/docker/cache-redis-sentinel.env @@ -0,0 +1,7 @@ +CACHE_TYPE=redis +REDIS_CONNECTION_TYPE=sentinel +REDIS_MASTER=mymaster +REDIS_SENTINELS=redis-sentinel:26379 +REDIS_SENTINEL_PASSWORD=sentinel +REDIS_USE_DEFAULT_POOL_CONFIG=false +REDIS_PASSWORD=thingsboard diff --git a/docker/compose-utils.sh b/docker/compose-utils.sh index 49f3e20e27..19ca342b9b 100755 --- a/docker/compose-utils.sh +++ b/docker/compose-utils.sh @@ -84,8 +84,11 @@ function additionalComposeCacheArgs() { redis-cluster) CACHE_COMPOSE_ARGS="-f docker-compose.redis-cluster.yml" ;; + redis-sentinel) + CACHE_COMPOSE_ARGS="-f docker-compose.redis-sentinel.yml" + ;; *) - echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster'." >&2 + echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster' or 'redis-sentinel'." >&2 exit 1 esac echo $CACHE_COMPOSE_ARGS @@ -114,8 +117,11 @@ function additionalStartupServices() { redis-cluster) ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5" ;; + redis-sentinel) + ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES redis-master redis-slave redis-sentinel" + ;; *) - echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster'." >&2 + echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster' or 'redis-sentinel'." >&2 exit 1 esac @@ -160,8 +166,15 @@ function permissionList() { 1001 1001 tb-node/redis-cluster-data-5 " ;; + redis-sentinel) + PERMISSION_LIST="$PERMISSION_LIST + 1001 1001 tb-node/redis-sentinel-data-master + 1001 1001 tb-node/redis-sentinel-data-slave + 1001 1001 tb-node/redis-sentinel-data-sentinel + " + ;; *) - echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster'." >&2 + echo "Unknown CACHE value specified in the .env file: '${CACHE}'. Should be either 'redis' or 'redis-cluster' or 'redis-sentinel'." >&2 exit 1 esac diff --git a/docker/docker-compose.redis-sentinel.volumes.yml b/docker/docker-compose.redis-sentinel.volumes.yml new file mode 100644 index 0000000000..38534368ea --- /dev/null +++ b/docker/docker-compose.redis-sentinel.volumes.yml @@ -0,0 +1,40 @@ +# +# Copyright © 2016-2023 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. +# + +version: '3.0' + +services: + # Redis sentinel + redis-master: + volumes: + - redis-sentinel-data-master:/bitnami/redis/data + redis-slave: + volumes: + - redis-sentinel-data-slave:/bitnami/redis/data + redis-sentinel: + volumes: + - redis-sentinel-data-sentinel:/bitnami/redis/data + +volumes: + redis-sentinel-data-master: + external: + name: ${REDIS_SENTINEL_DATA_VOLUME_MASTER} + redis-sentinel-data-slave: + external: + name: ${REDIS_SENTINEL_DATA_VOLUME_SLAVE} + redis-sentinel-data-sentinel: + external: + name: ${REDIS_SENTINEL_DATA_VOLUME_SENTINEL} diff --git a/docker/docker-compose.redis-sentinel.yml b/docker/docker-compose.redis-sentinel.yml new file mode 100644 index 0000000000..32b3f1e826 --- /dev/null +++ b/docker/docker-compose.redis-sentinel.yml @@ -0,0 +1,119 @@ +# +# Copyright © 2016-2023 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. +# + +version: '3.0' + +services: + # Redis sentinel + redis-master: + image: 'bitnami/redis:7.0' + volumes: + - ./tb-node/redis-sentinel-data-master:/bitnami/redis/data + environment: + - 'REDIS_REPLICATION_MODE=master' + - 'REDIS_PASSWORD=thingsboard' + + redis-slave: + image: 'bitnami/redis:7.0' + volumes: + - ./tb-node/redis-sentinel-data-slave:/bitnami/redis/data + environment: + - 'REDIS_REPLICATION_MODE=slave' + - 'REDIS_MASTER_HOST=redis-master' + - 'REDIS_MASTER_PASSWORD=thingsboard' + - 'REDIS_PASSWORD=thingsboard' + depends_on: + - redis-master + + redis-sentinel: + image: 'bitnami/redis-sentinel:7.0' + volumes: + - ./tb-node/redis-sentinel-data-sentinel:/bitnami/redis/data + environment: + - 'REDIS_MASTER_HOST=redis-master' + - 'REDIS_MASTER_SET=mymaster' + - 'REDIS_SENTINEL_PASSWORD=sentinel' + - 'REDIS_MASTER_PASSWORD=thingsboard' + depends_on: + - redis-master + - redis-slave + + # ThingsBoard setup to use redis-sentinel + tb-core1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-core2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-rule-engine1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-rule-engine2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-mqtt-transport1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-mqtt-transport2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-http-transport1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-http-transport2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-coap-transport: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-lwm2m-transport: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-snmp-transport: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-vc-executor1: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel + tb-vc-executor2: + env_file: + - cache-redis-sentinel.env + depends_on: + - redis-sentinel diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index 2daf5c5b7b..399d0f4a56 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -16,21 +16,47 @@ package org.thingsboard.monitoring; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.EnableScheduling; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.monitoring.service.BaseMonitoringService; +import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; @SpringBootApplication @EnableScheduling @Slf4j public class ThingsboardMonitoringApplication { + @Autowired + private List> monitoringServices; + + @Value("${monitoring.monitoring_rate_ms}") + private int monitoringRateMs; + public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) .properties(Map.of("spring.config.name", "tb-monitoring")) .run(args); } + @EventListener(ApplicationReadyEvent.class) + public void startMonitoring() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor")); + scheduler.scheduleWithFixedDelay(() -> { + monitoringServices.forEach(monitoringService -> { + monitoringService.runChecks(); + }); + }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java similarity index 84% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java index 5f1ab49e91..4304ecdf0e 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java @@ -15,12 +15,10 @@ */ package org.thingsboard.monitoring.config; -import lombok.Data; +import java.util.List; -@Data -public class MonitoringTargetConfig { +public interface MonitoringConfig { - private String baseUrl; - private DeviceConfig device; + List getTargets(); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java new file mode 100644 index 0000000000..0e62670f81 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 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.monitoring.config; + +import java.util.UUID; + +public interface MonitoringTarget { + + UUID getDeviceId(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java similarity index 91% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java index 905858b4eb..a95aca18f4 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.coap.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java similarity index 92% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java index 548ad6d08b..94c0ea3a84 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config; +package org.thingsboard.monitoring.config.transport; import lombok.Data; import org.apache.commons.lang3.StringUtils; @@ -25,6 +25,7 @@ import java.util.UUID; public class DeviceConfig { private UUID id; + private String name; private DeviceCredentials credentials; public void setId(String id) { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java similarity index 91% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java index 3a3e8f612c..3b2be2343d 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.http.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java similarity index 91% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java index 4d97de8366..7d375abd6e 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.lwm2m.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java similarity index 92% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java index 7fef5b95dc..ebc91a327e 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.TransportType; @Component @ConditionalOnProperty(name = "monitoring.transports.mqtt.enabled", havingValue = "true") diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java similarity index 80% rename from monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java index d7619ea32d..6fb2e25ec9 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.data; +package org.thingsboard.monitoring.config.transport; import lombok.Data; -import org.thingsboard.monitoring.config.TransportType; @Data public class TransportInfo { private final TransportType transportType; - private final String url; + private final String baseUrl; @Override public String toString() { - return String.format("%s (%s)", transportType, url); + return String.format("%s transport (%s)", transportType, baseUrl); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java similarity index 73% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java index 0712d1d919..77d702f779 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java @@ -13,20 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config.service; +package org.thingsboard.monitoring.config.transport; import lombok.Data; -import org.thingsboard.monitoring.config.MonitoringTargetConfig; -import org.thingsboard.monitoring.config.TransportType; +import org.thingsboard.monitoring.config.MonitoringConfig; import java.util.List; @Data -public abstract class TransportMonitoringConfig { +public abstract class TransportMonitoringConfig implements MonitoringConfig { private int requestTimeoutMs; - private List targets; + private List targets; public abstract TransportType getTransportType(); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java new file mode 100644 index 0000000000..816f64fbce --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 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.monitoring.config.transport; + +import lombok.Data; +import org.thingsboard.monitoring.config.MonitoringTarget; + +import java.util.UUID; + +@Data +public class TransportMonitoringTarget implements MonitoringTarget { + + private String baseUrl; + private DeviceConfig device; // set manually during initialization + + @Override + public UUID getDeviceId() { + return device.getId(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java similarity index 67% rename from monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java rename to monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java index a3aaa7ec98..eeb085348b 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.config; +package org.thingsboard.monitoring.config.transport; import lombok.AllArgsConstructor; import lombok.Getter; -import org.thingsboard.monitoring.transport.TransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.CoapTransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.HttpTransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.Lwm2mTransportHealthChecker; -import org.thingsboard.monitoring.transport.impl.MqttTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.CoapTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.HttpTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.Lwm2mTransportHealthChecker; +import org.thingsboard.monitoring.service.transport.impl.MqttTransportHealthChecker; @AllArgsConstructor @Getter diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java index 8141c34586..3370d42462 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -15,16 +15,14 @@ */ package org.thingsboard.monitoring.data; -import org.thingsboard.monitoring.config.TransportType; - public class Latencies { public static final String WS_UPDATE = "wsUpdate"; public static final String WS_CONNECT = "wsConnect"; public static final String LOG_IN = "logIn"; - public static String transportRequest(TransportType transportType) { - return String.format("%sTransportRequest", transportType.name().toLowerCase()); + public static String request(String key) { + return String.format("%sRequest", key); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java similarity index 80% rename from monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java rename to monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java index 8157c4af5f..dc91a56a3c 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java @@ -15,13 +15,13 @@ */ package org.thingsboard.monitoring.data; -public class TransportFailureException extends RuntimeException { +public class ServiceFailureException extends RuntimeException { - public TransportFailureException(Throwable cause) { + public ServiceFailureException(Throwable cause) { super(cause.getMessage(), cause); } - public TransportFailureException(String message) { + public ServiceFailureException(String message) { super(message); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java similarity index 54% rename from monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java rename to monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index 0c742e7709..affca1ce09 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -13,34 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.transport; +package org.thingsboard.monitoring.service; -import com.fasterxml.jackson.databind.node.TextNode; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.TbClient; import org.thingsboard.monitoring.client.WsClient; -import org.thingsboard.monitoring.config.MonitoringTargetConfig; -import org.thingsboard.monitoring.config.TransportType; -import org.thingsboard.monitoring.config.service.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.MonitoringConfig; +import org.thingsboard.monitoring.config.MonitoringTarget; import org.thingsboard.monitoring.data.Latencies; import org.thingsboard.monitoring.data.MonitoredServiceKey; -import org.thingsboard.monitoring.data.TransportFailureException; -import org.thingsboard.monitoring.data.TransportInfo; -import org.thingsboard.monitoring.service.MonitoringReporter; +import org.thingsboard.monitoring.data.ServiceFailureException; import org.thingsboard.monitoring.util.TbStopWatch; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.UUID; +@RequiredArgsConstructor @Slf4j -public abstract class TransportHealthChecker { +public abstract class BaseHealthChecker { protected final C config; - protected final MonitoringTargetConfig target; - private TransportInfo transportInfo; + protected final T target; + + private Object info; @Autowired private MonitoringReporter reporter; @@ -51,73 +50,66 @@ public abstract class TransportHealthChecker, T extends MonitoringTarget> { + + @Autowired + private List configs; + private final List> healthCheckers = new LinkedList<>(); + private final List devices = new LinkedList<>(); + + @Autowired + private TbClient tbClient; + @Autowired + private WsClientFactory wsClientFactory; + @Autowired + private TbStopWatch stopWatch; + @Autowired + private MonitoringReporter reporter; + @Autowired + protected ApplicationContext applicationContext; + + @PostConstruct + private void init() { + tbClient.logIn(); + configs.forEach(config -> { + config.getTargets().forEach(target -> { + BaseHealthChecker healthChecker = (BaseHealthChecker) createHealthChecker(config, target); + log.info("Initializing {}", healthChecker.getClass().getSimpleName()); + healthChecker.initialize(tbClient); + devices.add(target.getDeviceId()); + healthCheckers.add(healthChecker); + }); + }); + } + + public final void runChecks() { + if (healthCheckers.isEmpty()) { + return; + } + try { + log.info("Starting {}", getName()); + stopWatch.start(); + String accessToken = tbClient.logIn(); + reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); + + try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { + wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); + + for (BaseHealthChecker healthChecker : healthCheckers) { + healthChecker.check(wsClient); + } + } + reporter.reportLatencies(tbClient); + log.debug("Finished {}", getName()); + } catch (Throwable error) { + try { + reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); + } catch (Throwable reportError) { + log.error("Error occurred during service failure reporting", reportError); + } + } + } + + protected abstract BaseHealthChecker createHealthChecker(C config, T target); + + protected abstract String getName(); + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java index 4649e31ac4..f41454a76a 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -52,8 +52,8 @@ public class MonitoringReporter { @Value("${monitoring.failures_threshold}") private int failuresThreshold; - @Value("${monitoring.send_repeated_failure_notification}") - private boolean sendRepeatedFailureNotification; + @Value("${monitoring.repeated_failure_notification}") + private int repeatedFailureNotification; @Value("${monitoring.latency.enabled}") private boolean latencyReportingEnabled; @@ -75,7 +75,7 @@ public class MonitoringReporter { return; } log.info("Latencies:\n{}", latencies.stream().map(latency -> latency.getKey() + ": " + latency.getAvg() + " ms") - .collect(Collectors.joining("\n"))); + .collect(Collectors.joining("\n")) + "\n"); if (!latencyReportingEnabled) return; @@ -86,7 +86,7 @@ public class MonitoringReporter { try { if (StringUtils.isBlank(reportingAssetId)) { - String assetName = "Monitoring"; + String assetName = "[Monitoring] Latencies"; Asset monitoringAsset = tbClient.findAsset(assetName).orElseGet(() -> { Asset asset = new Asset(); asset.setType("Monitoring"); @@ -122,7 +122,7 @@ public class MonitoringReporter { int failuresCount = failuresCounters.computeIfAbsent(serviceKey, k -> new AtomicInteger()).incrementAndGet(); ServiceFailureNotification notification = new ServiceFailureNotification(serviceKey, error, failuresCount); log.error(notification.getText()); - if (failuresCount == failuresThreshold || (sendRepeatedFailureNotification && failuresCount % failuresThreshold == 0)) { + if (failuresCount == failuresThreshold || (repeatedFailureNotification != 0 && failuresCount % repeatedFailureNotification == 0)) { notificationService.sendNotification(notification); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java new file mode 100644 index 0000000000..c822720f23 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -0,0 +1,139 @@ +/** + * Copyright © 2016-2023 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.monitoring.service.transport; + +import com.fasterxml.jackson.databind.node.TextNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.config.transport.DeviceConfig; +import org.thingsboard.monitoring.config.transport.TransportInfo; +import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.BaseHealthChecker; +import org.thingsboard.monitoring.util.ResourceUtils; +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.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; + +@Slf4j +public abstract class TransportHealthChecker extends BaseHealthChecker { + + private static final String DEFAULT_DEVICE_NAME = "[Monitoring] %s transport (%s)"; + private static final String DEFAULT_PROFILE_NAME = "[Monitoring] %s"; + + public TransportHealthChecker(C config, TransportMonitoringTarget target) { + super(config, target); + } + + @Override + protected void initialize(TbClient tbClient) { + String deviceName = String.format(DEFAULT_DEVICE_NAME, config.getTransportType(), target.getBaseUrl()); + Device device = tbClient.getTenantDevice(deviceName) + .orElseGet(() -> { + log.info("Creating new device '{}'", deviceName); + return createDevice(config.getTransportType(), deviceName, tbClient); + }); + DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(device.getId()) + .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + device.getId())); + + DeviceConfig deviceConfig = new DeviceConfig(); + deviceConfig.setId(device.getId().toString()); + deviceConfig.setName(deviceName); + deviceConfig.setCredentials(credentials); + target.setDevice(deviceConfig); + } + + @Override + protected String createTestPayload(String testValue) { + return JacksonUtil.newObjectNode().set(TEST_TELEMETRY_KEY, new TextNode(testValue)).toString(); + } + + @Override + protected Object getInfo() { + return new TransportInfo(getTransportType(), target.getBaseUrl()); + } + + @Override + protected String getKey() { + return getTransportType().name().toLowerCase() + "Transport"; + } + + protected abstract TransportType getTransportType(); + + + private Device createDevice(TransportType transportType, String name, TbClient tbClient) { + Device device = new Device(); + device.setName(name); + + DeviceCredentials credentials = new DeviceCredentials(); + credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); + + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + if (transportType != TransportType.LWM2M) { + device.setType("default"); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + } else { + tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() + .stream().findFirst() + .orElseGet(() -> { + TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); + log.info("Creating LwM2M resource"); + return tbClient.saveResource(newResource); + }); + String profileName = String.format(DEFAULT_PROFILE_NAME, transportType); + DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() + .stream().findFirst() + .orElseGet(() -> { + DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); + newProfile.setName(profileName); + log.info("Creating LwM2M device profile"); + return tbClient.saveDeviceProfile(newProfile); + }); + device.setType(profileName); + device.setDeviceProfileId(profile.getId()); + deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); + + credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); + NoSecClientCredential client = new NoSecClientCredential(); + client.setEndpoint(credentials.getCredentialsId()); + lwm2mCreds.setClient(client); + LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); + bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); + bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); + lwm2mCreds.setBootstrap(bootstrap); + credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); + } + return tbClient.saveDeviceWithCredentials(device, credentials).get(); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java new file mode 100644 index 0000000000..b3ce86e799 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 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.monitoring.service.transport; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.service.BaseHealthChecker; +import org.thingsboard.monitoring.service.BaseMonitoringService; + +@Service +@RequiredArgsConstructor +@Slf4j +public final class TransportsMonitoringService extends BaseMonitoringService { + + @Override + protected BaseHealthChecker createHealthChecker(TransportMonitoringConfig config, TransportMonitoringTarget target) { + return applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); + } + + @Override + protected String getName() { + return "transports check"; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java similarity index 86% rename from monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java rename to monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java index 3e36e62c58..f56415fa6a 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.monitoring.transport.impl; +package org.thingsboard.monitoring.service.transport.impl; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapClient; @@ -23,10 +23,10 @@ import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import org.thingsboard.monitoring.config.MonitoringTargetConfig; -import org.thingsboard.monitoring.config.TransportType; -import org.thingsboard.monitoring.config.service.CoapTransportMonitoringConfig; -import org.thingsboard.monitoring.transport.TransportHealthChecker; +import org.thingsboard.monitoring.config.transport.CoapTransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.service.transport.TransportHealthChecker; import java.io.IOException; @@ -37,7 +37,7 @@ public class CoapTransportHealthChecker extends TransportHealthChecker configs; - private final List> transportHealthCheckers = new LinkedList<>(); - private final List devices = new LinkedList<>(); - - private final TbClient tbClient; - private final WsClientFactory wsClientFactory; - private final TbStopWatch stopWatch; - private final MonitoringReporter reporter; - private final ApplicationContext applicationContext; - private ScheduledExecutorService scheduler; - @Value("${monitoring.transports.monitoring_rate_ms}") - private int monitoringRateMs; - - @PostConstruct - private void init() { - configs.forEach(config -> { - config.getTargets().stream() - .filter(target -> StringUtils.isNotBlank(target.getBaseUrl())) - .peek(target -> checkMonitoringTarget(config, target, tbClient)) - .forEach(target -> { - TransportHealthChecker transportHealthChecker = applicationContext.getBean(config.getTransportType().getServiceClass(), config, target); - transportHealthCheckers.add(transportHealthChecker); - devices.add(target.getDevice().getId()); - }); - }); - scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("monitoring-executor")); - } - - @EventListener(ApplicationReadyEvent.class) - public void startMonitoring() { - scheduler.scheduleWithFixedDelay(() -> { - try { - log.debug("Starting transports check"); - stopWatch.start(); - String accessToken = tbClient.logIn(); - reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); - - try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { - wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); - - for (TransportHealthChecker transportHealthChecker : transportHealthCheckers) { - transportHealthChecker.check(wsClient); - } - } - reporter.reportLatencies(tbClient); - log.debug("Finished transports check"); - } catch (Throwable error) { - try { - reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); - } catch (Throwable reportError) { - log.error("Error occurred during service failure reporting", reportError); - } - } - }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); - } - - private void checkMonitoringTarget(TransportMonitoringConfig config, MonitoringTargetConfig target, TbClient tbClient) { - DeviceConfig deviceConfig = target.getDevice(); - tbClient.logIn(); - - DeviceId deviceId; - if (deviceConfig == null || deviceConfig.getId() == null) { - String deviceName = String.format("[%s] Monitoring device (%s)", config.getTransportType(), target.getBaseUrl()); - Device device = tbClient.getTenantDevice(deviceName) - .orElseGet(() -> { - log.info("Creating new device '{}'", deviceName); - return createDevice(config.getTransportType(), deviceName, tbClient); - }); - deviceId = device.getId(); - target.getDevice().setId(deviceId.toString()); - } else { - deviceId = new DeviceId(deviceConfig.getId()); - } - - log.info("Using device {} for {} monitoring", deviceId, config.getTransportType()); - DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(deviceId) - .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + deviceId)); - target.getDevice().setCredentials(credentials); - } - - private Device createDevice(TransportType transportType, String name, TbClient tbClient) { - Device device = new Device(); - device.setName(name); - - DeviceCredentials credentials = new DeviceCredentials(); - credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); - - DeviceData deviceData = new DeviceData(); - deviceData.setConfiguration(new DefaultDeviceConfiguration()); - if (transportType != TransportType.LWM2M) { - device.setType("default"); - deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); - credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); - } else { - tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() - .stream().findFirst() - .orElseGet(() -> { - TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); - log.info("Creating LwM2M resource"); - return tbClient.saveResource(newResource); - }); - String profileName = "LwM2M Monitoring"; - DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() - .stream().findFirst() - .orElseGet(() -> { - DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); - newProfile.setName(profileName); - log.info("Creating LwM2M device profile"); - return tbClient.saveDeviceProfile(newProfile); - }); - device.setType(profileName); - device.setDeviceProfileId(profile.getId()); - deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); - - credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); - LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); - NoSecClientCredential client = new NoSecClientCredential(); - client.setEndpoint(credentials.getCredentialsId()); - lwm2mCreds.setClient(client); - LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); - bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); - bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); - lwm2mCreds.setBootstrap(bootstrap); - credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); - } - return tbClient.saveDeviceWithCredentials(device, credentials).get(); - } - -} diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json index 7f93a7e6b6..095f4859e9 100644 --- a/monitoring/src/main/resources/lwm2m/device_profile.json +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -1,5 +1,4 @@ { - "name": "LwM2M Monitoring", "type": "DEFAULT", "image": null, "defaultQueueName": null, diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml index d89886cd72..88979bff1a 100644 --- a/monitoring/src/main/resources/tb-monitoring.yml +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -32,79 +32,62 @@ monitoring: # WebSocket request timeout request_timeout_ms: '${WS_REQUEST_TIMEOUT_MS:3000}' + # Checks frequency in milliseconds + monitoring_rate_ms: '${MONITORING_RATE_MS:10000}' # Maximum time between request to transport and WebSocket update check_timeout_ms: '${CHECK_TIMEOUT_MS:5000}' # Failures threshold for notifying failures_threshold: '${FAILURES_THRESHOLD:2}' - # Whether to notify about next failures after first notification (will notify after each FAILURES_THRESHOLD failures) - send_repeated_failure_notification: '${SEND_REPEATED_FAILURE_NOTIFICATION:true}' + # Notify after each REPEATED_FAILURE_NOTIFICATION subsequent failures, 0 to notify only once on first failure + repeated_failure_notification: '${REPEATED_FAILURE_NOTIFICATION:4}' transports: - # Transports check frequency in milliseconds - monitoring_rate_ms: '${TRANSPORTS_MONITORING_RATE_MS:10000}' - mqtt: - # Enable MQTT checks + # Enable MQTT transport checks enabled: '${MQTT_TRANSPORT_MONITORING_ENABLED:true}' # MQTT request timeout in milliseconds request_timeout_ms: '${MQTT_REQUEST_TIMEOUT_MS:4000}' # MQTT QoS qos: '${MQTT_QOS_LEVEL:1}' targets: - # MQTT base url, tcp://DOMAIN:1883 by default + # MQTT transport base url, tcp://DOMAIN:1883 by default - base_url: '${MQTT_TRANSPORT_BASE_URL:tcp://${monitoring.domain}:1883}' - device: - # MQTT device to push telemetry for. If not set - device will be found or created automatically - id: '${MQTT_TRANSPORT_TARGET_DEVICE_ID:}' # To add more targets, use following environment variables: - # monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[1].device.id, - # monitoring.transports.mqtt.targets[2].base_url, monitoring.transports.mqtt.targets[2].device.id, etc. + # monitoring.transports.mqtt.targets[1].base_url, monitoring.transports.mqtt.targets[2].base_url, etc. coap: - # Enable CoAP checks + # Enable CoAP transport checks enabled: '${COAP_TRANSPORT_MONITORING_ENABLED:true}' # CoAP request timeout in milliseconds request_timeout_ms: '${COAP_REQUEST_TIMEOUT_MS:4000}' targets: - # CoAP base url, coap://DOMAIN by default + # CoAP transport base url, coap://DOMAIN by default - base_url: '${COAP_TRANSPORT_BASE_URL:coap://${monitoring.domain}}' - # CoAP device to push telemetry for. If not set - device will be found or created automatically - device: - id: '${COAP_TRANSPORT_TARGET_DEVICE_ID:}' # To add more targets, use following environment variables: - # monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[1].device.id, - # monitoring.transports.coap.targets[2].base_url, monitoring.transports.coap.targets[2].device.id, etc. + # monitoring.transports.coap.targets[1].base_url, monitoring.transports.coap.targets[2].base_url, etc. http: - # Enable HTTP checks + # Enable HTTP transport checks enabled: '${HTTP_TRANSPORT_MONITORING_ENABLED:true}' # HTTP request timeout in milliseconds request_timeout_ms: '${HTTP_REQUEST_TIMEOUT_MS:4000}' targets: - # HTTP base url, https://DOMAIN by default - - base_url: '${HTTP_TRANSPORT_BASE_URL:https://${monitoring.domain}}' - device: - # HTTP device to push telemetry for. If not set - device will be found or created automatically - id: '${HTTP_TRANSPORT_TARGET_DEVICE_ID:}' + # HTTP transport base url, http://DOMAIN by default + - base_url: '${HTTP_TRANSPORT_BASE_URL:http://${monitoring.domain}}' # To add more targets, use following environment variables: - # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[1].device.id, - # monitoring.transports.http.targets[2].base_url, monitoring.transports.http.targets[2].device.id, etc. + # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[2].base_url, etc. lwm2m: - # Enable LwM2M checks + # Enable LwM2M transport checks enabled: '${LWM2M_TRANSPORT_MONITORING_ENABLED:true}' # LwM2M request timeout in milliseconds request_timeout_ms: '${LWM2M_REQUEST_TIMEOUT_MS:4000}' targets: - # LwM2M base url, coap://DOMAIN:5685 by default + # LwM2M transport base url, coap://DOMAIN:5685 by default - base_url: '${LWM2M_TRANSPORT_BASE_URL:coap://${monitoring.domain}:5685}' - # LwM2M device to push telemetry for. If not set - device will be found or created automatically - device: - id: '${LWM2M_TRANSPORT_TARGET_DEVICE_ID:}' # To add more targets, use following environment variables: - # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[1].device.id, - # monitoring.transports.lwm2m.targets[2].base_url, monitoring.transports.lwm2m.targets[2].device.id, etc. + # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[2].base_url, etc. notification_channels: slack: diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index 4a88d5e8cd..340f0d0eb8 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -26,6 +26,10 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.redisCluster=true +- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis sentinel: + + mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.redisSentinel=true + - Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory in Hybrid mode (postgres + cassandra): mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.hybridMode=true diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index ffbe14ed52..dd3948c495 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -43,6 +43,7 @@ import static org.testng.Assert.fail; @Slf4j public class ContainerTestSuite { final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); + final static boolean IS_REDIS_SENTINEL = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisSentinel")); final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode")); final static String QUEUE_TYPE = System.getProperty("blackBoxTests.queue", "kafka"); private static final String SOURCE_DIR = "./../../docker/"; @@ -80,8 +81,9 @@ public class ContainerTestSuite { installTb = new ThingsBoardDbInstaller(); installTb.createVolumes(); log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER); + log.info("System property of blackBoxTests.redisSentinel is {}", IS_REDIS_SENTINEL); log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE); - boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); + boolean skipTailChildContainers = Boolean.parseBoolean(System.getProperty("blackBoxTests.skipTailChildContainers")); try { final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/"; log.info("targetDir {}", targetDir); @@ -109,8 +111,8 @@ public class ContainerTestSuite { new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid-test-extras.yml" : "docker-compose.postgres-test-extras.yml")), new File(targetDir + "docker-compose.postgres.volumes.yml"), new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"), - new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")), - new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml")), + new File(targetDir + resolveRedisComposeFile()), + new File(targetDir + resolveRedisComposeVolumesFile()), new File(targetDir + ("docker-selenium.yml")) )); @@ -154,6 +156,7 @@ public class ContainerTestSuite { testContainer = new DockerComposeContainerImpl<>(composeFiles) .withPull(false) .withLocalCompose(true) + .withOptions("--compatibility") .withTailChildContainers(!skipTailChildContainers) .withEnv(installTb.getEnv()) .withEnv(queueEnv) @@ -175,6 +178,27 @@ public class ContainerTestSuite { fail("Failed to create test container"); } } + + private static String resolveRedisComposeFile() { + if (IS_REDIS_CLUSTER) { + return "docker-compose.redis-cluster.yml"; + } + if (IS_REDIS_SENTINEL) { + return "docker-compose.redis-sentinel.yml"; + } + return "docker-compose.redis.yml"; + } + + private static String resolveRedisComposeVolumesFile() { + if (IS_REDIS_CLUSTER) { + return "docker-compose.redis-cluster.volumes.yml"; + } + if (IS_REDIS_SENTINEL) { + return "docker-compose.redis-sentinel.volumes.yml"; + } + return "docker-compose.redis.volumes.yml"; + } + public void stop() { if (isActive) { testContainer.stop(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index e6c9856094..e41bbc3449 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -32,12 +32,14 @@ import java.util.stream.IntStream; public class ThingsBoardDbInstaller { final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster")); + final static boolean IS_REDIS_SENTINEL = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisSentinel")); final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode")); private final static String POSTGRES_DATA_VOLUME = "tb-postgres-test-data-volume"; private final static String CASSANDRA_DATA_VOLUME = "tb-cassandra-test-data-volume"; private final static String REDIS_DATA_VOLUME = "tb-redis-data-volume"; private final static String REDIS_CLUSTER_DATA_VOLUME = "tb-redis-cluster-data-volume"; + private final static String REDIS_SENTINEL_DATA_VOLUME = "tb-redis-sentinel-data-volume"; private final static String TB_LOG_VOLUME = "tb-log-test-volume"; private final static String TB_COAP_TRANSPORT_LOG_VOLUME = "tb-coap-transport-log-test-volume"; private final static String TB_LWM2M_TRANSPORT_LOG_VOLUME = "tb-lwm2m-transport-log-test-volume"; @@ -54,6 +56,7 @@ public class ThingsBoardDbInstaller { private final String redisDataVolume; private final String redisClusterDataVolume; + private final String redisSentinelDataVolume; private final String tbLogVolume; private final String tbCoapTransportLogVolume; private final String tbLwm2mTransportLogVolume; @@ -65,6 +68,7 @@ public class ThingsBoardDbInstaller { public ThingsBoardDbInstaller() { log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER); + log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_SENTINEL); log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE); List composeFiles = new ArrayList<>(Arrays.asList( new File("./../../docker/docker-compose.yml"), @@ -73,12 +77,8 @@ public class ThingsBoardDbInstaller { ? new File("./../../docker/docker-compose.hybrid.yml") : new File("./../../docker/docker-compose.postgres.yml"), new File("./../../docker/docker-compose.postgres.volumes.yml"), - IS_REDIS_CLUSTER - ? new File("./../../docker/docker-compose.redis-cluster.yml") - : new File("./../../docker/docker-compose.redis.yml"), - IS_REDIS_CLUSTER - ? new File("./../../docker/docker-compose.redis-cluster.volumes.yml") - : new File("./../../docker/docker-compose.redis.volumes.yml") + resolveRedisComposeFile(), + resolveRedisComposeVolumesFile() )); if (IS_HYBRID_MODE) { composeFiles.add(new File("./../../docker/docker-compose.cassandra.volumes.yml")); @@ -94,6 +94,7 @@ public class ThingsBoardDbInstaller { cassandraDataVolume = project + "_" + CASSANDRA_DATA_VOLUME; redisDataVolume = project + "_" + REDIS_DATA_VOLUME; redisClusterDataVolume = project + "_" + REDIS_CLUSTER_DATA_VOLUME; + redisSentinelDataVolume = project + "_" + REDIS_SENTINEL_DATA_VOLUME; tbLogVolume = project + "_" + TB_LOG_VOLUME; tbCoapTransportLogVolume = project + "_" + TB_COAP_TRANSPORT_LOG_VOLUME; tbLwm2mTransportLogVolume = project + "_" + TB_LWM2M_TRANSPORT_LOG_VOLUME; @@ -121,12 +122,36 @@ public class ThingsBoardDbInstaller { for (int i = 0; i < 6; i++) { env.put("REDIS_CLUSTER_DATA_VOLUME_" + i, redisClusterDataVolume + '-' + i); } + } else if (IS_REDIS_SENTINEL) { + env.put("REDIS_SENTINEL_DATA_VOLUME_MASTER", redisSentinelDataVolume + "-" + "master"); + env.put("REDIS_SENTINEL_DATA_VOLUME_SLAVE", redisSentinelDataVolume + "-" + "slave"); + env.put("REDIS_SENTINEL_DATA_VOLUME_SENTINEL", redisSentinelDataVolume + "-" + "sentinel"); } else { env.put("REDIS_DATA_VOLUME", redisDataVolume); } dockerCompose.withEnv(env); } + private static File resolveRedisComposeVolumesFile() { + if (IS_REDIS_CLUSTER) { + return new File("./../../docker/docker-compose.redis-cluster.volumes.yml"); + } + if (IS_REDIS_SENTINEL) { + return new File("./../../docker/docker-compose.redis-sentinel.volumes.yml"); + } + return new File("./../../docker/docker-compose.redis.volumes.yml"); + } + + private static File resolveRedisComposeFile() { + if (IS_REDIS_CLUSTER) { + return new File("./../../docker/docker-compose.redis-cluster.yml"); + } + if (IS_REDIS_SENTINEL) { + return new File("./../../docker/docker-compose.redis-sentinel.yml"); + } + return new File("./../../docker/docker-compose.redis.yml"); + } + public Map getEnv() { return env; } @@ -163,18 +188,30 @@ public class ThingsBoardDbInstaller { dockerCompose.withCommand("volume create " + tbVcExecutorLogVolume); dockerCompose.invokeDocker(); - String additionalServices = ""; + StringBuilder additionalServices = new StringBuilder(); if (IS_HYBRID_MODE) { - additionalServices += " cassandra"; + additionalServices.append(" cassandra"); } if (IS_REDIS_CLUSTER) { for (int i = 0; i < 6; i++) { - additionalServices = additionalServices + " redis-node-" + i; + additionalServices.append(" redis-node-").append(i); dockerCompose.withCommand("volume create " + redisClusterDataVolume + '-' + i); dockerCompose.invokeDocker(); } + } else if (IS_REDIS_SENTINEL) { + additionalServices.append(" redis-master"); + dockerCompose.withCommand("volume create " + redisSentinelDataVolume +"-" + "master"); + dockerCompose.invokeDocker(); + + additionalServices.append(" redis-slave"); + dockerCompose.withCommand("volume create " + redisSentinelDataVolume + '-' + "slave"); + dockerCompose.invokeDocker(); + + additionalServices.append(" redis-sentinel"); + dockerCompose.withCommand("volume create " + redisSentinelDataVolume + '-' + "sentinel"); + dockerCompose.invokeDocker(); } else { - additionalServices += " redis"; + additionalServices.append(" redis"); dockerCompose.withCommand("volume create " + redisDataVolume); dockerCompose.invokeDocker(); } @@ -189,7 +226,7 @@ public class ThingsBoardDbInstaller { try { dockerCompose.withCommand("down -v"); dockerCompose.invokeCompose(); - } catch (Exception e) {} + } catch (Exception ignored) {} } } @@ -204,13 +241,22 @@ public class ThingsBoardDbInstaller { dockerCompose.withCommand("volume rm -f " + postgresDataVolume + " " + tbLogVolume + " " + tbCoapTransportLogVolume + " " + tbLwm2mTransportLogVolume + " " + tbHttpTransportLogVolume + - " " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume + - (IS_REDIS_CLUSTER - ? IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + '-' + i).collect(Collectors.joining()) - : redisDataVolume)); + " " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume + resolveRedisComposeVolumeLog()); dockerCompose.invokeDocker(); } + private String resolveRedisComposeVolumeLog() { + if (IS_REDIS_CLUSTER) { + return IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + "-" + i).collect(Collectors.joining()); + } + if (IS_REDIS_SENTINEL) { + return redisSentinelDataVolume + "-" + "master " + " " + + redisSentinelDataVolume + "-" + "slave" + " " + + redisSentinelDataVolume + " " + "sentinel"; + } + return redisDataVolume; + } + private void copyLogs(String volumeName, String targetDir) { File tbLogsDir = new File(targetDir); tbLogsDir.mkdirs(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java index e400a67567..f6c901c76c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java @@ -60,6 +60,12 @@ public class DevicePageElements extends OtherPageElementsHelper { private static final String DEVICE_STATE_SELECT = "//div[contains(@class,'tb-filter-panel')]//mat-select[@role='combobox']"; private static final String LIST_OF_DEVICES_STATE = "//div[@class='status']"; private static final String LIST_OF_DEVICES_PROFILE = "//mat-cell[contains(@class,'deviceProfileName')]"; + private static final String MAKE_DEVICE_PUBLIC_BTN = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'share')]/parent::button"; + private static final String DEVICE_IS_PUBLIC_CHECKBOX = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'check_box')]"; + private static final String MAKE_DEVICE_PUBLIC_BTN_DETAILS_TAB = "//span[contains(text(),'Make device public')]/parent::button"; + private static final String MAKE_DEVICE_PRIVATE_BTN = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'reply')]/parent::button"; + private static final String DEVICE_IS_PRIVATE_CHECKBOX = DEVICE + "/ancestor::mat-row//mat-icon[contains(text(),'check_box_outline_blank')]"; + private static final String MAKE_DEVICE_PRIVATE_BTN_DETAILS_TAB = "//span[contains(text(),'Make device private')]/parent::button"; public WebElement device(String deviceName) { return waitUntilElementToBeClickable(String.format(DEVICE, deviceName)); @@ -204,4 +210,28 @@ public class DevicePageElements extends OtherPageElementsHelper { public List listOfDevicesProfile() { return waitUntilVisibilityOfElementsLocated(LIST_OF_DEVICES_PROFILE); } + + public WebElement makeDevicePublicBtn(String deviceName) { + return waitUntilElementToBeClickable(String.format(MAKE_DEVICE_PUBLIC_BTN, deviceName)); + } + + public WebElement deviceIsPublicCheckbox(String deviceName) { + return waitUntilVisibilityOfElementLocated(String.format(DEVICE_IS_PUBLIC_CHECKBOX, deviceName)); + } + + public WebElement makeDevicePublicBtnDetailsTab() { + return waitUntilElementToBeClickable(MAKE_DEVICE_PUBLIC_BTN_DETAILS_TAB); + } + + public WebElement makeDevicePrivateBtn(String deviceName) { + return waitUntilElementToBeClickable(String.format(MAKE_DEVICE_PRIVATE_BTN, deviceName)); + } + + public WebElement deviceIsPrivateCheckbox(String deviceName) { + return waitUntilVisibilityOfElementLocated(String.format(DEVICE_IS_PRIVATE_CHECKBOX, deviceName)); + } + + public WebElement makeDevicePrivateBtnDetailsTab() { + return waitUntilElementToBeClickable(MAKE_DEVICE_PRIVATE_BTN_DETAILS_TAB); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java index 80f4ff49b3..599b9f104f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java @@ -123,4 +123,24 @@ public class DevicePageHelper extends DevicePageElements { sleep(2); //wait until the action is counted submitBtn().click(); } + + public void makeDevicePublicByRightSideBtn(String deviceName) { + makeDevicePublicBtn(deviceName).click(); + warningPopUpYesBtn().click(); + } + + public void makeDevicePublicFromDetailsTab() { + makeDevicePublicBtnDetailsTab().click(); + warningPopUpYesBtn().click(); + } + + public void makeDevicePrivateByRightSideBtn(String deviceName) { + makeDevicePrivateBtn(deviceName).click(); + warningPopUpYesBtn().click(); + } + + public void makeDevicePrivateFromDetailsTab() { + makeDevicePrivateBtnDetailsTab().click(); + warningPopUpYesBtn().click(); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java index 8c0a45aa03..c4246e6df5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java @@ -34,6 +34,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; +import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; @Feature("Assign to customer") public class AssignToCustomerTest extends AbstractDeviceTest { @@ -58,7 +59,7 @@ public class AssignToCustomerTest extends AbstractDeviceTest { @AfterClass public void deleteCustomer() { deleteCustomerById(customerId); - deleteCustomerByName("Public"); + deleteCustomerByName(PUBLIC_CUSTOMER_NAME); deleteDeviceByName(device1.getName()); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePrivateTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePrivateTest.java new file mode 100644 index 0000000000..df14547bda --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePrivateTest.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2023 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.msa.ui.tests.devicessmoke; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import org.openqa.selenium.WebElement; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; +import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; + +@Feature("Make device private") +public class MakeDevicePrivateTest extends AbstractDeviceTest { + + private CustomerPageHelper customerPage; + + @BeforeMethod + public void createPublicDevice() { + customerPage = new CustomerPageHelper(driver); + Device device = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())); + testRestClient.setDevicePublic(device.getId()); + deviceName = device.getName(); + } + + @AfterClass + public void deletePublicCustomer() { + deleteCustomerByName(PUBLIC_CUSTOMER_NAME); + } + + @Test(groups = "smoke") + @Description("Make device private by right side btn") + public void makeDevicePrivateByRightSideBtn() { + sideBarMenuView.goToDevicesPage(); + devicePage.makeDevicePrivateByRightSideBtn(deviceName); + WebElement customerInColumn = devicePage.deviceCustomerOnPage(deviceName); + assertIsDisplayed(devicePage.deviceIsPrivateCheckbox(deviceName)); + assertInvisibilityOfElement(customerInColumn); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + devicePage.assertEntityIsNotPresent(deviceName); + } + + @Test(groups = "smoke") + @Description("Make device public by btn on details tab") + public void makeDevicePrivateFromDetailsTab() { + sideBarMenuView.goToDevicesPage(); + devicePage.device(deviceName).click(); + WebElement customerInColumn = devicePage.deviceCustomerOnPage(deviceName); + devicePage.makeDevicePrivateFromDetailsTab(); + devicePage.closeDeviceDetailsViewBtn().click(); + assertIsDisplayed(devicePage.deviceIsPrivateCheckbox(deviceName)); + assertInvisibilityOfElement(customerInColumn); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + devicePage.assertEntityIsNotPresent(deviceName); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePublicTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePublicTest.java new file mode 100644 index 0000000000..ad5b379437 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePublicTest.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2023 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.msa.ui.tests.devicessmoke; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; +import org.thingsboard.server.msa.ui.tabs.AssignDeviceTabHelper; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; +import static org.thingsboard.server.msa.ui.utils.Const.PUBLIC_CUSTOMER_NAME; + +@Feature("Make device public") +public class MakeDevicePublicTest extends AbstractDeviceTest { + + private CustomerPageHelper customerPage; + private AssignDeviceTabHelper assignDeviceTab; + private String deviceName1; + + @BeforeClass + public void createFirstDevice() { + customerPage = new CustomerPageHelper(driver); + assignDeviceTab = new AssignDeviceTabHelper(driver); + + deviceName1 = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())).getName(); + } + + @AfterClass + public void cleanUp() { + deleteCustomerByName(PUBLIC_CUSTOMER_NAME); + deleteDeviceByName(deviceName1); + } + + @BeforeMethod + public void createSecondDevice() { + deviceName = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME + random())).getName(); + } + + @Test(groups = "smoke", priority = 10) + @Description("Make device public by right side btn") + public void makeDevicePublicByRightSideBtn() { + sideBarMenuView.goToDevicesPage(); + devicePage.makeDevicePublicByRightSideBtn(deviceName); + + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer in customer column is Public customer") + .isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke", priority = 10) + @Description("Make device public by btn on details tab") + public void makeDevicePublicFromDetailsTab() { + sideBarMenuView.goToDevicesPage(); + devicePage.device(deviceName).click(); + devicePage.makeDevicePublicFromDetailsTab(); + devicePage.closeDeviceDetailsViewBtn().click(); + + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer in customer column is Public customer") + .isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke", priority = 20) + @Description("Make device public by assign to public customer") + public void makeDevicePublicByAssignToPublicCustomer() { + sideBarMenuView.goToDevicesPage(); + devicePage.assignBtn(deviceName).click(); + assignDeviceTab.assignOnCustomer(PUBLIC_CUSTOMER_NAME); + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()).isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke", priority = 20) + @Description("Make several devices public by assign to public customer") + public void makePublicSeveralDevicesByAssignOnPublicCustomer() { + sideBarMenuView.goToDevicesPage(); + devicePage.assignSelectedDevices(deviceName, deviceName1); + assignDeviceTab.assignOnCustomer(PUBLIC_CUSTOMER_NAME); + assertIsDisplayed(devicePage.deviceIsPublicCheckbox(deviceName)); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer in customer column is Public customer") + .isEqualTo(PUBLIC_CUSTOMER_NAME); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(PUBLIC_CUSTOMER_NAME).click(); + assertIsDisplayed(devicePage.device(deviceName)); + assertIsDisplayed(devicePage.device(deviceName1)); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java index 0b9e173e6d..892b4af9bc 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java @@ -47,4 +47,5 @@ public class Const { public static final String DEVICE_PROFILE_IS_REQUIRED_MESSAGE = "Device profile is required"; public static final String DEVICE_ACTIVE_STATE = "Active"; public static final String DEVICE_INACTIVE_STATE = "Inactive"; + public static final String PUBLIC_CUSTOMER_NAME = "Public"; } diff --git a/pom.xml b/pom.xml index a8c06268fd..f4c4970d7f 100755 --- a/pom.xml +++ b/pom.xml @@ -111,7 +111,7 @@ 1.11.747 1.105.0 2.1.0 - 3.2.0 + 3.6.7 1.5.0 1.5.4 1.9.4 @@ -1836,6 +1836,16 @@ com.microsoft.azure azure-servicebus ${azure-servicebus.version} + + + com.nimbusds + content-type + + + org.ow2.asm + asm + + org.passay diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 7ea553fe5c..332709e3fa 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 346ec48eae..c8a45c161b 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -73,7 +73,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -93,6 +93,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 4e8167d89d..5de2484860 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 1e0b1ebcd4..ed25d00732 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 9f086bcbc5..f7fa3902b8 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -46,7 +46,7 @@ cache: type: "${CACHE_TYPE:redis}" redis: - # standalone or cluster + # standalone or cluster or sentinel connection: type: "${REDIS_CONNECTION_TYPE:standalone}" standalone: @@ -66,6 +66,16 @@ redis: nodes: "${REDIS_NODES:}" # Maximum number of redirects to follow when executing commands across the cluster. max-redirects: "${REDIS_MAX_REDIRECTS:12}" + # if set false will be used pool config build from values of the pool config section + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + sentinel: + # name of master node + master: "${REDIS_MASTER:}" + # comma-separated list of "host:port" pairs of sentinels + sentinels: "${REDIS_SENTINELS:}" + # password to authenticate with sentinel + password: "${REDIS_SENTINEL_PASSWORD:}" + # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index ee3e232bba..12f6300b93 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -816,7 +816,8 @@ export class EntityService { ); } - public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[], config?: RequestConfig): Observable> { + public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[], + entityTypes?: EntityType[], config?: RequestConfig): Observable> { if (!types.length) { return of([]); } @@ -832,7 +833,7 @@ export class EntityService { entitiesKeysByQuery$ = of({ attribute: [], timeseries: [], - entityTypes: [], + entityTypes: entityTypes || [], }); } return entitiesKeysByQuery$.pipe( diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 243c5e369b..8355ce2f61 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -366,25 +366,18 @@ export class DashboardUtilsService { } const dataKeys = newDatasource.dataKeys; newDatasource.dataKeys = []; - dataKeys.forEach(dataKey => { - newDatasource.dataKeys.push(this.convertDataKeyFromWidgetType(widgetTypeDescriptor, config, dataKey)); - }); + if (widgetTypeDescriptor.type === widgetType.alarm) { + dataKeys.forEach(dataKey => { + const newDataKey = deepClone(dataKey); + newDataKey.funcBody = null; + newDataKey.type = DataKeyType.alarm; + newDatasource.dataKeys.push(newDataKey); + }); + } } return newDatasource; } - private convertDataKeyFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, dataKey: DataKey): DataKey { - const newDataKey = deepClone(dataKey); - newDataKey.funcBody = null; - if (widgetTypeDescriptor.type === widgetType.alarm) { - newDataKey.type = DataKeyType.alarm; - } else { - newDataKey.type = DataKeyType.timeseries; - newDataKey.name = newDataKey.label; - } - return newDataKey; - } - private validateAndUpdateState(state: DashboardState) { if (!state.layouts) { state.layouts = this.createDefaultLayouts(); diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index b3a14b0f61..5cc9a49d42 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -300,6 +300,7 @@ import * as QueueFormComponent from '@home/components/queue/queue-form.component import * as AssetProfileComponent from '@home/components/profile/asset-profile.component'; import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component'; import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component'; +import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component'; import { IModulesMap } from '@modules/common/modules-map.models'; @@ -418,6 +419,7 @@ class ModulesMap implements IModulesMap { '@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent, '@shared/components/dashboard-select.component': DashboardSelectComponent, '@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent, + '@shared/components/rule-chain/rule-chain-select.component': RuleChainSelectComponent, '@shared/components/time/datetime-period.component': DatetimePeriodComponent, '@shared/components/time/datetime.component': DatetimeComponent, '@shared/components/time/timezone-select.component': TimezoneSelectComponent, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index fb5336de66..5af51031bf 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -51,6 +51,7 @@ [widget]="widget" [widgetConfigMode]="widgetConfigMode" [hideHeader]="widgetConfigMode === widgetConfigModes.basic" + isAdd formControlName="widgetConfig"> } = { - 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent + 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent, + 'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent }; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html new file mode 100644 index 0000000000..ee76bdf472 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html @@ -0,0 +1,86 @@ + + + + + + + + +
+
widget-config.appearance
+
+ + {{ 'widget-config.card-title' | translate }} + + + + +
+
+ + {{ 'widget-config.card-icon' | translate }} + +
+ + + + + +
+
+
+
{{ 'widget-config.text-color' | translate }}
+
+ + + +
+
+
+
{{ 'widget-config.background' | translate }}
+
+ + + +
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts new file mode 100644 index 0000000000..4ddd068bb4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } 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'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + DataKey, + Datasource, + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation +} from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +@Component({ + selector: 'tb-entities-table-basic-config', + templateUrl: './entities-table-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponent { + + public get displayTimewindowConfig(): boolean { + const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + public get datasource(): Datasource { + const datasources: Datasource[] = this.entitiesTableWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + entitiesTableWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.entitiesTableWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [{ name: 'name', type: DataKeyType.entityField }]); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + this.entitiesTableWidgetConfigForm = this.fb.group({ + timewindowConfig: [{ + useDashboardTimewindow: configData.config.useDashboardTimewindow, + displayTimewindow: configData.config.useDashboardTimewindow, + timewindow: configData.config.timewindow + }, []], + datasources: [configData.config.datasources, []], + columns: [this.getColumns(configData.config.datasources), []], + showTitle: [configData.config.showTitle, []], + title: [configData.config.settings?.entitiesTitle, []], + showTitleIcon: [configData.config.showTitleIcon, []], + titleIcon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + color: [configData.config.color, []], + backgroundColor: [configData.config.backgroundColor, []], + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; + this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; + this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; + this.widgetConfig.config.datasources = config.datasources; + this.setColumns(config.columns, this.widgetConfig.config.datasources); + this.widgetConfig.config.actions = config.actions; + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.entitiesTitle = config.title; + this.widgetConfig.config.showTitleIcon = config.showTitleIcon; + this.widgetConfig.config.titleIcon = config.titleIcon; + this.widgetConfig.config.iconColor = config.iconColor; + this.widgetConfig.config.color = config.color; + this.widgetConfig.config.backgroundColor = config.backgroundColor; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showTitleIcon']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.entitiesTableWidgetConfigForm.get('showTitle').value; + const showTitleIcon: boolean = this.entitiesTableWidgetConfigForm.get('showTitleIcon').value; + if (showTitle) { + this.entitiesTableWidgetConfigForm.get('title').enable(); + this.entitiesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false}); + if (showTitleIcon) { + this.entitiesTableWidgetConfigForm.get('titleIcon').enable(); + this.entitiesTableWidgetConfigForm.get('iconColor').enable(); + } else { + this.entitiesTableWidgetConfigForm.get('titleIcon').disable(); + this.entitiesTableWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.entitiesTableWidgetConfigForm.get('title').disable(); + this.entitiesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false}); + this.entitiesTableWidgetConfigForm.get('titleIcon').disable(); + this.entitiesTableWidgetConfigForm.get('iconColor').disable(); + } + this.entitiesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false}); + this.entitiesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent}); + this.entitiesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent}); + } + + private getColumns(datasources?: Datasource[]): DataKey[] { + if (datasources && datasources.length) { + return datasources[0].dataKeys || []; + } + return []; + } + + private setColumns(columns: DataKey[], datasources?: Datasource[]) { + if (datasources && datasources.length) { + datasources[0].dataKeys = columns; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts index 62d25249e9..b59170fb89 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts @@ -23,13 +23,15 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models' import { Datasource, datasourcesHasAggregation, - datasourcesHasOnlyComparisonAggregation + datasourcesHasOnlyComparisonAggregation, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @Component({ selector: 'tb-simple-card-basic-config', templateUrl: './simple-card-basic-config.component.html', - styleUrls: ['../basic-config.scss', '../../widget-config.scss'] + styleUrls: ['../basic-config.scss'] }) export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { @@ -46,14 +48,19 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { simpleCardWidgetConfigForm: UntypedFormGroup; constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, private fb: UntypedFormBuilder) { - super(store); + super(store, widgetConfigComponent); } protected configForm(): UntypedFormGroup { return this.simpleCardWidgetConfigForm; } + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]); + } + protected onConfigSet(configData: WidgetConfigComponentData) { this.simpleCardWidgetConfigForm = this.fb.group({ timewindowConfig: [{ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html new file mode 100644 index 0000000000..0ae6ffb05e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -0,0 +1,170 @@ + +
+ + + +
+
+
+ + notifications + + + timeline + +
+
+ + + +
+
+ + +
+
+ +
+ + + + + notifications + + + timeline + + + + + +
+
+ entity.no-keys-found +
+ + + {{ translate.get('entity.no-key-matching', + {key: truncate.transform(keySearchText, true, 6, '...')}) | async }} + + + entity.create-new-key + + + {{'entity.create-new-key' | translate }} + notifications + + + timeline + + +
+
+
+
+ + + +
+ + +
+
+ + +
+
+ + + +
+
+ + + f() + + + + + + + + {{ modelValue?.aggregationType }}({{ modelValue?.name }}) + + + {{modelValue?.name}} + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss new file mode 100644 index 0000000000..5b3d4f1a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2023 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. + */ +.tb-data-key-row { + height: 38px; + display: flex; + flex-direction: row; + gap: 12px; + padding-left: 12px; + + .mat-mdc-form-field.tb-inline-field.tb-key-field { + .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { + .mat-mdc-form-field-infix { + padding-top: 0; + padding-bottom: 6px; + .mdc-evolution-chip-set .mdc-evolution-chip { + margin: 0; + } + input.mat-mdc-chip-input { + height: 32px; + margin-left: 0; + } + } + } + .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + .tb-attribute-chip { + .tb-chip-labels { + background: transparent; + } + } + } + } + + .tb-color-field, .tb-units-field, .tb-decimals-field { + width: 60px; + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts new file mode 100644 index 0000000000..d434ef74e3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts @@ -0,0 +1,426 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models'; +import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AggregationType } from '@shared/models/time/time.models'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { Observable, of } from 'rxjs'; +import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/config/data-key-config-dialog.component'; +import { deepClone } from '@core/utils'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { IAliasController } from '@core/api/widget-api.models'; + +export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => { + const dataKey: DataKey = control.value; + if (!dataKey || !dataKey.type || !dataKey.name) { + return { + dataKey: true + }; + } + return null; +}; + +@Component({ + selector: 'tb-data-key-row', + templateUrl: './data-key-row.component.html', + styleUrls: ['./data-key-row.component.scss', '../../data-keys.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges { + + dataKeyTypes = DataKeyType; + widgetTypes = widgetType; + + separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + @ViewChild('keyInput') keyInput: ElementRef; + @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger; + @ViewChild('chipList') chipList: MatChipGrid; + + @Input() + disabled: boolean; + + @Input() + datasourceType: DatasourceType; + + @Input() + entityAliasId: string; + + @Input() + deviceId: string; + + keyFormControl: UntypedFormControl; + + keyRowFormGroup: UntypedFormGroup; + + modelValue: DataKey; + + filteredKeys: Observable>; + + keySearchText = ''; + + private latestKeySearchTextResult: Array = null; + private keyFetchObservable$: Observable> = null; + + get dataKeyType(): DataKeyType { + return this.dataKeysPanelComponent.dataKeyType; + } + + get alarmKeys(): Array { + return this.dataKeysPanelComponent.alarmKeys; + } + + get functionTypeKeys(): Array { + return this.dataKeysPanelComponent.functionTypeKeys; + } + + get hideDataKeyColor(): boolean { + return this.dataKeysPanelComponent.hideDataKeyColor; + } + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get widget(): Widget { + return this.widgetConfigComponent.widget; + } + + get dashboard(): Dashboard { + return this.widgetConfigComponent.dashboard; + } + + get aliasController(): IAliasController { + return this.widgetConfigComponent.aliasController; + } + + get datakeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + get dataKeySettingsDirective(): string { + return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; + } + + get isEntityDatasource(): boolean { + return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); + } + + get displayUnitsOrDigits() { + return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type); + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + public translate: TranslateService, + public truncate: TruncatePipe, + private dataKeysPanelComponent: DataKeysPanelComponent, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keyFormControl = this.fb.control(''); + this.keyRowFormGroup = this.fb.group({ + label: [null, []], + color: [null, []], + units: [null, []], + decimals: [null, []], + }); + this.keyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.filteredKeys = this.keyFormControl.valueChanges + .pipe( + tap((value: string | DataKey) => { + if (value && typeof value !== 'string') { + this.addKeyFromChipValue(value); + } else if (value === null) { + this.clearKeyChip(this.keyInput.nativeElement.value); + } + }), + filter((value) => typeof value === 'string'), + map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchKeys(name) ), + share() + ); + } + + private reset() { + if (this.keyInput) { + this.keyInput.nativeElement.value = ''; + } + this.keyFormControl.patchValue('', {emitEvent: false}); + this.latestKeySearchTextResult = null; + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['deviceId', 'entityAliasId'].includes(propName)) { + this.clearKeySearchCache(); + } else if (['datasourceType'].includes(propName)) { + if ([DatasourceType.device, DatasourceType.entity].includes(change.previousValue) && + [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) { + this.clearKeySearchCache(); + } else { + this.clearKeySearchCache(); + setTimeout(() => { + this.reset(); + }, 1); + } + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keyRowFormGroup.disable({emitEvent: false}); + } else { + this.keyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey): void { + this.modelValue = value || {} as DataKey; + this.keyRowFormGroup.patchValue( + { + label: value?.label, + color: value?.color, + units: value?.units, + decimals: value?.decimals + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + dataKeyHasAggregation(): boolean { + return this.widgetConfigComponent.widgetType === widgetType.latest && this.modelValue?.type === DataKeyType.timeseries + && this.modelValue?.aggregationType && this.modelValue?.aggregationType !== AggregationType.NONE; + } + + dataKeyHasPostprocessing(): boolean { + return !!this.modelValue?.postFuncBody; + } + + displayKeyFn(key?: DataKey): string | undefined { + return key ? key.name : undefined; + } + + createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { + this.addKeyFromChipValue({name: name ? name.trim() : '', type: dataKeyType}); + } + + addKey(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim() && this.dataKeyType) { + this.addKeyFromChipValue({name: value.trim(), type: this.dataKeyType}); + } else { + this.clearKeyChip(); + } + } + + editKey() { + this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(this.modelValue), + dataKeySettingsSchema: this.datakeySettingsSchema, + dataKeySettingsDirective: this.dataKeySettingsDirective, + dashboard: this.dashboard, + aliasController: this.aliasController, + widget: this.widget, + widgetType: this.widgetType, + deviceId: this.deviceId, + entityAliasId: this.entityAliasId, + showPostProcessing: this.widgetType !== widgetType.alarm, + callbacks: this.callbacks, + hideDataKeyLabel: false, + hideDataKeyColor: this.hideDataKeyColor, + hideDataKeyUnits: !this.displayUnitsOrDigits, + hideDataKeyDecimals: !this.displayUnitsOrDigits + } + }).afterClosed().subscribe((updatedDataKey) => { + if (updatedDataKey) { + this.modelValue = updatedDataKey; + this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); + this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false}); + this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false}); + this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false}); + this.updateModel(); + } + }); + } + + removeKey() { + this.modelValue = {} as DataKey; + this.updateModel(); + this.clearKeyChip(); + } + + textIsNotEmpty(text: string): boolean { + return text && text.length > 0; + } + + clearKeyChip(value: string = '', focus = true) { + this.autocomplete.closePanel(); + this.keyInput.nativeElement.value = value; + this.keyFormControl.patchValue(value, {emitEvent: focus}); + if (focus) { + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + } + + onKeyInputFocus() { + if (!this.modelValue.type) { + this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchTextResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createDataKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchTextResult = res) + ); + } + return of(this.latestKeySearchTextResult); + } + + private getKeys(): Observable> { + if (this.keyFetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.datasourceType === DatasourceType.function) { + const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; + fetchObservable = of(targetKeysList); + } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || + this.datasourceType === DatasourceType.device && this.deviceId) { + const dataKeyTypes = [DataKeyType.timeseries]; + if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.attribute); + dataKeyTypes.push(DataKeyType.entityField); + if (this.widgetType === widgetType.alarm) { + dataKeyTypes.push(DataKeyType.alarm); + } + } + if (this.datasourceType === DatasourceType.device) { + fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes); + } else { + fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); + } + } else { + fetchObservable = of([]); + } + this.keyFetchObservable$ = fetchObservable.pipe( + publishReplay(1), + refCount() + ); + } + return this.keyFetchObservable$; + } + + private createDataKeyFilter(query: string): (key: DataKey) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.name.toLowerCase().startsWith(lowercaseQuery); + } + + private addKeyFromChipValue(chip: DataKey) { + this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); + if (!this.keyRowFormGroup.get('label').value) { + this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); + } + this.updateModel(); + this.clearKeyChip('', false); + } + + private clearKeySearchCache() { + this.keySearchText = ''; + this.keyFetchObservable$ = null; + this.latestKeySearchTextResult = null; + } + + private updateModel() { + const value: DataKey = this.keyRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html new file mode 100644 index 0000000000..754f0052c9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html @@ -0,0 +1,67 @@ + +
+
{{ panelTitle }}
+
+
+
datakey.key
+
datakey.label
+
datakey.color
+
widget-config.units-short
+
widget-config.decimals-short
+
+
+
+
+ + +
+ + +
+
+
+
+
+ +
+
+ + {{ noKeysText }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss new file mode 100644 index 0000000000..df8b187535 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2023 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. + */ +.tb-data-keys-table { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 6px; + display: flex; + flex-direction: column; + gap: 12px; + padding-bottom: 12px; + .tb-data-keys-header { + height: 48px; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; + gap: 12px; + padding-left: 12px; + .tb-data-keys-header-cell { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.54); + &.tb-color-header, &.tb-units-header, &.tb-decimals-header { + width: 60px; + } + &.tb-actions-header { + width: 76px; + } + } + } + .tb-data-keys-body { + display: flex; + flex-direction: column; + gap: 12px; + } + .tb-prompt { + height: 38px; + } +} + +.tb-data-keys-table-row { + height: 38px; + display: flex; + flex-direction: row; + gap: 12px; + background: #fff; + + .tb-data-keys-table-row-buttons { + display: flex; + flex-direction: row; + button.mat-mdc-icon-button.mat-mdc-button-base { + padding: 7px; + width: 38px; + height: 38px; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts new file mode 100644 index 0000000000..3d8b80c1df --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts @@ -0,0 +1,237 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; +import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { alarmFields } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-data-keys-panel', + templateUrl: './data-keys-panel.component.html', + styleUrls: ['./data-keys-panel.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DataKeysPanelComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges, Validator { + + @Input() + disabled: boolean; + + @Input() + panelTitle: string; + + @Input() + addKeyTitle: string; + + @Input() + removeKeyTitle: string; + + @Input() + noKeysText: string; + + @Input() + datasourceType: DatasourceType; + + @Input() + entityAliasId: string; + + @Input() + deviceId: string; + + @Input() + @coerceBoolean() + hideDataKeyColor = false; + + dataKeyType: DataKeyType; + alarmKeys: Array; + functionTypeKeys: Array; + + keysListFormGroup: UntypedFormGroup; + + get widgetType(): widgetType { + return this.widgetConfigComponent.widgetType; + } + + get callbacks(): DataKeysCallbacks { + return this.widgetConfigComponent.widgetConfigCallbacks; + } + + get datakeySettingsSchema(): JsonSettingsSchema { + return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private dialog: MatDialog, + private cd: ChangeDetectorRef, + private utils: UtilsService, + private widgetConfigComponent: WidgetConfigComponent) { + } + + ngOnInit() { + this.keysListFormGroup = this.fb.group({ + keys: [this.fb.array([]), []] + }); + this.keysListFormGroup.valueChanges.subscribe( + (val) => this.propagateChange(this.keysListFormGroup.get('keys').value) + ); + this.alarmKeys = []; + for (const name of Object.keys(alarmFields)) { + this.alarmKeys.push({ + name, + type: DataKeyType.alarm + }); + } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } + this.updateParams(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['datasourceType'].includes(propName)) { + this.updateParams(); + } + } + } + } + + private updateParams() { + if (this.datasourceType === DatasourceType.function) { + this.dataKeyType = DataKeyType.function; + } else { + if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) { + this.dataKeyType = DataKeyType.timeseries; + } else { + this.dataKeyType = null; + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.keysListFormGroup.disable({emitEvent: false}); + } else { + this.keysListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DataKey[] | undefined): void { + this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false}); + } + + public validate(c: UntypedFormControl) { + return this.keysListFormGroup.valid ? null : { + dataKeyRows: { + valid: false, + }, + }; + } + + keyDrop(event: CdkDragDrop) { + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const key = keysArray.at(event.previousIndex); + keysArray.removeAt(event.previousIndex); + keysArray.insert(event.currentIndex, key); + } + + keysFormArray(): UntypedFormArray { + return this.keysListFormGroup.get('keys') as UntypedFormArray; + } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + (this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index); + } + + addKey() { + const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema); + dataKey.label = ''; + dataKey.decimals = 0; + const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]); + keysArray.push(keyControl); + } + + private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray { + const keysControls: Array = []; + if (keys) { + keys.forEach((key) => { + keysControls.push(this.fb.control(key, [dataKeyRowValidator])); + }); + } + return this.fb.array(keysControls); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts index 68dfca5858..469ff191ed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts @@ -29,7 +29,7 @@ import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'tb-widget-actions-panel', templateUrl: './widget-actions-panel.component.html', - styleUrls: ['../../widget-config.scss'], + styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts index 9d9201e88a..a7cf923c44 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts @@ -155,6 +155,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con private dataKeySettingsData: JsonFormComponentData; private alarmKeys: Array; + private functionTypeKeys: Array; filteredKeys: Observable>; private latestKeySearchResult: Array = null; @@ -183,6 +184,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con type: DataKeyType.alarm }); } + this.functionTypeKeys = []; + for (const type of this.utils.getPredefinedFunctionsList()) { + this.functionTypeKeys.push({ + name: type, + type: DataKeyType.function + }); + } if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { this.displayAdvanced = true; @@ -396,6 +404,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con let fetchObservable: Observable>; if (this.modelValue.type === DataKeyType.alarm) { fetchObservable = of(this.alarmKeys); + } else if (this.modelValue.type === DataKeyType.function) { + fetchObservable = of(this.functionTypeKeys); } else { if (this.deviceId || this.entityAliasId) { const dataKeyTypes = [this.modelValue.type]; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss index 3013bf78ae..7d01f41cb5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss @@ -22,92 +22,92 @@ input.tb-dragging { display: none; } +} - .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { - overflow: hidden; - line-height: 20px; - height: 32px; +.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { + overflow: hidden; + line-height: 20px; + height: 32px; - &.mdc-evolution-chip--with-trailing-action { - .mdc-evolution-chip__action--primary { - padding-left: 4px; - padding-right: 12px; - } + &.mdc-evolution-chip--with-trailing-action { + .mdc-evolution-chip__action--primary { + padding-left: 4px; + padding-right: 12px; } + } - .mat-mdc-chip-action { + .mat-mdc-chip-action { + overflow: hidden; + .mat-mdc-chip-action-label { overflow: hidden; - .mat-mdc-chip-action-label { - overflow: hidden; - } } - .tb-attribute-chip { - max-width: 100%; - color: rgb(66, 66, 66); - .tb-chip-drag-handle { - padding: 3px; - height: 24px; - cursor: move; - mat-icon { - pointer-events: none; - } + } + .tb-attribute-chip { + max-width: 100%; + color: rgb(66, 66, 66); + .tb-chip-drag-handle { + padding: 3px; + height: 24px; + cursor: move; + mat-icon { + pointer-events: none; } - .tb-chip-labels { - display: flex; - flex-direction: row; - align-items: center; - min-width: 0; - background: rgba(0, 0, 0, 0.04); - border-radius: 100px; - padding: 2px 10px; - .tb-chip-label { - font-weight: normal; - font-size: 14px; - line-height: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - .mat-icon.tb-datakey-icon { - margin-right: 4px; - margin-left: 4px; - } - .tb-agg-func { - font-style: italic; - color: #0c959c; - } + } + .tb-chip-labels { + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + background: rgba(0, 0, 0, 0.04); + border-radius: 100px; + padding: 2px 10px; + .tb-chip-label { + font-weight: normal; + font-size: 14px; + line-height: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + .mat-icon.tb-datakey-icon { + margin-right: 4px; + margin-left: 4px; } - .tb-chip-separator { - white-space: pre; + .tb-agg-func { + font-style: italic; + color: #0c959c; } } - .mat-mdc-chip-remove.mat-mdc-icon-button { - color: inherit; - opacity: inherit; + .tb-chip-separator { + white-space: pre; } } - - &.tb-datakey-chip-dnd-placeholder { - min-width: 120px; - border: 2px dashed rgba(0, 0, 0, 0.2); - } - &.tb-chip-dragging { - display: none; + .mat-mdc-chip-remove.mat-mdc-icon-button { + color: inherit; + opacity: inherit; } + } + + &.tb-datakey-chip-dnd-placeholder { + min-width: 120px; + border: 2px dashed rgba(0, 0, 0, 0.2); + } + &.tb-chip-dragging { + display: none; + } + .tb-dragging-chip-image-fill { + background-color: rgba(0,0,0,0.3); + border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); + display: none; + pointer-events: none; + } + .tb-dragging-chip-image { + background-color: var(--mdc-chip-elevated-container-color, transparent); + border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); + overflow: hidden; + height: 32px; + line-height: 20px; .tb-dragging-chip-image-fill { - background-color: rgba(0,0,0,0.3); - border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); - display: none; - pointer-events: none; - } - .tb-dragging-chip-image { - background-color: var(--mdc-chip-elevated-container-color, transparent); - border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px); - overflow: hidden; - height: 32px; - line-height: 20px; - .tb-dragging-chip-image-fill { - display: block; - } + display: block; } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index 5cf519637b..ab7411488e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -61,7 +61,7 @@ -
+
; validateConfig(): boolean; @@ -40,6 +43,8 @@ export interface IBasicWidgetConfigComponent { export abstract class BasicWidgetConfigComponent extends PageComponent implements IBasicWidgetConfigComponent, OnInit, AfterViewInit { + isAdd = false; + basicMode = WidgetConfigMode.basic; widgetConfigValue: WidgetConfigComponentData; @@ -56,7 +61,8 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement widgetConfigChangedEmitter = new EventEmitter(); widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); - protected constructor(@Inject(Store) protected store: Store) { + protected constructor(@Inject(Store) protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent) { super(store); } @@ -65,12 +71,15 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement ngAfterViewInit(): void { setTimeout(() => { if (!this.validateConfig()) { - this.onConfigChanged(this.prepareOutputConfig(this.configForm().value)); + this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue())); } }, 0); } protected setupConfig(widgetConfig: WidgetConfigComponentData) { + if (this.isAdd) { + this.setupDefaults(widgetConfig); + } this.onConfigSet(widgetConfig); this.updateValidators(false); for (const trigger of this.validatorTriggers()) { @@ -83,11 +92,13 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement this.updateValidators(true, trigger); }); } - this.configForm().valueChanges.subscribe((updated: any) => { - this.onConfigChanged(this.prepareOutputConfig(updated)); + this.configForm().valueChanges.subscribe(() => { + this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue())); }); } + protected setupDefaults(configData: WidgetConfigComponentData) {} + protected updateValidators(emitEvent: boolean, trigger?: string) { } @@ -108,6 +119,41 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement return this.configForm().valid; } + protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) { + let datasources = configData.config.datasources; + if (!datasources || !datasources.length) { + datasources = [ + { + type: DatasourceType.device, + dataKeys: [] + } + ]; + configData.config.datasources = datasources; + } + let dataKeys = datasources[0].dataKeys; + if (!dataKeys) { + dataKeys = []; + datasources[0].dataKeys = dataKeys; + } + if (keys && keys.length) { + dataKeys.length = 0; + keys.forEach(key => { + const dataKey = + this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema); + if (key.label) { + dataKey.label = key.label; + } + if (key.units) { + dataKey.units = key.units; + } + if (isDefinedAndNotNull(key.decimals)) { + dataKey.decimals = key.decimals; + } + dataKeys.push(dataKey); + }); + } + } + protected abstract configForm(): UntypedFormGroup; protected abstract onConfigSet(configData: WidgetConfigComponentData); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss deleted file mode 100644 index 15e14e211e..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Copyright © 2016-2023 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. - */ -.tb-widget-config-panel { - box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); - border-radius: 4px; - padding: 16px; - gap: 16px; - display: flex; - flex-direction: column; - color: rgba(0, 0, 0, 0.87); - letter-spacing: 0.15px; - position: relative; - &.no-padding-bottom { - padding-bottom: 0; - } - &.stroked { - box-shadow: none; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 6px; - } -} -.tb-widget-config-panel-title { - font-weight: 500; - font-size: 16px; -} -.tb-widget-config-panel-hint { - font-size: 12px; - color: #808080; -} -.tb-widget-config-row { - height: 56px; - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; - padding-left: 16px; - padding-right: 12px; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 6px; - &.same-padding { - padding-right: 16px; - } - &.space-between { - justify-content: space-between; - } - .mat-divider-vertical { - height: 56px; - } -} - -:host ::ng-deep { - - .mat-slide { - margin: 8px 0; - .mdc-form-field>label { - font-weight: 400; - font-size: 16px; - line-height: 24px; - margin-left: 12px; - } - } - - .slide-block { - display: block; - &:not(:last-child) { - margin-bottom: 8px; - } - } - - .mat-mdc-form-field { - &.center { - .mat-mdc-text-field-wrapper.mdc-text-field--outlined { - .mat-mdc-form-field-infix { - .mdc-text-field__input { - text-align: center; - } - } - } - } - &.number { - .mat-mdc-text-field-wrapper.mdc-text-field--outlined { - padding-right: 4px; - .mat-mdc-form-field-infix { - width: 80px; - input.mdc-text-field__input[type=number]::-webkit-inner-spin-button, - input.mdc-text-field__input[type=number]::-webkit-outer-spin-button { - opacity: 1; - } - } - } - } - } - - .tb-widget-config-row { - .mat-mdc-form-field { - .mat-mdc-text-field-wrapper.mdc-text-field--outlined { - padding-right: 12px; - padding-left: 12px; - &:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) { - .mdc-notched-outline__leading, .mdc-notched-outline__trailing { - border-color: rgba(0, 0, 0, 0.12); - } - } - .mat-mdc-form-field-infix { - padding-top: 7px; - padding-bottom: 7px; - min-height: 38px; - width: 72px; - } - } - } - } - - .tb-widget-config-panel { - .mat-expansion-panel { - &.tb-settings { - box-shadow: none; - .mat-content { - overflow: visible; - } - .mat-expansion-panel-header { - font-weight: 500; - font-size: 16px; - line-height: 24px; - letter-spacing: 0.25px; - padding: 0; - .mat-content { - flex: 0; - white-space: nowrap; - } - &:hover { - background: none; - } - .mat-expansion-indicator { - height: 32px; - padding: 2px; - } - } - .mat-expansion-panel-header-description { - align-items: center; - } - > .mat-expansion-panel-content { - > .mat-expansion-panel-body { - padding: 0; - } - } - .tb-json-object-panel, .tb-css-content-panel { - margin: 0 0 8px; - } - } - .mat-expansion-panel-content { - font: inherit; - } - } - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html index d573e65183..a2ed480388 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html @@ -15,6 +15,6 @@ limitations under the License. --> - + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts index 4a1665b5b2..e61511698d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts @@ -20,7 +20,7 @@ import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilde @Component({ selector: 'tb-widget-units', templateUrl: './widget-units.component.html', - styleUrls: ['./widget-config.scss'], + styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -43,6 +43,7 @@ export class WidgetUnitsComponent implements ControlValueAccessor, OnInit { ngOnInit() { this.unitsFormControl = this.fb.control('', []); + this.unitsFormControl.valueChanges.subscribe(val => this.propagateChange(val)); } writeValue(units?: string): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts index b9080b93ff..4ecd21ad1d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts @@ -23,7 +23,7 @@ import { AppState } from '@core/core.state'; @Component({ selector: 'tb-simple-card-widget-settings', templateUrl: './simple-card-widget-settings.component.html', - styleUrls: ['../../../config/widget-config.scss'] + styleUrls: [] }) export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html index e7ce8a1a02..2dbedf96bd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html @@ -66,4 +66,29 @@ + + widgets.table.default-column-visibility + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} + + + + + widgets.table.column-selection-to-display + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts index 722df5bc5f..66a2c8b483 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts @@ -43,7 +43,9 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent useCellStyleFunction: false, cellStyleFunction: '', useCellContentFunction: false, - cellContentFunction: '' + cellContentFunction: '', + defaultColumnVisibility: 'visible', + columnSelectionToDisplay: 'enabled' }; } @@ -53,6 +55,8 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], useCellContentFunction: [settings.useCellContentFunction, []], cellContentFunction: [settings.cellContentFunction, [Validators.required]], + defaultColumnVisibility: [settings.defaultColumnVisibility, []], + columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html index a5b4f6bef2..8fb1629b13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html @@ -73,4 +73,29 @@ + + widgets.table.default-column-visibility + + + {{ 'widgets.table.column-visibility-visible' | translate }} + + + {{ 'widgets.table.column-visibility-hidden' | translate }} + + + {{ 'widgets.table.column-visibility-hidden-mobile' | translate }} + + + + + widgets.table.column-selection-to-display + + + {{ 'widgets.table.column-selection-to-display-enabled' | translate }} + + + {{ 'widgets.table.column-selection-to-display-disabled' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts index 70fac42f98..3fe1f1027e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts @@ -44,7 +44,9 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom useCellStyleFunction: false, cellStyleFunction: '', useCellContentFunction: false, - cellContentFunction: '' + cellContentFunction: '', + defaultColumnVisibility: 'visible', + columnSelectionToDisplay: 'enabled' }; } @@ -56,6 +58,8 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], useCellContentFunction: [settings.useCellContentFunction, []], cellContentFunction: [settings.cellContentFunction, [Validators.required]], + defaultColumnVisibility: [settings.defaultColumnVisibility, []], + columnSelectionToDisplay: [settings.columnSelectionToDisplay, []], }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index 075cb5583c..ed4428fe45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -23,6 +23,9 @@ {{ 'widgets.table.enable-search' | translate }} + + {{ 'widgets.table.enable-select-column-display' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index 273dfbd703..0a57402870 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -41,6 +41,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon protected defaultSettings(): WidgetSettings { return { enableSearch: true, + enableSelectColumnDisplay: true, enableStickyHeader: true, enableStickyAction: true, reserveSpaceForHiddenAction: 'true', @@ -59,6 +60,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.timeseriesTableWidgetSettingsForm = this.fb.group({ enableSearch: [settings.enableSearch, []], + enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []], enableStickyHeader: [settings.enableStickyHeader, []], enableStickyAction: [settings.enableStickyAction, []], reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts index d3858b89f6..6417ae4bcb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts @@ -215,7 +215,7 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr mergeMap((aliasInfo) => { return this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts index 2ada7e6cfb..22f8bf8e75 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts @@ -15,7 +15,13 @@ /// import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,6 +33,7 @@ import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/op import { DataKey } from '@shared/models/widget.models'; import { EntityService } from '@core/http/entity.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-device-key-autocomplete', @@ -199,7 +206,7 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI mergeMap((aliasInfo) => { return this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [EntityType.DEVICE], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts index 7e96d6ba0a..e79735e70d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts @@ -236,7 +236,7 @@ export class ImageMapProviderSettingsComponent extends PageComponent implements mergeMap((aliasInfo) => { return this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 3cd343ea57..2a29776e7e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -31,6 +31,7 @@ type ColumnSelectionOptions = 'enabled' | 'disabled'; export interface TableWidgetSettings { enableSearch: boolean; + enableSelectColumnDisplay: boolean; enableStickyAction: boolean; enableStickyHeader: boolean; displayPagination: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 9aae6523ed..9342a2dc4c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -19,9 +19,12 @@ import { ChangeDetectorRef, Component, ElementRef, + Injector, Input, + OnDestroy, OnInit, QueryList, + StaticProvider, ViewChild, ViewChildren, ViewContainerRef @@ -64,8 +67,9 @@ import { CellStyleInfo, checkHasActions, constructTableCssString, + DisplayColumn, getCellContentInfo, - getCellStyleInfo, + getCellStyleInfo, getColumnDefaultVisibility, getColumnSelectionAvailability, getRowStyleInfo, getTableCellButtonActions, noDataMessage, @@ -75,12 +79,17 @@ import { TableWidgetDataKeySettings, TableWidgetSettings } from '@home/components/widget/lib/table-widget.models'; -import { Overlay } from '@angular/cdk/overlay'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; +import { + DISPLAY_COLUMNS_PANEL_DATA, + DisplayColumnsPanelComponent +} from '@home/components/widget/lib/display-columns-panel.component'; +import { ComponentPortal } from '@angular/cdk/portal'; export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { showTimestamp: boolean; @@ -105,6 +114,8 @@ interface TimeseriesHeader { dataKey: DataKey; sortable: boolean; show: boolean; + columnDefaultVisibility?: boolean; + columnSelectionAvailability?: boolean; styleInfo: CellStyleInfo; contentInfo: CellContentInfo; order?: number; @@ -131,7 +142,7 @@ interface TimeseriesTableSource { templateUrl: './timeseries-table-widget.component.html', styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] }) -export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { +export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { @Input() ctx: WidgetContext; @@ -169,6 +180,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private useEntityLabel = false; private dateFormatFilter: string; + private displayedColumns: Array = []; + private rowStylesInfo: RowStyleInfo; private subscriptions: Subscription[] = []; @@ -184,6 +197,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } }; + private columnDisplayAction: WidgetAction = { + name: 'entity.columns-to-display', + show: true, + icon: 'view_column', + onAction: ($event) => { + this.editColumnsToDisplay($event); + } + }; + constructor(protected store: Store, private elementRef: ElementRef, private overlay: Overlay, @@ -275,11 +297,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } private initialize() { - this.ctx.widgetActions = [this.searchAction ]; + this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length; this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; + this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; @@ -365,9 +388,82 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.sources.push(source); } } + this.prepareDisplayedColumn(); + this.sources[this.sourceIndex].displayedColumns = + this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def); this.updateActiveEntityInfo(); } + private editColumnsToDisplay($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const source = this.sources[this.sourceIndex]; + + this.prepareDisplayedColumn(); + + const providers: StaticProvider[] = [ + { + provide: DISPLAY_COLUMNS_PANEL_DATA, + useValue: { + columns: this.displayedColumns[this.sourceIndex], + columnsUpdated: (newColumns) => { + source.displayedColumns = newColumns.filter(value => value.display).map(value => value.def); + this.clearCache(); + } + } + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, + this.viewContainerRef, injector)); + this.ctx.detectChanges(); + } + + private prepareDisplayedColumn() { + if (!this.displayedColumns[this.sourceIndex]) { + this.displayedColumns[this.sourceIndex] = this.sources[this.sourceIndex].displayedColumns.map(value => { + let title = ''; + const header = this.sources[this.sourceIndex].header.find(column => column.index.toString() === value); + if (value === '0') { + title = 'Timestamp'; + } else if (value === 'actions') { + title = 'Actions'; + } else { + title = header.dataKey.name; + } + return { + title, + def: value, + display: header?.columnDefaultVisibility ?? true, + selectable: header?.columnSelectionAvailability ?? true + }; + }); + } + } + private prepareHeader(datasource: Datasource): TimeseriesHeader[] { const dataKeys = datasource.dataKeys; const latestDataKeys = datasource.latestDataKeys; @@ -377,6 +473,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const keySettings: TableWidgetDataKeySettings = dataKey.settings; const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); + const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); + const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); contentInfo.units = dataKey.units; contentInfo.decimals = dataKey.decimals; header.push({ @@ -386,6 +484,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI styleInfo, contentInfo, show: true, + columnDefaultVisibility, + columnSelectionAvailability, order: index + 2 }); }); @@ -396,6 +496,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings; const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); + const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); + const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); contentInfo.units = dataKey.units; contentInfo.decimals = dataKey.decimals; header.push({ @@ -405,13 +507,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI styleInfo, contentInfo, show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true, + columnDefaultVisibility, + columnSelectionAvailability, order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2) }); }); } - header = header.sort((a, b) => { - return a.order - b.order; - }); + header = header.sort((a, b) => a.order - b.order); return header; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 744747a50a..999e7e55ff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -81,8 +81,8 @@ import { FilterDialogComponent, FilterDialogData } from '@home/components/filter import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { coerceBoolean } from '@shared/decorators/coercion'; import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; -import Timeout = NodeJS.Timeout; import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; +import Timeout = NodeJS.Timeout; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -96,7 +96,7 @@ const defaultSettingsForm = [ @Component({ selector: 'tb-widget-config', templateUrl: './widget-config.component.html', - styleUrls: ['./widget-config.component.scss', './config/widget-config.scss'], + styleUrls: ['./widget-config.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -143,6 +143,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe @coerceBoolean() hideToggleHeader = false; + @Input() + @coerceBoolean() + isAdd = false; + @Input() disabled: boolean; @Input() @@ -399,22 +403,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe writeValue(value: WidgetConfigComponentData): void { this.modelValue = value; this.widgetType = this.modelValue?.widgetType; - this.setupConfig(); + this.setupConfig(this.isAdd); } - private setupConfig() { + private setupConfig(isAdd = false) { if (this.modelValue) { this.destroyBasicModeComponent(); this.removeChangeSubscriptions(); if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) { - this.setupBasicModeConfig(); + this.setupBasicModeConfig(isAdd); } else { this.setupDefaultConfig(); } } } - private setupBasicModeConfig() { + private setupBasicModeConfig(isAdd = false) { const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective]; if (!componentType) { this.basicModeDirectiveError = this.translate.instant('widget-config.settings-component-not-found', @@ -425,6 +429,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe this.createBasicModeComponentTimeout = null; this.basicModeComponentRef = this.basicModeContainer.createComponent(factory); this.basicModeComponent = this.basicModeComponentRef.instance; + this.basicModeComponent.isAdd = isAdd; this.basicModeComponent.widgetConfig = this.modelValue; this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => { this.modelValue = data; @@ -826,7 +831,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe const entityFilter = singleEntityFilterFromDeviceId(deviceId); return this.entityService.getEntityKeysByEntityFilter( entityFilter, - dataKeyTypes, + dataKeyTypes, [EntityType.DEVICE], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) @@ -837,7 +842,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe return this.aliasController.getAliasInfo(entityAliasId).pipe( mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter( aliasInfo.entityFilter, - dataKeyTypes, + dataKeyTypes, [], {ignoreLoading: true, ignoreErrors: true} ).pipe( catchError(() => of([])) diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html index fc48392e8c..4e7a133009 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html @@ -59,8 +59,7 @@ diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 27b8968344..4ff13778e5 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -49,8 +49,7 @@
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index efb4851bd8..4cdaed942d 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -15,55 +15,48 @@ limitations under the License. --> -
+
-
- -
+ + + +
- + + - -
@@ -164,6 +157,14 @@ + +