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