diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 26292c60b3..22c9d20a23 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -166,7 +166,7 @@ "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", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", - "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}}" + "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}}" } } ] diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java index ddfaf69d3a..18433c6f53 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java @@ -247,4 +247,4 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index 260db325e5..756356581a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java @@ -209,4 +209,4 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java index e81788958d..41f90c642c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java @@ -39,8 +39,7 @@ public abstract class AbstractCleanUpService { protected String dbPassword; protected long executeQuery(Connection conn, String query) throws SQLException { - try (Statement statement = conn.createStatement()) { - ResultSet resultSet = statement.executeQuery(query); + try (Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(query)) { if (log.isDebugEnabled()) { getWarnings(statement); } diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java index 8a1b17c549..40febd1988 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java @@ -33,4 +33,4 @@ public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUp long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);"); log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved); } -} \ No newline at end of file +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index c4aa3fb846..5a44a88879 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -284,6 +284,8 @@ sql: batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}" + # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks + batch_sort: "${SQL_BATCH_SORT:false}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}" # Specify whether to log database queries and their parameters generated by entity query repository @@ -651,11 +653,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index b92b094af1..bee293bd6d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -37,6 +37,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { private final AdminClient client; private final Map topicConfigs; private final Set topics = ConcurrentHashMap.newKeySet(); + private final int numPartitions; private final short replicationFactor; @@ -50,6 +51,13 @@ public class TbKafkaAdmin implements TbQueueAdmin { log.error("Failed to get all topics.", e); } + String numPartitionsStr = topicConfigs.get("partitions"); + if (numPartitionsStr != null) { + numPartitions = Integer.parseInt(numPartitionsStr); + topicConfigs.remove("partitions"); + } else { + numPartitions = 1; + } replicationFactor = settings.getReplicationFactor(); } @@ -59,7 +67,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { return; } try { - NewTopic newTopic = new NewTopic(topic, 1, replicationFactor).configs(topicConfigs); + NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(topicConfigs); createTopic(newTopic).values().get(topic).get(); topics.add(topic); } catch (ExecutionException ee) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java index 22fda66759..6a3a66a320 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java @@ -22,6 +22,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.stats.MessagesStats; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; @@ -30,6 +31,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j public class TbSqlBlockingQueue implements TbSqlQueue { @@ -46,7 +48,7 @@ public class TbSqlBlockingQueue implements TbSqlQueue { } @Override - public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, int index) { + public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int index) { executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("sql-queue-" + index + "-" + params.getLogName().toLowerCase())); executor.submit(() -> { String logName = params.getLogName(); @@ -65,7 +67,11 @@ public class TbSqlBlockingQueue implements TbSqlQueue { queue.drainTo(entities, batchSize - 1); boolean fullPack = entities.size() == batchSize; log.debug("[{}] Going to save {} entities", logName, entities.size()); - saveFunction.accept(entities.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList())); + Stream entitiesStream = entities.stream().map(TbSqlQueueElement::getEntity); + saveFunction.accept( + (params.isBatchSortEnabled() ? entitiesStream.sorted(batchUpdateComparator) : entitiesStream) + .collect(Collectors.toList()) + ); entities.forEach(v -> v.getFuture().set(null)); stats.incrementSuccessful(entities.size()); if (!fullPack) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java index a63461787e..6100405711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java @@ -31,4 +31,5 @@ public class TbSqlBlockingQueueParams { private final long maxDelay; private final long statsPrintIntervalMs; private final String statsNamePrefix; + private final boolean batchSortEnabled; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java index d9596a6efd..884d4c7a3d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; +import java.util.Comparator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -36,12 +37,20 @@ public class TbSqlBlockingQueueWrapper { private final int maxThreads; private final StatsFactory statsFactory; - public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction) { + /** + * Starts TbSqlBlockingQueues. + * + * @param logExecutor executor that will be printing logs and statistics + * @param saveFunction function to save entities in database + * @param batchUpdateComparator comparator to sort entities by primary key to avoid deadlocks in cluster mode + * NOTE: you must use all of primary key parts in your comparator + */ + public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator) { for (int i = 0; i < maxThreads; i++) { MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i); TbSqlBlockingQueue queue = new TbSqlBlockingQueue<>(params, stats); queues.add(queue); - queue.init(logExecutor, saveFunction, i); + queue.init(logExecutor, saveFunction, batchUpdateComparator, i); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java index c3955a811c..135e636ea5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java @@ -17,12 +17,13 @@ package org.thingsboard.server.dao.sql; import com.google.common.util.concurrent.ListenableFuture; +import java.util.Comparator; import java.util.List; import java.util.function.Consumer; public interface TbSqlQueue { - void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, int queueIndex); + void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction, Comparator batchUpdateComparator, int queueIndex); void destroy(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 2803dec8dd..69616155d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -71,6 +72,9 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Value("${sql.attributes.batch_threads:4}") private int batchThreads; + @Value("${sql.batch_sort:false}") + private boolean batchSortEnabled; + private TbSqlBlockingQueueWrapper queue; @PostConstruct @@ -81,11 +85,17 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl .maxDelay(maxDelay) .statsPrintIntervalMs(statsPrintIntervalMs) .statsNamePrefix("attributes") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getId().getEntityId().hashCode(); queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, batchThreads, statsFactory); - queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v)); + queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v), + Comparator.comparing((AttributeKvEntity attributeKvEntity) -> attributeKvEntity.getId().getEntityId()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getEntityType().name()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeType()) + .thenComparing(attributeKvEntity -> attributeKvEntity.getId().getAttributeKey()) + ); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 079bf1b660..3cee730ee3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.thingsboard.server.common.data.id.EntityId; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; @@ -40,9 +42,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -68,11 +68,16 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq .maxDelay(tsMaxDelay) .statsPrintIntervalMs(tsStatsPrintIntervalMs) .statsNamePrefix("ts") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, tsBatchThreads, statsFactory); - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v), + Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparing(AbstractTsKvEntity::getKey) + .thenComparing(AbstractTsKvEntity::getTs) + ); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java index 9ab8aa3dfc..8dcefeca61 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -53,6 +53,9 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries @Value("${sql.timescale.batch_threads:4}") protected int timescaleBatchThreads; + @Value("${sql.batch_sort:false}") + protected boolean batchSortEnabled; + protected ListenableFuture> processFindAllAsync(TenantId tenantId, EntityId entityId, List queries) { List>> futures = queries .stream() diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index ee7c02d558..f6e5e8aa40 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; @@ -50,12 +51,10 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.stream.Collectors; @Slf4j @Component @@ -90,6 +89,9 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme @Value("${sql.ts_latest.batch_threads:4}") private int tsLatestBatchThreads; + @Value("${sql.batch_sort:false}") + protected boolean batchSortEnabled; + @Autowired protected ScheduledLogExecutorComponent logExecutor; @@ -104,6 +106,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme .maxDelay(tsLatestMaxDelay) .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs) .statsNamePrefix("ts.latest") + .batchSortEnabled(false) .build(); java.util.function.Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); @@ -113,14 +116,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme Map trueLatest = new HashMap<>(); v.forEach(ts -> { TsKey key = new TsKey(ts.getEntityId(), ts.getKey()); - TsKvLatestEntity old = trueLatest.get(key); - if (old == null || old.getTs() < ts.getTs()) { - trueLatest.put(key, ts); - } + trueLatest.merge(key, ts, (oldTs, newTs) -> oldTs.getTs() < newTs.getTs() ? newTs : oldTs); }); List latestEntities = new ArrayList<>(trueLatest.values()); + if (batchSortEnabled) { + latestEntities.sort(Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparingInt(AbstractTsKvEntity::getKey)); + } insertLatestTsRepository.saveOrUpdate(latestEntities); - }); + }, (l, r) -> 0); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java index 71647f88f9..6d5923c508 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; @@ -43,11 +44,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -78,12 +75,17 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements .maxDelay(tsMaxDelay) .statsPrintIntervalMs(tsStatsPrintIntervalMs) .statsNamePrefix("ts.timescale") + .batchSortEnabled(batchSortEnabled) .build(); Function hashcodeFunction = entity -> entity.getEntityId().hashCode(); tsQueue = new TbSqlBlockingQueueWrapper<>(tsParams, hashcodeFunction, timescaleBatchThreads, statsFactory); - tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v)); + tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v), + Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) + .thenComparing(AbstractTsKvEntity::getKey) + .thenComparing(AbstractTsKvEntity::getTs) + ); } @PreDestroy diff --git a/docker/queue-kafka.env b/docker/queue-kafka.env index 63107942fb..0207d64ef5 100644 --- a/docker/queue-kafka.env +++ b/docker/queue-kafka.env @@ -1,2 +1,3 @@ TB_QUEUE_TYPE=kafka TB_KAFKA_SERVERS=kafka:9092 +TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100 diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index fac1ac7e8a..e6ec6f5b36 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -25,7 +25,7 @@ kafka: # Kafka Bootstrap Servers servers: "localhost:9092" replication_factor: "1" - topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600" + topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100" use_confluent_cloud: false confluent: sasl: diff --git a/msa/js-executor/queue/kafkaTemplate.js b/msa/js-executor/queue/kafkaTemplate.js index 0420173188..2672a8df6a 100644 --- a/msa/js-executor/queue/kafkaTemplate.js +++ b/msa/js-executor/queue/kafkaTemplate.js @@ -34,7 +34,7 @@ function KafkaProducer() { this.send = async (responseTopic, scriptId, rawResponse, headers) => { if (!topics.includes(responseTopic)) { - let createResponseTopicResult = await createTopic(responseTopic); + let createResponseTopicResult = await createTopic(responseTopic, 1); topics.push(responseTopic); if (createResponseTopicResult) { logger.info('Created new topic: %s', requestTopic); @@ -88,7 +88,18 @@ function KafkaProducer() { kafkaAdmin = kafkaClient.admin(); await kafkaAdmin.connect(); - let createRequestTopicResult = await createTopic(requestTopic); + let partitions = 1; + + for (let i = 0; i < configEntries.length; i++) { + let param = configEntries[i]; + if (param.name === 'partitions') { + partitions = param.value; + configEntries.splice(i, 1); + break; + } + } + + let createRequestTopicResult = await createTopic(requestTopic, partitions); if (createRequestTopicResult) { logger.info('Created new topic: %s', requestTopic); @@ -121,10 +132,11 @@ function KafkaProducer() { } })(); -function createTopic(topic) { +function createTopic(topic, partitions) { return kafkaAdmin.createTopics({ topics: [{ topic: topic, + numPartitions: partitions, replicationFactor: replicationFactor, configEntries: configEntries }] diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index f04a7559b8..3f57b32fab 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -77,11 +77,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index a97a58cd85..77d5f30fa7 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -70,11 +70,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index b3804e3531..bc18112c8e 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -98,11 +98,11 @@ queue: security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" other: topic-properties: - rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}" - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}" + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" diff --git a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts index cd6dcb6367..6d3c7e1453 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts @@ -123,7 +123,7 @@ export abstract class EntityComponent, if (isString(obj[curr])) { acc[curr] = obj[curr].trim(); } else if (isObject(obj[curr])) { - acc[curr] = this.deepTrim(obj[curr]) + acc[curr] = this.deepTrim(obj[curr]); } else { acc[curr] = obj[curr]; } diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html index 5e4c597447..d05e4b3775 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html @@ -24,7 +24,8 @@ - diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index e7e1dd5c81..aa264697b6 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -39,6 +39,8 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On @Input() disabled: boolean; + @Input() allowUserDynamicSource = true; + valueTypeEnum = EntityKeyValueType; booleanFilterPredicateFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html index a2e01977ea..4502e9da67 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -38,6 +38,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts index 623ca4f968..8fe9e5a1b6 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -36,6 +36,7 @@ export interface ComplexFilterPredicateDialogData { isAdd: boolean; valueType: EntityKeyValueType; displayUserParameters: boolean; + allowUserDynamicSource: boolean; } @Component({ diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index 654eecdefd..87bad30d5a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -50,6 +50,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @Input() displayUserParameters = true; + @Input() allowUserDynamicSource = true; + private propagateChange = null; private complexFilterPredicate: ComplexFilterPredicateInfo; @@ -86,7 +88,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On valueType: this.valueType, isAdd: false, key: this.key, - displayUserParameters: this.displayUserParameters + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource } }).afterClosed().subscribe( (result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html index 2da9a68e4d..ffef5e2349 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html @@ -52,6 +52,7 @@ fxFlex [valueType]="valueType" [displayUserParameters]="displayUserParameters" + [allowUserDynamicSource]="allowUserDynamicSource" [key]="key" [formControl]="predicateControl"> diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index c5eb501b16..2e59e85123 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -64,6 +64,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @Input() displayUserParameters = true; + @Input() allowUserDynamicSource = true; + filterListFormGroup: FormGroup; valueTypeEnum = EntityKeyValueType; @@ -156,7 +158,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni valueType: this.valueType, key: this.key, isAdd: true, - displayUserParameters: this.displayUserParameters + displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource } }).afterClosed().pipe( map((result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html index e9e38c71f5..524285ea3f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html @@ -55,7 +55,7 @@ {{'filter.no-dynamic-value' | translate}} - {{dynamicValueSourceTypeTranslations.get(dynamicValueSourceTypeEnum[sourceType]) | translate}} + {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts index 548137976e..aa430e596e 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts @@ -46,13 +46,23 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn @Input() disabled: boolean; + @Input() + set allowUserDynamicSource(allow: boolean) { + this.dynamicValueSourceTypes = [DynamicValueSourceType.CURRENT_TENANT, + DynamicValueSourceType.CURRENT_CUSTOMER]; + if (allow) { + this.dynamicValueSourceTypes.push(DynamicValueSourceType.CURRENT_USER); + } + } + @Input() valueType: EntityKeyValueType; valueTypeEnum = EntityKeyValueType; - dynamicValueSourceTypes = Object.keys(DynamicValueSourceType); - dynamicValueSourceTypeEnum = DynamicValueSourceType; + dynamicValueSourceTypes: DynamicValueSourceType[] = [DynamicValueSourceType.CURRENT_TENANT, + DynamicValueSourceType.CURRENT_CUSTOMER, DynamicValueSourceType.CURRENT_USER]; + dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap; filterPredicateValueFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html index abe231219b..3b9d89008b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html @@ -19,20 +19,27 @@
- + - + - + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss new file mode 100644 index 0000000000..84fabbf213 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-filter-text { + overflow-y: auto; + &.disabled { + opacity: 0.7; + } + &.required { + color: #f44336; + } + } +} + +:host ::ng-deep { + .tb-filter-text { + line-height: 1.8em; + span { + display: inline-block; + vertical-align: middle; + line-height: 1.4em; + } + .tb-filter-predicate { + padding-right: 4px; + padding-left: 4px; + } + .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source { + font-weight: bold; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + padding-left: 4px; + padding-right: 4px; + } + .tb-filter-entity-key, .tb-filter-value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; + } + .tb-filter-dynamic-source { + } + .tb-filter-entity-key { + color: #305680; + } + .tb-filter-value { + color: #ff5722; + } + .tb-filter-simple-operation { + font-size: 0.9em; + } + .tb-filter-complex-operation { + text-transform: uppercase; + font-weight: bold; + } + .tb-filter-dynamic-value { + .tb-filter-dynamic-source, .tb-filter-value { + color: #0c959c; + } + } + .tb-filter-bracket { + .tb-left-bracket, .tb-right-bracket { + font-size: 1.2em; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts new file mode 100644 index 0000000000..e6eb5955b0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { KeyFilter, keyFiltersToText } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-filter-text', + templateUrl: './filter-text.component.html', + styleUrls: ['./filter-text.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FilterTextComponent), + multi: true + } + ] +}) +export class FilterTextComponent implements ControlValueAccessor, OnInit { + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + noFilterText = this.translate.instant('filter.no-filter-text'); + + @Input() + addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); + + requiredClass = false; + + private filterText: string; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private translate: TranslateService, + private datePipe: DatePipe) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: Array): void { + this.updateFilterText(value); + } + + private updateFilterText(value: Array) { + this.requiredClass = false; + if (value && value.length) { + this.filterText = keyFiltersToText(this.translate, this.datePipe, value); + } else { + if (this.required && !this.disabled) { + this.filterText = this.addFilterPrompt; + this.requiredClass = true; + } else { + this.filterText = this.noFilterText; + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index fe820e58c6..f601814920 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -70,6 +70,7 @@
- - - -
filter.key-filters
-
-
-
- -
- - -   -   -
-
- -
-
-
- filter.operation.and + + + + +
filter.key-filters
+
+
+
+ +
+ + +   +  
-
-
-
{{ keyFilterControl.value.key.key }}
-
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}
- - +
+ +
+
+
+ filter.operation.and +
+
+
+
{{ keyFilterControl.value.key.key }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}
+ + +
+
-
+ filter.no-key-filters +
+
+ +
+ + + + +
filter.preview
+
+
+
+
- filter.no-key-filters -
-
- -
- + +
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss index 3951b5d640..bc47f04915 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss @@ -26,4 +26,17 @@ color: #666; font-weight: 500; } + .tb-filter-preview { + padding: 8px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + } +} + +:host ::ng-deep { + .tb-filter-preview { + .tb-filter-text { + max-height: 200px; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts index f78effb739..2779a35be9 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts @@ -19,13 +19,18 @@ import { AbstractControl, ControlValueAccessor, FormArray, - FormBuilder, + FormBuilder, FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; -import { EntityKeyType, entityKeyTypeTranslationMap, KeyFilterInfo } from '@shared/models/query/query.models'; +import { + EntityKeyType, + entityKeyTypeTranslationMap, + KeyFilter, + KeyFilterInfo, keyFilterInfosToKeyFilters +} from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; import { deepClone } from '@core/utils'; import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; @@ -48,12 +53,16 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { @Input() displayUserParameters = true; + @Input() allowUserDynamicSource = true; + @Input() telemetryKeysOnly = false; keyFilterListFormGroup: FormGroup; entityKeyTypeTranslations = entityKeyTypeTranslationMap; + keyFiltersControl: FormControl; + private propagateChange = null; private valueChangeSubscription: Subscription = null; @@ -66,6 +75,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { this.keyFilterListFormGroup = this.fb.group({}); this.keyFilterListFormGroup.addControl('keyFilters', this.fb.array([])); + this.keyFiltersControl = this.fb.control(null); } keyFiltersFormArray(): FormArray { @@ -83,8 +93,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { this.disabled = isDisabled; if (this.disabled) { this.keyFilterListFormGroup.disable({emitEvent: false}); + this.keyFiltersControl.disable({emitEvent: false}); } else { this.keyFilterListFormGroup.enable({emitEvent: false}); + this.keyFiltersControl.enable({emitEvent: false}); } } @@ -107,6 +119,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { } else { this.keyFilterListFormGroup.enable({emitEvent: false}); } + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); } public removeKeyFilter(index: number) { @@ -155,6 +169,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { isAdd, readonly: this.disabled, displayUserParameters: this.displayUserParameters, + allowUserDynamicSource: this.allowUserDynamicSource, telemetryKeysOnly: this.telemetryKeysOnly } }).afterClosed(); @@ -167,5 +182,7 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { } else { this.propagateChange(null); } + const keyFiltersArray = keyFilterInfosToKeyFilters(keyFilters); + this.keyFiltersControl.patchValue(keyFiltersArray, {emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html index 7a4a9c552b..71cbbd0335 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html @@ -24,7 +24,8 @@ - diff --git a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts index e9e4f89c3b..32d5779c63 100644 --- a/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts @@ -40,6 +40,8 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On @Input() disabled: boolean; + @Input() allowUserDynamicSource = true; + @Input() valueType: EntityKeyValueType; numericFilterPredicateFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html index ef9f2c01e7..92d470c6b8 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html @@ -28,7 +28,8 @@
- diff --git a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts index f115748e48..dffd52b274 100644 --- a/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts @@ -40,6 +40,8 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI @Input() disabled: boolean; + @Input() allowUserDynamicSource = true; + valueTypeEnum = EntityKeyValueType; stringFilterPredicateFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 9421a2c9f8..0f8b4a88cf 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -104,6 +104,8 @@ import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.co import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; +import { FilterTextComponent } from './filter/filter-text.component'; +import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; @NgModule({ declarations: @@ -165,6 +167,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k FilterDialogComponent, FiltersDialogComponent, FilterSelectComponent, + FilterTextComponent, FiltersEditComponent, FiltersEditPanelComponent, UserFilterDialogComponent, @@ -190,7 +193,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k DeviceProfileAlarmsComponent, DeviceProfileDataComponent, DeviceProfileComponent, - DeviceProfileDialogComponent + DeviceProfileDialogComponent, + AddDeviceProfileDialogComponent ], imports: [ CommonModule, @@ -245,6 +249,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k FilterDialogComponent, FiltersDialogComponent, FilterSelectComponent, + FilterTextComponent, FiltersEditComponent, UserFilterDialogComponent, TenantProfileAutocompleteComponent, @@ -266,7 +271,8 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k DeviceProfileAlarmsComponent, DeviceProfileDataComponent, DeviceProfileComponent, - DeviceProfileDialogComponent + DeviceProfileDialogComponent, + AddDeviceProfileDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html new file mode 100644 index 0000000000..5e1c4adfd6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -0,0 +1,114 @@ + +
+ +

device-profile.add

+ + +
+ + +
+
+ + +
+ {{ 'device-profile.device-profile-details' | translate }} +
+ + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.type-required' | translate }} + + + + device-profile.description + + +
+
+
+ +
+ {{ 'device-profile.transport-configuration' | translate }} + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.transport-type-required' | translate }} + + + + +
+
+ +
+ {{'device-profile.alarm-rules' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+
+
+
+ + +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss new file mode 100644 index 0000000000..cafcce74b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-dialog-content { + display: flex; + flex-direction: column; + overflow: hidden; + + .mat-stepper-horizontal { + display: flex; + flex-direction: column; + overflow: hidden; + } + } +} + +:host ::ng-deep { + .mat-dialog-content { + .mat-stepper-horizontal { + .mat-horizontal-content-container { + overflow: auto; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts new file mode 100644 index 0000000000..476c69488f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileType, + deviceProfileTypeTranslationMap, + DeviceTransportType, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MatHorizontalStepper } from '@angular/material/stepper'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; + +export interface AddDeviceProfileDialogData { + deviceProfileName: string; +} + +@Component({ + selector: 'tb-add-device-profile-dialog', + templateUrl: './add-device-profile-dialog.component.html', + providers: [], + styleUrls: ['./add-device-profile-dialog.component.scss'] +}) +export class AddDeviceProfileDialogComponent extends + DialogComponent implements AfterViewInit { + + @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatHorizontalStepper; + + selectedIndex = 0; + + entityType = EntityType; + + deviceProfileTypes = Object.keys(DeviceProfileType); + + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + deviceProfileDetailsFormGroup: FormGroup; + + transportConfigFormGroup: FormGroup; + + alarmRulesFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.deviceProfileDetailsFormGroup = this.fb.group( + { + name: [data.deviceProfileName, [Validators.required]], + type: [DeviceProfileType.DEFAULT, [Validators.required]], + defaultRuleChainId: [null, []], + description: ['', []] + } + ); + this.transportConfigFormGroup = this.fb.group( + { + transportType: [DeviceTransportType.DEFAULT, [Validators.required]], + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), + [Validators.required]] + } + ); + this.transportConfigFormGroup.get('transportType').valueChanges.subscribe(() => { + this.deviceProfileTransportTypeChanged(); + }); + + this.alarmRulesFormGroup = this.fb.group( + { + alarms: [null] + } + ); + } + + private deviceProfileTransportTypeChanged() { + const deviceTransportType: DeviceTransportType = this.transportConfigFormGroup.get('transportType').value; + this.transportConfigFormGroup.patchValue( + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); + } + + ngAfterViewInit(): void { + } + + cancel(): void { + this.dialogRef.close(null); + } + + previousStep() { + this.addDeviceProfileStepper.previous(); + } + + nextStep() { + if (this.selectedIndex < 2) { + this.addDeviceProfileStepper.next(); + } else { + this.add(); + } + } + + selectedForm(): FormGroup { + switch (this.selectedIndex) { + case 0: + return this.deviceProfileDetailsFormGroup; + case 1: + return this.transportConfigFormGroup; + case 2: + return this.alarmRulesFormGroup; + } + } + + private add(): void { + const deviceProfile: DeviceProfile = { + name: this.deviceProfileDetailsFormGroup.get('name').value, + type: this.deviceProfileDetailsFormGroup.get('type').value, + transportType: this.transportConfigFormGroup.get('transportType').value, + description: this.deviceProfileDetailsFormGroup.get('description').value, + profileData: { + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, + alarms: this.alarmRulesFormGroup.get('alarms').value + } + }; + if (this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value) { + deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value); + } + this.deviceProfileService.saveDeviceProfile(deviceProfile).subscribe( + (savedDeviceProfile) => { + this.dialogRef.close(savedDeviceProfile); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html index 9a3dd2e6fa..2160b6b04a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -15,16 +15,22 @@ limitations under the License. --> - - - - - {{ disabled ? 'more_vert' : 'edit' }} - - + diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss index c6146a53e6..1d654bc0f4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss @@ -14,9 +14,24 @@ * limitations under the License. */ :host { - a.mat-icon-button { + display: flex; + a.mat-button { &:hover, &:focus { border-bottom: none; } } + .tb-alarm-rule-condition { + padding: 8px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + cursor: pointer; + } +} + +:host ::ng-deep { + .tb-alarm-rule-condition { + .tb-filter-text { + max-height: 200px; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts index 306aeee3aa..7416b944eb 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts @@ -30,6 +30,8 @@ import { AlarmRuleKeyFiltersDialogComponent, AlarmRuleKeyFiltersDialogData } from './alarm-rule-key-filters-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; @Component({ selector: 'tb-alarm-rule-condition', @@ -60,7 +62,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit private propagateChange = (v: any) => { }; constructor(private dialog: MatDialog, - private fb: FormBuilder) { + private fb: FormBuilder, + private translate: TranslateService, + private datePipe: DatePipe) { } registerOnChange(fn: any): void { @@ -76,6 +80,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleConditionControl.disable({emitEvent: false}); + } else { + this.alarmRuleConditionControl.enable({emitEvent: false}); + } } writeValue(value: Array): void { @@ -83,8 +92,12 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit this.updateConditionInfo(); } + public conditionSet() { + return this.modelValue && this.modelValue.length; + } + public validate(c: FormControl) { - return (this.modelValue && this.modelValue.length) ? null : { + return this.conditionSet() ? null : { alarmRuleCondition: { valid: false, }, @@ -112,11 +125,7 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit } private updateConditionInfo() { - if (this.modelValue && this.modelValue.length) { - this.alarmRuleConditionControl.patchValue('Condition set'); - } else { - this.alarmRuleConditionControl.patchValue(null); - } + this.alarmRuleConditionControl.patchValue(this.modelValue); } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html index 3712ddff39..26defe62ff 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html @@ -32,6 +32,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html index 3b7354c05a..ef69037727 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -16,56 +16,56 @@ -->
-
-
+
+ + +
- -
- - -
-
-
- +
device-profile.condition-duration
+
-
- - - - - {{ 'device-profile.condition-duration-value-required' | translate }} - - - {{ 'device-profile.condition-duration-value-range' | translate }} - - - {{ 'device-profile.condition-duration-value-range' | translate }} - - - - - - - {{ timeUnitTranslations.get(timeUnit) | translate }} - - - - {{ 'device-profile.condition-duration-time-unit-required' | translate }} - - +
+ +
+ + + + + {{ 'device-profile.condition-duration-value-required' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + + + + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + + + {{ 'device-profile.condition-duration-time-unit-required' | translate }} + + +
@@ -73,7 +73,7 @@
-
device-profile.advanced-settings
+
device-profile.alarm-rule-details
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss index 00885ed393..ed8d08806f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss @@ -14,6 +14,11 @@ * limitations under the License. */ :host { + .tb-condition-duration { + padding: 8px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + } .mat-expansion-panel.advanced-settings { box-shadow: none; border: none; diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html index 9a5835d556..121df96386 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -25,7 +25,8 @@ - + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} @@ -36,7 +37,7 @@
-
+
+ device-profile.no-create-alarm-rules +
+
+ device-profile.no-clear-alarm-rule +
+
+ device-profile.no-alarm-rules +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts index 91db1cfdf7..632623115e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts @@ -162,11 +162,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni } private updateModel() { -// if (this.deviceProfileAlarmsFormGroup.valid) { - const alarms: Array = this.deviceProfileAlarmsFormGroup.get('alarms').value; - this.propagateChange(alarms); - /* } else { - this.propagateChange(null); - } */ + const alarms: Array = this.deviceProfileAlarmsFormGroup.get('alarms').value; + this.propagateChange(alarms); } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts index 6f6e59af88..c419b7f11c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts @@ -49,6 +49,7 @@ import { import { DeviceProfileService } from '@core/http/device-profile.service'; import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; import { MatAutocomplete } from '@angular/material/autocomplete'; +import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component'; @Component({ selector: 'tb-device-profile-autocomplete', @@ -279,15 +280,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, createDeviceProfile($event: Event, profileName: string) { $event.preventDefault(); const deviceProfile: DeviceProfile = { - id: null, - name: profileName, - type: DeviceProfileType.DEFAULT, - transportType: DeviceTransportType.DEFAULT, - profileData: { - configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), - transportConfiguration: createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT) - } - }; + name: profileName + } as DeviceProfile; this.openDeviceProfileDialog(deviceProfile, true); } @@ -301,15 +295,28 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, } openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) { - this.dialog.open(DeviceProfileDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - isAdd, - deviceProfile - } - }).afterClosed().subscribe( + let deviceProfileObservable: Observable; + if (!isAdd) { + deviceProfileObservable = this.dialog.open(DeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: false, + deviceProfile + } + }).afterClosed(); + } else { + deviceProfileObservable = this.dialog.open(AddDeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceProfileName: deviceProfile.name + } + }).afterClosed(); + } + deviceProfileObservable.subscribe( (savedDeviceProfile) => { if (!savedDeviceProfile) { setTimeout(() => { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html index 4b159c5a62..daae838285 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html @@ -39,7 +39,7 @@ required> - +
{{'device-profile.alarm-rules' | translate: diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 5789e97705..5fa01f739d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -81,7 +81,7 @@ required> - tenant-profile.description + device-profile.description diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts index f1f3b01963..c4927fbe99 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts @@ -35,6 +35,12 @@ import { import { DeviceProfileService } from '@core/http/device-profile.service'; import { DeviceProfileComponent } from '../../components/profile/device-profile.component'; import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; +import { Observable } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { + AddDeviceProfileDialogComponent, + AddDeviceProfileDialogData +} from '../../components/profile/add-device-profile-dialog.component'; @Injectable() export class DeviceProfilesTableConfigResolver implements Resolve> { @@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve this.onDeviceProfileAction(action); this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; + this.config.addEntity = () => this.addDeviceProfile(); } resolve(): EntityTableConfig { @@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve { + return this.dialog.open(AddDeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceProfileName: null + } + }).afterClosed(); + } + setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts index 5edd4c0825..c59dd6cc63 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts @@ -37,6 +37,7 @@ import { getCurrentAuthUser } from '@app/core/auth/auth.selectors'; import { Authority } from '@shared/models/authority.enum'; import { DialogService } from '@core/services/dialog.service'; import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { Direction } from "@shared/models/page/sort-order"; @Injectable() export class WidgetsBundlesTableConfigResolver implements Resolve> { @@ -55,6 +56,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve widgetsBundle ? widgetsBundle.title : ''; diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index e6019fe023..3d3223bf34 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -25,6 +25,8 @@ import { PageData } from '@shared/models/page/page-data'; import { isDefined, isEqual } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models'; +import { Filter } from '@material-ui/icons'; +import { DatePipe } from '@angular/common'; export enum EntityKeyType { ATTRIBUTE = 'ATTRIBUTE', @@ -358,7 +360,102 @@ export interface FiltersInfo { datasourceFilters: {[datasourceIndex: number]: FilterInfo}; } +export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array): string { + const filtersText = keyFilters.map(keyFilter => + keyFilterToText(translate, datePipe, keyFilter, + keyFilters.length > 1 ? ComplexOperation.AND : undefined)); + let result: string; + if (filtersText.length > 1) { + const andText = translate.instant('filter.operation.and'); + result = filtersText.join(' ' + andText + ' '); + } else { + result = filtersText[0]; + } + return result; +} + +export function keyFilterToText(translate: TranslateService, datePipe: DatePipe, keyFilter: KeyFilter, + parentComplexOperation?: ComplexOperation): string { + const keyFilterPredicate = keyFilter.predicate; + return keyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate, parentComplexOperation); +} + +export function keyFilterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: KeyFilter, + keyFilterPredicate: KeyFilterPredicate, + parentComplexOperation?: ComplexOperation): string { + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate; + const complexOperation = complexPredicate.operation; + const complexPredicatesText = + complexPredicate.predicates.map(predicate => keyFilterPredicateToText(translate, datePipe, keyFilter, predicate, complexOperation)); + if (complexPredicatesText.length > 1) { + const operationText = translate.instant(complexOperationTranslationMap.get(complexOperation)); + let result = complexPredicatesText.join(' ' + operationText + ' '); + if (complexOperation === ComplexOperation.OR && parentComplexOperation && ComplexOperation.OR !== parentComplexOperation) { + result = `(${result})`; + } + return result; + } else { + return complexPredicatesText[0]; + } + } else { + return simpleKeyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate); + } +} + +function simpleKeyFilterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: KeyFilter, + keyFilterPredicate: StringFilterPredicate | + NumericFilterPredicate | + BooleanFilterPredicate): string { + const key = keyFilter.key.key; + let operation: string; + let value: string; + const val = keyFilterPredicate.value; + const dynamicValue = !!val.dynamicValue && !!val.dynamicValue.sourceType; + if (dynamicValue) { + value = '' + + translate.instant(dynamicValueSourceTypeTranslationMap.get(val.dynamicValue.sourceType)) + ''; + value += '.' + val.dynamicValue.sourceAttribute + ''; + } + switch (keyFilterPredicate.type) { + case FilterPredicateType.STRING: + operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); + if (keyFilterPredicate.ignoreCase) { + operation += ' ' + translate.instant('filter.ignore-case'); + } + if (!dynamicValue) { + value = `'${keyFilterPredicate.value.defaultValue}'`; + } + break; + case FilterPredicateType.NUMERIC: + operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { + value = datePipe.transform(keyFilterPredicate.value.defaultValue, 'yyyy-MM-dd HH:mm'); + } else { + value = keyFilterPredicate.value.defaultValue + ''; + } + } + break; + case FilterPredicateType.BOOLEAN: + operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); + value = translate.instant(keyFilterPredicate.value.defaultValue ? 'value.true' : 'value.false'); + break; + } + if (!dynamicValue) { + value = `${value}`; + } + return `${key} ${operation} ${value}`; +} + export function keyFilterInfosToKeyFilters(keyFilterInfos: Array): Array { + if (!keyFilterInfos) { + return []; + } const keyFilters: Array = []; for (const keyFilterInfo of keyFilterInfos) { const key = keyFilterInfo.key; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 75165699fe..29b5ceb303 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -811,17 +811,19 @@ "single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes.", "multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/#.", "alarm-rules": "Alarm rules ({{count}})", + "no-alarm-rules": "No alarm rules configured", "add-alarm-rule": "Add alarm rule", "edit-alarm-rule": "Edit alarm rule", - "alarm-rule-details": "Alarm rule details", "alarm-type": "Alarm type", "alarm-type-required": "Alarm type is required.", "alarm-type-pattern-hint": "Alarm type pattern, use ${metaKeyName} to substitute variables from metadata", "create-alarm-pattern": "Create {{alarmType}} alarm", "create-alarm-rules": "Create alarm rules", + "no-create-alarm-rules": "No create conditions configured", "clear-alarm-rule": "Clear alarm rule", - "add-create-alarm-rule": "Add create alarm rule", - "add-clear-alarm-rule": "Add clear alarm rule", + "no-clear-alarm-rule": "No clear condition configured", + "add-create-alarm-rule": "Add create condition", + "add-clear-alarm-rule": "Add clear condition", "select-alarm-severity": "Select alarm severity", "alarm-severity-required": "Alarm severity is required.", "condition-duration": "Condition duration", @@ -831,7 +833,10 @@ "condition-duration-value-required": "Duration value is required.", "condition-duration-time-unit-required": "Time unit is required.", "advanced-settings": "Advanced settings", + "alarm-rule-details": "Details", "propagate-alarm": "Propagate alarm", + "alarm-rule-relation-types-list": "Relation types to propagate", + "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", "alarm-details": "Alarm details", "alarm-rule-condition": "Alarm rule condition", "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", @@ -1277,6 +1282,8 @@ "filter": "Filter", "editable": "Editable", "no-filters-found": "No filters found.", + "no-filter-text": "No filter specified", + "add-filter-prompt": "Please add filter", "no-filter-matching": "'{{filter}}' not found.", "create-new-filter": "Create a new one!", "filter-required": "Filter is required.", @@ -1295,9 +1302,10 @@ "and": "and", "or": "or" }, - "ignore-case": "Ignore case", + "ignore-case": "ignore case", "value": "Value", "remove-filter": "Remove filter", + "preview": "Filter preview", "no-filters": "No filters configured", "add-filter": "Add filter", "add-complex-filter": "Add complex filter",