Commit e353ab3c8190cfff9c68554965d113ed66c1c4f7

Authored by Andrii Shvaika
2 parents 4d012ac6 3dc7fde4

Merge branch 'develop/3.2' of github.com:thingsboard/thingsboard into develop/3.2

Showing 69 changed files with 1175 additions and 236 deletions
@@ -166,7 +166,7 @@ @@ -166,7 +166,7 @@
166 "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", 166 "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
167 "settingsSchema": "{}", 167 "settingsSchema": "{}",
168 "dataKeySettingsSchema": "{}", 168 "dataKeySettingsSchema": "{}",
169 - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" 169 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
170 } 170 }
171 } 171 }
172 ] 172 ]
@@ -247,4 +247,4 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe @@ -247,4 +247,4 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
247 log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); 247 log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
248 } 248 }
249 } 249 }
250 -}  
  250 +}
@@ -209,4 +209,4 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr @@ -209,4 +209,4 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
209 log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); 209 log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
210 } 210 }
211 } 211 }
212 -}  
  212 +}
@@ -39,8 +39,7 @@ public abstract class AbstractCleanUpService { @@ -39,8 +39,7 @@ public abstract class AbstractCleanUpService {
39 protected String dbPassword; 39 protected String dbPassword;
40 40
41 protected long executeQuery(Connection conn, String query) throws SQLException { 41 protected long executeQuery(Connection conn, String query) throws SQLException {
42 - try (Statement statement = conn.createStatement()) {  
43 - ResultSet resultSet = statement.executeQuery(query); 42 + try (Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(query)) {
44 if (log.isDebugEnabled()) { 43 if (log.isDebugEnabled()) {
45 getWarnings(statement); 44 getWarnings(statement);
46 } 45 }
@@ -33,4 +33,4 @@ public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUp @@ -33,4 +33,4 @@ public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUp
33 long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);"); 33 long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);");
34 log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved); 34 log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
35 } 35 }
36 -}  
  36 +}
@@ -284,6 +284,8 @@ sql: @@ -284,6 +284,8 @@ sql:
284 batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" 284 batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}"
285 stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" 285 stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}"
286 batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}" 286 batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}"
  287 + # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks
  288 + batch_sort: "${SQL_BATCH_SORT:false}"
287 # Specify whether to remove null characters from strValue of attributes and timeseries before insert 289 # Specify whether to remove null characters from strValue of attributes and timeseries before insert
288 remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}" 290 remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}"
289 # Specify whether to log database queries and their parameters generated by entity query repository 291 # Specify whether to log database queries and their parameters generated by entity query repository
@@ -651,11 +653,11 @@ queue: @@ -651,11 +653,11 @@ queue:
651 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" 653 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
652 other: 654 other:
653 topic-properties: 655 topic-properties:
654 - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
655 - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
656 - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
657 - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
658 - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" 656 + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  657 + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  658 + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  659 + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  660 + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}"
659 aws_sqs: 661 aws_sqs:
660 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" 662 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
661 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" 663 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
@@ -37,6 +37,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { @@ -37,6 +37,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
37 private final AdminClient client; 37 private final AdminClient client;
38 private final Map<String, String> topicConfigs; 38 private final Map<String, String> topicConfigs;
39 private final Set<String> topics = ConcurrentHashMap.newKeySet(); 39 private final Set<String> topics = ConcurrentHashMap.newKeySet();
  40 + private final int numPartitions;
40 41
41 private final short replicationFactor; 42 private final short replicationFactor;
42 43
@@ -50,6 +51,13 @@ public class TbKafkaAdmin implements TbQueueAdmin { @@ -50,6 +51,13 @@ public class TbKafkaAdmin implements TbQueueAdmin {
50 log.error("Failed to get all topics.", e); 51 log.error("Failed to get all topics.", e);
51 } 52 }
52 53
  54 + String numPartitionsStr = topicConfigs.get("partitions");
  55 + if (numPartitionsStr != null) {
  56 + numPartitions = Integer.parseInt(numPartitionsStr);
  57 + topicConfigs.remove("partitions");
  58 + } else {
  59 + numPartitions = 1;
  60 + }
53 replicationFactor = settings.getReplicationFactor(); 61 replicationFactor = settings.getReplicationFactor();
54 } 62 }
55 63
@@ -59,7 +67,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { @@ -59,7 +67,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
59 return; 67 return;
60 } 68 }
61 try { 69 try {
62 - NewTopic newTopic = new NewTopic(topic, 1, replicationFactor).configs(topicConfigs); 70 + NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(topicConfigs);
63 createTopic(newTopic).values().get(topic).get(); 71 createTopic(newTopic).values().get(topic).get();
64 topics.add(topic); 72 topics.add(topic);
65 } catch (ExecutionException ee) { 73 } catch (ExecutionException ee) {
@@ -22,6 +22,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -22,6 +22,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
22 import org.thingsboard.server.common.stats.MessagesStats; 22 import org.thingsboard.server.common.stats.MessagesStats;
23 23
24 import java.util.ArrayList; 24 import java.util.ArrayList;
  25 +import java.util.Comparator;
25 import java.util.List; 26 import java.util.List;
26 import java.util.concurrent.BlockingQueue; 27 import java.util.concurrent.BlockingQueue;
27 import java.util.concurrent.ExecutorService; 28 import java.util.concurrent.ExecutorService;
@@ -30,6 +31,7 @@ import java.util.concurrent.LinkedBlockingQueue; @@ -30,6 +31,7 @@ import java.util.concurrent.LinkedBlockingQueue;
30 import java.util.concurrent.TimeUnit; 31 import java.util.concurrent.TimeUnit;
31 import java.util.function.Consumer; 32 import java.util.function.Consumer;
32 import java.util.stream.Collectors; 33 import java.util.stream.Collectors;
  34 +import java.util.stream.Stream;
33 35
34 @Slf4j 36 @Slf4j
35 public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> { 37 public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
@@ -46,7 +48,7 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> { @@ -46,7 +48,7 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
46 } 48 }
47 49
48 @Override 50 @Override
49 - public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, int index) { 51 + public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator, int index) {
50 executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase())); 52 executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase()));
51 executor.submit(() -> { 53 executor.submit(() -> {
52 String logName = params.getLogName(); 54 String logName = params.getLogName();
@@ -65,7 +67,11 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> { @@ -65,7 +67,11 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
65 queue.drainTo(entities, batchSize - 1); 67 queue.drainTo(entities, batchSize - 1);
66 boolean fullPack = entities.size() == batchSize; 68 boolean fullPack = entities.size() == batchSize;
67 log.debug("[{}] Going to save {} entities", logName, entities.size()); 69 log.debug("[{}] Going to save {} entities", logName, entities.size());
68 - saveFunction.accept(entities.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList())); 70 + Stream<E> entitiesStream = entities.stream().map(TbSqlQueueElement::getEntity);
  71 + saveFunction.accept(
  72 + (params.isBatchSortEnabled() ? entitiesStream.sorted(batchUpdateComparator) : entitiesStream)
  73 + .collect(Collectors.toList())
  74 + );
69 entities.forEach(v -> v.getFuture().set(null)); 75 entities.forEach(v -> v.getFuture().set(null));
70 stats.incrementSuccessful(entities.size()); 76 stats.incrementSuccessful(entities.size());
71 if (!fullPack) { 77 if (!fullPack) {
@@ -31,4 +31,5 @@ public class TbSqlBlockingQueueParams { @@ -31,4 +31,5 @@ public class TbSqlBlockingQueueParams {
31 private final long maxDelay; 31 private final long maxDelay;
32 private final long statsPrintIntervalMs; 32 private final long statsPrintIntervalMs;
33 private final String statsNamePrefix; 33 private final String statsNamePrefix;
  34 + private final boolean batchSortEnabled;
34 } 35 }
@@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
21 import org.thingsboard.server.common.stats.MessagesStats; 21 import org.thingsboard.server.common.stats.MessagesStats;
22 import org.thingsboard.server.common.stats.StatsFactory; 22 import org.thingsboard.server.common.stats.StatsFactory;
23 23
  24 +import java.util.Comparator;
24 import java.util.List; 25 import java.util.List;
25 import java.util.concurrent.CopyOnWriteArrayList; 26 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.function.Consumer; 27 import java.util.function.Consumer;
@@ -36,12 +37,20 @@ public class TbSqlBlockingQueueWrapper<E> { @@ -36,12 +37,20 @@ public class TbSqlBlockingQueueWrapper<E> {
36 private final int maxThreads; 37 private final int maxThreads;
37 private final StatsFactory statsFactory; 38 private final StatsFactory statsFactory;
38 39
39 - public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction) { 40 + /**
  41 + * Starts TbSqlBlockingQueues.
  42 + *
  43 + * @param logExecutor executor that will be printing logs and statistics
  44 + * @param saveFunction function to save entities in database
  45 + * @param batchUpdateComparator comparator to sort entities by primary key to avoid deadlocks in cluster mode
  46 + * NOTE: you must use all of primary key parts in your comparator
  47 + */
  48 + public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator) {
40 for (int i = 0; i < maxThreads; i++) { 49 for (int i = 0; i < maxThreads; i++) {
41 MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i); 50 MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i);
42 TbSqlBlockingQueue<E> queue = new TbSqlBlockingQueue<>(params, stats); 51 TbSqlBlockingQueue<E> queue = new TbSqlBlockingQueue<>(params, stats);
43 queues.add(queue); 52 queues.add(queue);
44 - queue.init(logExecutor, saveFunction, i); 53 + queue.init(logExecutor, saveFunction, batchUpdateComparator, i);
45 } 54 }
46 } 55 }
47 56
@@ -17,12 +17,13 @@ package org.thingsboard.server.dao.sql; @@ -17,12 +17,13 @@ package org.thingsboard.server.dao.sql;
17 17
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
19 19
  20 +import java.util.Comparator;
20 import java.util.List; 21 import java.util.List;
21 import java.util.function.Consumer; 22 import java.util.function.Consumer;
22 23
23 public interface TbSqlQueue<E> { 24 public interface TbSqlQueue<E> {
24 25
25 - void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, int queueIndex); 26 + void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator, int queueIndex);
26 27
27 void destroy(); 28 void destroy();
28 29
@@ -38,6 +38,7 @@ import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
38 import javax.annotation.PostConstruct; 38 import javax.annotation.PostConstruct;
39 import javax.annotation.PreDestroy; 39 import javax.annotation.PreDestroy;
40 import java.util.Collection; 40 import java.util.Collection;
  41 +import java.util.Comparator;
41 import java.util.List; 42 import java.util.List;
42 import java.util.Optional; 43 import java.util.Optional;
43 import java.util.function.Function; 44 import java.util.function.Function;
@@ -71,6 +72,9 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @@ -71,6 +72,9 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
71 @Value("${sql.attributes.batch_threads:4}") 72 @Value("${sql.attributes.batch_threads:4}")
72 private int batchThreads; 73 private int batchThreads;
73 74
  75 + @Value("${sql.batch_sort:false}")
  76 + private boolean batchSortEnabled;
  77 +
74 private TbSqlBlockingQueueWrapper<AttributeKvEntity> queue; 78 private TbSqlBlockingQueueWrapper<AttributeKvEntity> queue;
75 79
76 @PostConstruct 80 @PostConstruct
@@ -81,11 +85,17 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @@ -81,11 +85,17 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
81 .maxDelay(maxDelay) 85 .maxDelay(maxDelay)
82 .statsPrintIntervalMs(statsPrintIntervalMs) 86 .statsPrintIntervalMs(statsPrintIntervalMs)
83 .statsNamePrefix("attributes") 87 .statsNamePrefix("attributes")
  88 + .batchSortEnabled(batchSortEnabled)
84 .build(); 89 .build();
85 90
86 Function<AttributeKvEntity, Integer> hashcodeFunction = entity -> entity.getId().getEntityId().hashCode(); 91 Function<AttributeKvEntity, Integer> hashcodeFunction = entity -> entity.getId().getEntityId().hashCode();
87 queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, batchThreads, statsFactory); 92 queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, batchThreads, statsFactory);
88 - queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v)); 93 + queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v),
  94 + Comparator.comparing((AttributeKvEntity attributeKvEntity) -> attributeKvEntity.getId().getEntityId())
  95 + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getEntityType().name())
  96 + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeType())
  97 + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey())
  98 + );
89 } 99 }
90 100
91 @PreDestroy 101 @PreDestroy
@@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors; @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors;
21 import com.google.common.util.concurrent.SettableFuture; 21 import com.google.common.util.concurrent.SettableFuture;
22 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
23 import org.springframework.beans.factory.annotation.Autowired; 23 import org.springframework.beans.factory.annotation.Autowired;
  24 +import org.springframework.beans.factory.annotation.Value;
24 import org.springframework.data.domain.PageRequest; 25 import org.springframework.data.domain.PageRequest;
25 import org.springframework.data.domain.Sort; 26 import org.springframework.data.domain.Sort;
26 import org.thingsboard.server.common.data.id.EntityId; 27 import org.thingsboard.server.common.data.id.EntityId;
@@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
31 import org.thingsboard.server.common.data.kv.TsKvEntry; 32 import org.thingsboard.server.common.data.kv.TsKvEntry;
32 import org.thingsboard.server.common.stats.StatsFactory; 33 import org.thingsboard.server.common.stats.StatsFactory;
33 import org.thingsboard.server.dao.DaoUtil; 34 import org.thingsboard.server.dao.DaoUtil;
  35 +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
34 import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; 36 import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
35 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; 37 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
36 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; 38 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
@@ -40,9 +42,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; @@ -40,9 +42,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
40 42
41 import javax.annotation.PostConstruct; 43 import javax.annotation.PostConstruct;
42 import javax.annotation.PreDestroy; 44 import javax.annotation.PreDestroy;
43 -import java.util.ArrayList;  
44 -import java.util.List;  
45 -import java.util.Optional; 45 +import java.util.*;
46 import java.util.concurrent.CompletableFuture; 46 import java.util.concurrent.CompletableFuture;
47 import java.util.function.Function; 47 import java.util.function.Function;
48 import java.util.stream.Collectors; 48 import java.util.stream.Collectors;
@@ -68,11 +68,16 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq @@ -68,11 +68,16 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
68 .maxDelay(tsMaxDelay) 68 .maxDelay(tsMaxDelay)
69 .statsPrintIntervalMs(tsStatsPrintIntervalMs) 69 .statsPrintIntervalMs(tsStatsPrintIntervalMs)
70 .statsNamePrefix("ts") 70 .statsNamePrefix("ts")
  71 + .batchSortEnabled(batchSortEnabled)
71 .build(); 72 .build();
72 73
73 Function<TsKvEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode(); 74 Function<TsKvEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
74 tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, tsBatchThreads, statsFactory); 75 tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, tsBatchThreads, statsFactory);
75 - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); 76 + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v),
  77 + Comparator.comparing((Function<TsKvEntity, UUID>) AbstractTsKvEntity::getEntityId)
  78 + .thenComparing(AbstractTsKvEntity::getKey)
  79 + .thenComparing(AbstractTsKvEntity::getTs)
  80 + );
76 } 81 }
77 82
78 @PreDestroy 83 @PreDestroy
@@ -53,6 +53,9 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries @@ -53,6 +53,9 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries
53 @Value("${sql.timescale.batch_threads:4}") 53 @Value("${sql.timescale.batch_threads:4}")
54 protected int timescaleBatchThreads; 54 protected int timescaleBatchThreads;
55 55
  56 + @Value("${sql.batch_sort:false}")
  57 + protected boolean batchSortEnabled;
  58 +
56 protected ListenableFuture<List<TsKvEntry>> processFindAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) { 59 protected ListenableFuture<List<TsKvEntry>> processFindAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
57 List<ListenableFuture<List<TsKvEntry>>> futures = queries 60 List<ListenableFuture<List<TsKvEntry>>> futures = queries
58 .stream() 61 .stream()
@@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
35 import org.thingsboard.server.common.data.kv.TsKvEntry; 35 import org.thingsboard.server.common.data.kv.TsKvEntry;
36 import org.thingsboard.server.common.stats.StatsFactory; 36 import org.thingsboard.server.common.stats.StatsFactory;
37 import org.thingsboard.server.dao.DaoUtil; 37 import org.thingsboard.server.dao.DaoUtil;
  38 +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
38 import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; 39 import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey;
39 import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; 40 import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
40 import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; 41 import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
@@ -50,12 +51,10 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; @@ -50,12 +51,10 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao;
50 import javax.annotation.Nullable; 51 import javax.annotation.Nullable;
51 import javax.annotation.PostConstruct; 52 import javax.annotation.PostConstruct;
52 import javax.annotation.PreDestroy; 53 import javax.annotation.PreDestroy;
53 -import java.util.ArrayList;  
54 -import java.util.HashMap;  
55 -import java.util.List;  
56 -import java.util.Map;  
57 -import java.util.Optional; 54 +import java.util.*;
58 import java.util.concurrent.ExecutionException; 55 import java.util.concurrent.ExecutionException;
  56 +import java.util.function.Function;
  57 +import java.util.stream.Collectors;
59 58
60 @Slf4j 59 @Slf4j
61 @Component 60 @Component
@@ -90,6 +89,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @@ -90,6 +89,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
90 @Value("${sql.ts_latest.batch_threads:4}") 89 @Value("${sql.ts_latest.batch_threads:4}")
91 private int tsLatestBatchThreads; 90 private int tsLatestBatchThreads;
92 91
  92 + @Value("${sql.batch_sort:false}")
  93 + protected boolean batchSortEnabled;
  94 +
93 @Autowired 95 @Autowired
94 protected ScheduledLogExecutorComponent logExecutor; 96 protected ScheduledLogExecutorComponent logExecutor;
95 97
@@ -104,6 +106,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @@ -104,6 +106,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
104 .maxDelay(tsLatestMaxDelay) 106 .maxDelay(tsLatestMaxDelay)
105 .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) 107 .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs)
106 .statsNamePrefix("ts.latest") 108 .statsNamePrefix("ts.latest")
  109 + .batchSortEnabled(false)
107 .build(); 110 .build();
108 111
109 java.util.function.Function<TsKvLatestEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode(); 112 java.util.function.Function<TsKvLatestEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
@@ -113,14 +116,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @@ -113,14 +116,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
113 Map<TsKey, TsKvLatestEntity> trueLatest = new HashMap<>(); 116 Map<TsKey, TsKvLatestEntity> trueLatest = new HashMap<>();
114 v.forEach(ts -> { 117 v.forEach(ts -> {
115 TsKey key = new TsKey(ts.getEntityId(), ts.getKey()); 118 TsKey key = new TsKey(ts.getEntityId(), ts.getKey());
116 - TsKvLatestEntity old = trueLatest.get(key);  
117 - if (old == null || old.getTs() < ts.getTs()) {  
118 - trueLatest.put(key, ts);  
119 - } 119 + trueLatest.merge(key, ts, (oldTs, newTs) -> oldTs.getTs() < newTs.getTs() ? newTs : oldTs);
120 }); 120 });
121 List<TsKvLatestEntity> latestEntities = new ArrayList<>(trueLatest.values()); 121 List<TsKvLatestEntity> latestEntities = new ArrayList<>(trueLatest.values());
  122 + if (batchSortEnabled) {
  123 + latestEntities.sort(Comparator.comparing((Function<TsKvLatestEntity, UUID>) AbstractTsKvEntity::getEntityId)
  124 + .thenComparingInt(AbstractTsKvEntity::getKey));
  125 + }
122 insertLatestTsRepository.saveOrUpdate(latestEntities); 126 insertLatestTsRepository.saveOrUpdate(latestEntities);
123 - }); 127 + }, (l, r) -> 0);
124 } 128 }
125 129
126 @PreDestroy 130 @PreDestroy
@@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
33 import org.thingsboard.server.common.data.kv.TsKvEntry; 33 import org.thingsboard.server.common.data.kv.TsKvEntry;
34 import org.thingsboard.server.common.stats.StatsFactory; 34 import org.thingsboard.server.common.stats.StatsFactory;
35 import org.thingsboard.server.dao.DaoUtil; 35 import org.thingsboard.server.dao.DaoUtil;
  36 +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
36 import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; 37 import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity;
37 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; 38 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
38 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; 39 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
@@ -43,11 +44,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao; @@ -43,11 +44,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao;
43 44
44 import javax.annotation.PostConstruct; 45 import javax.annotation.PostConstruct;
45 import javax.annotation.PreDestroy; 46 import javax.annotation.PreDestroy;
46 -import java.util.ArrayList;  
47 -import java.util.Collections;  
48 -import java.util.List;  
49 -import java.util.Optional;  
50 -import java.util.UUID; 47 +import java.util.*;
51 import java.util.concurrent.CompletableFuture; 48 import java.util.concurrent.CompletableFuture;
52 import java.util.function.Function; 49 import java.util.function.Function;
53 50
@@ -78,12 +75,17 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements @@ -78,12 +75,17 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
78 .maxDelay(tsMaxDelay) 75 .maxDelay(tsMaxDelay)
79 .statsPrintIntervalMs(tsStatsPrintIntervalMs) 76 .statsPrintIntervalMs(tsStatsPrintIntervalMs)
80 .statsNamePrefix("ts.timescale") 77 .statsNamePrefix("ts.timescale")
  78 + .batchSortEnabled(batchSortEnabled)
81 .build(); 79 .build();
82 80
83 Function<TimescaleTsKvEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode(); 81 Function<TimescaleTsKvEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
84 tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, timescaleBatchThreads, statsFactory); 82 tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, timescaleBatchThreads, statsFactory);
85 83
86 - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); 84 + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v),
  85 + Comparator.comparing((Function<TimescaleTsKvEntity, UUID>) AbstractTsKvEntity::getEntityId)
  86 + .thenComparing(AbstractTsKvEntity::getKey)
  87 + .thenComparing(AbstractTsKvEntity::getTs)
  88 + );
87 } 89 }
88 90
89 @PreDestroy 91 @PreDestroy
1 TB_QUEUE_TYPE=kafka 1 TB_QUEUE_TYPE=kafka
2 TB_KAFKA_SERVERS=kafka:9092 2 TB_KAFKA_SERVERS=kafka:9092
  3 +TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100
@@ -25,7 +25,7 @@ kafka: @@ -25,7 +25,7 @@ kafka:
25 # Kafka Bootstrap Servers 25 # Kafka Bootstrap Servers
26 servers: "localhost:9092" 26 servers: "localhost:9092"
27 replication_factor: "1" 27 replication_factor: "1"
28 - topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600" 28 + topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100"
29 use_confluent_cloud: false 29 use_confluent_cloud: false
30 confluent: 30 confluent:
31 sasl: 31 sasl:
@@ -34,7 +34,7 @@ function KafkaProducer() { @@ -34,7 +34,7 @@ function KafkaProducer() {
34 this.send = async (responseTopic, scriptId, rawResponse, headers) => { 34 this.send = async (responseTopic, scriptId, rawResponse, headers) => {
35 35
36 if (!topics.includes(responseTopic)) { 36 if (!topics.includes(responseTopic)) {
37 - let createResponseTopicResult = await createTopic(responseTopic); 37 + let createResponseTopicResult = await createTopic(responseTopic, 1);
38 topics.push(responseTopic); 38 topics.push(responseTopic);
39 if (createResponseTopicResult) { 39 if (createResponseTopicResult) {
40 logger.info('Created new topic: %s', requestTopic); 40 logger.info('Created new topic: %s', requestTopic);
@@ -88,7 +88,18 @@ function KafkaProducer() { @@ -88,7 +88,18 @@ function KafkaProducer() {
88 kafkaAdmin = kafkaClient.admin(); 88 kafkaAdmin = kafkaClient.admin();
89 await kafkaAdmin.connect(); 89 await kafkaAdmin.connect();
90 90
91 - let createRequestTopicResult = await createTopic(requestTopic); 91 + let partitions = 1;
  92 +
  93 + for (let i = 0; i < configEntries.length; i++) {
  94 + let param = configEntries[i];
  95 + if (param.name === 'partitions') {
  96 + partitions = param.value;
  97 + configEntries.splice(i, 1);
  98 + break;
  99 + }
  100 + }
  101 +
  102 + let createRequestTopicResult = await createTopic(requestTopic, partitions);
92 103
93 if (createRequestTopicResult) { 104 if (createRequestTopicResult) {
94 logger.info('Created new topic: %s', requestTopic); 105 logger.info('Created new topic: %s', requestTopic);
@@ -121,10 +132,11 @@ function KafkaProducer() { @@ -121,10 +132,11 @@ function KafkaProducer() {
121 } 132 }
122 })(); 133 })();
123 134
124 -function createTopic(topic) { 135 +function createTopic(topic, partitions) {
125 return kafkaAdmin.createTopics({ 136 return kafkaAdmin.createTopics({
126 topics: [{ 137 topics: [{
127 topic: topic, 138 topic: topic,
  139 + numPartitions: partitions,
128 replicationFactor: replicationFactor, 140 replicationFactor: replicationFactor,
129 configEntries: configEntries 141 configEntries: configEntries
130 }] 142 }]
@@ -77,11 +77,11 @@ queue: @@ -77,11 +77,11 @@ queue:
77 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" 77 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
78 other: 78 other:
79 topic-properties: 79 topic-properties:
80 - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
81 - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
82 - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
83 - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
84 - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" 80 + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  81 + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  82 + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  83 + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  84 + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}"
85 aws_sqs: 85 aws_sqs:
86 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" 86 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
87 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" 87 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
@@ -70,11 +70,11 @@ queue: @@ -70,11 +70,11 @@ queue:
70 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" 70 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
71 other: 71 other:
72 topic-properties: 72 topic-properties:
73 - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
74 - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
75 - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
76 - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
77 - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" 73 + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  74 + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  75 + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  76 + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  77 + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}"
78 aws_sqs: 78 aws_sqs:
79 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" 79 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
80 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" 80 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
@@ -98,11 +98,11 @@ queue: @@ -98,11 +98,11 @@ queue:
98 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" 98 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
99 other: 99 other:
100 topic-properties: 100 topic-properties:
101 - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
102 - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
103 - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
104 - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"  
105 - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" 101 + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  102 + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  103 + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  104 + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
  105 + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}"
106 aws_sqs: 106 aws_sqs:
107 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" 107 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
108 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" 108 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
@@ -123,7 +123,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>, @@ -123,7 +123,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
123 if (isString(obj[curr])) { 123 if (isString(obj[curr])) {
124 acc[curr] = obj[curr].trim(); 124 acc[curr] = obj[curr].trim();
125 } else if (isObject(obj[curr])) { 125 } else if (isObject(obj[curr])) {
126 - acc[curr] = this.deepTrim(obj[curr]) 126 + acc[curr] = this.deepTrim(obj[curr]);
127 } else { 127 } else {
128 acc[curr] = obj[curr]; 128 acc[curr] = obj[curr];
129 } 129 }
@@ -24,7 +24,8 @@ @@ -24,7 +24,8 @@
24 </mat-option> 24 </mat-option>
25 </mat-select> 25 </mat-select>
26 </mat-form-field> 26 </mat-form-field>
27 - <tb-filter-predicate-value fxFlex="60" 27 + <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
  28 + fxFlex="60"
28 [valueType]="valueTypeEnum.BOOLEAN" 29 [valueType]="valueTypeEnum.BOOLEAN"
29 formControlName="value"> 30 formControlName="value">
30 </tb-filter-predicate-value> 31 </tb-filter-predicate-value>
@@ -39,6 +39,8 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On @@ -39,6 +39,8 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
39 39
40 @Input() disabled: boolean; 40 @Input() disabled: boolean;
41 41
  42 + @Input() allowUserDynamicSource = true;
  43 +
42 valueTypeEnum = EntityKeyValueType; 44 valueTypeEnum = EntityKeyValueType;
43 45
44 booleanFilterPredicateFormGroup: FormGroup; 46 booleanFilterPredicateFormGroup: FormGroup;
@@ -38,6 +38,7 @@ @@ -38,6 +38,7 @@
38 <tb-filter-predicate-list 38 <tb-filter-predicate-list
39 [valueType]="data.valueType" 39 [valueType]="data.valueType"
40 [displayUserParameters]="data.displayUserParameters" 40 [displayUserParameters]="data.displayUserParameters"
  41 + [allowUserDynamicSource]="data.allowUserDynamicSource"
41 [operation]="complexFilterFormGroup.get('operation').value" 42 [operation]="complexFilterFormGroup.get('operation').value"
42 [key]="data.key" 43 [key]="data.key"
43 formControlName="predicates"> 44 formControlName="predicates">
@@ -36,6 +36,7 @@ export interface ComplexFilterPredicateDialogData { @@ -36,6 +36,7 @@ export interface ComplexFilterPredicateDialogData {
36 isAdd: boolean; 36 isAdd: boolean;
37 valueType: EntityKeyValueType; 37 valueType: EntityKeyValueType;
38 displayUserParameters: boolean; 38 displayUserParameters: boolean;
  39 + allowUserDynamicSource: boolean;
39 } 40 }
40 41
41 @Component({ 42 @Component({
@@ -50,6 +50,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @@ -50,6 +50,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
50 50
51 @Input() displayUserParameters = true; 51 @Input() displayUserParameters = true;
52 52
  53 + @Input() allowUserDynamicSource = true;
  54 +
53 private propagateChange = null; 55 private propagateChange = null;
54 56
55 private complexFilterPredicate: ComplexFilterPredicateInfo; 57 private complexFilterPredicate: ComplexFilterPredicateInfo;
@@ -86,7 +88,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @@ -86,7 +88,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
86 valueType: this.valueType, 88 valueType: this.valueType,
87 isAdd: false, 89 isAdd: false,
88 key: this.key, 90 key: this.key,
89 - displayUserParameters: this.displayUserParameters 91 + displayUserParameters: this.displayUserParameters,
  92 + allowUserDynamicSource: this.allowUserDynamicSource
90 } 93 }
91 }).afterClosed().subscribe( 94 }).afterClosed().subscribe(
92 (result) => { 95 (result) => {
@@ -52,6 +52,7 @@ @@ -52,6 +52,7 @@
52 fxFlex 52 fxFlex
53 [valueType]="valueType" 53 [valueType]="valueType"
54 [displayUserParameters]="displayUserParameters" 54 [displayUserParameters]="displayUserParameters"
  55 + [allowUserDynamicSource]="allowUserDynamicSource"
55 [key]="key" 56 [key]="key"
56 [formControl]="predicateControl"> 57 [formControl]="predicateControl">
57 </tb-filter-predicate> 58 </tb-filter-predicate>
@@ -64,6 +64,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @@ -64,6 +64,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
64 64
65 @Input() displayUserParameters = true; 65 @Input() displayUserParameters = true;
66 66
  67 + @Input() allowUserDynamicSource = true;
  68 +
67 filterListFormGroup: FormGroup; 69 filterListFormGroup: FormGroup;
68 70
69 valueTypeEnum = EntityKeyValueType; 71 valueTypeEnum = EntityKeyValueType;
@@ -156,7 +158,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @@ -156,7 +158,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
156 valueType: this.valueType, 158 valueType: this.valueType,
157 key: this.key, 159 key: this.key,
158 isAdd: true, 160 isAdd: true,
159 - displayUserParameters: this.displayUserParameters 161 + displayUserParameters: this.displayUserParameters,
  162 + allowUserDynamicSource: this.allowUserDynamicSource
160 } 163 }
161 }).afterClosed().pipe( 164 }).afterClosed().pipe(
162 map((result) => { 165 map((result) => {
@@ -55,7 +55,7 @@ @@ -55,7 +55,7 @@
55 {{'filter.no-dynamic-value' | translate}} 55 {{'filter.no-dynamic-value' | translate}}
56 </mat-option> 56 </mat-option>
57 <mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType"> 57 <mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
58 - {{dynamicValueSourceTypeTranslations.get(dynamicValueSourceTypeEnum[sourceType]) | translate}} 58 + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
59 </mat-option> 59 </mat-option>
60 </mat-select> 60 </mat-select>
61 </mat-form-field> 61 </mat-form-field>
@@ -47,12 +47,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @@ -47,12 +47,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
47 @Input() disabled: boolean; 47 @Input() disabled: boolean;
48 48
49 @Input() 49 @Input()
  50 + set allowUserDynamicSource(allow: boolean) {
  51 + this.dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_TENANT,
  52 + DynamicValueSourceType.CURRENT_CUSTOMER];
  53 + if (allow) {
  54 + this.dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_USER);
  55 + }
  56 + }
  57 +
  58 + @Input()
50 valueType: EntityKeyValueType; 59 valueType: EntityKeyValueType;
51 60
52 valueTypeEnum = EntityKeyValueType; 61 valueTypeEnum = EntityKeyValueType;
53 62
54 - dynamicValueSourceTypes = Object.keys(DynamicValueSourceType);  
55 - dynamicValueSourceTypeEnum = DynamicValueSourceType; 63 + dynamicValueSourceTypes: DynamicValueSourceType[] = [DynamicValueSourceType.CURRENT_TENANT,
  64 + DynamicValueSourceType.CURRENT_CUSTOMER, DynamicValueSourceType.CURRENT_USER];
  65 +
56 dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap; 66 dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap;
57 67
58 filterPredicateValueFormGroup: FormGroup; 68 filterPredicateValueFormGroup: FormGroup;
@@ -19,20 +19,27 @@ @@ -19,20 +19,27 @@
19 <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateFormGroup"> 19 <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateFormGroup">
20 <div fxFlex fxLayout="column" [ngSwitch]="type"> 20 <div fxFlex fxLayout="column" [ngSwitch]="type">
21 <ng-template [ngSwitchCase]="filterPredicateType.STRING"> 21 <ng-template [ngSwitchCase]="filterPredicateType.STRING">
22 - <tb-string-filter-predicate formControlName="predicate"> 22 + <tb-string-filter-predicate
  23 + [allowUserDynamicSource]="allowUserDynamicSource"
  24 + formControlName="predicate">
23 </tb-string-filter-predicate> 25 </tb-string-filter-predicate>
24 </ng-template> 26 </ng-template>
25 <ng-template [ngSwitchCase]="filterPredicateType.NUMERIC"> 27 <ng-template [ngSwitchCase]="filterPredicateType.NUMERIC">
26 - <tb-numeric-filter-predicate [valueType]="valueType"  
27 - formControlName="predicate"> 28 + <tb-numeric-filter-predicate
  29 + [allowUserDynamicSource]="allowUserDynamicSource"
  30 + [valueType]="valueType"
  31 + formControlName="predicate">
28 </tb-numeric-filter-predicate> 32 </tb-numeric-filter-predicate>
29 </ng-template> 33 </ng-template>
30 <ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN"> 34 <ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN">
31 - <tb-boolean-filter-predicate formControlName="predicate"> 35 + <tb-boolean-filter-predicate
  36 + [allowUserDynamicSource]="allowUserDynamicSource"
  37 + formControlName="predicate">
32 </tb-boolean-filter-predicate> 38 </tb-boolean-filter-predicate>
33 </ng-template> 39 </ng-template>
34 <ng-template [ngSwitchCase]="filterPredicateType.COMPLEX"> 40 <ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
35 <tb-complex-filter-predicate 41 <tb-complex-filter-predicate
  42 + [allowUserDynamicSource]="allowUserDynamicSource"
36 [key]="key" 43 [key]="key"
37 [valueType]="valueType" 44 [valueType]="valueType"
38 [displayUserParameters]="displayUserParameters" 45 [displayUserParameters]="displayUserParameters"
@@ -43,6 +43,8 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit { @@ -43,6 +43,8 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
43 43
44 @Input() displayUserParameters = true; 44 @Input() displayUserParameters = true;
45 45
  46 + @Input() allowUserDynamicSource = true;
  47 +
46 filterPredicateFormGroup: FormGroup; 48 filterPredicateFormGroup: FormGroup;
47 49
48 type: FilterPredicateType; 50 type: FilterPredicateType;
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass}"
  19 + [innerHTML]="filterText"></div>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-filter-text {
  18 + overflow-y: auto;
  19 + &.disabled {
  20 + opacity: 0.7;
  21 + }
  22 + &.required {
  23 + color: #f44336;
  24 + }
  25 + }
  26 +}
  27 +
  28 +:host ::ng-deep {
  29 + .tb-filter-text {
  30 + line-height: 1.8em;
  31 + span {
  32 + display: inline-block;
  33 + vertical-align: middle;
  34 + line-height: 1.4em;
  35 + }
  36 + .tb-filter-predicate {
  37 + padding-right: 4px;
  38 + padding-left: 4px;
  39 + }
  40 + .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source {
  41 + font-weight: bold;
  42 + border: 1px groove rgba(0, 0, 0, .25);
  43 + border-radius: 4px;
  44 + padding-left: 4px;
  45 + padding-right: 4px;
  46 + }
  47 + .tb-filter-entity-key, .tb-filter-value {
  48 + white-space: nowrap;
  49 + overflow: hidden;
  50 + text-overflow: ellipsis;
  51 + max-width: 150px;
  52 + }
  53 + .tb-filter-dynamic-source {
  54 + }
  55 + .tb-filter-entity-key {
  56 + color: #305680;
  57 + }
  58 + .tb-filter-value {
  59 + color: #ff5722;
  60 + }
  61 + .tb-filter-simple-operation {
  62 + font-size: 0.9em;
  63 + }
  64 + .tb-filter-complex-operation {
  65 + text-transform: uppercase;
  66 + font-weight: bold;
  67 + }
  68 + .tb-filter-dynamic-value {
  69 + .tb-filter-dynamic-source, .tb-filter-value {
  70 + color: #0c959c;
  71 + }
  72 + }
  73 + .tb-filter-bracket {
  74 + .tb-left-bracket, .tb-right-bracket {
  75 + font-size: 1.2em;
  76 + }
  77 + }
  78 + }
  79 +}
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { MatDialog } from '@angular/material/dialog';
  20 +import { KeyFilter, keyFiltersToText } from '@shared/models/query/query.models';
  21 +import { TranslateService } from '@ngx-translate/core';
  22 +import { DatePipe } from '@angular/common';
  23 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  24 +
  25 +@Component({
  26 + selector: 'tb-filter-text',
  27 + templateUrl: './filter-text.component.html',
  28 + styleUrls: ['./filter-text.component.scss'],
  29 + providers: [
  30 + {
  31 + provide: NG_VALUE_ACCESSOR,
  32 + useExisting: forwardRef(() => FilterTextComponent),
  33 + multi: true
  34 + }
  35 + ]
  36 +})
  37 +export class FilterTextComponent implements ControlValueAccessor, OnInit {
  38 +
  39 + private requiredValue: boolean;
  40 + get required(): boolean {
  41 + return this.requiredValue;
  42 + }
  43 + @Input()
  44 + set required(value: boolean) {
  45 + this.requiredValue = coerceBooleanProperty(value);
  46 + }
  47 +
  48 + @Input()
  49 + disabled: boolean;
  50 +
  51 + @Input()
  52 + noFilterText = this.translate.instant('filter.no-filter-text');
  53 +
  54 + @Input()
  55 + addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
  56 +
  57 + requiredClass = false;
  58 +
  59 + private filterText: string;
  60 +
  61 + private propagateChange = (v: any) => { };
  62 +
  63 + constructor(private dialog: MatDialog,
  64 + private fb: FormBuilder,
  65 + private translate: TranslateService,
  66 + private datePipe: DatePipe) {
  67 + }
  68 +
  69 + registerOnChange(fn: any): void {
  70 + this.propagateChange = fn;
  71 + }
  72 +
  73 + registerOnTouched(fn: any): void {
  74 + }
  75 +
  76 + ngOnInit() {
  77 + }
  78 +
  79 + setDisabledState(isDisabled: boolean): void {
  80 + this.disabled = isDisabled;
  81 + }
  82 +
  83 + writeValue(value: Array<KeyFilter>): void {
  84 + this.updateFilterText(value);
  85 + }
  86 +
  87 + private updateFilterText(value: Array<KeyFilter>) {
  88 + this.requiredClass = false;
  89 + if (value && value.length) {
  90 + this.filterText = keyFiltersToText(this.translate, this.datePipe, value);
  91 + } else {
  92 + if (this.required && !this.disabled) {
  93 + this.filterText = this.addFilterPrompt;
  94 + this.requiredClass = true;
  95 + } else {
  96 + this.filterText = this.noFilterText;
  97 + }
  98 + }
  99 + }
  100 +
  101 +}
@@ -70,6 +70,7 @@ @@ -70,6 +70,7 @@
70 </mat-form-field> 70 </mat-form-field>
71 </section> 71 </section>
72 <tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value" 72 <tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
  73 + [allowUserDynamicSource]="data.allowUserDynamicSource"
73 [displayUserParameters]="data.displayUserParameters" 74 [displayUserParameters]="data.displayUserParameters"
74 [valueType]="keyFilterFormGroup.get('valueType').value" 75 [valueType]="keyFilterFormGroup.get('valueType').value"
75 [key]="keyFilterFormGroup.get('key.key').value" 76 [key]="keyFilterFormGroup.get('key.key').value"
@@ -40,6 +40,7 @@ export interface KeyFilterDialogData { @@ -40,6 +40,7 @@ export interface KeyFilterDialogData {
40 keyFilter: KeyFilterInfo; 40 keyFilter: KeyFilterInfo;
41 isAdd: boolean; 41 isAdd: boolean;
42 displayUserParameters: boolean; 42 displayUserParameters: boolean;
  43 + allowUserDynamicSource: boolean;
43 readonly: boolean; 44 readonly: boolean;
44 telemetryKeysOnly: boolean; 45 telemetryKeysOnly: boolean;
45 } 46 }
@@ -16,65 +16,77 @@ @@ -16,65 +16,77 @@
16 16
17 --> 17 -->
18 <section fxLayout="column" [formGroup]="keyFilterListFormGroup"> 18 <section fxLayout="column" [formGroup]="keyFilterListFormGroup">
19 - <mat-expansion-panel [expanded]="true">  
20 - <mat-expansion-panel-header>  
21 - <mat-panel-title>  
22 - <div translate>filter.key-filters</div>  
23 - </mat-panel-title>  
24 - </mat-expansion-panel-header>  
25 - <div fxLayout="row">  
26 - <span fxFlex="8"></span>  
27 - <div fxLayout="row" fxLayoutAlign="start center" fxFlex="92">  
28 - <label fxFlex translate class="tb-title no-padding">filter.key-name</label>  
29 - <label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label>  
30 - <span [fxShow]="!disabled" style="min-width: 80px;">&nbsp;</span>  
31 - <span [fxShow]="disabled" style="min-width: 40px;">&nbsp;</span>  
32 - </div>  
33 - </div>  
34 - <mat-divider></mat-divider>  
35 - <div class="key-filter-list">  
36 - <div fxLayout="row" fxLayoutAlign="start center" style="max-height: 40px;"  
37 - formArrayName="keyFilters"  
38 - *ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">  
39 - <div fxFlex="8" class="filters-operation">  
40 - <span *ngIf="$index > 0" translate>filter.operation.and</span> 19 + <mat-accordion [multi]="true">
  20 + <mat-expansion-panel [expanded]="true">
  21 + <mat-expansion-panel-header>
  22 + <mat-panel-title>
  23 + <div translate>filter.key-filters</div>
  24 + </mat-panel-title>
  25 + </mat-expansion-panel-header>
  26 + <div fxLayout="row">
  27 + <span fxFlex="8"></span>
  28 + <div fxLayout="row" fxLayoutAlign="start center" fxFlex="92">
  29 + <label fxFlex translate class="tb-title no-padding">filter.key-name</label>
  30 + <label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label>
  31 + <span [fxShow]="!disabled" style="min-width: 80px;">&nbsp;</span>
  32 + <span [fxShow]="disabled" style="min-width: 40px;">&nbsp;</span>
41 </div> 33 </div>
42 - <div fxLayout="column" fxFlex="92">  
43 - <div fxLayout="row" fxLayoutAlign="start center" fxFlex>  
44 - <div fxFlex>{{ keyFilterControl.value.key.key }}</div>  
45 - <div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>  
46 - <button mat-icon-button color="primary"  
47 - type="button"  
48 - (click)="editKeyFilter($index)"  
49 - matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}"  
50 - matTooltipPosition="above">  
51 - <mat-icon>{{disabled ? 'more_vert' : 'edit'}}</mat-icon>  
52 - </button>  
53 - <button mat-icon-button color="primary"  
54 - [fxShow]="!disabled"  
55 - type="button"  
56 - (click)="removeKeyFilter($index)"  
57 - matTooltip="{{ 'filter.remove-key-filter' | translate }}"  
58 - matTooltipPosition="above">  
59 - <mat-icon>close</mat-icon>  
60 - </button> 34 + </div>
  35 + <mat-divider></mat-divider>
  36 + <div class="key-filter-list">
  37 + <div fxLayout="row" fxLayoutAlign="start center" style="max-height: 40px;"
  38 + formArrayName="keyFilters"
  39 + *ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
  40 + <div fxFlex="8" class="filters-operation">
  41 + <span *ngIf="$index > 0" translate>filter.operation.and</span>
  42 + </div>
  43 + <div fxLayout="column" fxFlex="92">
  44 + <div fxLayout="row" fxLayoutAlign="start center" fxFlex>
  45 + <div fxFlex>{{ keyFilterControl.value.key.key }}</div>
  46 + <div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>
  47 + <button mat-icon-button color="primary"
  48 + type="button"
  49 + (click)="editKeyFilter($index)"
  50 + matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}"
  51 + matTooltipPosition="above">
  52 + <mat-icon>{{disabled ? 'more_vert' : 'edit'}}</mat-icon>
  53 + </button>
  54 + <button mat-icon-button color="primary"
  55 + [fxShow]="!disabled"
  56 + type="button"
  57 + (click)="removeKeyFilter($index)"
  58 + matTooltip="{{ 'filter.remove-key-filter' | translate }}"
  59 + matTooltipPosition="above">
  60 + <mat-icon>close</mat-icon>
  61 + </button>
  62 + </div>
  63 + <mat-divider></mat-divider>
61 </div> 64 </div>
62 - <mat-divider></mat-divider>  
63 </div> 65 </div>
  66 + <span [fxShow]="!keyFiltersFormArray().length"
  67 + fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
  68 + class="no-data-found" translate>filter.no-key-filters</span>
  69 + </div>
  70 + <div style="margin-top: 16px;">
  71 + <button mat-button mat-raised-button color="primary"
  72 + [fxShow]="!disabled"
  73 + (click)="addKeyFilter()"
  74 + type="button"
  75 + matTooltip="{{ 'filter.add-key-filter' | translate }}"
  76 + matTooltipPosition="above">
  77 + {{ 'filter.add-key-filter' | translate }}
  78 + </button>
  79 + </div>
  80 + </mat-expansion-panel>
  81 + <mat-expansion-panel [expanded]="true">
  82 + <mat-expansion-panel-header>
  83 + <mat-panel-title>
  84 + <div translate>filter.preview</div>
  85 + </mat-panel-title>
  86 + </mat-expansion-panel-header>
  87 + <div class="tb-filter-preview">
  88 + <tb-filter-text [formControl]="keyFiltersControl"></tb-filter-text>
64 </div> 89 </div>
65 - <span [fxShow]="!keyFiltersFormArray().length"  
66 - fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"  
67 - class="no-data-found" translate>filter.no-key-filters</span>  
68 - </div>  
69 - <div style="margin-top: 16px;">  
70 - <button mat-button mat-raised-button color="primary"  
71 - [fxShow]="!disabled"  
72 - (click)="addKeyFilter()"  
73 - type="button"  
74 - matTooltip="{{ 'filter.add-key-filter' | translate }}"  
75 - matTooltipPosition="above">  
76 - {{ 'filter.add-key-filter' | translate }}  
77 - </button>  
78 - </div>  
79 - </mat-expansion-panel> 90 + </mat-expansion-panel>
  91 + </mat-accordion>
80 </section> 92 </section>
@@ -26,4 +26,17 @@ @@ -26,4 +26,17 @@
26 color: #666; 26 color: #666;
27 font-weight: 500; 27 font-weight: 500;
28 } 28 }
  29 + .tb-filter-preview {
  30 + padding: 8px;
  31 + border: 1px groove rgba(0, 0, 0, .25);
  32 + border-radius: 4px;
  33 + }
  34 +}
  35 +
  36 +:host ::ng-deep {
  37 + .tb-filter-preview {
  38 + .tb-filter-text {
  39 + max-height: 200px;
  40 + }
  41 + }
29 } 42 }
@@ -19,13 +19,18 @@ import { @@ -19,13 +19,18 @@ import {
19 AbstractControl, 19 AbstractControl,
20 ControlValueAccessor, 20 ControlValueAccessor,
21 FormArray, 21 FormArray,
22 - FormBuilder, 22 + FormBuilder, FormControl,
23 FormGroup, 23 FormGroup,
24 NG_VALUE_ACCESSOR, 24 NG_VALUE_ACCESSOR,
25 Validators 25 Validators
26 } from '@angular/forms'; 26 } from '@angular/forms';
27 import { Observable, Subscription } from 'rxjs'; 27 import { Observable, Subscription } from 'rxjs';
28 -import { EntityKeyType, entityKeyTypeTranslationMap, KeyFilterInfo } from '@shared/models/query/query.models'; 28 +import {
  29 + EntityKeyType,
  30 + entityKeyTypeTranslationMap,
  31 + KeyFilter,
  32 + KeyFilterInfo, keyFilterInfosToKeyFilters
  33 +} from '@shared/models/query/query.models';
29 import { MatDialog } from '@angular/material/dialog'; 34 import { MatDialog } from '@angular/material/dialog';
30 import { deepClone } from '@core/utils'; 35 import { deepClone } from '@core/utils';
31 import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; 36 import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
@@ -48,12 +53,16 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -48,12 +53,16 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
48 53
49 @Input() displayUserParameters = true; 54 @Input() displayUserParameters = true;
50 55
  56 + @Input() allowUserDynamicSource = true;
  57 +
51 @Input() telemetryKeysOnly = false; 58 @Input() telemetryKeysOnly = false;
52 59
53 keyFilterListFormGroup: FormGroup; 60 keyFilterListFormGroup: FormGroup;
54 61
55 entityKeyTypeTranslations = entityKeyTypeTranslationMap; 62 entityKeyTypeTranslations = entityKeyTypeTranslationMap;
56 63
  64 + keyFiltersControl: FormControl;
  65 +
57 private propagateChange = null; 66 private propagateChange = null;
58 67
59 private valueChangeSubscription: Subscription = null; 68 private valueChangeSubscription: Subscription = null;
@@ -66,6 +75,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -66,6 +75,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
66 this.keyFilterListFormGroup = this.fb.group({}); 75 this.keyFilterListFormGroup = this.fb.group({});
67 this.keyFilterListFormGroup.addControl('keyFilters', 76 this.keyFilterListFormGroup.addControl('keyFilters',
68 this.fb.array([])); 77 this.fb.array([]));
  78 + this.keyFiltersControl = this.fb.control(null);
69 } 79 }
70 80
71 keyFiltersFormArray(): FormArray { 81 keyFiltersFormArray(): FormArray {
@@ -83,8 +93,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -83,8 +93,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
83 this.disabled = isDisabled; 93 this.disabled = isDisabled;
84 if (this.disabled) { 94 if (this.disabled) {
85 this.keyFilterListFormGroup.disable({emitEvent: false}); 95 this.keyFilterListFormGroup.disable({emitEvent: false});
  96 + this.keyFiltersControl.disable({emitEvent: false});
86 } else { 97 } else {
87 this.keyFilterListFormGroup.enable({emitEvent: false}); 98 this.keyFilterListFormGroup.enable({emitEvent: false});
  99 + this.keyFiltersControl.enable({emitEvent: false});
88 } 100 }
89 } 101 }
90 102
@@ -107,6 +119,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -107,6 +119,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
107 } else { 119 } else {
108 this.keyFilterListFormGroup.enable({emitEvent: false}); 120 this.keyFilterListFormGroup.enable({emitEvent: false});
109 } 121 }
  122 + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
  123 + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false});
110 } 124 }
111 125
112 public removeKeyFilter(index: number) { 126 public removeKeyFilter(index: number) {
@@ -155,6 +169,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -155,6 +169,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
155 isAdd, 169 isAdd,
156 readonly: this.disabled, 170 readonly: this.disabled,
157 displayUserParameters: this.displayUserParameters, 171 displayUserParameters: this.displayUserParameters,
  172 + allowUserDynamicSource: this.allowUserDynamicSource,
158 telemetryKeysOnly: this.telemetryKeysOnly 173 telemetryKeysOnly: this.telemetryKeysOnly
159 } 174 }
160 }).afterClosed(); 175 }).afterClosed();
@@ -167,5 +182,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @@ -167,5 +182,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
167 } else { 182 } else {
168 this.propagateChange(null); 183 this.propagateChange(null);
169 } 184 }
  185 + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters);
  186 + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false});
170 } 187 }
171 } 188 }
@@ -24,7 +24,8 @@ @@ -24,7 +24,8 @@
24 </mat-option> 24 </mat-option>
25 </mat-select> 25 </mat-select>
26 </mat-form-field> 26 </mat-form-field>
27 - <tb-filter-predicate-value fxFlex="60" 27 + <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
  28 + fxFlex="60"
28 [valueType]="valueType" 29 [valueType]="valueType"
29 formControlName="value"> 30 formControlName="value">
30 </tb-filter-predicate-value> 31 </tb-filter-predicate-value>
@@ -40,6 +40,8 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On @@ -40,6 +40,8 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
40 40
41 @Input() disabled: boolean; 41 @Input() disabled: boolean;
42 42
  43 + @Input() allowUserDynamicSource = true;
  44 +
43 @Input() valueType: EntityKeyValueType; 45 @Input() valueType: EntityKeyValueType;
44 46
45 numericFilterPredicateFormGroup: FormGroup; 47 numericFilterPredicateFormGroup: FormGroup;
@@ -28,7 +28,8 @@ @@ -28,7 +28,8 @@
28 <mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;"> 28 <mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;">
29 </mat-checkbox> 29 </mat-checkbox>
30 </div> 30 </div>
31 - <tb-filter-predicate-value fxFlex="60" 31 + <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
  32 + fxFlex="60"
32 [valueType]="valueTypeEnum.STRING" 33 [valueType]="valueTypeEnum.STRING"
33 formControlName="value"> 34 formControlName="value">
34 </tb-filter-predicate-value> 35 </tb-filter-predicate-value>
@@ -40,6 +40,8 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI @@ -40,6 +40,8 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
40 40
41 @Input() disabled: boolean; 41 @Input() disabled: boolean;
42 42
  43 + @Input() allowUserDynamicSource = true;
  44 +
43 valueTypeEnum = EntityKeyValueType; 45 valueTypeEnum = EntityKeyValueType;
44 46
45 stringFilterPredicateFormGroup: FormGroup; 47 stringFilterPredicateFormGroup: FormGroup;
@@ -104,6 +104,8 @@ import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.co @@ -104,6 +104,8 @@ import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.co
104 import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; 104 import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
105 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; 105 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
106 import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; 106 import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';
  107 +import { FilterTextComponent } from './filter/filter-text.component';
  108 +import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
107 109
108 @NgModule({ 110 @NgModule({
109 declarations: 111 declarations:
@@ -165,6 +167,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k @@ -165,6 +167,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
165 FilterDialogComponent, 167 FilterDialogComponent,
166 FiltersDialogComponent, 168 FiltersDialogComponent,
167 FilterSelectComponent, 169 FilterSelectComponent,
  170 + FilterTextComponent,
168 FiltersEditComponent, 171 FiltersEditComponent,
169 FiltersEditPanelComponent, 172 FiltersEditPanelComponent,
170 UserFilterDialogComponent, 173 UserFilterDialogComponent,
@@ -190,7 +193,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k @@ -190,7 +193,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
190 DeviceProfileAlarmsComponent, 193 DeviceProfileAlarmsComponent,
191 DeviceProfileDataComponent, 194 DeviceProfileDataComponent,
192 DeviceProfileComponent, 195 DeviceProfileComponent,
193 - DeviceProfileDialogComponent 196 + DeviceProfileDialogComponent,
  197 + AddDeviceProfileDialogComponent
194 ], 198 ],
195 imports: [ 199 imports: [
196 CommonModule, 200 CommonModule,
@@ -245,6 +249,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k @@ -245,6 +249,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
245 FilterDialogComponent, 249 FilterDialogComponent,
246 FiltersDialogComponent, 250 FiltersDialogComponent,
247 FilterSelectComponent, 251 FilterSelectComponent,
  252 + FilterTextComponent,
248 FiltersEditComponent, 253 FiltersEditComponent,
249 UserFilterDialogComponent, 254 UserFilterDialogComponent,
250 TenantProfileAutocompleteComponent, 255 TenantProfileAutocompleteComponent,
@@ -266,7 +271,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k @@ -266,7 +271,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
266 DeviceProfileAlarmsComponent, 271 DeviceProfileAlarmsComponent,
267 DeviceProfileDataComponent, 272 DeviceProfileDataComponent,
268 DeviceProfileComponent, 273 DeviceProfileComponent,
269 - DeviceProfileDialogComponent 274 + DeviceProfileDialogComponent,
  275 + AddDeviceProfileDialogComponent
270 ], 276 ],
271 providers: [ 277 providers: [
272 WidgetComponentService, 278 WidgetComponentService,
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div style="min-width: 1000px;">
  19 + <mat-toolbar color="primary">
  20 + <h2 translate>device-profile.add</h2>
  21 + <span fxFlex></span>
  22 + <button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <mat-horizontal-stepper [linear]="true" #addDeviceProfileStepper (selectionChange)="selectedIndex = $event.selectedIndex">
  33 + <mat-step [stepControl]="deviceProfileDetailsFormGroup">
  34 + <form [formGroup]="deviceProfileDetailsFormGroup" style="padding-bottom: 16px;">
  35 + <ng-template matStepLabel>{{ 'device-profile.device-profile-details' | translate }}</ng-template>
  36 + <fieldset [disabled]="isLoading$ | async">
  37 + <mat-form-field class="mat-block">
  38 + <mat-label translate>device-profile.name</mat-label>
  39 + <input matInput formControlName="name" required/>
  40 + <mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('required')">
  41 + {{ 'device-profile.name-required' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 + <tb-entity-autocomplete
  45 + labelText="device-profile.default-rule-chain"
  46 + [entityType]="entityType.RULE_CHAIN"
  47 + formControlName="defaultRuleChainId">
  48 + </tb-entity-autocomplete>
  49 + <mat-form-field class="mat-block">
  50 + <mat-label translate>device-profile.type</mat-label>
  51 + <mat-select formControlName="type" required>
  52 + <mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
  53 + {{deviceProfileTypeTranslations.get(type) | translate}}
  54 + </mat-option>
  55 + </mat-select>
  56 + <mat-error *ngIf="deviceProfileDetailsFormGroup.get('type').hasError('required')">
  57 + {{ 'device-profile.type-required' | translate }}
  58 + </mat-error>
  59 + </mat-form-field>
  60 + <mat-form-field class="mat-block">
  61 + <mat-label translate>device-profile.description</mat-label>
  62 + <textarea matInput formControlName="description" rows="2"></textarea>
  63 + </mat-form-field>
  64 + </fieldset>
  65 + </form>
  66 + </mat-step>
  67 + <mat-step [stepControl]="transportConfigFormGroup">
  68 + <form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
  69 + <ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
  70 + <mat-form-field class="mat-block">
  71 + <mat-label translate>device-profile.transport-type</mat-label>
  72 + <mat-select formControlName="transportType" required>
  73 + <mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
  74 + {{deviceTransportTypeTranslations.get(type) | translate}}
  75 + </mat-option>
  76 + </mat-select>
  77 + <mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
  78 + {{ 'device-profile.transport-type-required' | translate }}
  79 + </mat-error>
  80 + </mat-form-field>
  81 + <tb-device-profile-transport-configuration
  82 + formControlName="transportConfiguration"
  83 + required>
  84 + </tb-device-profile-transport-configuration>
  85 + </form>
  86 + </mat-step>
  87 + <mat-step [stepControl]="alarmRulesFormGroup">
  88 + <form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
  89 + <ng-template matStepLabel>{{'device-profile.alarm-rules' | translate:
  90 + {count: alarmRulesFormGroup.get('alarms').value ?
  91 + alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
  92 + <tb-device-profile-alarms
  93 + formControlName="alarms">
  94 + </tb-device-profile-alarms>
  95 + </form>
  96 + </mat-step>
  97 + </mat-horizontal-stepper>
  98 + </div>
  99 + <div mat-dialog-actions fxLayout="row wrap" fxLayoutAlign="space-between center">
  100 + <button mat-button *ngIf="selectedIndex > 0"
  101 + [disabled]="(isLoading$ | async)"
  102 + (click)="previousStep()">{{ 'action.back' | translate }}</button>
  103 + <span *ngIf="selectedIndex <= 0"></span>
  104 + <div fxLayout="row wrap" fxLayoutGap="20px">
  105 + <button mat-button
  106 + [disabled]="(isLoading$ | async)"
  107 + (click)="cancel()">{{ 'action.cancel' | translate }}</button>
  108 + <button mat-raised-button
  109 + [disabled]="(isLoading$ | async) || selectedForm().invalid"
  110 + color="primary"
  111 + (click)="nextStep()">{{ (selectedIndex === 2 ? 'action.add' : 'action.continue') | translate }}</button>
  112 + </div>
  113 + </div>
  114 +</div>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .mat-dialog-content {
  18 + display: flex;
  19 + flex-direction: column;
  20 + overflow: hidden;
  21 +
  22 + .mat-stepper-horizontal {
  23 + display: flex;
  24 + flex-direction: column;
  25 + overflow: hidden;
  26 + }
  27 + }
  28 +}
  29 +
  30 +:host ::ng-deep {
  31 + .mat-dialog-content {
  32 + .mat-stepper-horizontal {
  33 + .mat-horizontal-content-container {
  34 + overflow: auto;
  35 + }
  36 + }
  37 + }
  38 +}
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterViewInit,
  19 + Component,
  20 + ComponentFactoryResolver,
  21 + Inject,
  22 + Injector,
  23 + SkipSelf,
  24 + ViewChild
  25 +} from '@angular/core';
  26 +import { ErrorStateMatcher } from '@angular/material/core';
  27 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  28 +import { Store } from '@ngrx/store';
  29 +import { AppState } from '@core/core.state';
  30 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  31 +import { DialogComponent } from '@shared/components/dialog.component';
  32 +import { Router } from '@angular/router';
  33 +import {
  34 + createDeviceProfileConfiguration,
  35 + createDeviceProfileTransportConfiguration,
  36 + DeviceProfile,
  37 + DeviceProfileType,
  38 + deviceProfileTypeTranslationMap,
  39 + DeviceTransportType,
  40 + deviceTransportTypeTranslationMap
  41 +} from '@shared/models/device.models';
  42 +import { DeviceProfileService } from '@core/http/device-profile.service';
  43 +import { EntityType } from '@shared/models/entity-type.models';
  44 +import { MatHorizontalStepper } from '@angular/material/stepper';
  45 +import { RuleChainId } from '@shared/models/id/rule-chain-id';
  46 +
  47 +export interface AddDeviceProfileDialogData {
  48 + deviceProfileName: string;
  49 +}
  50 +
  51 +@Component({
  52 + selector: 'tb-add-device-profile-dialog',
  53 + templateUrl: './add-device-profile-dialog.component.html',
  54 + providers: [],
  55 + styleUrls: ['./add-device-profile-dialog.component.scss']
  56 +})
  57 +export class AddDeviceProfileDialogComponent extends
  58 + DialogComponent<AddDeviceProfileDialogComponent, DeviceProfile> implements AfterViewInit {
  59 +
  60 + @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatHorizontalStepper;
  61 +
  62 + selectedIndex = 0;
  63 +
  64 + entityType = EntityType;
  65 +
  66 + deviceProfileTypes = Object.keys(DeviceProfileType);
  67 +
  68 + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap;
  69 +
  70 + deviceTransportTypes = Object.keys(DeviceTransportType);
  71 +
  72 + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
  73 +
  74 + deviceProfileDetailsFormGroup: FormGroup;
  75 +
  76 + transportConfigFormGroup: FormGroup;
  77 +
  78 + alarmRulesFormGroup: FormGroup;
  79 +
  80 + constructor(protected store: Store<AppState>,
  81 + protected router: Router,
  82 + @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData,
  83 + public dialogRef: MatDialogRef<AddDeviceProfileDialogComponent, DeviceProfile>,
  84 + private componentFactoryResolver: ComponentFactoryResolver,
  85 + private injector: Injector,
  86 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  87 + private deviceProfileService: DeviceProfileService,
  88 + private fb: FormBuilder) {
  89 + super(store, router, dialogRef);
  90 + this.deviceProfileDetailsFormGroup = this.fb.group(
  91 + {
  92 + name: [data.deviceProfileName, [Validators.required]],
  93 + type: [DeviceProfileType.DEFAULT, [Validators.required]],
  94 + defaultRuleChainId: [null, []],
  95 + description: ['', []]
  96 + }
  97 + );
  98 + this.transportConfigFormGroup = this.fb.group(
  99 + {
  100 + transportType: [DeviceTransportType.DEFAULT, [Validators.required]],
  101 + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT),
  102 + [Validators.required]]
  103 + }
  104 + );
  105 + this.transportConfigFormGroup.get('transportType').valueChanges.subscribe(() => {
  106 + this.deviceProfileTransportTypeChanged();
  107 + });
  108 +
  109 + this.alarmRulesFormGroup = this.fb.group(
  110 + {
  111 + alarms: [null]
  112 + }
  113 + );
  114 + }
  115 +
  116 + private deviceProfileTransportTypeChanged() {
  117 + const deviceTransportType: DeviceTransportType = this.transportConfigFormGroup.get('transportType').value;
  118 + this.transportConfigFormGroup.patchValue(
  119 + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)});
  120 + }
  121 +
  122 + ngAfterViewInit(): void {
  123 + }
  124 +
  125 + cancel(): void {
  126 + this.dialogRef.close(null);
  127 + }
  128 +
  129 + previousStep() {
  130 + this.addDeviceProfileStepper.previous();
  131 + }
  132 +
  133 + nextStep() {
  134 + if (this.selectedIndex < 2) {
  135 + this.addDeviceProfileStepper.next();
  136 + } else {
  137 + this.add();
  138 + }
  139 + }
  140 +
  141 + selectedForm(): FormGroup {
  142 + switch (this.selectedIndex) {
  143 + case 0:
  144 + return this.deviceProfileDetailsFormGroup;
  145 + case 1:
  146 + return this.transportConfigFormGroup;
  147 + case 2:
  148 + return this.alarmRulesFormGroup;
  149 + }
  150 + }
  151 +
  152 + private add(): void {
  153 + const deviceProfile: DeviceProfile = {
  154 + name: this.deviceProfileDetailsFormGroup.get('name').value,
  155 + type: this.deviceProfileDetailsFormGroup.get('type').value,
  156 + transportType: this.transportConfigFormGroup.get('transportType').value,
  157 + description: this.deviceProfileDetailsFormGroup.get('description').value,
  158 + profileData: {
  159 + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
  160 + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
  161 + alarms: this.alarmRulesFormGroup.get('alarms').value
  162 + }
  163 + };
  164 + if (this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value) {
  165 + deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value);
  166 + }
  167 + this.deviceProfileService.saveDeviceProfile(deviceProfile).subscribe(
  168 + (savedDeviceProfile) => {
  169 + this.dialogRef.close(savedDeviceProfile);
  170 + }
  171 + );
  172 + }
  173 +}
@@ -15,16 +15,22 @@ @@ -15,16 +15,22 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<mat-form-field class="mat-block" (click)="openFilterDialog($event)" floatLabel="always" hideRequiredMarker>  
19 - <mat-label></mat-label>  
20 - <input readonly  
21 - required matInput [formControl]="alarmRuleConditionControl"  
22 - placeholder="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">  
23 - <a matSuffix mat-icon-button color="primary"  
24 - type="button"  
25 - (click)="openFilterDialog($event)"  
26 - matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"  
27 - matTooltipPosition="above">  
28 - <mat-icon>{{ disabled ? 'more_vert' : 'edit' }}</mat-icon>  
29 - </a>  
30 -</mat-form-field> 18 +<div fxLayout="column" fxFlex>
  19 + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
  20 + <div class="tb-small" translate>device-profile.alarm-rule-condition</div>
  21 + <span fxFlex></span>
  22 + <a mat-button color="primary"
  23 + type="button"
  24 + (click)="openFilterDialog($event)"
  25 + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
  26 + matTooltipPosition="above">
  27 + {{ (disabled ? 'action.view' : (conditionSet() ? 'action.edit' : 'action.add')) | translate }}
  28 + </a>
  29 + </div>
  30 + <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
  31 + <tb-filter-text [formControl]="alarmRuleConditionControl"
  32 + required
  33 + addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
  34 + </tb-filter-text>
  35 + </div>
  36 +</div>
@@ -14,9 +14,24 @@ @@ -14,9 +14,24 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 :host { 16 :host {
17 - a.mat-icon-button { 17 + display: flex;
  18 + a.mat-button {
18 &:hover, &:focus { 19 &:hover, &:focus {
19 border-bottom: none; 20 border-bottom: none;
20 } 21 }
21 } 22 }
  23 + .tb-alarm-rule-condition {
  24 + padding: 8px;
  25 + border: 1px groove rgba(0, 0, 0, .25);
  26 + border-radius: 4px;
  27 + cursor: pointer;
  28 + }
  29 +}
  30 +
  31 +:host ::ng-deep {
  32 + .tb-alarm-rule-condition {
  33 + .tb-filter-text {
  34 + max-height: 200px;
  35 + }
  36 + }
22 } 37 }
@@ -30,6 +30,8 @@ import { @@ -30,6 +30,8 @@ import {
30 AlarmRuleKeyFiltersDialogComponent, 30 AlarmRuleKeyFiltersDialogComponent,
31 AlarmRuleKeyFiltersDialogData 31 AlarmRuleKeyFiltersDialogData
32 } from './alarm-rule-key-filters-dialog.component'; 32 } from './alarm-rule-key-filters-dialog.component';
  33 +import { TranslateService } from '@ngx-translate/core';
  34 +import { DatePipe } from '@angular/common';
33 35
34 @Component({ 36 @Component({
35 selector: 'tb-alarm-rule-condition', 37 selector: 'tb-alarm-rule-condition',
@@ -60,7 +62,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -60,7 +62,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
60 private propagateChange = (v: any) => { }; 62 private propagateChange = (v: any) => { };
61 63
62 constructor(private dialog: MatDialog, 64 constructor(private dialog: MatDialog,
63 - private fb: FormBuilder) { 65 + private fb: FormBuilder,
  66 + private translate: TranslateService,
  67 + private datePipe: DatePipe) {
64 } 68 }
65 69
66 registerOnChange(fn: any): void { 70 registerOnChange(fn: any): void {
@@ -76,6 +80,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -76,6 +80,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
76 80
77 setDisabledState(isDisabled: boolean): void { 81 setDisabledState(isDisabled: boolean): void {
78 this.disabled = isDisabled; 82 this.disabled = isDisabled;
  83 + if (this.disabled) {
  84 + this.alarmRuleConditionControl.disable({emitEvent: false});
  85 + } else {
  86 + this.alarmRuleConditionControl.enable({emitEvent: false});
  87 + }
79 } 88 }
80 89
81 writeValue(value: Array<KeyFilter>): void { 90 writeValue(value: Array<KeyFilter>): void {
@@ -83,8 +92,12 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -83,8 +92,12 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
83 this.updateConditionInfo(); 92 this.updateConditionInfo();
84 } 93 }
85 94
  95 + public conditionSet() {
  96 + return this.modelValue && this.modelValue.length;
  97 + }
  98 +
86 public validate(c: FormControl) { 99 public validate(c: FormControl) {
87 - return (this.modelValue && this.modelValue.length) ? null : { 100 + return this.conditionSet() ? null : {
88 alarmRuleCondition: { 101 alarmRuleCondition: {
89 valid: false, 102 valid: false,
90 }, 103 },
@@ -112,11 +125,7 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -112,11 +125,7 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
112 } 125 }
113 126
114 private updateConditionInfo() { 127 private updateConditionInfo() {
115 - if (this.modelValue && this.modelValue.length) {  
116 - this.alarmRuleConditionControl.patchValue('Condition set');  
117 - } else {  
118 - this.alarmRuleConditionControl.patchValue(null);  
119 - } 128 + this.alarmRuleConditionControl.patchValue(this.modelValue);
120 } 129 }
121 130
122 private updateModel() { 131 private updateModel() {
@@ -32,6 +32,7 @@ @@ -32,6 +32,7 @@
32 <div fxFlex fxLayout="column"> 32 <div fxFlex fxLayout="column">
33 <tb-key-filter-list 33 <tb-key-filter-list
34 [displayUserParameters]="false" 34 [displayUserParameters]="false"
  35 + [allowUserDynamicSource]="false"
35 [telemetryKeysOnly]="true" 36 [telemetryKeysOnly]="true"
36 formControlName="keyFilters"> 37 formControlName="keyFilters">
37 </tb-key-filter-list> 38 </tb-key-filter-list>
@@ -16,56 +16,56 @@ @@ -16,56 +16,56 @@
16 16
17 --> 17 -->
18 <div fxLayout="column" [formGroup]="alarmRuleFormGroup"> 18 <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
19 - <div formGroupName="condition" fxLayout="row" fxLayoutAlign="start" fxLayoutGap="8px" fxFlex>  
20 - <div fxLayout="column" fxFlex> 19 + <div formGroupName="condition" fxLayout="row" fxLayoutGap="8px" fxFlex>
  20 + <tb-alarm-rule-condition fxFlex
  21 + formControlName="condition">
  22 + </tb-alarm-rule-condition>
  23 + <div fxLayout="column">
21 <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> 24 <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
22 - <label class="tb-small" translate>device-profile.alarm-rule-condition</label>  
23 - </div>  
24 - <tb-alarm-rule-condition fxFlex  
25 - formControlName="condition">  
26 - </tb-alarm-rule-condition>  
27 - </div>  
28 - <div fxLayout="column" style="min-width: 250px;">  
29 - <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">  
30 - <label fxFlex class="tb-small" translate>device-profile.condition-duration</label> 25 + <div class="tb-small" translate>device-profile.condition-duration</div>
  26 + <span fxFlex></span>
31 <mat-slide-toggle [disabled]="disabled" 27 <mat-slide-toggle [disabled]="disabled"
  28 + color="primary"
32 [ngModelOptions]="{standalone: true}" 29 [ngModelOptions]="{standalone: true}"
33 (ngModelChange)="enableDurationChanged($event)" 30 (ngModelChange)="enableDurationChanged($event)"
34 [ngModel]="enableDuration"> 31 [ngModel]="enableDuration">
35 </mat-slide-toggle> 32 </mat-slide-toggle>
36 </div> 33 </div>
37 - <div fxLayout="row" fxLayoutGap="8px" [fxShow]="enableDuration">  
38 - <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">  
39 - <mat-label></mat-label>  
40 - <input type="number"  
41 - [required]="enableDuration"  
42 - step="1"  
43 - min="1" max="2147483647" matInput  
44 - placeholder="{{ 'device-profile.condition-duration-value' | translate }}"  
45 - formControlName="durationValue">  
46 - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">  
47 - {{ 'device-profile.condition-duration-value-required' | translate }}  
48 - </mat-error>  
49 - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">  
50 - {{ 'device-profile.condition-duration-value-range' | translate }}  
51 - </mat-error>  
52 - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">  
53 - {{ 'device-profile.condition-duration-value-range' | translate }}  
54 - </mat-error>  
55 - </mat-form-field>  
56 - <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">  
57 - <mat-label></mat-label>  
58 - <mat-select formControlName="durationUnit"  
59 - [required]="enableDuration"  
60 - placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">  
61 - <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">  
62 - {{ timeUnitTranslations.get(timeUnit) | translate }}  
63 - </mat-option>  
64 - </mat-select>  
65 - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">  
66 - {{ 'device-profile.condition-duration-time-unit-required' | translate }}  
67 - </mat-error>  
68 - </mat-form-field> 34 + <div class="tb-condition-duration" fxFlex fxLayout="row" fxLayoutGap="8px">
  35 + <span style="min-width: 250px;" *ngIf="!enableDuration"></span>
  36 + <div style="min-width: 250px;" fxLayout="row" fxLayoutGap="8px" *ngIf="enableDuration">
  37 + <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
  38 + <mat-label></mat-label>
  39 + <input type="number"
  40 + required
  41 + step="1"
  42 + min="1" max="2147483647" matInput
  43 + placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
  44 + formControlName="durationValue">
  45 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">
  46 + {{ 'device-profile.condition-duration-value-required' | translate }}
  47 + </mat-error>
  48 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">
  49 + {{ 'device-profile.condition-duration-value-range' | translate }}
  50 + </mat-error>
  51 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">
  52 + {{ 'device-profile.condition-duration-value-range' | translate }}
  53 + </mat-error>
  54 + </mat-form-field>
  55 + <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
  56 + <mat-label></mat-label>
  57 + <mat-select formControlName="durationUnit"
  58 + required
  59 + placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
  60 + <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
  61 + {{ timeUnitTranslations.get(timeUnit) | translate }}
  62 + </mat-option>
  63 + </mat-select>
  64 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">
  65 + {{ 'device-profile.condition-duration-time-unit-required' | translate }}
  66 + </mat-error>
  67 + </mat-form-field>
  68 + </div>
69 </div> 69 </div>
70 </div> 70 </div>
71 </div> 71 </div>
@@ -73,7 +73,7 @@ @@ -73,7 +73,7 @@
73 <mat-expansion-panel-header> 73 <mat-expansion-panel-header>
74 <mat-panel-title> 74 <mat-panel-title>
75 <div fxFlex fxLayout="row" fxLayoutAlign="end center"> 75 <div fxFlex fxLayout="row" fxLayoutAlign="end center">
76 - <div class="tb-small" translate>device-profile.advanced-settings</div> 76 + <div class="tb-small" translate>device-profile.alarm-rule-details</div>
77 </div> 77 </div>
78 </mat-panel-title> 78 </mat-panel-title>
79 </mat-expansion-panel-header> 79 </mat-expansion-panel-header>
@@ -14,6 +14,11 @@ @@ -14,6 +14,11 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 :host { 16 :host {
  17 + .tb-condition-duration {
  18 + padding: 8px;
  19 + border: 1px groove rgba(0, 0, 0, .25);
  20 + border-radius: 4px;
  21 + }
17 .mat-expansion-panel.advanced-settings { 22 .mat-expansion-panel.advanced-settings {
18 box-shadow: none; 23 box-shadow: none;
19 border: none; 24 border: none;
@@ -25,7 +25,8 @@ @@ -25,7 +25,8 @@
25 <mat-select formControlName="severity" 25 <mat-select formControlName="severity"
26 required 26 required
27 placeholder="{{ 'device-profile.select-alarm-severity' | translate }}"> 27 placeholder="{{ 'device-profile.select-alarm-severity' | translate }}">
28 - <mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"> 28 + <mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"
  29 + [disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], $index)">
29 {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} 30 {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
30 </mat-option> 31 </mat-option>
31 </mat-select> 32 </mat-select>
@@ -36,7 +37,7 @@ @@ -36,7 +37,7 @@
36 <tb-alarm-rule formControlName="alarmRule" required fxFlex> 37 <tb-alarm-rule formControlName="alarmRule" required fxFlex>
37 </tb-alarm-rule> 38 </tb-alarm-rule>
38 </div> 39 </div>
39 - <button *ngIf="!disabled && createAlarmRulesFormArray().controls.length > 1" 40 + <button *ngIf="!disabled"
40 mat-icon-button color="primary" style="min-width: 40px;" 41 mat-icon-button color="primary" style="min-width: 40px;"
41 type="button" 42 type="button"
42 (click)="removeCreateAlarmRule($index)" 43 (click)="removeCreateAlarmRule($index)"
@@ -45,6 +46,10 @@ @@ -45,6 +46,10 @@
45 <mat-icon>remove_circle_outline</mat-icon> 46 <mat-icon>remove_circle_outline</mat-icon>
46 </button> 47 </button>
47 </div> 48 </div>
  49 + <div *ngIf="!createAlarmRulesFormArray().controls.length">
  50 + <span translate fxLayoutAlign="center center"
  51 + class="tb-prompt">device-profile.no-create-alarm-rules</span>
  52 + </div>
48 <div fxLayout="row" *ngIf="!disabled"> 53 <div fxLayout="row" *ngIf="!disabled">
49 <button mat-stroked-button color="primary" 54 <button mat-stroked-button color="primary"
50 type="button" 55 type="button"
@@ -61,6 +61,8 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, @@ -61,6 +61,8 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
61 61
62 createAlarmRulesFormGroup: FormGroup; 62 createAlarmRulesFormGroup: FormGroup;
63 63
  64 + private usedSeverities: AlarmSeverity[] = [];
  65 +
64 private valueChangeSubscription: Subscription = null; 66 private valueChangeSubscription: Subscription = null;
65 67
66 private propagateChange = (v: any) => { }; 68 private propagateChange = (v: any) => { };
@@ -121,6 +123,10 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, @@ -121,6 +123,10 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
121 this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => { 123 this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => {
122 this.updateModel(); 124 this.updateModel();
123 }); 125 });
  126 + this.updateUsedSeverities();
  127 + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
  128 + this.updateModel();
  129 + }
124 } 130 }
125 131
126 public removeCreateAlarmRule(index: number) { 132 public removeCreateAlarmRule(index: number) {
@@ -142,19 +148,33 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, @@ -142,19 +148,33 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
142 } 148 }
143 149
144 public validate(c: FormControl) { 150 public validate(c: FormControl) {
145 - return (this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormGroup.get('createAlarmRules').value.length) ? null : { 151 + return (this.createAlarmRulesFormGroup.valid) ? null : {
146 createAlarmRules: { 152 createAlarmRules: {
147 valid: false, 153 valid: false,
148 }, 154 },
149 }; 155 };
150 } 156 }
151 157
  158 + public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean {
  159 + const usedIndex = this.usedSeverities.indexOf(severity);
  160 + return usedIndex > -1 && usedIndex !== index;
  161 + }
  162 +
  163 + private updateUsedSeverities() {
  164 + this.usedSeverities = [];
  165 + const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
  166 + value.forEach((rule, index) => {
  167 + this.usedSeverities[index] = AlarmSeverity[rule.severity];
  168 + });
  169 + }
  170 +
152 private updateModel() { 171 private updateModel() {
153 const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; 172 const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
154 const createAlarmRules: {[severity: string]: AlarmRule} = {}; 173 const createAlarmRules: {[severity: string]: AlarmRule} = {};
155 value.forEach(v => { 174 value.forEach(v => {
156 createAlarmRules[v.severity] = v.alarmRule; 175 createAlarmRules[v.severity] = v.alarmRule;
157 }); 176 });
  177 + this.updateUsedSeverities();
158 this.propagateChange(createAlarmRules); 178 this.propagateChange(createAlarmRules);
159 } 179 }
160 } 180 }
@@ -26,6 +26,7 @@ @@ -26,6 +26,7 @@
26 <mat-form-field floatLabel="always" 26 <mat-form-field floatLabel="always"
27 style="width: 600px;" 27 style="width: 600px;"
28 [fxShow]="expanded" 28 [fxShow]="expanded"
  29 + (keydown)="!disabled ? $event.stopPropagation() : null;"
29 (click)="!disabled ? $event.stopPropagation() : null;"> 30 (click)="!disabled ? $event.stopPropagation() : null;">
30 <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label> 31 <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
31 <input required matInput formControlName="alarmType" placeholder="Enter alarm type"> 32 <input required matInput formControlName="alarmType" placeholder="Enter alarm type">
@@ -66,6 +67,10 @@ @@ -66,6 +67,10 @@
66 <mat-icon>remove_circle_outline</mat-icon> 67 <mat-icon>remove_circle_outline</mat-icon>
67 </button> 68 </button>
68 </div> 69 </div>
  70 + <div *ngIf="!alarmFormGroup.get('clearRule').value">
  71 + <span translate fxLayoutAlign="center center"
  72 + class="tb-prompt">device-profile.no-clear-alarm-rule</span>
  73 + </div>
69 <div fxLayout="row" *ngIf="!disabled" 74 <div fxLayout="row" *ngIf="!disabled"
70 [fxShow]="!alarmFormGroup.get('clearRule').value"> 75 [fxShow]="!alarmFormGroup.get('clearRule').value">
71 <button mat-stroked-button color="primary" 76 <button mat-stroked-button color="primary"
@@ -86,9 +91,28 @@ @@ -86,9 +91,28 @@
86 </div> 91 </div>
87 </mat-panel-title> 92 </mat-panel-title>
88 </mat-expansion-panel-header> 93 </mat-expansion-panel-header>
89 - <mat-checkbox formControlName="propagate" style="padding-bottom: 16px;"> 94 + <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
90 {{ 'device-profile.propagate-alarm' | translate }} 95 {{ 'device-profile.propagate-alarm' | translate }}
91 </mat-checkbox> 96 </mat-checkbox>
92 - <div>TODO: Propagate relation types</div> 97 + <section *ngIf="alarmFormGroup.get('propagate').value === true">
  98 + <mat-form-field floatLabel="always" class="mat-block">
  99 + <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
  100 + <mat-chip-list #relationTypesChipList [disabled]="disabled">
  101 + <mat-chip
  102 + *ngFor="let key of alarmFormGroup.get('propagateRelationTypes').value;"
  103 + (removed)="removeRelationType(key)">
  104 + {{key}}
  105 + <mat-icon matChipRemove>close</mat-icon>
  106 + </mat-chip>
  107 + <input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}"
  108 + style="max-width: 200px;"
  109 + [matChipInputFor]="relationTypesChipList"
  110 + [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
  111 + (matChipInputTokenEnd)="addRelationType($event)"
  112 + [matChipInputAddOnBlur]="true">
  113 + </mat-chip-list>
  114 + <mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
  115 + </mat-form-field>
  116 + </section>
93 </mat-expansion-panel> 117 </mat-expansion-panel>
94 </mat-expansion-panel> 118 </mat-expansion-panel>
@@ -27,6 +27,8 @@ import { @@ -27,6 +27,8 @@ import {
27 } from '@angular/forms'; 27 } from '@angular/forms';
28 import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models'; 28 import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models';
29 import { MatDialog } from '@angular/material/dialog'; 29 import { MatDialog } from '@angular/material/dialog';
  30 +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
  31 +import { MatChipInputEvent } from '@angular/material/chips';
30 32
31 @Component({ 33 @Component({
32 selector: 'tb-device-profile-alarm', 34 selector: 'tb-device-profile-alarm',
@@ -53,13 +55,16 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit @@ -53,13 +55,16 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
53 @Output() 55 @Output()
54 removeAlarm = new EventEmitter(); 56 removeAlarm = new EventEmitter();
55 57
  58 + separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
  59 +
56 expanded = false; 60 expanded = false;
57 61
58 private modelValue: DeviceProfileAlarm; 62 private modelValue: DeviceProfileAlarm;
59 63
60 alarmFormGroup: FormGroup; 64 alarmFormGroup: FormGroup;
61 65
62 - private propagateChange = (v: any) => { }; 66 + private propagateChange = null;
  67 + private propagateChangePending = false;
63 68
64 constructor(private dialog: MatDialog, 69 constructor(private dialog: MatDialog,
65 private fb: FormBuilder) { 70 private fb: FormBuilder) {
@@ -67,6 +72,12 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit @@ -67,6 +72,12 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
67 72
68 registerOnChange(fn: any): void { 73 registerOnChange(fn: any): void {
69 this.propagateChange = fn; 74 this.propagateChange = fn;
  75 + if (this.propagateChangePending) {
  76 + this.propagateChangePending = false;
  77 + setTimeout(() => {
  78 + this.propagateChange(this.modelValue);
  79 + }, 0);
  80 + }
70 } 81 }
71 82
72 registerOnTouched(fn: any): void { 83 registerOnTouched(fn: any): void {
@@ -96,11 +107,15 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit @@ -96,11 +107,15 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
96 } 107 }
97 108
98 writeValue(value: DeviceProfileAlarm): void { 109 writeValue(value: DeviceProfileAlarm): void {
  110 + this.propagateChangePending = false;
99 this.modelValue = value; 111 this.modelValue = value;
100 if (!this.modelValue.alarmType) { 112 if (!this.modelValue.alarmType) {
101 this.expanded = true; 113 this.expanded = true;
102 } 114 }
103 this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); 115 this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
  116 + if (!this.disabled && !this.alarmFormGroup.valid) {
  117 + this.updateModel();
  118 + }
104 } 119 }
105 120
106 public addClearAlarmRule() { 121 public addClearAlarmRule() {
@@ -124,9 +139,42 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit @@ -124,9 +139,42 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
124 }; 139 };
125 } 140 }
126 141
  142 + removeRelationType(key: string): void {
  143 + const keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value;
  144 + const index = keys.indexOf(key);
  145 + if (index >= 0) {
  146 + keys.splice(index, 1);
  147 + this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
  148 + }
  149 + }
  150 +
  151 + addRelationType(event: MatChipInputEvent): void {
  152 + const input = event.input;
  153 + let value = event.value;
  154 + if ((value || '').trim()) {
  155 + value = value.trim();
  156 + let keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value;
  157 + if (!keys || keys.indexOf(value) === -1) {
  158 + if (!keys) {
  159 + keys = [];
  160 + }
  161 + keys.push(value);
  162 + this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
  163 + }
  164 + }
  165 + if (input) {
  166 + input.value = '';
  167 + }
  168 + }
  169 +
  170 +
127 private updateModel() { 171 private updateModel() {
128 const value = this.alarmFormGroup.value; 172 const value = this.alarmFormGroup.value;
129 this.modelValue = {...this.modelValue, ...value}; 173 this.modelValue = {...this.modelValue, ...value};
130 - this.propagateChange(this.modelValue); 174 + if (this.propagateChange) {
  175 + this.propagateChange(this.modelValue);
  176 + } else {
  177 + this.propagateChangePending = true;
  178 + }
131 } 179 }
132 } 180 }
@@ -25,6 +25,10 @@ @@ -25,6 +25,10 @@
25 </tb-device-profile-alarm> 25 </tb-device-profile-alarm>
26 </div> 26 </div>
27 </div> 27 </div>
  28 + <div *ngIf="!alarmsFormArray().controls.length">
  29 + <span translate fxLayoutAlign="center center"
  30 + class="tb-prompt">device-profile.no-alarm-rules</span>
  31 + </div>
28 <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center" 32 <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center"
29 style="padding-top: 16px;"> 33 style="padding-top: 16px;">
30 <button mat-raised-button color="primary" 34 <button mat-raised-button color="primary"
@@ -32,8 +36,7 @@ @@ -32,8 +36,7 @@
32 (click)="addAlarm()" 36 (click)="addAlarm()"
33 matTooltip="{{ 'device-profile.add-alarm-rule' | translate }}" 37 matTooltip="{{ 'device-profile.add-alarm-rule' | translate }}"
34 matTooltipPosition="above"> 38 matTooltipPosition="above">
35 - <mat-icon>add</mat-icon>  
36 - <span translate>action.add</span> 39 + <span translate>device-profile.add-alarm-rule</span>
37 </button> 40 </button>
38 </div> 41 </div>
39 </div> 42 </div>
@@ -162,11 +162,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni @@ -162,11 +162,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
162 } 162 }
163 163
164 private updateModel() { 164 private updateModel() {
165 -// if (this.deviceProfileAlarmsFormGroup.valid) {  
166 - const alarms: Array<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value;  
167 - this.propagateChange(alarms);  
168 - /* } else {  
169 - this.propagateChange(null);  
170 - } */ 165 + const alarms: Array<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value;
  166 + this.propagateChange(alarms);
171 } 167 }
172 } 168 }
@@ -49,6 +49,7 @@ import { @@ -49,6 +49,7 @@ import {
49 import { DeviceProfileService } from '@core/http/device-profile.service'; 49 import { DeviceProfileService } from '@core/http/device-profile.service';
50 import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; 50 import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component';
51 import { MatAutocomplete } from '@angular/material/autocomplete'; 51 import { MatAutocomplete } from '@angular/material/autocomplete';
  52 +import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component';
52 53
53 @Component({ 54 @Component({
54 selector: 'tb-device-profile-autocomplete', 55 selector: 'tb-device-profile-autocomplete',
@@ -279,15 +280,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, @@ -279,15 +280,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
279 createDeviceProfile($event: Event, profileName: string) { 280 createDeviceProfile($event: Event, profileName: string) {
280 $event.preventDefault(); 281 $event.preventDefault();
281 const deviceProfile: DeviceProfile = { 282 const deviceProfile: DeviceProfile = {
282 - id: null,  
283 - name: profileName,  
284 - type: DeviceProfileType.DEFAULT,  
285 - transportType: DeviceTransportType.DEFAULT,  
286 - profileData: {  
287 - configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),  
288 - transportConfiguration: createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT)  
289 - }  
290 - }; 283 + name: profileName
  284 + } as DeviceProfile;
291 this.openDeviceProfileDialog(deviceProfile, true); 285 this.openDeviceProfileDialog(deviceProfile, true);
292 } 286 }
293 287
@@ -301,15 +295,28 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, @@ -301,15 +295,28 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
301 } 295 }
302 296
303 openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) { 297 openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) {
304 - this.dialog.open<DeviceProfileDialogComponent, DeviceProfileDialogData,  
305 - DeviceProfile>(DeviceProfileDialogComponent, {  
306 - disableClose: true,  
307 - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],  
308 - data: {  
309 - isAdd,  
310 - deviceProfile  
311 - }  
312 - }).afterClosed().subscribe( 298 + let deviceProfileObservable: Observable<DeviceProfile>;
  299 + if (!isAdd) {
  300 + deviceProfileObservable = this.dialog.open<DeviceProfileDialogComponent, DeviceProfileDialogData,
  301 + DeviceProfile>(DeviceProfileDialogComponent, {
  302 + disableClose: true,
  303 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  304 + data: {
  305 + isAdd: false,
  306 + deviceProfile
  307 + }
  308 + }).afterClosed();
  309 + } else {
  310 + deviceProfileObservable = this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
  311 + DeviceProfile>(AddDeviceProfileDialogComponent, {
  312 + disableClose: true,
  313 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  314 + data: {
  315 + deviceProfileName: deviceProfile.name
  316 + }
  317 + }).afterClosed();
  318 + }
  319 + deviceProfileObservable.subscribe(
313 (savedDeviceProfile) => { 320 (savedDeviceProfile) => {
314 if (!savedDeviceProfile) { 321 if (!savedDeviceProfile) {
315 setTimeout(() => { 322 setTimeout(() => {
@@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
39 required> 39 required>
40 </tb-device-profile-transport-configuration> 40 </tb-device-profile-transport-configuration>
41 </mat-expansion-panel> 41 </mat-expansion-panel>
42 - <mat-expansion-panel [expanded]="false"> 42 + <mat-expansion-panel [expanded]="true">
43 <mat-expansion-panel-header> 43 <mat-expansion-panel-header>
44 <mat-panel-title> 44 <mat-panel-title>
45 <div>{{'device-profile.alarm-rules' | translate: 45 <div>{{'device-profile.alarm-rules' | translate:
@@ -81,7 +81,7 @@ @@ -81,7 +81,7 @@
81 required> 81 required>
82 </tb-device-profile-data> 82 </tb-device-profile-data>
83 <mat-form-field class="mat-block"> 83 <mat-form-field class="mat-block">
84 - <mat-label translate>tenant-profile.description</mat-label> 84 + <mat-label translate>device-profile.description</mat-label>
85 <textarea matInput formControlName="description" rows="2"></textarea> 85 <textarea matInput formControlName="description" rows="2"></textarea>
86 </mat-form-field> 86 </mat-form-field>
87 </fieldset> 87 </fieldset>
@@ -35,6 +35,12 @@ import { @@ -35,6 +35,12 @@ import {
35 import { DeviceProfileService } from '@core/http/device-profile.service'; 35 import { DeviceProfileService } from '@core/http/device-profile.service';
36 import { DeviceProfileComponent } from '../../components/profile/device-profile.component'; 36 import { DeviceProfileComponent } from '../../components/profile/device-profile.component';
37 import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; 37 import { DeviceProfileTabsComponent } from './device-profile-tabs.component';
  38 +import { Observable } from 'rxjs';
  39 +import { MatDialog } from '@angular/material/dialog';
  40 +import {
  41 + AddDeviceProfileDialogComponent,
  42 + AddDeviceProfileDialogData
  43 +} from '../../components/profile/add-device-profile-dialog.component';
38 44
39 @Injectable() 45 @Injectable()
40 export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> { 46 export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> {
@@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon @@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
44 constructor(private deviceProfileService: DeviceProfileService, 50 constructor(private deviceProfileService: DeviceProfileService,
45 private translate: TranslateService, 51 private translate: TranslateService,
46 private datePipe: DatePipe, 52 private datePipe: DatePipe,
47 - private dialogService: DialogService) { 53 + private dialogService: DialogService,
  54 + private dialog: MatDialog) {
48 55
49 this.config.entityType = EntityType.DEVICE_PROFILE; 56 this.config.entityType = EntityType.DEVICE_PROFILE;
50 this.config.entityComponent = DeviceProfileComponent; 57 this.config.entityComponent = DeviceProfileComponent;
@@ -92,6 +99,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon @@ -92,6 +99,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
92 this.config.onEntityAction = action => this.onDeviceProfileAction(action); 99 this.config.onEntityAction = action => this.onDeviceProfileAction(action);
93 this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; 100 this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
94 this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; 101 this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
  102 + this.config.addEntity = () => this.addDeviceProfile();
95 } 103 }
96 104
97 resolve(): EntityTableConfig<DeviceProfile> { 105 resolve(): EntityTableConfig<DeviceProfile> {
@@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon @@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
100 return this.config; 108 return this.config;
101 } 109 }
102 110
  111 + addDeviceProfile(): Observable<DeviceProfile> {
  112 + return this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
  113 + DeviceProfile>(AddDeviceProfileDialogComponent, {
  114 + disableClose: true,
  115 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  116 + data: {
  117 + deviceProfileName: null
  118 + }
  119 + }).afterClosed();
  120 + }
  121 +
103 setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) { 122 setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) {
104 if ($event) { 123 if ($event) {
105 $event.stopPropagation(); 124 $event.stopPropagation();
@@ -37,6 +37,7 @@ import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; @@ -37,6 +37,7 @@ import { getCurrentAuthUser } from '@app/core/auth/auth.selectors';
37 import { Authority } from '@shared/models/authority.enum'; 37 import { Authority } from '@shared/models/authority.enum';
38 import { DialogService } from '@core/services/dialog.service'; 38 import { DialogService } from '@core/services/dialog.service';
39 import { ImportExportService } from '@home/components/import-export/import-export.service'; 39 import { ImportExportService } from '@home/components/import-export/import-export.service';
  40 +import { Direction } from "@shared/models/page/sort-order";
40 41
41 @Injectable() 42 @Injectable()
42 export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> { 43 export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> {
@@ -55,6 +56,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon @@ -55,6 +56,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
55 this.config.entityComponent = WidgetsBundleComponent; 56 this.config.entityComponent = WidgetsBundleComponent;
56 this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGETS_BUNDLE); 57 this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGETS_BUNDLE);
57 this.config.entityResources = entityTypeResources.get(EntityType.WIDGETS_BUNDLE); 58 this.config.entityResources = entityTypeResources.get(EntityType.WIDGETS_BUNDLE);
  59 + this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
58 60
59 this.config.entityTitle = (widgetsBundle) => widgetsBundle ? 61 this.config.entityTitle = (widgetsBundle) => widgetsBundle ?
60 widgetsBundle.title : ''; 62 widgetsBundle.title : '';
@@ -25,6 +25,8 @@ import { PageData } from '@shared/models/page/page-data'; @@ -25,6 +25,8 @@ import { PageData } from '@shared/models/page/page-data';
25 import { isDefined, isEqual } from '@core/utils'; 25 import { isDefined, isEqual } from '@core/utils';
26 import { TranslateService } from '@ngx-translate/core'; 26 import { TranslateService } from '@ngx-translate/core';
27 import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models'; 27 import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models';
  28 +import { Filter } from '@material-ui/icons';
  29 +import { DatePipe } from '@angular/common';
28 30
29 export enum EntityKeyType { 31 export enum EntityKeyType {
30 ATTRIBUTE = 'ATTRIBUTE', 32 ATTRIBUTE = 'ATTRIBUTE',
@@ -358,7 +360,102 @@ export interface FiltersInfo { @@ -358,7 +360,102 @@ export interface FiltersInfo {
358 datasourceFilters: {[datasourceIndex: number]: FilterInfo}; 360 datasourceFilters: {[datasourceIndex: number]: FilterInfo};
359 } 361 }
360 362
  363 +export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array<KeyFilter>): string {
  364 + const filtersText = keyFilters.map(keyFilter =>
  365 + keyFilterToText(translate, datePipe, keyFilter,
  366 + keyFilters.length > 1 ? ComplexOperation.AND : undefined));
  367 + let result: string;
  368 + if (filtersText.length > 1) {
  369 + const andText = translate.instant('filter.operation.and');
  370 + result = filtersText.join(' <span class="tb-filter-complex-operation">' + andText + '</span> ');
  371 + } else {
  372 + result = filtersText[0];
  373 + }
  374 + return result;
  375 +}
  376 +
  377 +export function keyFilterToText(translate: TranslateService, datePipe: DatePipe, keyFilter: KeyFilter,
  378 + parentComplexOperation?: ComplexOperation): string {
  379 + const keyFilterPredicate = keyFilter.predicate;
  380 + return keyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate, parentComplexOperation);
  381 +}
  382 +
  383 +export function keyFilterPredicateToText(translate: TranslateService,
  384 + datePipe: DatePipe,
  385 + keyFilter: KeyFilter,
  386 + keyFilterPredicate: KeyFilterPredicate,
  387 + parentComplexOperation?: ComplexOperation): string {
  388 + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
  389 + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate;
  390 + const complexOperation = complexPredicate.operation;
  391 + const complexPredicatesText =
  392 + complexPredicate.predicates.map(predicate => keyFilterPredicateToText(translate, datePipe, keyFilter, predicate, complexOperation));
  393 + if (complexPredicatesText.length > 1) {
  394 + const operationText = translate.instant(complexOperationTranslationMap.get(complexOperation));
  395 + let result = complexPredicatesText.join(' <span class="tb-filter-complex-operation">' + operationText + '</span> ');
  396 + if (complexOperation === ComplexOperation.OR && parentComplexOperation && ComplexOperation.OR !== parentComplexOperation) {
  397 + result = `<span class="tb-filter-bracket"><span class="tb-left-bracket">(</span>${result}<span class="tb-right-bracket">)</span></span>`;
  398 + }
  399 + return result;
  400 + } else {
  401 + return complexPredicatesText[0];
  402 + }
  403 + } else {
  404 + return simpleKeyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate);
  405 + }
  406 +}
  407 +
  408 +function simpleKeyFilterPredicateToText(translate: TranslateService,
  409 + datePipe: DatePipe,
  410 + keyFilter: KeyFilter,
  411 + keyFilterPredicate: StringFilterPredicate |
  412 + NumericFilterPredicate |
  413 + BooleanFilterPredicate): string {
  414 + const key = keyFilter.key.key;
  415 + let operation: string;
  416 + let value: string;
  417 + const val = keyFilterPredicate.value;
  418 + const dynamicValue = !!val.dynamicValue && !!val.dynamicValue.sourceType;
  419 + if (dynamicValue) {
  420 + value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-dynamic-source">' +
  421 + translate.instant(dynamicValueSourceTypeTranslationMap.get(val.dynamicValue.sourceType)) + '</span>';
  422 + value += '.<span class="tb-filter-value">' + val.dynamicValue.sourceAttribute + '</span></span>';
  423 + }
  424 + switch (keyFilterPredicate.type) {
  425 + case FilterPredicateType.STRING:
  426 + operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation));
  427 + if (keyFilterPredicate.ignoreCase) {
  428 + operation += ' ' + translate.instant('filter.ignore-case');
  429 + }
  430 + if (!dynamicValue) {
  431 + value = `'${keyFilterPredicate.value.defaultValue}'`;
  432 + }
  433 + break;
  434 + case FilterPredicateType.NUMERIC:
  435 + operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation));
  436 + if (!dynamicValue) {
  437 + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) {
  438 + value = datePipe.transform(keyFilterPredicate.value.defaultValue, 'yyyy-MM-dd HH:mm');
  439 + } else {
  440 + value = keyFilterPredicate.value.defaultValue + '';
  441 + }
  442 + }
  443 + break;
  444 + case FilterPredicateType.BOOLEAN:
  445 + operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation));
  446 + value = translate.instant(keyFilterPredicate.value.defaultValue ? 'value.true' : 'value.false');
  447 + break;
  448 + }
  449 + if (!dynamicValue) {
  450 + value = `<span class="tb-filter-value">${value}</span>`;
  451 + }
  452 + return `<span class="tb-filter-predicate"><span class="tb-filter-entity-key">${key}</span> <span class="tb-filter-simple-operation">${operation}</span> ${value}</span>`;
  453 +}
  454 +
361 export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>): Array<KeyFilter> { 455 export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>): Array<KeyFilter> {
  456 + if (!keyFilterInfos) {
  457 + return [];
  458 + }
362 const keyFilters: Array<KeyFilter> = []; 459 const keyFilters: Array<KeyFilter> = [];
363 for (const keyFilterInfo of keyFilterInfos) { 460 for (const keyFilterInfo of keyFilterInfos) {
364 const key = keyFilterInfo.key; 461 const key = keyFilterInfo.key;
@@ -811,17 +811,19 @@ @@ -811,17 +811,19 @@
811 "single-level-wildcards-hint": "<code>[+]</code> is suitable for any topic filter level. Ex.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.", 811 "single-level-wildcards-hint": "<code>[+]</code> is suitable for any topic filter level. Ex.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
812 "multi-level-wildcards-hint": "<code>[#]</code> can replace the topic filter itself and must be the last symbol of the topic. Ex.: <b>#</b> or <b>v1/devices/me/#</b>.", 812 "multi-level-wildcards-hint": "<code>[#]</code> can replace the topic filter itself and must be the last symbol of the topic. Ex.: <b>#</b> or <b>v1/devices/me/#</b>.",
813 "alarm-rules": "Alarm rules ({{count}})", 813 "alarm-rules": "Alarm rules ({{count}})",
  814 + "no-alarm-rules": "No alarm rules configured",
814 "add-alarm-rule": "Add alarm rule", 815 "add-alarm-rule": "Add alarm rule",
815 "edit-alarm-rule": "Edit alarm rule", 816 "edit-alarm-rule": "Edit alarm rule",
816 - "alarm-rule-details": "Alarm rule details",  
817 "alarm-type": "Alarm type", 817 "alarm-type": "Alarm type",
818 "alarm-type-required": "Alarm type is required.", 818 "alarm-type-required": "Alarm type is required.",
819 "alarm-type-pattern-hint": "Alarm type pattern, use <code>${metaKeyName}</code> to substitute variables from metadata", 819 "alarm-type-pattern-hint": "Alarm type pattern, use <code>${metaKeyName}</code> to substitute variables from metadata",
820 "create-alarm-pattern": "Create <b>{{alarmType}}</b> alarm", 820 "create-alarm-pattern": "Create <b>{{alarmType}}</b> alarm",
821 "create-alarm-rules": "Create alarm rules", 821 "create-alarm-rules": "Create alarm rules",
  822 + "no-create-alarm-rules": "No create conditions configured",
822 "clear-alarm-rule": "Clear alarm rule", 823 "clear-alarm-rule": "Clear alarm rule",
823 - "add-create-alarm-rule": "Add create alarm rule",  
824 - "add-clear-alarm-rule": "Add clear alarm rule", 824 + "no-clear-alarm-rule": "No clear condition configured",
  825 + "add-create-alarm-rule": "Add create condition",
  826 + "add-clear-alarm-rule": "Add clear condition",
825 "select-alarm-severity": "Select alarm severity", 827 "select-alarm-severity": "Select alarm severity",
826 "alarm-severity-required": "Alarm severity is required.", 828 "alarm-severity-required": "Alarm severity is required.",
827 "condition-duration": "Condition duration", 829 "condition-duration": "Condition duration",
@@ -831,7 +833,10 @@ @@ -831,7 +833,10 @@
831 "condition-duration-value-required": "Duration value is required.", 833 "condition-duration-value-required": "Duration value is required.",
832 "condition-duration-time-unit-required": "Time unit is required.", 834 "condition-duration-time-unit-required": "Time unit is required.",
833 "advanced-settings": "Advanced settings", 835 "advanced-settings": "Advanced settings",
  836 + "alarm-rule-details": "Details",
834 "propagate-alarm": "Propagate alarm", 837 "propagate-alarm": "Propagate alarm",
  838 + "alarm-rule-relation-types-list": "Relation types to propagate",
  839 + "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.",
835 "alarm-details": "Alarm details", 840 "alarm-details": "Alarm details",
836 "alarm-rule-condition": "Alarm rule condition", 841 "alarm-rule-condition": "Alarm rule condition",
837 "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", 842 "enter-alarm-rule-condition-prompt": "Please add alarm rule condition",
@@ -1277,6 +1282,8 @@ @@ -1277,6 +1282,8 @@
1277 "filter": "Filter", 1282 "filter": "Filter",
1278 "editable": "Editable", 1283 "editable": "Editable",
1279 "no-filters-found": "No filters found.", 1284 "no-filters-found": "No filters found.",
  1285 + "no-filter-text": "No filter specified",
  1286 + "add-filter-prompt": "Please add filter",
1280 "no-filter-matching": "'{{filter}}' not found.", 1287 "no-filter-matching": "'{{filter}}' not found.",
1281 "create-new-filter": "Create a new one!", 1288 "create-new-filter": "Create a new one!",
1282 "filter-required": "Filter is required.", 1289 "filter-required": "Filter is required.",
@@ -1295,9 +1302,10 @@ @@ -1295,9 +1302,10 @@
1295 "and": "and", 1302 "and": "and",
1296 "or": "or" 1303 "or": "or"
1297 }, 1304 },
1298 - "ignore-case": "Ignore case", 1305 + "ignore-case": "ignore case",
1299 "value": "Value", 1306 "value": "Value",
1300 "remove-filter": "Remove filter", 1307 "remove-filter": "Remove filter",
  1308 + "preview": "Filter preview",
1301 "no-filters": "No filters configured", 1309 "no-filters": "No filters configured",
1302 "add-filter": "Add filter", 1310 "add-filter": "Add filter",
1303 "add-complex-filter": "Add complex filter", 1311 "add-complex-filter": "Add complex filter",