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