Browse Source

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

pull/3477/head
Andrii Shvaika 6 years ago
parent
commit
e353ab3c81
  1. 2
      application/src/main/data/json/system/widget_bundles/charts.json
  2. 2
      application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java
  6. 12
      application/src/main/resources/thingsboard.yml
  7. 10
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
  8. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java
  9. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java
  10. 13
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueWrapper.java
  11. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java
  12. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  13. 13
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  14. 3
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java
  15. 24
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  16. 14
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  17. 1
      docker/queue-kafka.env
  18. 2
      msa/js-executor/config/default.yml
  19. 18
      msa/js-executor/queue/kafkaTemplate.js
  20. 10
      transport/coap/src/main/resources/tb-coap-transport.yml
  21. 10
      transport/http/src/main/resources/tb-http-transport.yml
  22. 10
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  23. 2
      ui-ngx/src/app/modules/home/components/entity/entity.component.ts
  24. 3
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html
  25. 2
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  26. 1
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html
  27. 1
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts
  28. 5
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts
  29. 1
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html
  30. 5
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts
  31. 2
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html
  32. 14
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts
  33. 15
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html
  34. 2
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts
  35. 19
      ui-ngx/src/app/modules/home/components/filter/filter-text.component.html
  36. 79
      ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss
  37. 101
      ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts
  38. 1
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  39. 1
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  40. 126
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html
  41. 13
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.scss
  42. 21
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  43. 3
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html
  44. 2
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts
  45. 3
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html
  46. 2
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts
  47. 10
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  48. 114
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  49. 38
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss
  50. 173
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts
  51. 32
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html
  52. 17
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss
  53. 23
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
  54. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html
  55. 88
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  56. 5
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss
  57. 9
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
  58. 22
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts
  59. 28
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  60. 52
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts
  61. 7
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html
  62. 8
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts
  63. 43
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
  64. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html
  65. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  66. 21
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  67. 2
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts
  68. 97
      ui-ngx/src/app/shared/models/query/query.models.ts
  69. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
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}}"
}
}
]

2
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());
}
}
}
}

2
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());
}
}
}
}

3
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);
}

2
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);
}
}
}

12
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}"

10
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<String, String> topicConfigs;
private final Set<String> 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) {

10
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<E> implements TbSqlQueue<E> {
@ -46,7 +48,7 @@ public class TbSqlBlockingQueue<E> implements TbSqlQueue<E> {
}
@Override
public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, int index) {
public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> 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<E> implements TbSqlQueue<E> {
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<E> 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) {

1
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;
}

13
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<E> {
private final int maxThreads;
private final StatsFactory statsFactory;
public void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> 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<List<E>> saveFunction, Comparator<E> batchUpdateComparator) {
for (int i = 0; i < maxThreads; i++) {
MessagesStats stats = statsFactory.createMessagesStats(params.getStatsNamePrefix() + ".queue." + i);
TbSqlBlockingQueue<E> queue = new TbSqlBlockingQueue<>(params, stats);
queues.add(queue);
queue.init(logExecutor, saveFunction, i);
queue.init(logExecutor, saveFunction, batchUpdateComparator, i);
}
}

3
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<E> {
void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, int queueIndex);
void init(ScheduledLogExecutorComponent logExecutor, Consumer<List<E>> saveFunction, Comparator<E> batchUpdateComparator, int queueIndex);
void destroy();

12
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<AttributeKvEntity> queue;
@PostConstruct
@ -81,11 +85,17 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
.maxDelay(maxDelay)
.statsPrintIntervalMs(statsPrintIntervalMs)
.statsNamePrefix("attributes")
.batchSortEnabled(batchSortEnabled)
.build();
Function<AttributeKvEntity, Integer> 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

13
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<TsKvEntity, Integer> 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<TsKvEntity, UUID>) AbstractTsKvEntity::getEntityId)
.thenComparing(AbstractTsKvEntity::getKey)
.thenComparing(AbstractTsKvEntity::getTs)
);
}
@PreDestroy

3
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<List<TsKvEntry>> processFindAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries
.stream()

24
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<TsKvLatestEntity, Integer> hashcodeFunction = entity -> entity.getEntityId().hashCode();
@ -113,14 +116,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
Map<TsKey, TsKvLatestEntity> 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<TsKvLatestEntity> latestEntities = new ArrayList<>(trueLatest.values());
if (batchSortEnabled) {
latestEntities.sort(Comparator.comparing((Function<TsKvLatestEntity, UUID>) AbstractTsKvEntity::getEntityId)
.thenComparingInt(AbstractTsKvEntity::getKey));
}
insertLatestTsRepository.saveOrUpdate(latestEntities);
});
}, (l, r) -> 0);
}
@PreDestroy

14
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<TimescaleTsKvEntity, Integer> 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<TimescaleTsKvEntity, UUID>) AbstractTsKvEntity::getEntityId)
.thenComparing(AbstractTsKvEntity::getKey)
.thenComparing(AbstractTsKvEntity::getTs)
);
}
@PreDestroy

1
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

2
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:

18
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
}]

10
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}"

10
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}"

10
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}"

2
ui-ngx/src/app/modules/home/components/entity/entity.component.ts

@ -123,7 +123,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
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];
}

3
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html

@ -24,7 +24,8 @@
</mat-option>
</mat-select>
</mat-form-field>
<tb-filter-predicate-value fxFlex="60"
<tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
fxFlex="60"
[valueType]="valueTypeEnum.BOOLEAN"
formControlName="value">
</tb-filter-predicate-value>

2
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;

1
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html

@ -38,6 +38,7 @@
<tb-filter-predicate-list
[valueType]="data.valueType"
[displayUserParameters]="data.displayUserParameters"
[allowUserDynamicSource]="data.allowUserDynamicSource"
[operation]="complexFilterFormGroup.get('operation').value"
[key]="data.key"
formControlName="predicates">

1
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({

5
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) => {

1
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">
</tb-filter-predicate>

5
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) => {

2
ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html

@ -55,7 +55,7 @@
{{'filter.no-dynamic-value' | translate}}
</mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(dynamicValueSourceTypeEnum[sourceType]) | translate}}
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
</mat-select>
</mat-form-field>

14
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;

15
ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html

@ -19,20 +19,27 @@
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateFormGroup">
<div fxFlex fxLayout="column" [ngSwitch]="type">
<ng-template [ngSwitchCase]="filterPredicateType.STRING">
<tb-string-filter-predicate formControlName="predicate">
<tb-string-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
formControlName="predicate">
</tb-string-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC">
<tb-numeric-filter-predicate [valueType]="valueType"
formControlName="predicate">
<tb-numeric-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[valueType]="valueType"
formControlName="predicate">
</tb-numeric-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN">
<tb-boolean-filter-predicate formControlName="predicate">
<tb-boolean-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
formControlName="predicate">
</tb-boolean-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
<tb-complex-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[key]="key"
[valueType]="valueType"
[displayUserParameters]="displayUserParameters"

2
ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts

@ -43,6 +43,8 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() displayUserParameters = true;
@Input() allowUserDynamicSource = true;
filterPredicateFormGroup: FormGroup;
type: FilterPredicateType;

19
ui-ngx/src/app/modules/home/components/filter/filter-text.component.html

@ -0,0 +1,19 @@
<!--
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.
-->
<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass}"
[innerHTML]="filterText"></div>

79
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;
}
}
}
}

101
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<KeyFilter>): void {
this.updateFilterText(value);
}
private updateFilterText(value: Array<KeyFilter>) {
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;
}
}
}
}

1
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -70,6 +70,7 @@
</mat-form-field>
</section>
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
[allowUserDynamicSource]="data.allowUserDynamicSource"
[displayUserParameters]="data.displayUserParameters"
[valueType]="keyFilterFormGroup.get('valueType').value"
[key]="keyFilterFormGroup.get('key.key').value"

1
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts

@ -40,6 +40,7 @@ export interface KeyFilterDialogData {
keyFilter: KeyFilterInfo;
isAdd: boolean;
displayUserParameters: boolean;
allowUserDynamicSource: boolean;
readonly: boolean;
telemetryKeysOnly: boolean;
}

126
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html

@ -16,65 +16,77 @@
-->
<section fxLayout="column" [formGroup]="keyFilterListFormGroup">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>filter.key-filters</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row">
<span fxFlex="8"></span>
<div fxLayout="row" fxLayoutAlign="start center" fxFlex="92">
<label fxFlex translate class="tb-title no-padding">filter.key-name</label>
<label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label>
<span [fxShow]="!disabled" style="min-width: 80px;">&nbsp;</span>
<span [fxShow]="disabled" style="min-width: 40px;">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="key-filter-list">
<div fxLayout="row" fxLayoutAlign="start center" style="max-height: 40px;"
formArrayName="keyFilters"
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
<div fxFlex="8" class="filters-operation">
<span *ngIf="$index > 0" translate>filter.operation.and</span>
<mat-accordion [multi]="true">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>filter.key-filters</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row">
<span fxFlex="8"></span>
<div fxLayout="row" fxLayoutAlign="start center" fxFlex="92">
<label fxFlex translate class="tb-title no-padding">filter.key-name</label>
<label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label>
<span [fxShow]="!disabled" style="min-width: 80px;">&nbsp;</span>
<span [fxShow]="disabled" style="min-width: 40px;">&nbsp;</span>
</div>
<div fxLayout="column" fxFlex="92">
<div fxLayout="row" fxLayoutAlign="start center" fxFlex>
<div fxFlex>{{ keyFilterControl.value.key.key }}</div>
<div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>
<button mat-icon-button color="primary"
type="button"
(click)="editKeyFilter($index)"
matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}"
matTooltipPosition="above">
<mat-icon>{{disabled ? 'more_vert' : 'edit'}}</mat-icon>
</button>
<button mat-icon-button color="primary"
[fxShow]="!disabled"
type="button"
(click)="removeKeyFilter($index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<div class="key-filter-list">
<div fxLayout="row" fxLayoutAlign="start center" style="max-height: 40px;"
formArrayName="keyFilters"
*ngFor="let keyFilterControl of keyFiltersFormArray().controls; let $index = index">
<div fxFlex="8" class="filters-operation">
<span *ngIf="$index > 0" translate>filter.operation.and</span>
</div>
<div fxLayout="column" fxFlex="92">
<div fxLayout="row" fxLayoutAlign="start center" fxFlex>
<div fxFlex>{{ keyFilterControl.value.key.key }}</div>
<div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>
<button mat-icon-button color="primary"
type="button"
(click)="editKeyFilter($index)"
matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}"
matTooltipPosition="above">
<mat-icon>{{disabled ? 'more_vert' : 'edit'}}</mat-icon>
</button>
<button mat-icon-button color="primary"
[fxShow]="!disabled"
type="button"
(click)="removeKeyFilter($index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
</div>
<mat-divider></mat-divider>
</div>
<span [fxShow]="!keyFiltersFormArray().length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-key-filters</span>
</div>
<div style="margin-top: 16px;">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled"
(click)="addKeyFilter()"
type="button"
matTooltip="{{ 'filter.add-key-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-key-filter' | translate }}
</button>
</div>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>filter.preview</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-filter-preview">
<tb-filter-text [formControl]="keyFiltersControl"></tb-filter-text>
</div>
<span [fxShow]="!keyFiltersFormArray().length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>filter.no-key-filters</span>
</div>
<div style="margin-top: 16px;">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled"
(click)="addKeyFilter()"
type="button"
matTooltip="{{ 'filter.add-key-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-key-filter' | translate }}
</button>
</div>
</mat-expansion-panel>
</mat-expansion-panel>
</mat-accordion>
</section>

13
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;
}
}
}

21
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});
}
}

3
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html

@ -24,7 +24,8 @@
</mat-option>
</mat-select>
</mat-form-field>
<tb-filter-predicate-value fxFlex="60"
<tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
fxFlex="60"
[valueType]="valueType"
formControlName="value">
</tb-filter-predicate-value>

2
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;

3
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html

@ -28,7 +28,8 @@
<mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;">
</mat-checkbox>
</div>
<tb-filter-predicate-value fxFlex="60"
<tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"
fxFlex="60"
[valueType]="valueTypeEnum.STRING"
formControlName="value">
</tb-filter-predicate-value>

2
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;

10
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,

114
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -0,0 +1,114 @@
<!--
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.
-->
<div style="min-width: 1000px;">
<mat-toolbar color="primary">
<h2 translate>device-profile.add</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" #addDeviceProfileStepper (selectionChange)="selectedIndex = $event.selectedIndex">
<mat-step [stepControl]="deviceProfileDetailsFormGroup">
<form [formGroup]="deviceProfileDetailsFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-profile-details' | translate }}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('required')">
{{ 'device-profile.name-required' | translate }}
</mat-error>
</mat-form-field>
<tb-entity-autocomplete
labelText="device-profile.default-rule-chain"
[entityType]="entityType.RULE_CHAIN"
formControlName="defaultRuleChainId">
</tb-entity-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
{{deviceProfileTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="deviceProfileDetailsFormGroup.get('type').hasError('required')">
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms">
</tb-device-profile-alarms>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="row wrap" fxLayoutAlign="space-between center">
<button mat-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
<span *ngIf="selectedIndex <= 0"></span>
<div fxLayout="row wrap" fxLayoutGap="20px">
<button mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button
[disabled]="(isLoading$ | async) || selectedForm().invalid"
color="primary"
(click)="nextStep()">{{ (selectedIndex === 2 ? 'action.add' : 'action.continue') | translate }}</button>
</div>
</div>
</div>

38
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;
}
}
}
}

173
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<AddDeviceProfileDialogComponent, DeviceProfile> 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<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData,
public dialogRef: MatDialogRef<AddDeviceProfileDialogComponent, DeviceProfile>,
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);
}
);
}
}

32
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html

@ -15,16 +15,22 @@
limitations under the License.
-->
<mat-form-field class="mat-block" (click)="openFilterDialog($event)" floatLabel="always" hideRequiredMarker>
<mat-label></mat-label>
<input readonly
required matInput [formControl]="alarmRuleConditionControl"
placeholder="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
<a matSuffix mat-icon-button color="primary"
type="button"
(click)="openFilterDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'more_vert' : 'edit' }}</mat-icon>
</a>
</mat-form-field>
<div fxLayout="column" fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<div class="tb-small" translate>device-profile.alarm-rule-condition</div>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
(click)="openFilterDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
{{ (disabled ? 'action.view' : (conditionSet() ? 'action.edit' : 'action.add')) | translate }}
</a>
</div>
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
<tb-filter-text [formControl]="alarmRuleConditionControl"
required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text>
</div>
</div>

17
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;
}
}
}

23
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<KeyFilter>): 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() {

1
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html

@ -32,6 +32,7 @@
<div fxFlex fxLayout="column">
<tb-key-filter-list
[displayUserParameters]="false"
[allowUserDynamicSource]="false"
[telemetryKeysOnly]="true"
formControlName="keyFilters">
</tb-key-filter-list>

88
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -16,56 +16,56 @@
-->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<div formGroupName="condition" fxLayout="row" fxLayoutAlign="start" fxLayoutGap="8px" fxFlex>
<div fxLayout="column" fxFlex>
<div formGroupName="condition" fxLayout="row" fxLayoutGap="8px" fxFlex>
<tb-alarm-rule-condition fxFlex
formControlName="condition">
</tb-alarm-rule-condition>
<div fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-small" translate>device-profile.alarm-rule-condition</label>
</div>
<tb-alarm-rule-condition fxFlex
formControlName="condition">
</tb-alarm-rule-condition>
</div>
<div fxLayout="column" style="min-width: 250px;">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label fxFlex class="tb-small" translate>device-profile.condition-duration</label>
<div class="tb-small" translate>device-profile.condition-duration</div>
<span fxFlex></span>
<mat-slide-toggle [disabled]="disabled"
color="primary"
[ngModelOptions]="{standalone: true}"
(ngModelChange)="enableDurationChanged($event)"
[ngModel]="enableDuration">
</mat-slide-toggle>
</div>
<div fxLayout="row" fxLayoutGap="8px" [fxShow]="enableDuration">
<mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<input type="number"
[required]="enableDuration"
step="1"
min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="durationValue">
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="durationUnit"
[required]="enableDuration"
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
<div class="tb-condition-duration" fxFlex fxLayout="row" fxLayoutGap="8px">
<span style="min-width: 250px;" *ngIf="!enableDuration"></span>
<div style="min-width: 250px;" fxLayout="row" fxLayoutGap="8px" *ngIf="enableDuration">
<mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<input type="number"
required
step="1"
min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="durationValue">
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="durationUnit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</div>
@ -73,7 +73,7 @@
<mat-expansion-panel-header>
<mat-panel-title>
<div fxFlex fxLayout="row" fxLayoutAlign="end center">
<div class="tb-small" translate>device-profile.advanced-settings</div>
<div class="tb-small" translate>device-profile.alarm-rule-details</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>

5
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;

9
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html

@ -25,7 +25,8 @@
<mat-select formControlName="severity"
required
placeholder="{{ 'device-profile.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], $index)">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
@ -36,7 +37,7 @@
<tb-alarm-rule formControlName="alarmRule" required fxFlex>
</tb-alarm-rule>
</div>
<button *ngIf="!disabled && createAlarmRulesFormArray().controls.length > 1"
<button *ngIf="!disabled"
mat-icon-button color="primary" style="min-width: 40px;"
type="button"
(click)="removeCreateAlarmRule($index)"
@ -45,6 +46,10 @@
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!createAlarmRulesFormArray().controls.length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-create-alarm-rules</span>
</div>
<div fxLayout="row" *ngIf="!disabled">
<button mat-stroked-button color="primary"
type="button"

22
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts

@ -61,6 +61,8 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
createAlarmRulesFormGroup: FormGroup;
private usedSeverities: AlarmSeverity[] = [];
private valueChangeSubscription: Subscription = null;
private propagateChange = (v: any) => { };
@ -121,6 +123,10 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.updateUsedSeverities();
if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
this.updateModel();
}
}
public removeCreateAlarmRule(index: number) {
@ -142,19 +148,33 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
}
public validate(c: FormControl) {
return (this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormGroup.get('createAlarmRules').value.length) ? null : {
return (this.createAlarmRulesFormGroup.valid) ? null : {
createAlarmRules: {
valid: false,
},
};
}
public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean {
const usedIndex = this.usedSeverities.indexOf(severity);
return usedIndex > -1 && usedIndex !== index;
}
private updateUsedSeverities() {
this.usedSeverities = [];
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
value.forEach((rule, index) => {
this.usedSeverities[index] = AlarmSeverity[rule.severity];
});
}
private updateModel() {
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: AlarmRule} = {};
value.forEach(v => {
createAlarmRules[v.severity] = v.alarmRule;
});
this.updateUsedSeverities();
this.propagateChange(createAlarmRules);
}
}

28
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html

@ -26,6 +26,7 @@
<mat-form-field floatLabel="always"
style="width: 600px;"
[fxShow]="expanded"
(keydown)="!disabled ? $event.stopPropagation() : null;"
(click)="!disabled ? $event.stopPropagation() : null;">
<mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
<input required matInput formControlName="alarmType" placeholder="Enter alarm type">
@ -66,6 +67,10 @@
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!alarmFormGroup.get('clearRule').value">
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-clear-alarm-rule</span>
</div>
<div fxLayout="row" *ngIf="!disabled"
[fxShow]="!alarmFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
@ -86,9 +91,28 @@
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-checkbox formControlName="propagate" style="padding-bottom: 16px;">
<mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
{{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox>
<div>TODO: Propagate relation types</div>
<section *ngIf="alarmFormGroup.get('propagate').value === true">
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-list #relationTypesChipList [disabled]="disabled">
<mat-chip
*ngFor="let key of alarmFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
<input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}"
style="max-width: 200px;"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addRelationType($event)"
[matChipInputAddOnBlur]="true">
</mat-chip-list>
<mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
</section>
</mat-expansion-panel>
</mat-expansion-panel>

52
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts

@ -27,6 +27,8 @@ import {
} from '@angular/forms';
import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
@Component({
selector: 'tb-device-profile-alarm',
@ -53,13 +55,16 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
@Output()
removeAlarm = new EventEmitter();
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
expanded = false;
private modelValue: DeviceProfileAlarm;
alarmFormGroup: FormGroup;
private propagateChange = (v: any) => { };
private propagateChange = null;
private propagateChangePending = false;
constructor(private dialog: MatDialog,
private fb: FormBuilder) {
@ -67,6 +72,12 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
setTimeout(() => {
this.propagateChange(this.modelValue);
}, 0);
}
}
registerOnTouched(fn: any): void {
@ -96,11 +107,15 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
}
writeValue(value: DeviceProfileAlarm): void {
this.propagateChangePending = false;
this.modelValue = value;
if (!this.modelValue.alarmType) {
this.expanded = true;
}
this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
if (!this.disabled && !this.alarmFormGroup.valid) {
this.updateModel();
}
}
public addClearAlarmRule() {
@ -124,9 +139,42 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
};
}
removeRelationType(key: string): void {
const keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value;
const index = keys.indexOf(key);
if (index >= 0) {
keys.splice(index, 1);
this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
}
}
addRelationType(event: MatChipInputEvent): void {
const input = event.input;
let value = event.value;
if ((value || '').trim()) {
value = value.trim();
let keys: string[] = this.alarmFormGroup.get('propagateRelationTypes').value;
if (!keys || keys.indexOf(value) === -1) {
if (!keys) {
keys = [];
}
keys.push(value);
this.alarmFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
}
}
if (input) {
input.value = '';
}
}
private updateModel() {
const value = this.alarmFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
if (this.propagateChange) {
this.propagateChange(this.modelValue);
} else {
this.propagateChangePending = true;
}
}
}

7
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html

@ -25,6 +25,10 @@
</tb-device-profile-alarm>
</div>
</div>
<div *ngIf="!alarmsFormArray().controls.length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-alarm-rules</span>
</div>
<div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center"
style="padding-top: 16px;">
<button mat-raised-button color="primary"
@ -32,8 +36,7 @@
(click)="addAlarm()"
matTooltip="{{ 'device-profile.add-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
<span translate>action.add</span>
<span translate>device-profile.add-alarm-rule</span>
</button>
</div>
</div>

8
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<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value;
this.propagateChange(alarms);
/* } else {
this.propagateChange(null);
} */
const alarms: Array<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value;
this.propagateChange(alarms);
}
}

43
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, DeviceProfileDialogData,
DeviceProfile>(DeviceProfileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
deviceProfile
}
}).afterClosed().subscribe(
let deviceProfileObservable: Observable<DeviceProfile>;
if (!isAdd) {
deviceProfileObservable = this.dialog.open<DeviceProfileDialogComponent, DeviceProfileDialogData,
DeviceProfile>(DeviceProfileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: false,
deviceProfile
}
}).afterClosed();
} else {
deviceProfileObservable = this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
DeviceProfile>(AddDeviceProfileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
deviceProfileName: deviceProfile.name
}
}).afterClosed();
}
deviceProfileObservable.subscribe(
(savedDeviceProfile) => {
if (!savedDeviceProfile) {
setTimeout(() => {

2
ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html

@ -39,7 +39,7 @@
required>
</tb-device-profile-transport-configuration>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="false">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div>{{'device-profile.alarm-rules' | translate:

2
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -81,7 +81,7 @@
required>
</tb-device-profile-data>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.description</mat-label>
<mat-label translate>device-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>

21
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<EntityTableConfig<DeviceProfile>> {
@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
constructor(private deviceProfileService: DeviceProfileService,
private translate: TranslateService,
private datePipe: DatePipe,
private dialogService: DialogService) {
private dialogService: DialogService,
private dialog: MatDialog) {
this.config.entityType = EntityType.DEVICE_PROFILE;
this.config.entityComponent = DeviceProfileComponent;
@ -92,6 +99,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
this.config.onEntityAction = action => this.onDeviceProfileAction(action);
this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
this.config.addEntity = () => this.addDeviceProfile();
}
resolve(): EntityTableConfig<DeviceProfile> {
@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
return this.config;
}
addDeviceProfile(): Observable<DeviceProfile> {
return this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
DeviceProfile>(AddDeviceProfileDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
deviceProfileName: null
}
}).afterClosed();
}
setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) {
if ($event) {
$event.stopPropagation();

2
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<EntityTableConfig<WidgetsBundle>> {
@ -55,6 +56,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
this.config.entityComponent = WidgetsBundleComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGETS_BUNDLE);
this.config.entityResources = entityTypeResources.get(EntityType.WIDGETS_BUNDLE);
this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
this.config.entityTitle = (widgetsBundle) => widgetsBundle ?
widgetsBundle.title : '';

97
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<KeyFilter>): 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(' <span class="tb-filter-complex-operation">' + andText + '</span> ');
} 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(' <span class="tb-filter-complex-operation">' + operationText + '</span> ');
if (complexOperation === ComplexOperation.OR && parentComplexOperation && ComplexOperation.OR !== parentComplexOperation) {
result = `<span class="tb-filter-bracket"><span class="tb-left-bracket">(</span>${result}<span class="tb-right-bracket">)</span></span>`;
}
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 = '<span class="tb-filter-dynamic-value"><span class="tb-filter-dynamic-source">' +
translate.instant(dynamicValueSourceTypeTranslationMap.get(val.dynamicValue.sourceType)) + '</span>';
value += '.<span class="tb-filter-value">' + val.dynamicValue.sourceAttribute + '</span></span>';
}
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 = `<span class="tb-filter-value">${value}</span>`;
}
return `<span class="tb-filter-predicate"><span class="tb-filter-entity-key">${key}</span> <span class="tb-filter-simple-operation">${operation}</span> ${value}</span>`;
}
export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>): Array<KeyFilter> {
if (!keyFilterInfos) {
return [];
}
const keyFilters: Array<KeyFilter> = [];
for (const keyFilterInfo of keyFilterInfos) {
const key = keyFilterInfo.key;

16
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -811,17 +811,19 @@
"single-level-wildcards-hint": "<code>[+]</code> is suitable for any topic filter level. Ex.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
"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>.",
"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 <code>${metaKeyName}</code> to substitute variables from metadata",
"create-alarm-pattern": "Create <b>{{alarmType}}</b> 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",

Loading…
Cancel
Save