Browse Source

Merge branch 'develop/3.5.2' of github.com:thingsboard/thingsboard into feature/rpc-response-handling-improvements

pull/8713/head
ShvaykaD 3 years ago
parent
commit
8b5fe7845b
  1. 6
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 12
      application/src/main/resources/thingsboard.yml
  3. 23
      common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java
  4. 26
      common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java
  5. 62
      common/cache/src/main/java/org/thingsboard/server/cache/TBRedisSentinelConfiguration.java
  6. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
  7. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
  8. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
  9. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
  10. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
  11. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
  12. 2
      docker/.env
  13. 3
      docker/.gitignore
  14. 3
      docker/README.md
  15. 7
      docker/cache-redis-sentinel.env
  16. 19
      docker/compose-utils.sh
  17. 40
      docker/docker-compose.redis-sentinel.volumes.yml
  18. 119
      docker/docker-compose.redis-sentinel.yml
  19. 26
      monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java
  20. 8
      monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringConfig.java
  21. 24
      monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTarget.java
  22. 3
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/CoapTransportMonitoringConfig.java
  23. 3
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/DeviceConfig.java
  24. 3
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/HttpTransportMonitoringConfig.java
  25. 3
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/Lwm2mTransportMonitoringConfig.java
  26. 3
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/MqttTransportMonitoringConfig.java
  27. 7
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java
  28. 9
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringConfig.java
  29. 34
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java
  30. 12
      monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportType.java
  31. 6
      monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java
  32. 6
      monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java
  33. 74
      monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java
  34. 101
      monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java
  35. 10
      monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java
  36. 139
      monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java
  37. 41
      monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportsMonitoringService.java
  38. 12
      monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/CoapTransportHealthChecker.java
  39. 12
      monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/HttpTransportHealthChecker.java
  40. 12
      monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/Lwm2mTransportHealthChecker.java
  41. 12
      monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/MqttTransportHealthChecker.java
  42. 198
      monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java
  43. 1
      monitoring/src/main/resources/lwm2m/device_profile.json
  44. 51
      monitoring/src/main/resources/tb-monitoring.yml
  45. 4
      msa/black-box-tests/README.md
  46. 30
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  47. 76
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  48. 30
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java
  49. 20
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java
  50. 3
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java
  51. 79
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePrivateTest.java
  52. 127
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/MakeDevicePublicTest.java
  53. 1
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/Const.java
  54. 12
      pom.xml
  55. 12
      transport/coap/src/main/resources/tb-coap-transport.yml
  56. 12
      transport/http/src/main/resources/tb-http-transport.yml
  57. 12
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  58. 12
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  59. 12
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  60. 5
      ui-ngx/src/app/core/http/entity.service.ts
  61. 23
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  62. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  63. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  64. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  65. 86
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  66. 154
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts
  67. 13
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  68. 170
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  69. 53
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  70. 426
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  71. 67
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  72. 76
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  73. 237
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  74. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts
  75. 10
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  76. 144
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss
  77. 2
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  78. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  79. 6
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  80. 2
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts
  81. 58
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  82. 169
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss
  83. 2
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html
  84. 3
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts
  85. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts
  86. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html
  87. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts
  88. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html
  89. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts
  90. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  91. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts
  92. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts
  93. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts
  94. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts
  95. 1
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  96. 116
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  97. 21
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  98. 3
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html
  99. 3
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  100. 67
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

6
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}"
}
},
{

12
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}"

23
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<RedisNode> getNodes(String nodes) {
List<RedisNode> 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;
}
}

26
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<RedisNode> getNodes(String nodes) {
List<RedisNode> 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;
}
}
}

62
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());
}
}
}

2
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;

2
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;

2
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;

2
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;

2
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;

2
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;

2
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

3
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

3
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):

7
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

19
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

40
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}

119
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

26
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<BaseMonitoringService<?, ?>> 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);
}
}

8
monitoring/src/main/java/org/thingsboard/monitoring/config/MonitoringTargetConfig.java → 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<T extends MonitoringTarget> {
private String baseUrl;
private DeviceConfig device;
List<T> getTargets();
}

24
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();
}

3
monitoring/src/main/java/org/thingsboard/monitoring/config/service/CoapTransportMonitoringConfig.java → 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")

3
monitoring/src/main/java/org/thingsboard/monitoring/config/DeviceConfig.java → 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) {

3
monitoring/src/main/java/org/thingsboard/monitoring/config/service/HttpTransportMonitoringConfig.java → 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")

3
monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java → 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")

3
monitoring/src/main/java/org/thingsboard/monitoring/config/service/MqttTransportMonitoringConfig.java → 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")

7
monitoring/src/main/java/org/thingsboard/monitoring/data/TransportInfo.java → 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);
}
}

9
monitoring/src/main/java/org/thingsboard/monitoring/config/service/TransportMonitoringConfig.java → 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<TransportMonitoringTarget> {
private int requestTimeoutMs;
private List<MonitoringTargetConfig> targets;
private List<TransportMonitoringTarget> targets;
public abstract TransportType getTransportType();

34
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();
}
}

12
monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java → 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

6
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);
}
}

6
monitoring/src/main/java/org/thingsboard/monitoring/data/TransportFailureException.java → 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);
}

74
monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java → 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<C extends TransportMonitoringConfig> {
public abstract class BaseHealthChecker<C extends MonitoringConfig, T extends MonitoringTarget> {
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<C extends TransportMonitoringConfig
public static final String TEST_TELEMETRY_KEY = "testData";
protected TransportHealthChecker(C config, MonitoringTargetConfig target) {
this.config = config;
this.target = target;
}
@PostConstruct
private void init() {
transportInfo = new TransportInfo(getTransportType(), target.getBaseUrl());
info = getInfo();
}
protected abstract void initialize(TbClient tbClient);
public final void check(WsClient wsClient) {
log.debug("[{}] Checking", transportInfo);
log.debug("[{}] Checking", info);
try {
wsClient.registerWaitForUpdate();
String testValue = UUID.randomUUID().toString();
String testPayload = createTestPayload(testValue);
try {
initClientAndSendPayload(testPayload);
log.trace("[{}] Sent test payload ({})", transportInfo, testPayload);
initClient();
stopWatch.start();
sendTestPayload(testPayload);
reporter.reportLatency(Latencies.request(getKey()), stopWatch.getTime());
log.trace("[{}] Sent test payload ({})", info, testPayload);
} catch (Throwable e) {
throw new TransportFailureException(e);
throw new ServiceFailureException(e);
}
log.trace("[{}] Waiting for WS update", transportInfo);
log.trace("[{}] Waiting for WS update", info);
checkWsUpdate(wsClient, testValue);
reporter.serviceIsOk(transportInfo);
reporter.serviceIsOk(info);
reporter.serviceIsOk(MonitoredServiceKey.GENERAL);
} catch (TransportFailureException transportFailureException) {
reporter.serviceFailure(transportInfo, transportFailureException);
} catch (ServiceFailureException serviceFailureException) {
reporter.serviceFailure(info, serviceFailureException);
} catch (Exception e) {
reporter.serviceFailure(MonitoredServiceKey.GENERAL, e);
}
}
private void initClientAndSendPayload(String payload) throws Throwable {
initClient();
stopWatch.start();
sendTestPayload(payload);
reporter.reportLatency(Latencies.transportRequest(getTransportType()), stopWatch.getTime());
}
private void checkWsUpdate(WsClient wsClient, String testValue) {
stopWatch.start();
wsClient.waitForUpdate(resultCheckTimeoutMs);
log.trace("[{}] Waited for WS update. Last WS msg: {}", transportInfo, wsClient.lastMsg);
Object update = wsClient.getTelemetryUpdate(target.getDevice().getId(), TEST_TELEMETRY_KEY);
log.trace("[{}] Waited for WS update. Last WS msg: {}", info, wsClient.lastMsg);
Object update = wsClient.getTelemetryUpdate(target.getDeviceId(), TEST_TELEMETRY_KEY);
if (update == null) {
throw new TransportFailureException("No WS update arrived within " + resultCheckTimeoutMs + " ms");
throw new ServiceFailureException("No WS update arrived within " + resultCheckTimeoutMs + " ms");
} else if (!update.toString().equals(testValue)) {
throw new TransportFailureException("Was expecting value " + testValue + " but got " + update);
throw new ServiceFailureException("Was expecting value " + testValue + " but got " + update);
}
reporter.reportLatency(Latencies.WS_UPDATE, stopWatch.getTime());
}
protected String createTestPayload(String testValue) {
return JacksonUtil.newObjectNode().set(TEST_TELEMETRY_KEY, new TextNode(testValue)).toString();
}
protected abstract void initClient() throws Exception;
protected abstract String createTestPayload(String testValue);
protected abstract void sendTestPayload(String payload) throws Exception;
@PreDestroy
protected abstract void destroyClient() throws Exception;
protected abstract TransportType getTransportType();
protected abstract Object getInfo();
protected abstract String getKey();
}

101
monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java

@ -0,0 +1,101 @@
/**
* 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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.thingsboard.monitoring.client.TbClient;
import org.thingsboard.monitoring.client.WsClient;
import org.thingsboard.monitoring.client.WsClientFactory;
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.service.transport.TransportHealthChecker;
import org.thingsboard.monitoring.util.TbStopWatch;
import javax.annotation.PostConstruct;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@Slf4j
public abstract class BaseMonitoringService<C extends MonitoringConfig<T>, T extends MonitoringTarget> {
@Autowired
private List<C> configs;
private final List<BaseHealthChecker<C, T>> healthCheckers = new LinkedList<>();
private final List<UUID> 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<C, T> healthChecker = (BaseHealthChecker<C, T>) 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<C, T> 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();
}

10
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);
}
}

139
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<C extends TransportMonitoringConfig> extends BaseHealthChecker<C, TransportMonitoringTarget> {
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();
}
}

41
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<TransportMonitoringConfig, TransportMonitoringTarget> {
@Override
protected BaseHealthChecker<?, ?> createHealthChecker(TransportMonitoringConfig config, TransportMonitoringTarget target) {
return applicationContext.getBean(config.getTransportType().getServiceClass(), config, target);
}
@Override
protected String getName() {
return "transports check";
}
}

12
monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/CoapTransportHealthChecker.java → 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<CoapTrans
private CoapClient coapClient;
protected CoapTransportHealthChecker(CoapTransportMonitoringConfig config, MonitoringTargetConfig target) {
protected CoapTransportHealthChecker(CoapTransportMonitoringConfig config, TransportMonitoringTarget target) {
super(config, target);
}

12
monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/HttpTransportHealthChecker.java → monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/HttpTransportHealthChecker.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.springframework.beans.factory.config.ConfigurableBeanFactory;
@ -21,10 +21,10 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import org.thingsboard.monitoring.config.service.HttpTransportMonitoringConfig;
import org.thingsboard.monitoring.transport.TransportHealthChecker;
import org.thingsboard.monitoring.config.transport.HttpTransportMonitoringConfig;
import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget;
import org.thingsboard.monitoring.config.transport.TransportType;
import org.thingsboard.monitoring.service.transport.TransportHealthChecker;
import java.time.Duration;
@ -35,7 +35,7 @@ public class HttpTransportHealthChecker extends TransportHealthChecker<HttpTrans
private RestTemplate restTemplate;
protected HttpTransportHealthChecker(HttpTransportMonitoringConfig config, MonitoringTargetConfig target) {
protected HttpTransportHealthChecker(HttpTransportMonitoringConfig config, TransportMonitoringTarget target) {
super(config, target);
}

12
monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/Lwm2mTransportHealthChecker.java → monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/Lwm2mTransportHealthChecker.java

@ -13,17 +13,17 @@
* 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.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.thingsboard.monitoring.client.Lwm2mClient;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import org.thingsboard.monitoring.config.service.Lwm2mTransportMonitoringConfig;
import org.thingsboard.monitoring.transport.TransportHealthChecker;
import org.thingsboard.monitoring.config.transport.Lwm2mTransportMonitoringConfig;
import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget;
import org.thingsboard.monitoring.config.transport.TransportType;
import org.thingsboard.monitoring.service.transport.TransportHealthChecker;
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ -32,7 +32,7 @@ public class Lwm2mTransportHealthChecker extends TransportHealthChecker<Lwm2mTra
private Lwm2mClient lwm2mClient;
protected Lwm2mTransportHealthChecker(Lwm2mTransportMonitoringConfig config, MonitoringTargetConfig target) {
protected Lwm2mTransportHealthChecker(Lwm2mTransportMonitoringConfig config, TransportMonitoringTarget target) {
super(config, target);
}

12
monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/MqttTransportHealthChecker.java → monitoring/src/main/java/org/thingsboard/monitoring/service/transport/impl/MqttTransportHealthChecker.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.paho.client.mqttv3.IMqttToken;
@ -25,10 +25,10 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
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.MqttTransportMonitoringConfig;
import org.thingsboard.monitoring.transport.TransportHealthChecker;
import org.thingsboard.monitoring.config.transport.MqttTransportMonitoringConfig;
import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget;
import org.thingsboard.monitoring.config.transport.TransportType;
import org.thingsboard.monitoring.service.transport.TransportHealthChecker;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ -39,7 +39,7 @@ public class MqttTransportHealthChecker extends TransportHealthChecker<MqttTrans
private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
protected MqttTransportHealthChecker(MqttTransportMonitoringConfig config, MonitoringTargetConfig target) {
protected MqttTransportHealthChecker(MqttTransportMonitoringConfig config, TransportMonitoringTarget target) {
super(config, target);
}

198
monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportMonitoringService.java

@ -1,198 +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.
*/
package org.thingsboard.monitoring.transport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.monitoring.client.TbClient;
import org.thingsboard.monitoring.client.WsClient;
import org.thingsboard.monitoring.client.WsClientFactory;
import org.thingsboard.monitoring.config.DeviceConfig;
import org.thingsboard.monitoring.config.MonitoringTargetConfig;
import org.thingsboard.monitoring.config.TransportType;
import org.thingsboard.monitoring.config.service.TransportMonitoringConfig;
import org.thingsboard.monitoring.data.Latencies;
import org.thingsboard.monitoring.data.MonitoredServiceKey;
import org.thingsboard.monitoring.service.MonitoringReporter;
import org.thingsboard.monitoring.util.ResourceUtils;
import org.thingsboard.monitoring.util.TbStopWatch;
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.id.DeviceId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import javax.annotation.PostConstruct;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Slf4j
public final class TransportMonitoringService {
private final List<TransportMonitoringConfig> configs;
private final List<TransportHealthChecker<?>> transportHealthCheckers = new LinkedList<>();
private final List<UUID> 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();
}
}

1
monitoring/src/main/resources/lwm2m/device_profile.json

@ -1,5 +1,4 @@
{
"name": "LwM2M Monitoring",
"type": "DEFAULT",
"image": null,
"defaultQueueName": null,

51
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:

4
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

30
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();

76
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<File> 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<String, String> 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();

30
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<WebElement> 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);
}
}

20
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();
}
}

3
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());
}

79
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);
}
}

127
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));
}
}

1
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";
}

12
pom.xml

@ -111,7 +111,7 @@
<aws.sdk.version>1.11.747</aws.sdk.version>
<pubsub.client.version>1.105.0</pubsub.client.version>
<google.common.protos.version>2.1.0</google.common.protos.version> <!-- required by io.grpc:grpc-protobuf:1.38.0-->
<azure-servicebus.version>3.2.0</azure-servicebus.version>
<azure-servicebus.version>3.6.7</azure-servicebus.version>
<passay.version>1.5.0</passay.version>
<ua-parser.version>1.5.4</ua-parser.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
@ -1836,6 +1836,16 @@
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-servicebus</artifactId>
<version>${azure-servicebus.version}</version>
<exclusions>
<exclusion>
<groupId>com.nimbusds</groupId>
<artifactId>content-type</artifactId>
</exclusion>
<exclusion>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.passay</groupId>

12
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}"

12
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}"

12
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}"

12
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}"

12
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}"

5
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<Array<DataKey>> {
public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[],
entityTypes?: EntityType[], config?: RequestConfig): Observable<Array<DataKey>> {
if (!types.length) {
return of([]);
}
@ -832,7 +833,7 @@ export class EntityService {
entitiesKeysByQuery$ = of({
attribute: [],
timeseries: [],
entityTypes: [],
entityTypes: entityTypes || [],
});
}
return entitiesKeysByQuery$.pipe(

23
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();

2
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,

1
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-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"

18
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -25,11 +25,19 @@ import {
import {
WidgetActionsPanelComponent
} from '@home/components/widget/config/basic/common/widget-actions-panel.component';
import {
EntitiesTableBasicConfigComponent
} from '@home/components/widget/config/basic/cards/entities-table-basic-config.component';
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component';
import { DataKeyRowComponent } from '@home/components/widget/config/basic/common/data-key-row.component';
@NgModule({
declarations: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent
SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
],
imports: [
CommonModule,
@ -38,12 +46,16 @@ import {
],
exports: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent
SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
]
})
export class BasicWidgetConfigModule {
}
export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = {
'tb-simple-card-basic-config': SimpleCardBasicConfigComponent
'tb-simple-card-basic-config': SimpleCardBasicConfigComponent,
'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent
};

86
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html

@ -0,0 +1,86 @@
<!--
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.
-->
<ng-container [formGroup]="entitiesTableWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeys
formControlName="datasources">
</tb-datasources>
<tb-data-keys-panel
panelTitle="{{ 'widgets.table.columns' | translate }}"
addKeyTitle="{{ 'widgets.table.add-column' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}"
hideDataKeyColor
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="columns">
</tb-data-keys-panel>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ 'widget-config.card-title' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between same-padding">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-material-icon-select asBoxInput
[color]="entitiesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.background' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

154
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<AppState>,
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;
}
}
}

13
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<AppState>,
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: [{

170
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -0,0 +1,170 @@
<!--
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.
-->
<div [formGroup]="keyRowFormGroup" class="tb-data-key-row">
<mat-form-field fxFlex class="tb-inline-field tb-key-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type"
(removed)="removeKey()">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels">
<div class="tb-chip-label">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">timeline</mat-icon>
</ng-container>
</div>
<div class="tb-chip-label">
<strong>
<ng-container *ngTemplateOutlet="keyName"></ng-container>
</strong>
</div>
</div>
<button type="button"
(click)="editKey()" mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">edit</mat-icon>
</button>
<button matChipRemove
type="button"
mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">close</mat-icon>
</button>
</div>
</mat-chip-row>
<input matInput
type="text"
placeholder="{{ 'widget-config.set' | translate }}"
#keyInput
[formControl]="keyFormControl"
matAutocompleteOrigin
[fxHide]="!!modelValue.type"
[readonly]="!!modelValue.type"
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
(focusin)="onKeyInputFocus()"
(drop)="$event.preventDefault();"
[matAutocomplete]="keyAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addKey($event)"
/>
</mat-chip-grid>
<mat-autocomplete #keyAutocomplete="matAutocomplete"
class="tb-autocomplete"
panelWidth="fit-content"
[displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">timeline</mat-icon>
</ng-container>
<span [innerHTML]="key.name | highlight:keySearchText"></span>
</span>
</mat-option>
<mat-option *ngIf="!(filteredKeys | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(keySearchText); else searchNotEmpty">
<span translate>entity.no-keys-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('entity.no-key-matching',
{key: truncate.transform(keySearchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(keySearchText)">entity.create-new-key</a>
</span>
<ng-template #createEntityKey>
<span>{{'entity.create-new-key' | translate }} </span>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above"
(click)="createKey(keySearchText, dataKeyTypes.alarm)">notifications</mat-icon>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"
(click)="createKey(keySearchText, dataKeyTypes.attribute)"></mat-icon>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"
(click)="createKey(keySearchText, dataKeyTypes.entityField)"></mat-icon>
<mat-icon class="tb-datakey-icon new-key"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above"
(click)="createKey(keySearchText, dataKeyTypes.timeseries)">timeline</mat-icon>
</ng-template>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field fxFlex class="tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div *ngIf="!hideDataKeyColor" class="tb-color-field">
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-units-field">
<tb-widget-units *ngIf="displayUnitsOrDigits"
formControlName="units">
</tb-widget-units>
</div>
<div class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field center number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<ng-template #keyName>
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName">
<span>f(</span><ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container><span>)</span>
</ng-container>
<ng-template #keyName>
<ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container>
</ng-template>
</ng-template>
<ng-template #keyNameTemplate>
<ng-container *ngIf="dataKeyHasAggregation(); else keyName;">
<span class="tb-agg-func">{{ modelValue?.aggregationType }}</span><span>({{ modelValue?.name }})</span>
</ng-container>
<ng-template #keyName>
<span>{{modelValue?.name}}</span>
</ng-template>
</ng-template>

53
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;
}
}

426
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<HTMLInputElement>;
@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<Array<DataKey>>;
keySearchText = '';
private latestKeySearchTextResult: Array<DataKey> = null;
private keyFetchObservable$: Observable<Array<DataKey>> = null;
get dataKeyType(): DataKeyType {
return this.dataKeysPanelComponent.dataKeyType;
}
get alarmKeys(): Array<DataKey> {
return this.dataKeysPanelComponent.alarmKeys;
}
get functionTypeKeys(): Array<DataKey> {
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, DataKeyConfigDialogData, DataKey>(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<Array<DataKey>> {
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<Array<DataKey>> {
if (this.keyFetchObservable$ === null) {
let fetchObservable: Observable<Array<DataKey>>;
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);
}
}

67
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -0,0 +1,67 @@
<!--
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.
-->
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title">{{ panelTitle }}</div>
<div class="tb-data-keys-table">
<div class="tb-data-keys-header">
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.key</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.label</div>
<div *ngIf="!hideDataKeyColor" class="tb-data-keys-header-cell tb-color-header" translate>datakey.color</div>
<div class="tb-data-keys-header-cell tb-units-header" translate>widget-config.units-short</div>
<div class="tb-data-keys-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div class="tb-data-keys-header-cell tb-actions-header"></div>
</div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-data-keys-body tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="keyDrop($event)">
<div cdkDrag class="tb-data-keys-table-row tb-draggable" *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey;
let $index = index;">
<tb-data-key-row fxFlex
[formControl]="keyControl"
[datasourceType]="datasourceType"
[deviceId]="deviceId"
[entityAliasId]="entityAliasId">
</tb-data-key-row>
<div class="tb-data-keys-table-row-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
[matTooltip]="removeKeyTitle"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ addKeyTitle }}
</button>
</div>
</div>
<ng-template #noKeys>
<span fxLayoutAlign="center center"
class="tb-prompt">{{ noKeysText }}</span>
</ng-template>

76
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);
}
}
}
}

237
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<DataKey>;
functionTypeKeys: Array<DataKey>;
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<string[]>) {
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<AbstractControl> = [];
if (keys) {
keys.forEach((key) => {
keysControls.push(this.fb.control(key, [dataKeyRowValidator]));
});
}
return this.fb.array(keysControls);
}
}

2
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,

10
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<DataKey>;
private functionTypeKeys: Array<DataKey>;
filteredKeys: Observable<Array<string>>;
private latestKeySearchResult: Array<string> = 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<Array<DataKey>>;
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];

144
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;
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html

@ -61,7 +61,7 @@
</mat-form-field>
</ng-template>
</section>
<section fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<section *ngIf="!hideDataKeys" fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceFormGroup.get('type').value"

4
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts

@ -139,6 +139,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.datasourcesComponent?.hideDataKeyDecimals;
}
public get hideDataKeys(): boolean {
return this.datasourcesComponent?.hideDataKeys;
}
@Input()
disabled: boolean;

6
ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts

@ -46,7 +46,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-datasources',
templateUrl: './datasources.component.html',
styleUrls: ['./datasources.component.scss', './widget-config.scss'],
styleUrls: ['./datasources.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -104,6 +104,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean()
hideDataKeyDecimals = false;
@Input()
@coerceBoolean()
hideDataKeys = false;
@Input()
configMode: WidgetConfigMode;

2
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts

@ -31,7 +31,7 @@ export interface TimewindowConfigData {
@Component({
selector: 'tb-timewindow-config-panel',
templateUrl: './timewindow-config-panel.component.html',
styleUrls: ['./widget-config.scss'],
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,

58
ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts

@ -23,12 +23,15 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigMode } from '@shared/models/widget.models';
import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks;
export interface IBasicWidgetConfigComponent {
isAdd: boolean;
widgetConfig: WidgetConfigComponentData;
widgetConfigChanged: Observable<WidgetConfigComponentData>;
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<WidgetConfigComponentData>();
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable();
protected constructor(@Inject(Store) protected store: Store<AppState>) {
protected constructor(@Inject(Store) protected store: Store<AppState>,
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);

169
ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss

@ -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;
}
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html

@ -15,6 +15,6 @@
limitations under the License.
-->
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="tb-inline-field center" subscriptSizing="dynamic">
<input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>

3
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 {

2
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 {

25
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html

@ -66,4 +66,29 @@
</ng-template>
</mat-expansion-panel>
</fieldset>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

6
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, []],
});
}

25
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html

@ -73,4 +73,29 @@
</ng-template>
</mat-expansion-panel>
</fieldset>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

6
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, []],
});
}

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -23,6 +23,9 @@
<mat-checkbox formControlName="enableSearch">
{{ 'widgets.table.enable-search' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="enableSelectColumnDisplay">
{{ 'widgets.table.enable-select-column-display' | translate }}
</mat-checkbox>
</section>
<section fxLayout="column" fxFlex>
<mat-checkbox formControlName="enableStickyHeader">

2
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, []],

2
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([]))

11
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([]))

2
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([]))

1
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;

116
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<DisplayColumn[]> = [];
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<AppState>,
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;
}

21
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([]))

3
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html

@ -59,8 +59,7 @@
</mat-error>
</mat-form-field>
<tb-template-autocomplete
required
allowCreate
required allowCreate allowEdit
formControlName="templateId"
[notificationTypes]="ruleNotificationForm.get('triggerType').value">
</tb-template-autocomplete>

3
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -49,8 +49,7 @@
</div>
<div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate">
<tb-template-autocomplete
required
allowCreate
required allowCreate allowEdit
formControlName="templateId"
[notificationTypes]="notificationType.GENERAL">
</tb-template-autocomplete>

67
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -15,55 +15,48 @@
limitations under the License.
-->
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="cheatSheetComponent"
fxLayout="column" class="tb-rulechain">
<div class="mat-content tb-rulechain" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="cheatSheetComponent" fxLayout="column">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-rulechain-container" fxFlex fxLayout="column">
<div class="tb-rulechain-layout" fxFlex fxLayout="row">
<section fxLayout="row"
class="tb-header-buttons tb-library-open" [fxShow]="!isLibraryOpen">
<button color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-open-library"
(click)="isLibraryOpen = true"
matTooltip="{{ 'rulenode.open-node-library' | translate }}"
matTooltipPosition="above">
<mat-icon>menu</mat-icon>
</button>
</section>
<mat-drawer-container style="width: 100%; height: 100%;">
<mat-drawer class="tb-rulechain-library mat-elevation-z4"
disableClose="true"
mode="side"
[opened]="isLibraryOpen"
#drawer
opened
position="start"
fxLayout="column">
<mat-toolbar color="primary" class="tb-dark">
<tb-rule-chain-select
fxFlex
*ngIf="!isImport"
[ruleChainType]="ruleChainType"
[disabled]="isDirtyValue"
[(ngModel)]="ruleChain.id.id"
(ngModelChange)="currentRuleChainIdChanged(ruleChain.id?.id)">
</tb-rule-chain-select>
</mat-toolbar>
<mat-toolbar>
<div class="mat-toolbar-tools">
<button mat-icon-button class="tb-small"
matTooltip="{{'rulenode.search' | translate}}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex class="tb-appearance-transparent">
<button mat-icon-button matPrefix class="tb-small"
matTooltip="{{'rulenode.search' | translate}}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<input #ruleNodeSearchInput matInput
[(ngModel)]="ruleNodeTypeSearch"
placeholder="{{'rulenode.search' | translate}}"/>
<button mat-icon-button matSuffix class="tb-small"
[fxShow]="ruleNodeTypeSearch !== ''"
(click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()"
matTooltip="{{'action.clear-search' | translate}}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<button mat-icon-button class="tb-small"
[fxShow]="ruleNodeTypeSearch !== ''"
(click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()"
matTooltip="{{'action.clear-search' | translate}}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
<button mat-icon-button class="tb-small"
(click)="isLibraryOpen = false"
matTooltip="{{'action.close' | translate}}"
matTooltipPosition="above">
<mat-icon>chevron_left</mat-icon>
</button>
</div>
</mat-toolbar>
<div class="tb-rulechain-library-panel-group">
@ -164,6 +157,14 @@
</tb-details-panel>
</mat-drawer>
<mat-drawer-content class="tb-rulechain-graph-content">
<button color="primary"
mat-mini-fab
class="tb-library-node-btn"
(click)="drawer.toggle();"
matTooltip="{{ (drawer.opened ? 'rulenode.close-node-library' : 'rulenode.open-node-library') | translate }}"
matTooltipPosition="above">
<mat-icon class="tb-library-node-btn-icon" [ngClass]="{'tb-library-node-btn-icon-toggled' : drawer.opened}">chevron_right</mat-icon>
</button>
<button #versionControlButton
*ngIf="!isImport"
color="primary"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save