Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard

pull/3880/head
ShvaykaD 6 years ago
parent
commit
12667d89ea
  1. 6
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  2. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 5
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  4. 67
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  5. 4
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java
  7. 13
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  8. 13
      application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java
  9. 10
      application/src/main/resources/thingsboard.yml
  10. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  11. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  12. 10
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  13. 2
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
  14. 14
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
  15. 5
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java
  16. 53
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
  17. 4
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java
  18. 3
      common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRemoteJsInvokeSettings.java
  19. 3
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
  20. 6
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  21. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
  22. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  23. 19
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  24. 15
      dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java
  25. 7
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  26. 10
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  27. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
  28. 2
      dao/src/main/resources/sql/schema-entities-hsql.sql
  29. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  30. 2
      dao/src/main/resources/sql/schema-entities.sql
  31. 2
      docker/docker-compose.hybrid.yml
  32. 2
      docker/docker-compose.kafka.yml
  33. 2
      docker/docker-compose.postgres.yml
  34. 2
      msa/js-executor/package.json
  35. 8
      msa/js-executor/yarn.lock
  36. 5
      pom.xml
  37. 2
      transport/coap/src/main/resources/tb-coap-transport.yml
  38. 2
      transport/http/src/main/resources/tb-http-transport.yml
  39. 2
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  40. 5
      ui-ngx/angular.json
  41. 16
      ui-ngx/src/app/core/http/device-profile.service.ts
  42. 1
      ui-ngx/src/app/core/translate/translate-default-compiler.ts
  43. 18
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  44. 124
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  45. 13
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  46. 3
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  47. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
  48. 7
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
  49. 7
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
  50. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  51. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  52. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
  53. 6
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts
  54. 6
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  55. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts
  56. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html
  57. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts
  58. 3
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  59. 5
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
  60. 20
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw
  61. 7
      ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts
  62. 5
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  63. 2
      ui-ngx/src/app/modules/home/models/services.map.ts
  64. 2
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  65. 21
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  66. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  67. 2
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  68. 2479
      ui-ngx/src/assets/locale/locale.constant-sl_SI.json
  69. 2
      ui-ngx/src/index.html
  70. 1
      ui-ngx/src/styles.scss

6
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

File diff suppressed because one or more lines are too long

12
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -244,8 +244,18 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) { if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th); mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th);
} }
String failureMessage;
if (th != null) {
if (!StringUtils.isEmpty(th.getMessage())) {
failureMessage = th.getMessage();
} else {
failureMessage = th.getClass().getSimpleName();
}
} else {
failureMessage = null;
}
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE),
msg, th != null ? th.getMessage() : null)); msg, failureMessage));
} }
public void updateSelf(RuleNode self) { public void updateSelf(RuleNode self) {

5
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -56,6 +56,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -288,10 +289,10 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private void putToQueue(TopicPartitionInfo tpi, TbMsg msg, TbQueueCallback callbackWrapper, EntityId target) { private void putToQueue(TopicPartitionInfo tpi, TbMsg msg, TbQueueCallback callbackWrapper, EntityId target) {
switch (target.getEntityType()) { switch (target.getEntityType()) {
case RULE_NODE: case RULE_NODE:
putToQueue(tpi, msg.copyWithRuleNodeId(entityId, new RuleNodeId(target.getId())), callbackWrapper); putToQueue(tpi, msg.copyWithRuleNodeId(entityId, new RuleNodeId(target.getId()), UUID.randomUUID()), callbackWrapper);
break; break;
case RULE_CHAIN: case RULE_CHAIN:
putToQueue(tpi, msg.copyWithRuleChainId(new RuleChainId(target.getId())), callbackWrapper); putToQueue(tpi, msg.copyWithRuleChainId(new RuleChainId(target.getId()), UUID.randomUUID()), callbackWrapper);
break; break;
} }
} }

67
application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -35,21 +37,30 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
import java.util.UUID;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@RequestMapping("/api") @RequestMapping("/api")
@Slf4j @Slf4j
public class DeviceProfileController extends BaseController { public class DeviceProfileController extends BaseController {
private static final String DEVICE_PROFILE_ID = "deviceProfileId";
@Autowired
private TimeseriesService timeseriesService;
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET) @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public DeviceProfile getDeviceProfileById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { public DeviceProfile getDeviceProfileById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
checkParameter("deviceProfileId", strDeviceProfileId); checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try { try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
return checkDeviceProfileId(deviceProfileId, Operation.READ); return checkDeviceProfileId(deviceProfileId, Operation.READ);
@ -61,8 +72,8 @@ public class DeviceProfileController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET) @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET)
@ResponseBody @ResponseBody
public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
checkParameter("deviceProfileId", strDeviceProfileId); checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try { try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId)); return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId));
@ -82,6 +93,46 @@ public class DeviceProfileController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET)
@ResponseBody
public List<String> getTimeseriesKeys(
@RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
DeviceProfileId deviceProfileId;
if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
deviceProfileId = new DeviceProfileId(UUID.fromString(deviceProfileIdStr));
checkDeviceProfileId(deviceProfileId, Operation.READ);
} else {
deviceProfileId = null;
}
try {
return timeseriesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/devices/keys/attributes", method = RequestMethod.GET)
@ResponseBody
public List<String> getAttributesKeys(
@RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
DeviceProfileId deviceProfileId;
if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
deviceProfileId = new DeviceProfileId(UUID.fromString(deviceProfileIdStr));
checkDeviceProfileId(deviceProfileId, Operation.READ);
} else {
deviceProfileId = null;
}
try {
return attributesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile", method = RequestMethod.POST) @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST)
@ResponseBody @ResponseBody
@ -113,8 +164,8 @@ public class DeviceProfileController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE) @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { public void deleteDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
checkParameter("deviceProfileId", strDeviceProfileId); checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try { try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE); DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
@ -139,8 +190,8 @@ public class DeviceProfileController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST) @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public DeviceProfile setDefaultDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException { public DeviceProfile setDefaultDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
checkParameter("deviceProfileId", strDeviceProfileId); checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try { try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE); DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE);

4
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -186,6 +186,10 @@ public class ThingsboardInstallService {
systemDataLoaderService.updateSystemWidgets(); systemDataLoaderService.updateSystemWidgets();
systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createOAuth2Templates();
break; break;
case "3.2.0":
log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
break;
default: default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);

2
application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java

@ -181,7 +181,7 @@ public class TenantApiUsageState {
long threshold = getProfileThreshold(recordKey); long threshold = getProfileThreshold(recordKey);
long warnThreshold = getProfileWarnThreshold(recordKey); long warnThreshold = getProfileWarnThreshold(recordKey);
ApiUsageStateValue tmpValue; ApiUsageStateValue tmpValue;
if (threshold == 0 || value < warnThreshold) { if (threshold == 0 || value == 0 || value < warnThreshold) {
tmpValue = ApiUsageStateValue.ENABLED; tmpValue = ApiUsageStateValue.ENABLED;
} else if (value < threshold) { } else if (value < threshold) {
tmpValue = ApiUsageStateValue.WARNING; tmpValue = ApiUsageStateValue.WARNING;

13
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -421,6 +421,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.error("Failed updating schema!!!", e); log.error("Failed updating schema!!!", e);
} }
break; break;
case "3.2.0":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_device_device_profile_id ON device(tenant_id, device_profile_id);");
conn.createStatement().execute("ALTER TABLE dashboard ALTER COLUMN configuration TYPE varchar;");
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3002001;");
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
log.info("Schema updated.");
}
break;
default: default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
} }

13
application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java

@ -20,6 +20,7 @@ import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sns.AmazonSNS; import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClient; import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.MessageAttributeValue;
import com.amazonaws.services.sns.model.PublishRequest; import com.amazonaws.services.sns.model.PublishRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -28,9 +29,20 @@ import org.thingsboard.rule.engine.api.sms.exception.SmsException;
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException; import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
import org.thingsboard.server.service.sms.AbstractSmsSender; import org.thingsboard.server.service.sms.AbstractSmsSender;
import java.util.HashMap;
import java.util.Map;
@Slf4j @Slf4j
public class AwsSmsSender extends AbstractSmsSender { public class AwsSmsSender extends AbstractSmsSender {
private static final Map<String, MessageAttributeValue> SMS_ATTRIBUTES = new HashMap<>();
static {
SMS_ATTRIBUTES.put("AWS.SNS.SMS.SMSType", new MessageAttributeValue()
.withStringValue("Transactional")
.withDataType("String"));
}
private AmazonSNS snsClient; private AmazonSNS snsClient;
public AwsSmsSender(AwsSnsSmsProviderConfiguration config) { public AwsSmsSender(AwsSnsSmsProviderConfiguration config) {
@ -51,6 +63,7 @@ public class AwsSmsSender extends AbstractSmsSender {
message = this.prepareMessage(message); message = this.prepareMessage(message);
try { try {
PublishRequest publishRequest = new PublishRequest() PublishRequest publishRequest = new PublishRequest()
.withMessageAttributes(SMS_ATTRIBUTES)
.withPhoneNumber(numberTo) .withPhoneNumber(numberTo)
.withMessage(message); .withMessage(message);
this.snsClient.publish(publishRequest); this.snsClient.publish(publishRequest);

10
application/src/main/resources/thingsboard.yml

@ -64,9 +64,9 @@ server:
# Minimum value of the server side RPC timeout. May override value provided in the REST API call. # Minimum value of the server side RPC timeout. May override value provided in the REST API call.
# Since 2.5 migration to queues, the RPC delay depends on the size of the pending messages in the queue, # Since 2.5 migration to queues, the RPC delay depends on the size of the pending messages in the queue,
# so default UI parameter of 500ms may not be sufficient for loaded environments. # so default UI parameter of 500ms may not be sufficient for loaded environments.
min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}" min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}"
# Default value of the server side RPC timeout. # Default value of the server side RPC timeout.
default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}" default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}"
# Zookeeper connection parameters. Used for service discovery. # Zookeeper connection parameters. Used for service discovery.
zk: zk:
@ -522,7 +522,7 @@ transport:
# Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check)
max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}"
client_side_rpc: client_side_rpc:
timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}"
# Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property) # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property)
api_enabled: "${TB_TRANSPORT_API_ENABLED:true}" api_enabled: "${TB_TRANSPORT_API_ENABLED:true}"
# Local HTTP transport parameters # Local HTTP transport parameters
@ -595,7 +595,7 @@ queue:
linger.ms: "${TB_KAFKA_LINGER_MS:1}" linger.ms: "${TB_KAFKA_LINGER_MS:1}"
buffer.memory: "${TB_BUFFER_MEMORY:33554432}" buffer.memory: "${TB_BUFFER_MEMORY:33554432}"
replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}"
max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:0}" max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}"
max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}"
max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}"
fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}"
@ -695,8 +695,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval # JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
# JS response auto commit interval
response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine: rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"

4
common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.attributes; package org.thingsboard.server.dao.attributes;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -38,4 +39,7 @@ public interface AttributesService {
ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes); ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes);
ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys); ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
} }

3
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.timeseries; package org.thingsboard.server.dao.timeseries;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
@ -47,4 +48,6 @@ public interface TimeseriesService {
ListenableFuture<List<Void>> removeLatest(TenantId tenantId, EntityId entityId, Collection<String> keys); ListenableFuture<List<Void>> removeLatest(TenantId tenantId, EntityId entityId, Collection<String> keys);
ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId); ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
} }

10
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -195,11 +195,15 @@ public final class TbMsg implements Serializable {
} }
public TbMsg copyWithRuleChainId(RuleChainId ruleChainId) { public TbMsg copyWithRuleChainId(RuleChainId ruleChainId) {
return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, null, this.ruleNodeExecCounter.get(), callback); return copyWithRuleChainId(ruleChainId, this.id);
} }
public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId) { public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) {
return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ruleNodeExecCounter.get(), callback); return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, null, this.ruleNodeExecCounter.get(), callback);
}
public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) {
return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ruleNodeExecCounter.get(), callback);
} }
public TbMsgCallback getCallback() { public TbMsgCallback getCallback() {

2
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java

@ -42,7 +42,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
private final short replicationFactor; private final short replicationFactor;
public TbKafkaAdmin(TbKafkaSettings settings, Map<String, String> topicConfigs) { public TbKafkaAdmin(TbKafkaSettings settings, Map<String, String> topicConfigs) {
client = AdminClient.create(settings.toProps()); client = AdminClient.create(settings.toAdminProps());
this.topicConfigs = topicConfigs; this.topicConfigs = topicConfigs;
try { try {

14
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java

@ -45,24 +45,14 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
@Builder @Builder
private TbKafkaConsumerTemplate(TbKafkaSettings settings, TbKafkaDecoder<T> decoder, private TbKafkaConsumerTemplate(TbKafkaSettings settings, TbKafkaDecoder<T> decoder,
String clientId, String groupId, String topic, String clientId, String groupId, String topic,
boolean autoCommit, int autoCommitIntervalMs,
TbQueueAdmin admin) { TbQueueAdmin admin) {
super(topic); super(topic);
Properties props = settings.toProps(); Properties props = settings.toConsumerProps();
props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId); props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId);
if (groupId != null) { if (groupId != null) {
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
} }
if (settings.getMaxPollIntervalMs() > 0) {
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, settings.getMaxPollIntervalMs());
}
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, settings.getMaxPollRecords());
props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, settings.getMaxPartitionFetchBytes());
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, settings.getFetchMaxBytes());
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, autoCommit);
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer");
this.admin = admin; this.admin = admin;
this.consumer = new KafkaConsumer<>(props); this.consumer = new KafkaConsumer<>(props);
this.decoder = decoder; this.decoder = decoder;

5
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java

@ -55,9 +55,8 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro
@Builder @Builder
private TbKafkaProducerTemplate(TbKafkaSettings settings, String defaultTopic, String clientId, TbQueueAdmin admin) { private TbKafkaProducerTemplate(TbKafkaSettings settings, String defaultTopic, String clientId, TbQueueAdmin admin) {
Properties props = settings.toProps(); Properties props = settings.toProducerProps();
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
if (!StringUtils.isEmpty(clientId)) { if (!StringUtils.isEmpty(clientId)) {
props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId); props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId);
} }

53
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java

@ -19,9 +19,11 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -32,7 +34,7 @@ import java.util.Properties;
* Created by ashvayka on 25.09.18. * Created by ashvayka on 25.09.18.
*/ */
@Slf4j @Slf4j
@ConditionalOnExpression("'${queue.type:null}'=='kafka'") @ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka")
@ConfigurationProperties(prefix = "queue.kafka") @ConfigurationProperties(prefix = "queue.kafka")
@Component @Component
public class TbKafkaSettings { public class TbKafkaSettings {
@ -60,19 +62,15 @@ public class TbKafkaSettings {
private short replicationFactor; private short replicationFactor;
@Value("${queue.kafka.max_poll_records:8192}") @Value("${queue.kafka.max_poll_records:8192}")
@Getter
private int maxPollRecords; private int maxPollRecords;
@Value("${queue.kafka.max_poll_interval_ms:0}") @Value("${queue.kafka.max_poll_interval_ms:300000}")
@Getter
private int maxPollIntervalMs; private int maxPollIntervalMs;
@Value("${queue.kafka.max_partition_fetch_bytes:16777216}") @Value("${queue.kafka.max_partition_fetch_bytes:16777216}")
@Getter
private int maxPartitionFetchBytes; private int maxPartitionFetchBytes;
@Value("${queue.kafka.fetch_max_bytes:134217728}") @Value("${queue.kafka.fetch_max_bytes:134217728}")
@Getter
private int fetchMaxBytes; private int fetchMaxBytes;
@Value("${queue.kafka.use_confluent_cloud:false}") @Value("${queue.kafka.use_confluent_cloud:false}")
@ -93,21 +91,48 @@ public class TbKafkaSettings {
@Setter @Setter
private List<TbKafkaProperty> other; private List<TbKafkaProperty> other;
public Properties toProps() { public Properties toAdminProps() {
Properties props = new Properties(); Properties props = toProps();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(AdminClientConfig.RETRIES_CONFIG, retries);
return props;
}
public Properties toConsumerProps() {
Properties props = toProps();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords);
props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, maxPartitionFetchBytes);
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes);
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer");
return props;
}
public Properties toProducerProps() {
Properties props = toProps();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ProducerConfig.RETRIES_CONFIG, retries); props.put(ProducerConfig.RETRIES_CONFIG, retries);
props.put(ProducerConfig.ACKS_CONFIG, acks);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
props.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
return props;
}
private Properties toProps() {
Properties props = new Properties();
if (useConfluent) { if (useConfluent) {
props.put("ssl.endpoint.identification.algorithm", sslAlgorithm); props.put("ssl.endpoint.identification.algorithm", sslAlgorithm);
props.put("sasl.mechanism", saslMechanism); props.put("sasl.mechanism", saslMechanism);
props.put("sasl.jaas.config", saslConfig); props.put("sasl.jaas.config", saslConfig);
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol);
} else {
props.put(ProducerConfig.ACKS_CONFIG, acks);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
props.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
} }
if (other != null) { if (other != null) {

4
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java

@ -17,7 +17,7 @@ package org.thingsboard.server.queue.kafka;
import lombok.Getter; import lombok.Getter;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -25,7 +25,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Component @Component
@ConditionalOnExpression("'${queue.type:null}'=='kafka'") @ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka")
public class TbKafkaTopicConfigs { public class TbKafkaTopicConfigs {
@Value("${queue.kafka.topic-properties.core}") @Value("${queue.kafka.topic-properties.core}")
private String coreProperties; private String coreProperties;

3
common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRemoteJsInvokeSettings.java

@ -34,9 +34,6 @@ public class TbQueueRemoteJsInvokeSettings {
@Value("${queue.js.response_poll_interval}") @Value("${queue.js.response_poll_interval}")
private int responsePollInterval; private int responsePollInterval;
@Value("${queue.js.response_auto_commit_interval}")
private int autoCommitInterval;
@Value("${queue.js.max_requests_timeout}") @Value("${queue.js.max_requests_timeout}")
private long maxRequestsTimeout; private long maxRequestsTimeout;
} }

3
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.attributes; package org.thingsboard.server.dao.attributes;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -38,4 +39,6 @@ public interface AttributesDao {
ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute); ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute);
ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys); ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
} }

6
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -59,6 +60,11 @@ public class BaseAttributesService implements AttributesService {
return attributesDao.findAll(tenantId, entityId, scope); return attributesDao.findAll(tenantId, entityId, scope);
} }
@Override
public List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
}
@Override @Override
public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) { public ListenableFuture<List<Void>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope); validate(entityId, scope);

10
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java

@ -46,5 +46,15 @@ public interface AttributeKvRepository extends CrudRepository<AttributeKvEntity,
@Param("entityId") UUID entityId, @Param("entityId") UUID entityId,
@Param("attributeType") String attributeType, @Param("attributeType") String attributeType,
@Param("attributeKey") String attributeKey); @Param("attributeKey") String attributeKey);
@Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE entity_type = 'DEVICE' " +
"AND entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId and device_profile_id = :deviceProfileId limit 100) ORDER BY attribute_key", nativeQuery = true)
List<String> findAllKeysByDeviceProfileId(@Param("tenantId") UUID tenantId,
@Param("deviceProfileId") UUID deviceProfileId);
@Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE entity_type = 'DEVICE' " +
"AND entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true)
List<String> findAllKeysByTenantId(@Param("tenantId") UUID tenantId);
} }

10
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -135,6 +136,15 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
attributeType)))); attributeType))));
} }
@Override
public List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
if (deviceProfileId != null) {
return attributeKvRepository.findAllKeysByDeviceProfileId(tenantId.getId(), deviceProfileId.getId());
} else {
return attributeKvRepository.findAllKeysByTenantId(tenantId.getId());
}
}
@Override @Override
public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) { public ListenableFuture<Void> save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) {
AttributeKvEntity entity = new AttributeKvEntity(); AttributeKvEntity entity = new AttributeKvEntity();

19
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
@ -51,10 +52,15 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.*; import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
@ -154,6 +160,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
return getFindAllLatestFuture(entityId); return getFindAllLatestFuture(entityId);
} }
@Override
public List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
if (deviceProfileId != null) {
return tsKvLatestRepository.getKeysByDeviceProfileId(tenantId.getId(), deviceProfileId.getId());
} else {
return tsKvLatestRepository.getKeysByTenantId(tenantId.getId());
}
}
private ListenableFuture<Void> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { private ListenableFuture<Void> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query); ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query);
return Futures.transformAsync(future, entryList -> { return Futures.transformAsync(future, entryList -> {

15
dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java

@ -15,10 +15,25 @@
*/ */
package org.thingsboard.server.dao.sqlts.latest; package org.thingsboard.server.dao.sqlts.latest;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import java.util.List;
import java.util.UUID;
public interface TsKvLatestRepository extends CrudRepository<TsKvLatestEntity, TsKvLatestCompositeKey> { public interface TsKvLatestRepository extends CrudRepository<TsKvLatestEntity, TsKvLatestCompositeKey> {
@Query(value = "SELECT DISTINCT ts_kv_dictionary.key AS strKey FROM ts_kv_latest " +
"INNER JOIN ts_kv_dictionary ON ts_kv_latest.key = ts_kv_dictionary.key_id " +
"WHERE ts_kv_latest.entity_id IN (SELECT id FROM device WHERE device_profile_id = :device_profile_id AND tenant_id = :tenant_id limit 100) ORDER BY ts_kv_dictionary.key", nativeQuery = true)
List<String> getKeysByDeviceProfileId(@Param("tenant_id") UUID tenantId, @Param("device_profile_id") UUID deviceProfileId);
@Query(value = "SELECT DISTINCT ts_kv_dictionary.key AS strKey FROM ts_kv_latest " +
"INNER JOIN ts_kv_dictionary ON ts_kv_latest.key = ts_kv_dictionary.key_id " +
"WHERE ts_kv_latest.entity_id IN (SELECT id FROM device WHERE tenant_id = :tenant_id limit 100) ORDER BY ts_kv_dictionary.key", nativeQuery = true)
List<String> getKeysByTenantId(@Param("tenant_id") UUID tenantId);
} }

7
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -40,7 +41,6 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -116,6 +116,11 @@ public class BaseTimeseriesService implements TimeseriesService {
return timeseriesLatestDao.findAllLatest(tenantId, entityId); return timeseriesLatestDao.findAllLatest(tenantId, entityId);
} }
@Override
public List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
return timeseriesLatestDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
}
@Override @Override
public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
validate(entityId); validate(entityId);

10
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.timeseries;
import com.datastax.oss.driver.api.core.cql.BoundStatement; import com.datastax.oss.driver.api.core.cql.BoundStatement;
import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;
import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.PreparedStatement;
import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.Statement; import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.api.querybuilder.QueryBuilder; import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
@ -28,14 +27,13 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.nosql.TbResultSet;
@ -43,6 +41,7 @@ import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao;
import org.thingsboard.server.dao.util.NoSqlTsLatestDao; import org.thingsboard.server.dao.util.NoSqlTsLatestDao;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -82,6 +81,11 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
return getFutureAsync(executeAsyncRead(tenantId, stmt), rs -> convertAsyncResultSetToTsKvEntryList(rs)); return getFutureAsync(executeAsyncRead(tenantId, stmt), rs -> convertAsyncResultSetToTsKvEntryList(rs));
} }
@Override
public List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
return Collections.emptyList();
}
@Override @Override
public ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { public ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind()); BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind());

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.timeseries; package org.thingsboard.server.dao.timeseries;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
@ -33,4 +34,5 @@ public interface TimeseriesLatestDao {
ListenableFuture<Void> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); ListenableFuture<Void> removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
} }

2
dao/src/main/resources/sql/schema-entities-hsql.sql

@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS customer (
CREATE TABLE IF NOT EXISTS dashboard ( CREATE TABLE IF NOT EXISTS dashboard (
id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
created_time bigint NOT NULL, created_time bigint NOT NULL,
configuration varchar(10000000), configuration varchar,
assigned_customers varchar(1000000), assigned_customers varchar(1000000),
search_text varchar(255), search_text varchar(255),
tenant_id uuid, tenant_id uuid,

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -34,6 +34,8 @@ CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id,
CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type); CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type);
CREATE INDEX IF NOT EXISTS idx_device_device_profile_id ON device(tenant_id, device_profile_id);
CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id); CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id);
CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type); CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type);

2
dao/src/main/resources/sql/schema-entities.sql

@ -132,7 +132,7 @@ CREATE TABLE IF NOT EXISTS customer (
CREATE TABLE IF NOT EXISTS dashboard ( CREATE TABLE IF NOT EXISTS dashboard (
id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
created_time bigint NOT NULL, created_time bigint NOT NULL,
configuration varchar(10000000), configuration varchar,
assigned_customers varchar(1000000), assigned_customers varchar(1000000),
search_text varchar(255), search_text varchar(255),
tenant_id uuid, tenant_id uuid,

2
docker/docker-compose.hybrid.yml

@ -19,7 +19,7 @@ version: '2.2'
services: services:
postgres: postgres:
restart: always restart: always
image: "postgres:11.6" image: "postgres:12"
ports: ports:
- "5432" - "5432"
environment: environment:

2
docker/docker-compose.kafka.yml

@ -19,7 +19,7 @@ version: '2.2'
services: services:
kafka: kafka:
restart: always restart: always
image: "wurstmeister/kafka:2.12-2.3.0" image: "wurstmeister/kafka:2.13-2.6.0"
ports: ports:
- "9092:9092" - "9092:9092"
env_file: env_file:

2
docker/docker-compose.postgres.yml

@ -19,7 +19,7 @@ version: '2.2'
services: services:
postgres: postgres:
restart: always restart: always
image: "postgres:11.6" image: "postgres:12"
ports: ports:
- "5432" - "5432"
environment: environment:

2
msa/js-executor/package.json

@ -19,7 +19,7 @@
"azure-sb": "^0.11.1", "azure-sb": "^0.11.1",
"config": "^3.3.1", "config": "^3.3.1",
"js-yaml": "^3.14.0", "js-yaml": "^3.14.0",
"kafkajs": "^1.14.0", "kafkajs": "^1.15.0",
"long": "^4.0.0", "long": "^4.0.0",
"uuid-parse": "^1.1.0", "uuid-parse": "^1.1.0",
"uuid-random": "^1.3.2", "uuid-random": "^1.3.2",

8
msa/js-executor/yarn.lock

@ -1665,10 +1665,10 @@ jws@^4.0.0:
jwa "^2.0.0" jwa "^2.0.0"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
kafkajs@^1.14.0: kafkajs@^1.15.0:
version "1.14.0" version "1.15.0"
resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-1.14.0.tgz#3d998a77bfde54dc502e8e88690eedf0b21a1ed6" resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-1.15.0.tgz#a5ada0d933edca2149177393562be6fb0875ec3a"
integrity sha512-W+WCekiooY5rJP3Me5N3gWcQ8O6uG6lw0vv9t+sI+WqXKjKwj2+CWIXJy241x+ITE+1M1D19ABSiL2J8lKja5A== integrity sha512-yjPyEnQCkPxAuQLIJnY5dI+xnmmgXmhuOQ1GVxClG5KTOV/rJcW1qA3UfvyEJKTp/RTSqQnUR3HJsKFvHyTpNg==
keyv@^3.0.0: keyv@^3.0.0:
version "3.1.0" version "3.1.0"

5
pom.xml

@ -92,7 +92,7 @@
</sonar.exclusions> </sonar.exclusions>
<elasticsearch.version>5.0.2</elasticsearch.version> <elasticsearch.version>5.0.2</elasticsearch.version>
<delight-nashorn-sandbox.version>0.1.14</delight-nashorn-sandbox.version> <delight-nashorn-sandbox.version>0.1.14</delight-nashorn-sandbox.version>
<kafka.version>2.3.0</kafka.version> <kafka.version>2.6.0</kafka.version>
<bucket4j.version>4.1.1</bucket4j.version> <bucket4j.version>4.1.1</bucket4j.version>
<fst.version>2.57</fst.version> <fst.version>2.57</fst.version>
<antlr.version>2.7.7</antlr.version> <antlr.version>2.7.7</antlr.version>
@ -729,7 +729,8 @@
<exclude>docker/haproxy/**</exclude> <exclude>docker/haproxy/**</exclude>
<exclude>docker/tb-node/**</exclude> <exclude>docker/tb-node/**</exclude>
<exclude>ui/**</exclude> <exclude>ui/**</exclude>
<exclude>src/browserslist</exclude> <exclude>src/.browserslistrc</exclude>
<exclude>**/yarn.lock</exclude>
<exclude>**/*.raw</exclude> <exclude>**/*.raw</exclude>
<exclude>**/apache/cassandra/io/**</exclude> <exclude>**/apache/cassandra/io/**</exclude>
<exclude>.run/**</exclude> <exclude>.run/**</exclude>

2
transport/coap/src/main/resources/tb-coap-transport.yml

@ -160,8 +160,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval # JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
# JS response auto commit interval
response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine: rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"

2
transport/http/src/main/resources/tb-http-transport.yml

@ -153,8 +153,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval # JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
# JS response auto commit interval
response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine: rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"

2
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -182,8 +182,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval # JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
# JS response auto commit interval
response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine: rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"

5
ui-ngx/angular.json

@ -59,6 +59,11 @@
"glob": "marker-shadow.png", "glob": "marker-shadow.png",
"input": "node_modules/leaflet/dist/images/", "input": "node_modules/leaflet/dist/images/",
"output": "/" "output": "/"
},
{
"glob": "**/*",
"input": "node_modules/material-design-icons/iconfont/",
"output": "assets/fonts"
} }
], ],
"styles": [ "styles": [

16
ui-ngx/src/app/core/http/device-profile.service.ts

@ -69,4 +69,20 @@ export class DeviceProfileService {
return this.http.get<PageData<DeviceProfileInfo>>(url, defaultHttpOptionsFromConfig(config)); return this.http.get<PageData<DeviceProfileInfo>>(url, defaultHttpOptionsFromConfig(config));
} }
public getDeviceProfileDevicesAttributesKeys(deviceProfileId?: string, config?: RequestConfig): Observable<Array<string>> {
let url = `/api/deviceProfile/devices/keys/attributes`;
if (isDefinedAndNotNull(deviceProfileId)) {
url += `?deviceProfileId=${deviceProfileId}`;
}
return this.http.get<Array<string>>(url, defaultHttpOptionsFromConfig(config));
}
public getDeviceProfileDevicesTimeseriesKeys(deviceProfileId?: string, config?: RequestConfig): Observable<Array<string>> {
let url = `/api/deviceProfile/devices/keys/timeseries`;
if (isDefinedAndNotNull(deviceProfileId)) {
url += `?deviceProfileId=${deviceProfileId}`;
}
return this.http.get<Array<string>>(url, defaultHttpOptionsFromConfig(config));
}
} }

1
ui-ngx/src/app/core/translate/translate-default-compiler.ts

@ -65,6 +65,7 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler {
} catch (e) { } catch (e) {
console.warn(`Failed to parse source: ${src}`); console.warn(`Failed to parse source: ${src}`);
console.error(e); console.error(e);
return false;
} }
const res = tokens.filter( const res = tokens.filter(
(value) => typeof value !== 'string' && value.type === 'plural' (value) => typeof value !== 'string' && value.type === 'plural'

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

@ -40,11 +40,19 @@
<mat-form-field fxFlex="60" class="mat-block"> <mat-form-field fxFlex="60" class="mat-block">
<mat-label translate>filter.key-name</mat-label> <mat-label translate>filter.key-name</mat-label>
<input matInput required formControlName="key" <input matInput required formControlName="key"
[matAutocomplete]="auto" #keyNameInput
[matAutocompleteDisabled]="keyFilterFormGroup.get('key.type').value !== entityField"> (focusin)="onFocus()"
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete"> [matAutocomplete]="keyName"
<mat-option *ngFor="let option of filteredEntityFields | async" [value]="option"> [matAutocompleteDisabled]="!showAutocomplete">
{{option}} <button *ngIf="keyFilterFormGroup.get('key.key').value && showAutocomplete"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete autoActiveFirstOption #keyName="matAutocomplete">
<mat-option *ngFor="let keyName of filteredKeysName | async" [value]="keyName">
<span [innerHTML]="keyName | highlight:searchText"></span>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')"> <mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')">

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, ElementRef, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -32,9 +32,12 @@ import {
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityField, entityFields } from '@shared/models/entity.models'; import { entityFields } from '@shared/models/entity.models';
import { Observable } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { filter, map, startWith } from 'rxjs/operators'; import { filter, map, mergeMap, publishReplay, refCount, startWith, takeUntil } from 'rxjs/operators';
import { isDefined } from '@core/utils';
import { EntityId } from '@shared/models/id/entity-id';
import { DeviceProfileService } from '@core/http/device-profile.service';
export interface KeyFilterDialogData { export interface KeyFilterDialogData {
keyFilter: KeyFilterInfo; keyFilter: KeyFilterInfo;
@ -43,6 +46,7 @@ export interface KeyFilterDialogData {
allowUserDynamicSource: boolean; allowUserDynamicSource: boolean;
readonly: boolean; readonly: boolean;
telemetryKeysOnly: boolean; telemetryKeysOnly: boolean;
entityId?: EntityId;
} }
@Component({ @Component({
@ -53,7 +57,13 @@ export interface KeyFilterDialogData {
}) })
export class KeyFilterDialogComponent extends export class KeyFilterDialogComponent extends
DialogComponent<KeyFilterDialogComponent, KeyFilterInfo> DialogComponent<KeyFilterDialogComponent, KeyFilterInfo>
implements OnInit, ErrorStateMatcher { implements OnInit, OnDestroy, ErrorStateMatcher {
@ViewChild('keyNameInput', {static: true}) private keyNameInput: ElementRef;
private dirty = false;
private entityKeysName: Observable<Array<string>>;
private destroy$ = new Subject();
keyFilterFormGroup: FormGroup; keyFilterFormGroup: FormGroup;
@ -72,19 +82,18 @@ export class KeyFilterDialogComponent extends
submitted = false; submitted = false;
entityFields: { [fieldName: string]: EntityField }; showAutocomplete = false;
entityFieldsList: string[];
readonly entityField = EntityKeyType.ENTITY_FIELD; filteredKeysName: Observable<Array<string>>;
filteredEntityFields: Observable<string[]>; searchText = '';
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData, @Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>, public dialogRef: MatDialogRef<KeyFilterDialogComponent, KeyFilterInfo>,
private deviceProfileService: DeviceProfileService,
private dialogs: DialogService, private dialogs: DialogService,
private translate: TranslateService, private translate: TranslateService,
private fb: FormBuilder) { private fb: FormBuilder) {
@ -104,7 +113,9 @@ export class KeyFilterDialogComponent extends
); );
if (!this.data.readonly) { if (!this.data.readonly) {
this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => { this.keyFilterFormGroup.get('valueType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((valueType: EntityKeyValueType) => {
const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value;
if (prevValue && prevValue !== valueType && predicates && predicates.length) { if (prevValue && prevValue !== valueType && predicates && predicates.length) {
@ -121,11 +132,26 @@ export class KeyFilterDialogComponent extends
} }
}); });
this.keyFilterFormGroup.get('key.type').valueChanges.pipe(
startWith(this.data.keyFilter.key.type),
takeUntil(this.destroy$)
).subscribe((type: EntityKeyType) => {
if (type === EntityKeyType.ENTITY_FIELD || isDefined(this.data.entityId)) {
this.entityKeysName = null;
this.dirty = false;
this.showAutocomplete = true;
} else {
this.showAutocomplete = false;
}
});
this.keyFilterFormGroup.get('key.key').valueChanges.pipe( this.keyFilterFormGroup.get('key.key').valueChanges.pipe(
filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName)) filter((keyName) =>
this.keyFilterFormGroup.get('key.type').value === EntityKeyType.ENTITY_FIELD && entityFields.hasOwnProperty(keyName)),
takeUntil(this.destroy$)
).subscribe((keyName: string) => { ).subscribe((keyName: string) => {
const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; const newValueType = entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING;
if (prevValueType !== newValueType) { if (prevValueType !== newValueType) {
this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false}); this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false});
} }
@ -133,18 +159,20 @@ export class KeyFilterDialogComponent extends
} else { } else {
this.keyFilterFormGroup.disable({emitEvent: false}); this.keyFilterFormGroup.disable({emitEvent: false});
} }
}
this.entityFields = entityFields; ngOnInit() {
this.entityFieldsList = Object.values(entityFields).map(entityField => entityField.keyName).sort(); this.filteredKeysName = this.keyFilterFormGroup.get('key.key').valueChanges
.pipe(
map(value => value ? value : ''),
mergeMap(name => this.fetchEntityName(name))
);
} }
ngOnInit(): void { ngOnDestroy() {
this.filteredEntityFields = this.keyFilterFormGroup.get('key.key').valueChanges.pipe( super.ngOnDestroy();
startWith(''), this.destroy$.next();
map(value => { this.destroy$.complete();
return this.entityFieldsList.filter(option => option.startsWith(value));
})
);
} }
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -157,6 +185,21 @@ export class KeyFilterDialogComponent extends
this.dialogRef.close(null); this.dialogRef.close(null);
} }
clear() {
this.keyFilterFormGroup.get('key.key').patchValue('', {emitEvent: true});
setTimeout(() => {
this.keyNameInput.nativeElement.blur();
this.keyNameInput.nativeElement.focus();
}, 0);
}
onFocus() {
if (!this.dirty && this.showAutocomplete) {
this.keyFilterFormGroup.get('key.key').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = true;
}
}
save(): void { save(): void {
this.submitted = true; this.submitted = true;
if (this.keyFilterFormGroup.valid) { if (this.keyFilterFormGroup.valid) {
@ -164,4 +207,41 @@ export class KeyFilterDialogComponent extends
this.dialogRef.close(keyFilter); this.dialogRef.close(keyFilter);
} }
} }
private fetchEntityName(searchText?: string): Observable<Array<string>> {
this.searchText = searchText;
return this.getEntityKeys().pipe(
map(keys => searchText ? keys.filter(key => key.toUpperCase().startsWith(searchText.toUpperCase())) : keys)
);
}
private getEntityKeys(): Observable<Array<string>> {
if (!this.entityKeysName) {
let keyNameObservable: Observable<Array<string>>;
switch (this.keyFilterFormGroup.get('key.type').value) {
case EntityKeyType.ENTITY_FIELD:
keyNameObservable = of(Object.values(entityFields).map(entityField => entityField.keyName).sort());
break;
case EntityKeyType.ATTRIBUTE:
keyNameObservable = this.deviceProfileService.getDeviceProfileDevicesAttributesKeys(
this.data.entityId?.id,
{ignoreLoading: true}
);
break;
case EntityKeyType.TIME_SERIES:
keyNameObservable = this.deviceProfileService.getDeviceProfileDevicesTimeseriesKeys(
this.data.entityId?.id,
{ignoreLoading: true}
);
break;
default:
keyNameObservable = of([]);
}
this.entityKeysName = keyNameObservable.pipe(
publishReplay(1),
refCount()
);
}
return this.entityKeysName;
}
} }

13
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts

@ -19,7 +19,8 @@ import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormArray, FormArray,
FormBuilder, FormControl, FormBuilder,
FormControl,
FormGroup, FormGroup,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
Validators Validators
@ -28,12 +29,13 @@ import { Observable, Subscription } from 'rxjs';
import { import {
EntityKeyType, EntityKeyType,
entityKeyTypeTranslationMap, entityKeyTypeTranslationMap,
KeyFilter, KeyFilterInfo,
KeyFilterInfo, keyFilterInfosToKeyFilters keyFilterInfosToKeyFilters
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component'; import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
@Component({ @Component({
selector: 'tb-key-filter-list', selector: 'tb-key-filter-list',
@ -57,6 +59,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
@Input() telemetryKeysOnly = false; @Input() telemetryKeysOnly = false;
@Input() entityId: EntityId;
keyFilterListFormGroup: FormGroup; keyFilterListFormGroup: FormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap; entityKeyTypeTranslations = entityKeyTypeTranslationMap;
@ -170,7 +174,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
readonly: this.disabled, readonly: this.disabled,
displayUserParameters: this.displayUserParameters, displayUserParameters: this.displayUserParameters,
allowUserDynamicSource: this.allowUserDynamicSource, allowUserDynamicSource: this.allowUserDynamicSource,
telemetryKeysOnly: this.telemetryKeysOnly telemetryKeysOnly: this.telemetryKeysOnly,
entityId: this.entityId
} }
}).afterClosed(); }).afterClosed();
} }

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

@ -96,7 +96,8 @@
{count: alarmRulesFormGroup.get('alarms').value ? {count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template> alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms <tb-device-profile-alarms
formControlName="alarms"> formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms> </tb-device-profile-alarms>
</form> </form>
</mat-step> </mat-step>

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

@ -34,6 +34,7 @@
[displayUserParameters]="false" [displayUserParameters]="false"
[allowUserDynamicSource]="false" [allowUserDynamicSource]="false"
[telemetryKeysOnly]="true" [telemetryKeysOnly]="true"
[entityId]="entityId"
formControlName="keyFilters"> formControlName="keyFilters">
</tb-key-filter-list> </tb-key-filter-list>
<section formGroupName="spec" class="row"> <section formGroupName="spec" class="row">

7
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts

@ -22,15 +22,16 @@ import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; import { keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models'; import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { EntityId } from '@shared/models/id/entity-id';
export interface AlarmRuleConditionDialogData { export interface AlarmRuleConditionDialogData {
readonly: boolean; readonly: boolean;
condition: AlarmCondition; condition: AlarmCondition;
entityId?: EntityId;
} }
@Component({ @Component({
@ -50,6 +51,7 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule
readonly = this.data.readonly; readonly = this.data.readonly;
condition = this.data.condition; condition = this.data.condition;
entityId = this.data.entityId;
conditionFormGroup: FormGroup; conditionFormGroup: FormGroup;
@ -61,7 +63,6 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRule
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>, public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>,
private fb: FormBuilder, private fb: FormBuilder,
private utils: UtilsService,
public translate: TranslateService) { public translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);

7
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts

@ -33,6 +33,7 @@ import {
AlarmRuleConditionDialogData AlarmRuleConditionDialogData
} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
import { TimeUnit } from '@shared/models/time/time.models'; import { TimeUnit } from '@shared/models/time/time.models';
import { EntityId } from '@shared/models/id/entity-id';
@Component({ @Component({
selector: 'tb-alarm-rule-condition', selector: 'tb-alarm-rule-condition',
@ -56,6 +57,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
deviceProfileId: EntityId;
alarmRuleConditionFormGroup: FormGroup; alarmRuleConditionFormGroup: FormGroup;
specText = ''; specText = '';
@ -123,7 +127,8 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
readonly: this.disabled, readonly: this.disabled,
condition: this.disabled ? this.modelValue : deepClone(this.modelValue) condition: this.disabled ? this.modelValue : deepClone(this.modelValue),
entityId: this.deviceProfileId
} }
}).afterClosed().subscribe((result) => { }).afterClosed().subscribe((result) => {
if (result) { if (result) {

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

@ -16,7 +16,7 @@
--> -->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup"> <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<tb-alarm-rule-condition formControlName="condition"> <tb-alarm-rule-condition formControlName="condition" [deviceProfileId]="deviceProfileId">
</tb-alarm-rule-condition> </tb-alarm-rule-condition>
<tb-alarm-schedule-info formControlName="schedule"> <tb-alarm-schedule-info formControlName="schedule">
</tb-alarm-schedule-info> </tb-alarm-schedule-info>

4
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts

@ -33,6 +33,7 @@ import {
EditAlarmDetailsDialogComponent, EditAlarmDetailsDialogComponent,
EditAlarmDetailsDialogData EditAlarmDetailsDialogData
} from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; } from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
import { EntityId } from '@shared/models/id/entity-id';
@Component({ @Component({
selector: 'tb-alarm-rule', selector: 'tb-alarm-rule',
@ -65,6 +66,9 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
this.requiredValue = coerceBooleanProperty(value); this.requiredValue = coerceBooleanProperty(value);
} }
@Input()
deviceProfileId: EntityId;
private modelValue: AlarmRule; private modelValue: AlarmRule;
alarmRuleFormGroup: FormGroup; alarmRuleFormGroup: FormGroup;

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

@ -35,7 +35,7 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-divider vertical></mat-divider> <mat-divider vertical></mat-divider>
<tb-alarm-rule formControlName="alarmRule" required fxFlex> <tb-alarm-rule formControlName="alarmRule" [deviceProfileId]="deviceProfileId" required fxFlex>
</tb-alarm-rule> </tb-alarm-rule>
</div> </div>
<button *ngIf="!disabled" <button *ngIf="!disabled"

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

@ -30,7 +30,8 @@ import {
import { AlarmRule, alarmRuleValidator } from '@shared/models/device.models'; import { AlarmRule, alarmRuleValidator } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AlarmSeverity, alarmSeverityTranslations } from '../../../../../shared/models/alarm.models'; import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models';
import { EntityId } from '@shared/models/id/entity-id';
@Component({ @Component({
selector: 'tb-create-alarm-rules', selector: 'tb-create-alarm-rules',
@ -59,6 +60,9 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
deviceProfileId: EntityId;
createAlarmRulesFormGroup: FormGroup; createAlarmRulesFormGroup: FormGroup;
private usedSeverities: AlarmSeverity[] = []; private usedSeverities: AlarmSeverity[] = [];

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

@ -80,14 +80,16 @@
</mat-expansion-panel> </mat-expansion-panel>
<div fxFlex fxLayout="column"> <div fxFlex fxLayout="column">
<div translate class="tb-small" style="padding-bottom: 8px;">device-profile.create-alarm-rules</div> <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.create-alarm-rules</div>
<tb-create-alarm-rules formControlName="createRules" style="padding-bottom: 16px;"> <tb-create-alarm-rules formControlName="createRules"
style="padding-bottom: 16px;"
[deviceProfileId]="deviceProfileId">
</tb-create-alarm-rules> </tb-create-alarm-rules>
<div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div> <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div>
<div fxLayout="row" fxLayoutGap="8px;" fxLayoutAlign="start center" <div fxLayout="row" fxLayoutGap="8px;" fxLayoutAlign="start center"
[fxShow]="alarmFormGroup.get('clearRule').value" [fxShow]="alarmFormGroup.get('clearRule').value"
style="padding-bottom: 8px;"> style="padding-bottom: 8px;">
<div class="clear-alarm-rule" fxFlex fxLayout="row"> <div class="clear-alarm-rule" fxFlex fxLayout="row">
<tb-alarm-rule formControlName="clearRule" fxFlex> <tb-alarm-rule formControlName="clearRule" fxFlex [deviceProfileId]="deviceProfileId">
</tb-alarm-rule> </tb-alarm-rule>
</div> </div>
<button *ngIf="!disabled" <button *ngIf="!disabled"

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

@ -29,6 +29,7 @@ import { AlarmRule, DeviceProfileAlarm, deviceProfileAlarmValidator } from '@sha
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { EntityId } from '@shared/models/id/entity-id';
@Component({ @Component({
selector: 'tb-device-profile-alarm', selector: 'tb-device-profile-alarm',
@ -60,6 +61,9 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
@Input() @Input()
expanded = false; expanded = false;
@Input()
deviceProfileId: EntityId;
private modelValue: DeviceProfileAlarm; private modelValue: DeviceProfileAlarm;
alarmFormGroup: FormGroup; alarmFormGroup: FormGroup;

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

@ -22,6 +22,7 @@
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-device-profile-alarm [formControl]="alarmControl" <tb-device-profile-alarm [formControl]="alarmControl"
[expanded]="$index === 0" [expanded]="$index === 0"
[deviceProfileId]="deviceProfileId"
(removeAlarm)="removeAlarm($index)"> (removeAlarm)="removeAlarm($index)">
</tb-device-profile-alarm> </tb-device-profile-alarm>
</div> </div>

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

@ -34,6 +34,7 @@ import { DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/
import { guid } from '@core/utils'; import { guid } from '@core/utils';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { EntityId } from '@shared/models/id/entity-id';
@Component({ @Component({
selector: 'tb-device-profile-alarms', selector: 'tb-device-profile-alarms',
@ -68,6 +69,9 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
deviceProfileId: EntityId;
private valueChangeSubscription: Subscription = null; private valueChangeSubscription: Subscription = null;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };

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

@ -116,7 +116,8 @@
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<tb-device-profile-alarms <tb-device-profile-alarms
formControlName="alarms"> formControlName="alarms"
[deviceProfileId]="deviceProfileId">
</tb-device-profile-alarms> </tb-device-profile-alarms>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel [expanded]="true"> <mat-expansion-panel [expanded]="true">

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

@ -39,6 +39,7 @@ import {
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { ServiceType } from '@shared/models/queue.models'; import { ServiceType } from '@shared/models/queue.models';
import { EntityId } from '@shared/models/id/entity-id';
@Component({ @Component({
selector: 'tb-device-profile', selector: 'tb-device-profile',
@ -66,6 +67,8 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
serviceType = ServiceType.TB_RULE_ENGINE; serviceType = ServiceType.TB_RULE_ENGINE;
deviceProfileId: EntityId;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected translate: TranslateService, protected translate: TranslateService,
@Optional() @Inject('entity') protected entityValue: DeviceProfile, @Optional() @Inject('entity') protected entityValue: DeviceProfile,
@ -83,6 +86,7 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
} }
buildForm(entity: DeviceProfile): FormGroup { buildForm(entity: DeviceProfile): FormGroup {
this.deviceProfileId = entity?.id ? entity.id : null;
this.displayProfileConfiguration = entity && entity.type && this.displayProfileConfiguration = entity && entity.type &&
deviceProfileTypeConfigurationInfoMap.get(entity.type).hasProfileConfiguration; deviceProfileTypeConfigurationInfoMap.get(entity.type).hasProfileConfiguration;
this.displayTransportConfiguration = entity && entity.transportType && this.displayTransportConfiguration = entity && entity.transportType &&
@ -157,6 +161,7 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> {
} }
updateForm(entity: DeviceProfile) { updateForm(entity: DeviceProfile) {
this.deviceProfileId = entity.id;
this.displayProfileConfiguration = entity.type && this.displayProfileConfiguration = entity.type &&
deviceProfileTypeConfigurationInfoMap.get(entity.type).hasProfileConfiguration; deviceProfileTypeConfigurationInfoMap.get(entity.type).hasProfileConfiguration;
this.displayTransportConfiguration = entity.transportType && this.displayTransportConfiguration = entity.transportType &&

20
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw

@ -178,17 +178,17 @@
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">--> <!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">-->
<!-- <button mat-button mat-raised-button color="primary"-->
<!-- type="submit"-->
<!-- [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty">-->
<!-- Save-->
<!-- </button>-->
<!-- <button mat-button color="primary"--> <!-- <button mat-button color="primary"-->
<!-- type="button"--> <!-- type="button"-->
<!-- [disabled]="(isLoading$ | async)"--> <!-- [disabled]="(isLoading$ | async)"-->
<!-- (click)="cancel()" cdkFocusInitial>--> <!-- (click)="cancel()" cdkFocusInitial>-->
<!-- Cancel--> <!-- Cancel-->
<!-- </button>--> <!-- </button>-->
<!-- <button mat-button mat-raised-button color="primary"-->
<!-- type="submit"-->
<!-- [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty">-->
<!-- Save-->
<!-- </button>-->
<!-- </div>--> <!-- </div>-->
<!--</form>--> <!--</form>-->
<!----> <!---->
@ -338,16 +338,16 @@
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
<!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">--> <!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">-->
<!-- <button mat-button mat-raised-button color="primary"-->
<!-- type="submit"-->
<!-- [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">-->
<!-- Create-->
<!-- </button>-->
<!-- <button mat-button color="primary"--> <!-- <button mat-button color="primary"-->
<!-- type="button"--> <!-- type="button"-->
<!-- [disabled]="(isLoading$ | async)"--> <!-- [disabled]="(isLoading$ | async)"-->
<!-- (click)="cancel()" cdkFocusInitial>--> <!-- (click)="cancel()" cdkFocusInitial>-->
<!-- Cancel--> <!-- Cancel-->
<!-- </button>--> <!-- </button>-->
<!-- <button mat-button mat-raised-button color="primary"-->
<!-- type="submit"-->
<!-- [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">-->
<!-- Create-->
<!-- </button>-->
<!-- </div>--> <!-- </div>-->
<!--</form>--> <!--</form>-->

7
ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts

@ -283,6 +283,13 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On
window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => { window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
if (init) { if (init) {
this.isShowCamera = true; this.isShowCamera = true;
if (this.availableVideoInputs.find((device) => device.deviceId === '')) {
PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
this.singleDevice = devices.length < 2;
this.availableVideoInputs = devices;
this.ctx.detectChanges();
});
}
} }
this.mediaStream = stream; this.mediaStream = stream;
this.videoElement.srcObject = stream; this.videoElement.srcObject = stream;

5
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html

@ -76,7 +76,7 @@
[required]="!createProfile" [required]="!createProfile"
[transportType]="deviceWizardFormGroup.get('transportType').value" [transportType]="deviceWizardFormGroup.get('transportType').value"
formControlName="deviceProfileId" formControlName="deviceProfileId"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}" [ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
(deviceProfileChanged)="$event?.transportType ? deviceWizardFormGroup.get('transportType').patchValue($event?.transportType) : {}" (deviceProfileChanged)="$event?.transportType ? deviceWizardFormGroup.get('transportType').patchValue($event?.transportType) : {}"
[addNewProfile]="false" [addNewProfile]="false"
[selectDefaultProfile]="true" [selectDefaultProfile]="true"
@ -133,7 +133,8 @@
{count: alarmRulesFormGroup.get('alarms').value ? {count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template> alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms <tb-device-profile-alarms
formControlName="alarms"> formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms> </tb-device-profile-alarms>
</form> </form>
</mat-step> </mat-step>

2
ui-ngx/src/app/modules/home/models/services.map.ts

@ -32,9 +32,11 @@ import { DashboardService } from '@core/http/dashboard.service';
import { UserService } from '@core/http/user.service'; import { UserService } from '@core/http/user.service';
import { AlarmService } from '@core/http/alarm.service'; import { AlarmService } from '@core/http/alarm.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { BroadcastService } from "@core/services/broadcast.service";
export const ServicesMap = new Map<string, Type<any>>( export const ServicesMap = new Map<string, Type<any>>(
[ [
['broadcastService', BroadcastService],
['deviceService', DeviceService], ['deviceService', DeviceService],
['alarmService', AlarmService], ['alarmService', AlarmService],
['assetService', AssetService], ['assetService', AssetService],

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

@ -46,7 +46,7 @@
}}" #alarmRules="matTab"> }}" #alarmRules="matTab">
<div class="mat-padding" [formGroup]="detailsForm"> <div class="mat-padding" [formGroup]="detailsForm">
<div formGroupName="profileData"> <div formGroupName="profileData">
<tb-device-profile-alarms formControlName="alarms"></tb-device-profile-alarms> <tb-device-profile-alarms formControlName="alarms" [deviceProfileId]="entity.id"></tb-device-profile-alarms>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>

21
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -1937,25 +1937,6 @@
} }
}, },
"language": { "language": {
"language": "Jazyk", "language": "Jazyk"
"locales": {
"de_DE": "Deutsch",
"fr_FR": "Français",
"zh_CN": "简体中文",
"zh_TW": "繁體中文",
"en_US": "English",
"it_IT": "Italiano",
"ko_KR": "한글",
"ru_RU": "Русский",
"es_ES": "Español",
"ja_JA": "日本語",
"tr_TR": "Türkçe",
"fa_IR": "فارسي",
"uk_UA": "Українська",
"cs_CZ": "Česky",
"el_GR": "Ελληνικά",
"ro_RO": "Română",
"lv_LV": "Latviešu"
}
} }
} }

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

@ -2494,7 +2494,8 @@
"ro_RO": "Română", "ro_RO": "Română",
"lv_LV": "Latviešu", "lv_LV": "Latviešu",
"ka_GE": "ქართული", "ka_GE": "ქართული",
"pt_BR": "Português do Brasil" "pt_BR": "Português do Brasil",
"sl_SI": "Slovenščina"
} }
} }
} }

2
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -266,7 +266,7 @@
"add-to-dashboard": "대시보드에 추가", "add-to-dashboard": "대시보드에 추가",
"add-widget-to-dashboard": "대시보드에 위젯 추가", "add-widget-to-dashboard": "대시보드에 위젯 추가",
"selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨", "selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨",
"selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨" "selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨",
"no-attributes-text": "아무 속성도 찾을 수 없습니다", "no-attributes-text": "아무 속성도 찾을 수 없습니다",
"no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다." "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다."
}, },

2479
ui-ngx/src/assets/locale/locale.constant-sl_SI.json

File diff suppressed because it is too large

2
ui-ngx/src/index.html

@ -24,6 +24,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="thingsboard.ico"> <link rel="icon" type="image/x-icon" href="thingsboard.ico">
<link rel="preload" href="assets/fonts/MaterialIcons-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
<link rel="stylesheet" href="assets/fonts/material-icons.css" />
<style type="text/css"> <style type="text/css">
body, html { body, html {

1
ui-ngx/src/styles.scss

@ -15,7 +15,6 @@
*/ */
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
@import '~material-design-icons/iconfont/material-icons.css';
@import '~typeface-roboto/index.css'; @import '~typeface-roboto/index.css';
@import '~font-awesome/css/font-awesome.min.css'; @import '~font-awesome/css/font-awesome.min.css';
@import 'theme.scss'; @import 'theme.scss';

Loading…
Cancel
Save