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 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 167 "settingsSchema": "{}",
168 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 247 log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
248 248 }
249 249 }
250   -}
\ No newline at end of file
  250 +}
... ...
... ... @@ -209,4 +209,4 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
209 209 log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
210 210 }
211 211 }
212   -}
\ No newline at end of file
  212 +}
... ...
... ... @@ -39,8 +39,7 @@ public abstract class AbstractCleanUpService {
39 39 protected String dbPassword;
40 40
41 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 43 if (log.isDebugEnabled()) {
45 44 getWarnings(statement);
46 45 }
... ...
... ... @@ -33,4 +33,4 @@ public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUp
33 33 long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);");
34 34 log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
35 35 }
36   -}
\ No newline at end of file
  36 +}
... ...
... ... @@ -284,6 +284,8 @@ sql:
284 284 batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}"
285 285 stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}"
286 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 289 # Specify whether to remove null characters from strValue of attributes and timeseries before insert
288 290 remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}"
289 291 # Specify whether to log database queries and their parameters generated by entity query repository
... ... @@ -651,11 +653,11 @@ queue:
651 653 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
652 654 other:
653 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 661 aws_sqs:
660 662 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
661 663 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
... ...
... ... @@ -37,6 +37,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
37 37 private final AdminClient client;
38 38 private final Map<String, String> topicConfigs;
39 39 private final Set<String> topics = ConcurrentHashMap.newKeySet();
  40 + private final int numPartitions;
40 41
41 42 private final short replicationFactor;
42 43
... ... @@ -50,6 +51,13 @@ public class TbKafkaAdmin implements TbQueueAdmin {
50 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 61 replicationFactor = settings.getReplicationFactor();
54 62 }
55 63
... ... @@ -59,7 +67,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
59 67 return;
60 68 }
61 69 try {
62   - NewTopic newTopic = new NewTopic(topic, 1, replicationFactor).configs(topicConfigs);
  70 + NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(topicConfigs);
63 71 createTopic(newTopic).values().get(topic).get();
64 72 topics.add(topic);
65 73 } catch (ExecutionException ee) {
... ...
... ... @@ -22,6 +22,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
22 22 import org.thingsboard.server.common.stats.MessagesStats;
23 23
24 24 import java.util.ArrayList;
  25 +import java.util.Comparator;
25 26 import java.util.List;
26 27 import java.util.concurrent.BlockingQueue;
27 28 import java.util.concurrent.ExecutorService;
... ... @@ -30,6 +31,7 @@ import java.util.concurrent.LinkedBlockingQueue;
30 31 import java.util.concurrent.TimeUnit;
31 32 import java.util.function.Consumer;
32 33 import java.util.stream.Collectors;
  34 +import java.util.stream.Stream;
33 35
34 36 @Slf4j
35 37 public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
... ... @@ -46,7 +48,7 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
46 48 }
47 49
48 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 52 executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase()));
51 53 executor.submit(() -> {
52 54 String logName = params.getLogName();
... ... @@ -65,7 +67,11 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
65 67 queue.drainTo(entities, batchSize - 1);
66 68 boolean fullPack = entities.size() == batchSize;
67 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 75 entities.forEach(v -> v.getFuture().set(null));
70 76 stats.incrementSuccessful(entities.size());
71 77 if (!fullPack) {
... ...
... ... @@ -31,4 +31,5 @@ public class TbSqlBlockingQueueParams {
31 31 private final long maxDelay;
32 32 private final long statsPrintIntervalMs;
33 33 private final String statsNamePrefix;
  34 + private final boolean batchSortEnabled;
34 35 }
... ...
... ... @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
21 21 import org.thingsboard.server.common.stats.MessagesStats;
22 22 import org.thingsboard.server.common.stats.StatsFactory;
23 23
  24 +import java.util.Comparator;
24 25 import java.util.List;
25 26 import java.util.concurrent.CopyOnWriteArrayList;
26 27 import java.util.function.Consumer;
... ... @@ -36,12 +37,20 @@ public class TbSqlBlockingQueueWrapper<E> {
36 37 private final int maxThreads;
37 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 49 for (int i = 0; i < maxThreads; i++) {
41 50 MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i);
42 51 TbSqlBlockingQueue<E> queue = new TbSqlBlockingQueue<>(params, stats);
43 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 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
19 19
  20 +import java.util.Comparator;
20 21 import java.util.List;
21 22 import java.util.function.Consumer;
22 23
23 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 28 void destroy();
28 29
... ...
... ... @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
38 38 import javax.annotation.PostConstruct;
39 39 import javax.annotation.PreDestroy;
40 40 import java.util.Collection;
  41 +import java.util.Comparator;
41 42 import java.util.List;
42 43 import java.util.Optional;
43 44 import java.util.function.Function;
... ... @@ -71,6 +72,9 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
71 72 @Value("${sql.attributes.batch_threads:4}")
72 73 private int batchThreads;
73 74
  75 + @Value("${sql.batch_sort:false}")
  76 + private boolean batchSortEnabled;
  77 +
74 78 private TbSqlBlockingQueueWrapper<AttributeKvEntity> queue;
75 79
76 80 @PostConstruct
... ... @@ -81,11 +85,17 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
81 85 .maxDelay(maxDelay)
82 86 .statsPrintIntervalMs(statsPrintIntervalMs)
83 87 .statsNamePrefix("attributes")
  88 + .batchSortEnabled(batchSortEnabled)
84 89 .build();
85 90
86 91 Function<AttributeKvEntity, Integer> hashcodeFunction = entity -> entity.getId().getEntityId().hashCode();
87 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 101 @PreDestroy
... ...
... ... @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors;
21 21 import com.google.common.util.concurrent.SettableFuture;
22 22 import lombok.extern.slf4j.Slf4j;
23 23 import org.springframework.beans.factory.annotation.Autowired;
  24 +import org.springframework.beans.factory.annotation.Value;
24 25 import org.springframework.data.domain.PageRequest;
25 26 import org.springframework.data.domain.Sort;
26 27 import org.thingsboard.server.common.data.id.EntityId;
... ... @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
31 32 import org.thingsboard.server.common.data.kv.TsKvEntry;
32 33 import org.thingsboard.server.common.stats.StatsFactory;
33 34 import org.thingsboard.server.dao.DaoUtil;
  35 +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
34 36 import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
35 37 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
36 38 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
... ... @@ -40,9 +42,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
40 42
41 43 import javax.annotation.PostConstruct;
42 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 46 import java.util.concurrent.CompletableFuture;
47 47 import java.util.function.Function;
48 48 import java.util.stream.Collectors;
... ... @@ -68,11 +68,16 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
68 68 .maxDelay(tsMaxDelay)
69 69 .statsPrintIntervalMs(tsStatsPrintIntervalMs)
70 70 .statsNamePrefix("ts")
  71 + .batchSortEnabled(batchSortEnabled)
71 72 .build();
72 73
73 74 Function<TsKvEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
74 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 83 @PreDestroy
... ...
... ... @@ -53,6 +53,9 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries
53 53 @Value("${sql.timescale.batch_threads:4}")
54 54 protected int timescaleBatchThreads;
55 55
  56 + @Value("${sql.batch_sort:false}")
  57 + protected boolean batchSortEnabled;
  58 +
56 59 protected ListenableFuture<List<TsKvEntry>> processFindAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
57 60 List<ListenableFuture<List<TsKvEntry>>> futures = queries
58 61 .stream()
... ...
... ... @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
35 35 import org.thingsboard.server.common.data.kv.TsKvEntry;
36 36 import org.thingsboard.server.common.stats.StatsFactory;
37 37 import org.thingsboard.server.dao.DaoUtil;
  38 +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
38 39 import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey;
39 40 import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
40 41 import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
... ... @@ -50,12 +51,10 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao;
50 51 import javax.annotation.Nullable;
51 52 import javax.annotation.PostConstruct;
52 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 55 import java.util.concurrent.ExecutionException;
  56 +import java.util.function.Function;
  57 +import java.util.stream.Collectors;
59 58
60 59 @Slf4j
61 60 @Component
... ... @@ -90,6 +89,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
90 89 @Value("${sql.ts_latest.batch_threads:4}")
91 90 private int tsLatestBatchThreads;
92 91
  92 + @Value("${sql.batch_sort:false}")
  93 + protected boolean batchSortEnabled;
  94 +
93 95 @Autowired
94 96 protected ScheduledLogExecutorComponent logExecutor;
95 97
... ... @@ -104,6 +106,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
104 106 .maxDelay(tsLatestMaxDelay)
105 107 .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs)
106 108 .statsNamePrefix("ts.latest")
  109 + .batchSortEnabled(false)
107 110 .build();
108 111
109 112 java.util.function.Function<TsKvLatestEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
... ... @@ -113,14 +116,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
113 116 Map<TsKey, TsKvLatestEntity> trueLatest = new HashMap<>();
114 117 v.forEach(ts -> {
115 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 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 126 insertLatestTsRepository.saveOrUpdate(latestEntities);
123   - });
  127 + }, (l, r) -> 0);
124 128 }
125 129
126 130 @PreDestroy
... ...
... ... @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
33 33 import org.thingsboard.server.common.data.kv.TsKvEntry;
34 34 import org.thingsboard.server.common.stats.StatsFactory;
35 35 import org.thingsboard.server.dao.DaoUtil;
  36 +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
36 37 import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity;
37 38 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
38 39 import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
... ... @@ -43,11 +44,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao;
43 44
44 45 import javax.annotation.PostConstruct;
45 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 48 import java.util.concurrent.CompletableFuture;
52 49 import java.util.function.Function;
53 50
... ... @@ -78,12 +75,17 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
78 75 .maxDelay(tsMaxDelay)
79 76 .statsPrintIntervalMs(tsStatsPrintIntervalMs)
80 77 .statsNamePrefix("ts.timescale")
  78 + .batchSortEnabled(batchSortEnabled)
81 79 .build();
82 80
83 81 Function<TimescaleTsKvEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
84 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 91 @PreDestroy
... ...
1 1 TB_QUEUE_TYPE=kafka
2 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 25 # Kafka Bootstrap Servers
26 26 servers: "localhost:9092"
27 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 29 use_confluent_cloud: false
30 30 confluent:
31 31 sasl:
... ...
... ... @@ -34,7 +34,7 @@ function KafkaProducer() {
34 34 this.send = async (responseTopic, scriptId, rawResponse, headers) => {
35 35
36 36 if (!topics.includes(responseTopic)) {
37   - let createResponseTopicResult = await createTopic(responseTopic);
  37 + let createResponseTopicResult = await createTopic(responseTopic, 1);
38 38 topics.push(responseTopic);
39 39 if (createResponseTopicResult) {
40 40 logger.info('Created new topic: %s', requestTopic);
... ... @@ -88,7 +88,18 @@ function KafkaProducer() {
88 88 kafkaAdmin = kafkaClient.admin();
89 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 104 if (createRequestTopicResult) {
94 105 logger.info('Created new topic: %s', requestTopic);
... ... @@ -121,10 +132,11 @@ function KafkaProducer() {
121 132 }
122 133 })();
123 134
124   -function createTopic(topic) {
  135 +function createTopic(topic, partitions) {
125 136 return kafkaAdmin.createTopics({
126 137 topics: [{
127 138 topic: topic,
  139 + numPartitions: partitions,
128 140 replicationFactor: replicationFactor,
129 141 configEntries: configEntries
130 142 }]
... ...
... ... @@ -77,11 +77,11 @@ queue:
77 77 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
78 78 other:
79 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 85 aws_sqs:
86 86 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
87 87 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
... ...
... ... @@ -70,11 +70,11 @@ queue:
70 70 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
71 71 other:
72 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 78 aws_sqs:
79 79 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
80 80 access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
... ...
... ... @@ -98,11 +98,11 @@ queue:
98 98 security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
99 99 other:
100 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 106 aws_sqs:
107 107 use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
108 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 123 if (isString(obj[curr])) {
124 124 acc[curr] = obj[curr].trim();
125 125 } else if (isObject(obj[curr])) {
126   - acc[curr] = this.deepTrim(obj[curr])
  126 + acc[curr] = this.deepTrim(obj[curr]);
127 127 } else {
128 128 acc[curr] = obj[curr];
129 129 }
... ...
... ... @@ -24,7 +24,8 @@
24 24 </mat-option>
25 25 </mat-select>
26 26 </mat-form-field>
27   - <tb-filter-predicate-value fxFlex="60"
  27 + <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
  28 + fxFlex="60"
28 29 [valueType]="valueTypeEnum.BOOLEAN"
29 30 formControlName="value">
30 31 </tb-filter-predicate-value>
... ...
... ... @@ -39,6 +39,8 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
39 39
40 40 @Input() disabled: boolean;
41 41
  42 + @Input() allowUserDynamicSource = true;
  43 +
42 44 valueTypeEnum = EntityKeyValueType;
43 45
44 46 booleanFilterPredicateFormGroup: FormGroup;
... ...
... ... @@ -38,6 +38,7 @@
38 38 <tb-filter-predicate-list
39 39 [valueType]="data.valueType"
40 40 [displayUserParameters]="data.displayUserParameters"
  41 + [allowUserDynamicSource]="data.allowUserDynamicSource"
41 42 [operation]="complexFilterFormGroup.get('operation').value"
42 43 [key]="data.key"
43 44 formControlName="predicates">
... ...
... ... @@ -36,6 +36,7 @@ export interface ComplexFilterPredicateDialogData {
36 36 isAdd: boolean;
37 37 valueType: EntityKeyValueType;
38 38 displayUserParameters: boolean;
  39 + allowUserDynamicSource: boolean;
39 40 }
40 41
41 42 @Component({
... ...
... ... @@ -50,6 +50,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
50 50
51 51 @Input() displayUserParameters = true;
52 52
  53 + @Input() allowUserDynamicSource = true;
  54 +
53 55 private propagateChange = null;
54 56
55 57 private complexFilterPredicate: ComplexFilterPredicateInfo;
... ... @@ -86,7 +88,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
86 88 valueType: this.valueType,
87 89 isAdd: false,
88 90 key: this.key,
89   - displayUserParameters: this.displayUserParameters
  91 + displayUserParameters: this.displayUserParameters,
  92 + allowUserDynamicSource: this.allowUserDynamicSource
90 93 }
91 94 }).afterClosed().subscribe(
92 95 (result) => {
... ...
... ... @@ -52,6 +52,7 @@
52 52 fxFlex
53 53 [valueType]="valueType"
54 54 [displayUserParameters]="displayUserParameters"
  55 + [allowUserDynamicSource]="allowUserDynamicSource"
55 56 [key]="key"
56 57 [formControl]="predicateControl">
57 58 </tb-filter-predicate>
... ...
... ... @@ -64,6 +64,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
64 64
65 65 @Input() displayUserParameters = true;
66 66
  67 + @Input() allowUserDynamicSource = true;
  68 +
67 69 filterListFormGroup: FormGroup;
68 70
69 71 valueTypeEnum = EntityKeyValueType;
... ... @@ -156,7 +158,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
156 158 valueType: this.valueType,
157 159 key: this.key,
158 160 isAdd: true,
159   - displayUserParameters: this.displayUserParameters
  161 + displayUserParameters: this.displayUserParameters,
  162 + allowUserDynamicSource: this.allowUserDynamicSource
160 163 }
161 164 }).afterClosed().pipe(
162 165 map((result) => {
... ...
... ... @@ -55,7 +55,7 @@
55 55 {{'filter.no-dynamic-value' | translate}}
56 56 </mat-option>
57 57 <mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
58   - {{dynamicValueSourceTypeTranslations.get(dynamicValueSourceTypeEnum[sourceType]) | translate}}
  58 + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
59 59 </mat-option>
60 60 </mat-select>
61 61 </mat-form-field>
... ...
... ... @@ -47,12 +47,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
47 47 @Input() disabled: boolean;
48 48
49 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 59 valueType: EntityKeyValueType;
51 60
52 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 66 dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap;
57 67
58 68 filterPredicateValueFormGroup: FormGroup;
... ...
... ... @@ -19,20 +19,27 @@
19 19 <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateFormGroup">
20 20 <div fxFlex fxLayout="column" [ngSwitch]="type">
21 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 25 </tb-string-filter-predicate>
24 26 </ng-template>
25 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 32 </tb-numeric-filter-predicate>
29 33 </ng-template>
30 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 38 </tb-boolean-filter-predicate>
33 39 </ng-template>
34 40 <ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
35 41 <tb-complex-filter-predicate
  42 + [allowUserDynamicSource]="allowUserDynamicSource"
36 43 [key]="key"
37 44 [valueType]="valueType"
38 45 [displayUserParameters]="displayUserParameters"
... ...
... ... @@ -43,6 +43,8 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
43 43
44 44 @Input() displayUserParameters = true;
45 45
  46 + @Input() allowUserDynamicSource = true;
  47 +
46 48 filterPredicateFormGroup: FormGroup;
47 49
48 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 70 </mat-form-field>
71 71 </section>
72 72 <tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
  73 + [allowUserDynamicSource]="data.allowUserDynamicSource"
73 74 [displayUserParameters]="data.displayUserParameters"
74 75 [valueType]="keyFilterFormGroup.get('valueType').value"
75 76 [key]="keyFilterFormGroup.get('key.key').value"
... ...
... ... @@ -40,6 +40,7 @@ export interface KeyFilterDialogData {
40 40 keyFilter: KeyFilterInfo;
41 41 isAdd: boolean;
42 42 displayUserParameters: boolean;
  43 + allowUserDynamicSource: boolean;
43 44 readonly: boolean;
44 45 telemetryKeysOnly: boolean;
45 46 }
... ...
... ... @@ -16,65 +16,77 @@
16 16
17 17 -->
18 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 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 64 </div>
62   - <mat-divider></mat-divider>
63 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 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 92 </section>
... ...
... ... @@ -26,4 +26,17 @@
26 26 color: #666;
27 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 19 AbstractControl,
20 20 ControlValueAccessor,
21 21 FormArray,
22   - FormBuilder,
  22 + FormBuilder, FormControl,
23 23 FormGroup,
24 24 NG_VALUE_ACCESSOR,
25 25 Validators
26 26 } from '@angular/forms';
27 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 34 import { MatDialog } from '@angular/material/dialog';
30 35 import { deepClone } from '@core/utils';
31 36 import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
... ... @@ -48,12 +53,16 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
48 53
49 54 @Input() displayUserParameters = true;
50 55
  56 + @Input() allowUserDynamicSource = true;
  57 +
51 58 @Input() telemetryKeysOnly = false;
52 59
53 60 keyFilterListFormGroup: FormGroup;
54 61
55 62 entityKeyTypeTranslations = entityKeyTypeTranslationMap;
56 63
  64 + keyFiltersControl: FormControl;
  65 +
57 66 private propagateChange = null;
58 67
59 68 private valueChangeSubscription: Subscription = null;
... ... @@ -66,6 +75,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
66 75 this.keyFilterListFormGroup = this.fb.group({});
67 76 this.keyFilterListFormGroup.addControl('keyFilters',
68 77 this.fb.array([]));
  78 + this.keyFiltersControl = this.fb.control(null);
69 79 }
70 80
71 81 keyFiltersFormArray(): FormArray {
... ... @@ -83,8 +93,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
83 93 this.disabled = isDisabled;
84 94 if (this.disabled) {
85 95 this.keyFilterListFormGroup.disable({emitEvent: false});
  96 + this.keyFiltersControl.disable({emitEvent: false});
86 97 } else {
87 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 119 } else {
108 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 126 public removeKeyFilter(index: number) {
... ... @@ -155,6 +169,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
155 169 isAdd,
156 170 readonly: this.disabled,
157 171 displayUserParameters: this.displayUserParameters,
  172 + allowUserDynamicSource: this.allowUserDynamicSource,
158 173 telemetryKeysOnly: this.telemetryKeysOnly
159 174 }
160 175 }).afterClosed();
... ... @@ -167,5 +182,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
167 182 } else {
168 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 24 </mat-option>
25 25 </mat-select>
26 26 </mat-form-field>
27   - <tb-filter-predicate-value fxFlex="60"
  27 + <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
  28 + fxFlex="60"
28 29 [valueType]="valueType"
29 30 formControlName="value">
30 31 </tb-filter-predicate-value>
... ...
... ... @@ -40,6 +40,8 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
40 40
41 41 @Input() disabled: boolean;
42 42
  43 + @Input() allowUserDynamicSource = true;
  44 +
43 45 @Input() valueType: EntityKeyValueType;
44 46
45 47 numericFilterPredicateFormGroup: FormGroup;
... ...
... ... @@ -28,7 +28,8 @@
28 28 <mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;">
29 29 </mat-checkbox>
30 30 </div>
31   - <tb-filter-predicate-value fxFlex="60"
  31 + <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
  32 + fxFlex="60"
32 33 [valueType]="valueTypeEnum.STRING"
33 34 formControlName="value">
34 35 </tb-filter-predicate-value>
... ...
... ... @@ -40,6 +40,8 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
40 40
41 41 @Input() disabled: boolean;
42 42
  43 + @Input() allowUserDynamicSource = true;
  44 +
43 45 valueTypeEnum = EntityKeyValueType;
44 46
45 47 stringFilterPredicateFormGroup: FormGroup;
... ...
... ... @@ -104,6 +104,8 @@ import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.co
104 104 import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
105 105 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
106 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 110 @NgModule({
109 111 declarations:
... ... @@ -165,6 +167,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
165 167 FilterDialogComponent,
166 168 FiltersDialogComponent,
167 169 FilterSelectComponent,
  170 + FilterTextComponent,
168 171 FiltersEditComponent,
169 172 FiltersEditPanelComponent,
170 173 UserFilterDialogComponent,
... ... @@ -190,7 +193,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
190 193 DeviceProfileAlarmsComponent,
191 194 DeviceProfileDataComponent,
192 195 DeviceProfileComponent,
193   - DeviceProfileDialogComponent
  196 + DeviceProfileDialogComponent,
  197 + AddDeviceProfileDialogComponent
194 198 ],
195 199 imports: [
196 200 CommonModule,
... ... @@ -245,6 +249,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
245 249 FilterDialogComponent,
246 250 FiltersDialogComponent,
247 251 FilterSelectComponent,
  252 + FilterTextComponent,
248 253 FiltersEditComponent,
249 254 UserFilterDialogComponent,
250 255 TenantProfileAutocompleteComponent,
... ... @@ -266,7 +271,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
266 271 DeviceProfileAlarmsComponent,
267 272 DeviceProfileDataComponent,
268 273 DeviceProfileComponent,
269   - DeviceProfileDialogComponent
  274 + DeviceProfileDialogComponent,
  275 + AddDeviceProfileDialogComponent
270 276 ],
271 277 providers: [
272 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 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 14 * limitations under the License.
15 15 */
16 16 :host {
17   - a.mat-icon-button {
  17 + display: flex;
  18 + a.mat-button {
18 19 &:hover, &:focus {
19 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 30 AlarmRuleKeyFiltersDialogComponent,
31 31 AlarmRuleKeyFiltersDialogData
32 32 } from './alarm-rule-key-filters-dialog.component';
  33 +import { TranslateService } from '@ngx-translate/core';
  34 +import { DatePipe } from '@angular/common';
33 35
34 36 @Component({
35 37 selector: 'tb-alarm-rule-condition',
... ... @@ -60,7 +62,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
60 62 private propagateChange = (v: any) => { };
61 63
62 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 70 registerOnChange(fn: any): void {
... ... @@ -76,6 +80,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
76 80
77 81 setDisabledState(isDisabled: boolean): void {
78 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 90 writeValue(value: Array<KeyFilter>): void {
... ... @@ -83,8 +92,12 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
83 92 this.updateConditionInfo();
84 93 }
85 94
  95 + public conditionSet() {
  96 + return this.modelValue && this.modelValue.length;
  97 + }
  98 +
86 99 public validate(c: FormControl) {
87   - return (this.modelValue && this.modelValue.length) ? null : {
  100 + return this.conditionSet() ? null : {
88 101 alarmRuleCondition: {
89 102 valid: false,
90 103 },
... ... @@ -112,11 +125,7 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
112 125 }
113 126
114 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 131 private updateModel() {
... ...
... ... @@ -32,6 +32,7 @@
32 32 <div fxFlex fxLayout="column">
33 33 <tb-key-filter-list
34 34 [displayUserParameters]="false"
  35 + [allowUserDynamicSource]="false"
35 36 [telemetryKeysOnly]="true"
36 37 formControlName="keyFilters">
37 38 </tb-key-filter-list>
... ...
... ... @@ -16,56 +16,56 @@
16 16
17 17 -->
18 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 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 27 <mat-slide-toggle [disabled]="disabled"
  28 + color="primary"
32 29 [ngModelOptions]="{standalone: true}"
33 30 (ngModelChange)="enableDurationChanged($event)"
34 31 [ngModel]="enableDuration">
35 32 </mat-slide-toggle>
36 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 69 </div>
70 70 </div>
71 71 </div>
... ... @@ -73,7 +73,7 @@
73 73 <mat-expansion-panel-header>
74 74 <mat-panel-title>
75 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 77 </div>
78 78 </mat-panel-title>
79 79 </mat-expansion-panel-header>
... ...
... ... @@ -14,6 +14,11 @@
14 14 * limitations under the License.
15 15 */
16 16 :host {
  17 + .tb-condition-duration {
  18 + padding: 8px;
  19 + border: 1px groove rgba(0, 0, 0, .25);
  20 + border-radius: 4px;
  21 + }
17 22 .mat-expansion-panel.advanced-settings {
18 23 box-shadow: none;
19 24 border: none;
... ...
... ... @@ -25,7 +25,8 @@
25 25 <mat-select formControlName="severity"
26 26 required
27 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 30 {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
30 31 </mat-option>
31 32 </mat-select>
... ... @@ -36,7 +37,7 @@
36 37 <tb-alarm-rule formControlName="alarmRule" required fxFlex>
37 38 </tb-alarm-rule>
38 39 </div>
39   - <button *ngIf="!disabled && createAlarmRulesFormArray().controls.length > 1"
  40 + <button *ngIf="!disabled"
40 41 mat-icon-button color="primary" style="min-width: 40px;"
41 42 type="button"
42 43 (click)="removeCreateAlarmRule($index)"
... ... @@ -45,6 +46,10 @@
45 46 <mat-icon>remove_circle_outline</mat-icon>
46 47 </button>
47 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 53 <div fxLayout="row" *ngIf="!disabled">
49 54 <button mat-stroked-button color="primary"
50 55 type="button"
... ...
... ... @@ -61,6 +61,8 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
61 61
62 62 createAlarmRulesFormGroup: FormGroup;
63 63
  64 + private usedSeverities: AlarmSeverity[] = [];
  65 +
64 66 private valueChangeSubscription: Subscription = null;
65 67
66 68 private propagateChange = (v: any) => { };
... ... @@ -121,6 +123,10 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
121 123 this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => {
122 124 this.updateModel();
123 125 });
  126 + this.updateUsedSeverities();
  127 + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
  128 + this.updateModel();
  129 + }
124 130 }
125 131
126 132 public removeCreateAlarmRule(index: number) {
... ... @@ -142,19 +148,33 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
142 148 }
143 149
144 150 public validate(c: FormControl) {
145   - return (this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormGroup.get('createAlarmRules').value.length) ? null : {
  151 + return (this.createAlarmRulesFormGroup.valid) ? null : {
146 152 createAlarmRules: {
147 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 171 private updateModel() {
153 172 const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
154 173 const createAlarmRules: {[severity: string]: AlarmRule} = {};
155 174 value.forEach(v => {
156 175 createAlarmRules[v.severity] = v.alarmRule;
157 176 });
  177 + this.updateUsedSeverities();
158 178 this.propagateChange(createAlarmRules);
159 179 }
160 180 }
... ...
... ... @@ -26,6 +26,7 @@
26 26 <mat-form-field floatLabel="always"
27 27 style="width: 600px;"
28 28 [fxShow]="expanded"
  29 + (keydown)="!disabled ? $event.stopPropagation() : null;"
29 30 (click)="!disabled ? $event.stopPropagation() : null;">
30 31 <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
31 32 <input required matInput formControlName="alarmType" placeholder="Enter alarm type">
... ... @@ -66,6 +67,10 @@
66 67 <mat-icon>remove_circle_outline</mat-icon>
67 68 </button>
68 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 74 <div fxLayout="row" *ngIf="!disabled"
70 75 [fxShow]="!alarmFormGroup.get('clearRule').value">
71 76 <button mat-stroked-button color="primary"
... ... @@ -86,9 +91,28 @@
86 91 </div>
87 92 </mat-panel-title>
88 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 95 {{ 'device-profile.propagate-alarm' | translate }}
91 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 117 </mat-expansion-panel>
94 118 </mat-expansion-panel>
... ...
... ... @@ -27,6 +27,8 @@ import {
27 27 } from '@angular/forms';
28 28 import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models';
29 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 33 @Component({
32 34 selector: 'tb-device-profile-alarm',
... ... @@ -53,13 +55,16 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
53 55 @Output()
54 56 removeAlarm = new EventEmitter();
55 57
  58 + separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
  59 +
56 60 expanded = false;
57 61
58 62 private modelValue: DeviceProfileAlarm;
59 63
60 64 alarmFormGroup: FormGroup;
61 65
62   - private propagateChange = (v: any) => { };
  66 + private propagateChange = null;
  67 + private propagateChangePending = false;
63 68
64 69 constructor(private dialog: MatDialog,
65 70 private fb: FormBuilder) {
... ... @@ -67,6 +72,12 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
67 72
68 73 registerOnChange(fn: any): void {
69 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 83 registerOnTouched(fn: any): void {
... ... @@ -96,11 +107,15 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
96 107 }
97 108
98 109 writeValue(value: DeviceProfileAlarm): void {
  110 + this.propagateChangePending = false;
99 111 this.modelValue = value;
100 112 if (!this.modelValue.alarmType) {
101 113 this.expanded = true;
102 114 }
103 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 121 public addClearAlarmRule() {
... ... @@ -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 171 private updateModel() {
128 172 const value = this.alarmFormGroup.value;
129 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 25 </tb-device-profile-alarm>
26 26 </div>
27 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 32 <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center"
29 33 style="padding-top: 16px;">
30 34 <button mat-raised-button color="primary"
... ... @@ -32,8 +36,7 @@
32 36 (click)="addAlarm()"
33 37 matTooltip="{{ 'device-profile.add-alarm-rule' | translate }}"
34 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 40 </button>
38 41 </div>
39 42 </div>
... ...
... ... @@ -162,11 +162,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
162 162 }
163 163
164 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 49 import { DeviceProfileService } from '@core/http/device-profile.service';
50 50 import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component';
51 51 import { MatAutocomplete } from '@angular/material/autocomplete';
  52 +import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component';
52 53
53 54 @Component({
54 55 selector: 'tb-device-profile-autocomplete',
... ... @@ -279,15 +280,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
279 280 createDeviceProfile($event: Event, profileName: string) {
280 281 $event.preventDefault();
281 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 285 this.openDeviceProfileDialog(deviceProfile, true);
292 286 }
293 287
... ... @@ -301,15 +295,28 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
301 295 }
302 296
303 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 320 (savedDeviceProfile) => {
314 321 if (!savedDeviceProfile) {
315 322 setTimeout(() => {
... ...
... ... @@ -39,7 +39,7 @@
39 39 required>
40 40 </tb-device-profile-transport-configuration>
41 41 </mat-expansion-panel>
42   - <mat-expansion-panel [expanded]="false">
  42 + <mat-expansion-panel [expanded]="true">
43 43 <mat-expansion-panel-header>
44 44 <mat-panel-title>
45 45 <div>{{'device-profile.alarm-rules' | translate:
... ...
... ... @@ -81,7 +81,7 @@
81 81 required>
82 82 </tb-device-profile-data>
83 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 85 <textarea matInput formControlName="description" rows="2"></textarea>
86 86 </mat-form-field>
87 87 </fieldset>
... ...
... ... @@ -35,6 +35,12 @@ import {
35 35 import { DeviceProfileService } from '@core/http/device-profile.service';
36 36 import { DeviceProfileComponent } from '../../components/profile/device-profile.component';
37 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 45 @Injectable()
40 46 export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> {
... ... @@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
44 50 constructor(private deviceProfileService: DeviceProfileService,
45 51 private translate: TranslateService,
46 52 private datePipe: DatePipe,
47   - private dialogService: DialogService) {
  53 + private dialogService: DialogService,
  54 + private dialog: MatDialog) {
48 55
49 56 this.config.entityType = EntityType.DEVICE_PROFILE;
50 57 this.config.entityComponent = DeviceProfileComponent;
... ... @@ -92,6 +99,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
92 99 this.config.onEntityAction = action => this.onDeviceProfileAction(action);
93 100 this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
94 101 this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
  102 + this.config.addEntity = () => this.addDeviceProfile();
95 103 }
96 104
97 105 resolve(): EntityTableConfig<DeviceProfile> {
... ... @@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
100 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 122 setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) {
104 123 if ($event) {
105 124 $event.stopPropagation();
... ...
... ... @@ -37,6 +37,7 @@ import { getCurrentAuthUser } from '@app/core/auth/auth.selectors';
37 37 import { Authority } from '@shared/models/authority.enum';
38 38 import { DialogService } from '@core/services/dialog.service';
39 39 import { ImportExportService } from '@home/components/import-export/import-export.service';
  40 +import { Direction } from "@shared/models/page/sort-order";
40 41
41 42 @Injectable()
42 43 export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> {
... ... @@ -55,6 +56,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
55 56 this.config.entityComponent = WidgetsBundleComponent;
56 57 this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGETS_BUNDLE);
57 58 this.config.entityResources = entityTypeResources.get(EntityType.WIDGETS_BUNDLE);
  59 + this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
58 60
59 61 this.config.entityTitle = (widgetsBundle) => widgetsBundle ?
60 62 widgetsBundle.title : '';
... ...
... ... @@ -25,6 +25,8 @@ import { PageData } from '@shared/models/page/page-data';
25 25 import { isDefined, isEqual } from '@core/utils';
26 26 import { TranslateService } from '@ngx-translate/core';
27 27 import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models';
  28 +import { Filter } from '@material-ui/icons';
  29 +import { DatePipe } from '@angular/common';
28 30
29 31 export enum EntityKeyType {
30 32 ATTRIBUTE = 'ATTRIBUTE',
... ... @@ -358,7 +360,102 @@ export interface FiltersInfo {
358 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 455 export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>): Array<KeyFilter> {
  456 + if (!keyFilterInfos) {
  457 + return [];
  458 + }
362 459 const keyFilters: Array<KeyFilter> = [];
363 460 for (const keyFilterInfo of keyFilterInfos) {
364 461 const key = keyFilterInfo.key;
... ...
... ... @@ -811,17 +811,19 @@
811 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 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 813 "alarm-rules": "Alarm rules ({{count}})",
  814 + "no-alarm-rules": "No alarm rules configured",
814 815 "add-alarm-rule": "Add alarm rule",
815 816 "edit-alarm-rule": "Edit alarm rule",
816   - "alarm-rule-details": "Alarm rule details",
817 817 "alarm-type": "Alarm type",
818 818 "alarm-type-required": "Alarm type is required.",
819 819 "alarm-type-pattern-hint": "Alarm type pattern, use <code>${metaKeyName}</code> to substitute variables from metadata",
820 820 "create-alarm-pattern": "Create <b>{{alarmType}}</b> alarm",
821 821 "create-alarm-rules": "Create alarm rules",
  822 + "no-create-alarm-rules": "No create conditions configured",
822 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 827 "select-alarm-severity": "Select alarm severity",
826 828 "alarm-severity-required": "Alarm severity is required.",
827 829 "condition-duration": "Condition duration",
... ... @@ -831,7 +833,10 @@
831 833 "condition-duration-value-required": "Duration value is required.",
832 834 "condition-duration-time-unit-required": "Time unit is required.",
833 835 "advanced-settings": "Advanced settings",
  836 + "alarm-rule-details": "Details",
834 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 840 "alarm-details": "Alarm details",
836 841 "alarm-rule-condition": "Alarm rule condition",
837 842 "enter-alarm-rule-condition-prompt": "Please add alarm rule condition",
... ... @@ -1277,6 +1282,8 @@
1277 1282 "filter": "Filter",
1278 1283 "editable": "Editable",
1279 1284 "no-filters-found": "No filters found.",
  1285 + "no-filter-text": "No filter specified",
  1286 + "add-filter-prompt": "Please add filter",
1280 1287 "no-filter-matching": "'{{filter}}' not found.",
1281 1288 "create-new-filter": "Create a new one!",
1282 1289 "filter-required": "Filter is required.",
... ... @@ -1295,9 +1302,10 @@
1295 1302 "and": "and",
1296 1303 "or": "or"
1297 1304 },
1298   - "ignore-case": "Ignore case",
  1305 + "ignore-case": "ignore case",
1299 1306 "value": "Value",
1300 1307 "remove-filter": "Remove filter",
  1308 + "preview": "Filter preview",
1301 1309 "no-filters": "No filters configured",
1302 1310 "add-filter": "Add filter",
1303 1311 "add-complex-filter": "Add complex filter",
... ...