Browse Source

Merged with master

pull/10011/head
Ivan Raznatovskyi 3 years ago
parent
commit
bac366b720
  1. 18
      .github/ISSUE_TEMPLATE/---bug-report.md
  2. 4
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 10
      .github/ISSUE_TEMPLATE/question.md
  4. 2
      application/src/main/data/json/system/widget_types/single_switch.json
  5. 14
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  6. 8
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  7. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
  8. 14
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  9. 6
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java
  10. 2
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  11. 6
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java
  12. 10
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
  13. 2
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
  14. 44
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
  15. 2
      dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
  16. 23
      dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java
  17. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  18. 43
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java
  19. 26
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java
  20. 33
      dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
  21. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
  22. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  23. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java
  24. 2
      dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java
  25. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  26. 33
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html
  27. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts
  28. 659
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts
  29. 626
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widget.models.ts
  30. 2
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
  31. 81
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts
  32. 54
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts
  33. 114
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html
  34. 182
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts
  35. 62
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts
  36. 96
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html
  37. 182
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts
  38. 68
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts
  39. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-button.component.html
  40. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-button.scss
  41. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-panel.component.scss
  42. 176
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.ts
  43. 180
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.ts
  44. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  45. 33
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html
  46. 2
      ui-ngx/src/app/shared/components/image/image-dialog.component.ts
  47. 128
      ui-ngx/src/app/shared/models/action-widget-settings.models.ts
  48. 125
      ui-ngx/src/app/shared/models/rpc-widget-settings.models.ts
  49. 110
      ui-ngx/src/assets/locale/locale.constant-en_US.json

18
.github/ISSUE_TEMPLATE/---bug-report.md

@ -1,9 +1,9 @@
--- ---
name: "\U0001F41E Bug report" name: "\U0001F41E Bug report"
about: Create a report to help us improve about: Create a report to help us improve
title: "[Bug] " title: "Your title here"
labels: bug labels: ['bug', 'unconfirmed']
assignees: ashvayka, vvlladd28 assignees: AndriichnekoDm
--- ---
@ -12,11 +12,13 @@ A clear and concise description of what the bug is.
**Your Server Environment** **Your Server Environment**
<!-- 🔅🔅🔅🔅🔅🔅🔅 Choose one of the following or write your own 🔅🔅🔅🔅🔅🔅🔅--> <!-- 🔅🔅🔅🔅🔅🔅🔅 Choose one of the following or write your own 🔅🔅🔅🔅🔅🔅🔅-->
* demo.thingsboard.io * [https://demo.thingsboard.io](demo.thingsboard.io)
* cloud.thingsboard.io * [https://thingsboard.cloud](thingsboard.cloud)
* own setup * own setup
* cloud or local infrastructure or docker deployment * Deployment: monolith or microservices
* Deployment type: deb, rpm, exe, docker-compose, k8s, ami
* ThingsBoard Version * ThingsBoard Version
* Community or Professional Edition
* OS Name and Version * OS Name and Version
**Your Client Environment** **Your Client Environment**
@ -54,7 +56,7 @@ Steps to reproduce the behavior:
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, please add screenshots to help explain your problem.
**Additional context** **Additional context**
Add any other context about the problem here. Please feel free to add any other context about the problem here.

4
.github/ISSUE_TEMPLATE/feature_request.md

@ -1,8 +1,8 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: "[Feature Request]" title: "Your title here"
labels: ['Feature'] labels: ['feature']
assignees: 'AndriichnekoDm' assignees: 'AndriichnekoDm'
--- ---

10
.github/ISSUE_TEMPLATE/question.md

@ -1,9 +1,9 @@
--- ---
name: Question name: Question
about: Describe your questions in details about: Describe your questions in detail
title: "[Question] your title here" title: "Your title here"
labels: question labels: ['question']
assignees: ashvayka assignees: 'AndriichnekoDm'
--- ---
@ -16,7 +16,7 @@ assignees: ashvayka
* Generic * Generic
**Description** **Description**
A clear and concise details. Clear and concise details.
**Environment** **Environment**
<!-- Add information about your environment and ThingsBoard version if applicable --> <!-- Add information about your environment and ThingsBoard version if applicable -->

2
application/src/main/data/json/system/widget_types/single_switch.json

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<tb-single-switch-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-single-switch-widget>", "templateHtml": "<tb-single-switch-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-single-switch-widget>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.rpcWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '80px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '80px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-single-switch-widget-settings", "settingsDirective": "tb-single-switch-widget-settings",

14
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -16,14 +16,13 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
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.dao.DataAccessException;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@ -376,9 +375,14 @@ public abstract class BaseController {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL); return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) { } else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else { } else if (exception instanceof DataAccessException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL); String errorType = exception.getClass().getSimpleName();
if (!logControllerErrorStackTrace) { // not to log the error twice
log.warn("Database error: {} - {}", errorType, ExceptionUtils.getRootCauseMessage(exception));
}
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
} }
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
} }
/** /**

8
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -37,15 +36,16 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.TopicService;
import org.thingsboard.server.queue.discovery.event.OtherServiceShutdownEvent; import org.thingsboard.server.queue.discovery.event.OtherServiceShutdownEvent;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.state.DefaultDeviceStateService;
@ -154,7 +154,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
entitySubscriptions.values().removeIf(sub -> entitySubscriptions.values().removeIf(sub ->
!partitionService.resolve(ServiceType.TB_CORE, sub.getTenantId(), sub.getEntityId()).isMyPartition()); !partitionService.isMyPartition(ServiceType.TB_CORE, sub.getTenantId(), sub.getEntityId()));
} }
} }

2
common/dao-api/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java

@ -38,7 +38,7 @@ public interface AuditLogService {
PageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, List<ActionType> actionTypes, TimePageLink pageLink); PageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, List<ActionType> actionTypes, TimePageLink pageLink);
<E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction( <E extends HasName, I extends EntityId> ListenableFuture<Void> logEntityAction(
TenantId tenantId, TenantId tenantId,
CustomerId customerId, CustomerId customerId,
UserId userId, UserId userId,

14
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -251,7 +251,12 @@ public class HashPartitionService implements PartitionService {
@Override @Override
public boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId) { public boolean isMyPartition(ServiceType serviceType, TenantId tenantId, EntityId entityId) {
return resolve(serviceType, tenantId, entityId).isMyPartition(); try {
return resolve(serviceType, tenantId, entityId).isMyPartition();
} catch (TenantNotFoundException e) {
log.warn("Tenant with id {} not found", tenantId, new RuntimeException("stacktrace"));
return false;
}
} }
private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) { private TopicPartitionInfo resolve(QueueKey queueKey, EntityId entityId) {
@ -376,7 +381,12 @@ public class HashPartitionService implements PartitionService {
.collect(Collectors.toList())) .collect(Collectors.toList()))
.collect(Collectors.joining(System.lineSeparator()))); .collect(Collectors.joining(System.lineSeparator())));
} }
applicationEventPublisher.publishEvent(new PartitionChangeEvent(this, serviceType, partitionsMap)); PartitionChangeEvent event = new PartitionChangeEvent(this, serviceType, partitionsMap);
try {
applicationEventPublisher.publishEvent(event);
} catch (Exception e) {
log.error("Failed to publish partition change event {}", event, e);
}
} }
@Override @Override

6
common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java

@ -41,7 +41,11 @@ public abstract class TbApplicationEventListener<T extends TbApplicationEvent> i
seqNumberLock.unlock(); seqNumberLock.unlock();
} }
if (validUpdate && filterTbApplicationEvent(event)) { if (validUpdate && filterTbApplicationEvent(event)) {
onTbApplicationEvent(event); try {
onTbApplicationEvent(event);
} catch (Exception e) {
log.error("Failed to handle partition change event: {}", event, e);
}
} else { } else {
log.info("Application event ignored due to invalid sequence number ({} > {}). Event: {}", lastProcessedSequenceNumber, event.getSequenceNumber(), event); log.info("Application event ignored due to invalid sequence number ({} > {}). Event: {}", lastProcessedSequenceNumber, event.getSequenceNumber(), event);
} }

2
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java

@ -160,7 +160,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
@Override @Override
protected void onTbApplicationEvent(PartitionChangeEvent event) { protected void onTbApplicationEvent(PartitionChangeEvent event) {
for (TenantId tenantId : vcService.getActiveRepositoryTenants()) { for (TenantId tenantId : vcService.getActiveRepositoryTenants()) {
if (!partitionService.resolve(ServiceType.TB_VC_EXECUTOR, tenantId, tenantId).isMyPartition()) { if (!partitionService.isMyPartition(ServiceType.TB_VC_EXECUTOR, tenantId, tenantId)) {
var lock = getRepoLock(tenantId); var lock = getRepoLock(tenantId);
lock.lock(); lock.lock();
try { try {

6
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.alarm;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -29,13 +28,10 @@ import java.util.UUID;
public interface AlarmCommentDao extends Dao<AlarmComment> { public interface AlarmCommentDao extends Dao<AlarmComment> {
AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment);
void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId);
AlarmComment findAlarmCommentById(TenantId tenantId, UUID key); AlarmComment findAlarmCommentById(TenantId tenantId, UUID key);
PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink); PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink);
ListenableFuture<AlarmComment> findAlarmCommentByIdAsync(TenantId tenantId, UUID key); ListenableFuture<AlarmComment> findAlarmCommentByIdAsync(TenantId tenantId, UUID key);
} }

10
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.dao.alarm; package org.thingsboard.server.dao.alarm;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -35,8 +34,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import java.util.UUID;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
@Service @Service
@ -101,12 +98,7 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
if (alarmComment.getType() == null) { if (alarmComment.getType() == null) {
alarmComment.setType(AlarmCommentType.OTHER); alarmComment.setType(AlarmCommentType.OTHER);
} }
if (alarmComment.getId() == null) { return alarmCommentDao.save(tenantId, alarmComment);
UUID uuid = Uuids.timeBased();
alarmComment.setId(new AlarmCommentId(uuid));
alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid));
}
return alarmCommentDao.createAlarmComment(tenantId, alarmComment);
} }
private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) { private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) {

2
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java

@ -30,8 +30,6 @@ import java.util.UUID;
public interface AuditLogDao extends Dao<AuditLog> { public interface AuditLogDao extends Dao<AuditLog> {
ListenableFuture<Void> saveByTenantId(AuditLog auditLog);
PageData<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink); PageData<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink);
PageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink); PageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, List<ActionType> actionTypes, TimePageLink pageLink);

44
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java

@ -15,11 +15,9 @@
*/ */
package org.thingsboard.server.dao.audit; package org.thingsboard.server.dao.audit;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -34,7 +32,6 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionStatus;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.id.AuditLogId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
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;
@ -50,11 +47,11 @@ import org.thingsboard.server.dao.audit.sink.AuditLogSink;
import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.dao.service.Validator.validateEntityId; import static org.thingsboard.server.dao.service.Validator.validateEntityId;
@ -66,7 +63,6 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
public class AuditLogServiceImpl implements AuditLogService { public class AuditLogServiceImpl implements AuditLogService {
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final int INSERTS_PER_ENTRY = 3;
@Autowired @Autowired
private AuditLogLevelFilter auditLogLevelFilter; private AuditLogLevelFilter auditLogLevelFilter;
@ -80,6 +76,9 @@ public class AuditLogServiceImpl implements AuditLogService {
@Autowired @Autowired
private AuditLogSink auditLogSink; private AuditLogSink auditLogSink;
@Autowired
private JpaExecutorService executor;
@Autowired @Autowired
private DataValidator<AuditLog> auditLogValidator; private DataValidator<AuditLog> auditLogValidator;
@ -115,7 +114,7 @@ public class AuditLogServiceImpl implements AuditLogService {
} }
@Override @Override
public <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> public <E extends HasName, I extends EntityId> ListenableFuture<Void>
logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,
ActionType actionType, Exception e, Object... additionalInfo) { ActionType actionType, Exception e, Object... additionalInfo) {
if (canLog(entityId.getEntityType(), actionType)) { if (canLog(entityId.getEntityType(), actionType)) {
@ -370,9 +369,6 @@ public class AuditLogServiceImpl implements AuditLogService {
ActionStatus actionStatus, ActionStatus actionStatus,
String actionFailureDetails) { String actionFailureDetails) {
AuditLog result = new AuditLog(); AuditLog result = new AuditLog();
UUID id = Uuids.timeBased();
result.setId(new AuditLogId(id));
result.setCreatedTime(Uuids.unixTimestamp(id));
result.setTenantId(tenantId); result.setTenantId(tenantId);
result.setEntityId(entityId); result.setEntityId(entityId);
result.setEntityName(entityName); result.setEntityName(entityName);
@ -386,16 +382,16 @@ public class AuditLogServiceImpl implements AuditLogService {
return result; return result;
} }
private ListenableFuture<List<Void>> logAction(TenantId tenantId, private ListenableFuture<Void> logAction(TenantId tenantId,
EntityId entityId, EntityId entityId,
String entityName, String entityName,
CustomerId customerId, CustomerId customerId,
UserId userId, UserId userId,
String userName, String userName,
ActionType actionType, ActionType actionType,
JsonNode actionData, JsonNode actionData,
ActionStatus actionStatus, ActionStatus actionStatus,
String actionFailureDetails) { String actionFailureDetails) {
AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName, AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName,
actionType, actionData, actionStatus, actionFailureDetails); actionType, actionData, actionStatus, actionFailureDetails);
log.trace("Executing logAction [{}]", auditLogEntry); log.trace("Executing logAction [{}]", auditLogEntry);
@ -408,12 +404,12 @@ public class AuditLogServiceImpl implements AuditLogService {
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }
} }
List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
futures.add(auditLogDao.saveByTenantId(auditLogEntry));
auditLogSink.logAction(auditLogEntry); return executor.submit(() -> {
AuditLog auditLog = auditLogDao.save(tenantId, auditLogEntry);
return Futures.allAsList(futures); auditLogSink.logAction(auditLog);
return null;
});
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java

@ -55,7 +55,7 @@ public class DummyAuditLogServiceImpl implements AuditLogService {
} }
@Override @Override
public <E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) { public <E extends HasName, I extends EntityId> ListenableFuture<Void> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
return null; return null;
} }

23
dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java

@ -34,14 +34,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Component @Component
@ConditionalOnProperty(prefix = "audit-log.sink", value = "type", havingValue = "elasticsearch") @ConditionalOnProperty(prefix = "audit-log.sink", value = "type", havingValue = "elasticsearch")
@ -68,6 +72,7 @@ public class ElasticsearchAuditLogSink implements AuditLogSink {
private String dateFormat; private String dateFormat;
private RestClient restClient; private RestClient restClient;
private ExecutorService executor;
@PostConstruct @PostConstruct
public void init() { public void init() {
@ -87,14 +92,32 @@ public class ElasticsearchAuditLogSink implements AuditLogSink {
} }
this.restClient = builder.build(); this.restClient = builder.build();
this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("elasticsearch-audit-log"));
} catch (Exception e) { } catch (Exception e) {
log.error("Sink init failed!", e); log.error("Sink init failed!", e);
throw new RuntimeException(e.getMessage(), e); throw new RuntimeException(e.getMessage(), e);
} }
} }
@PreDestroy
private void destroy() {
if (executor != null) {
executor.shutdownNow();
}
}
@Override @Override
public void logAction(AuditLog auditLogEntry) { public void logAction(AuditLog auditLogEntry) {
executor.execute(() -> {
try {
doLogAction(auditLogEntry);
} catch (Exception e) {
log.error("Failed to log action", e);
}
});
}
private void doLogAction(AuditLog auditLogEntry) {
String jsonContent = createElasticJsonRecord(auditLogEntry); String jsonContent = createElasticJsonRecord(auditLogEntry);
HttpEntity entity = new NStringEntity( HttpEntity entity = new NStringEntity(

16
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java

@ -27,6 +27,8 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -45,9 +47,6 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
protected abstract JpaRepository<E, UUID> getRepository(); protected abstract JpaRepository<E, UUID> getRepository();
protected void setSearchText(E entity) {
}
@Override @Override
@Transactional @Transactional
public D save(TenantId tenantId, D domain) { public D save(TenantId tenantId, D domain) {
@ -58,17 +57,21 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
log.error("Can't create entity for domain object {}", domain, e); log.error("Can't create entity for domain object {}", domain, e);
throw new IllegalArgumentException("Can't create entity for domain object {" + domain + "}", e); throw new IllegalArgumentException("Can't create entity for domain object {" + domain + "}", e);
} }
setSearchText(entity);
log.debug("Saving entity {}", entity); log.debug("Saving entity {}", entity);
if (entity.getUuid() == null) { boolean isNew = entity.getUuid() == null;
if (isNew) {
UUID uuid = Uuids.timeBased(); UUID uuid = Uuids.timeBased();
entity.setUuid(uuid); entity.setUuid(uuid);
entity.setCreatedTime(Uuids.unixTimestamp(uuid)); entity.setCreatedTime(Uuids.unixTimestamp(uuid));
} }
entity = getRepository().save(entity); entity = doSave(entity, isNew);
return DaoUtil.getData(entity); return DaoUtil.getData(entity);
} }
protected E doSave(E entity, boolean isNew) {
return getRepository().save(entity);
}
@Override @Override
@Transactional @Transactional
public D saveAndFlush(TenantId tenantId, D domain) { public D saveAndFlush(TenantId tenantId, D domain) {
@ -121,4 +124,5 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
List<E> entities = Lists.newArrayList(getRepository().findAll()); List<E> entities = Lists.newArrayList(getRepository().findAll());
return DaoUtil.convertDataList(entities); return DaoUtil.convertDataList(entities);
} }
} }

43
dao/src/main/java/org/thingsboard/server/dao/sql/JpaPartitionedAbstractDao.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.util.SqlDao;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@SqlDao
public abstract class JpaPartitionedAbstractDao<E extends BaseEntity<D>, D> extends JpaAbstractDao<E, D> {
@PersistenceContext
private EntityManager entityManager;
@Override
protected E doSave(E entity, boolean isNew) {
createPartition(entity);
if (isNew) {
entityManager.persist(entity);
} else {
entity = entityManager.merge(entity);
}
return entity;
}
public abstract void createPartition(E entity);
}

26
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java

@ -24,7 +24,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -32,7 +31,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmCommentDao; import org.thingsboard.server.dao.alarm.AlarmCommentDao;
import org.thingsboard.server.dao.model.sql.AlarmCommentEntity; import org.thingsboard.server.dao.model.sql.AlarmCommentEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
@ -45,7 +44,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_TABL
@Component @Component
@SqlDao @SqlDao
@RequiredArgsConstructor @RequiredArgsConstructor
public class JpaAlarmCommentDao extends JpaAbstractDao<AlarmCommentEntity, AlarmComment> implements AlarmCommentDao { public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao<AlarmCommentEntity, AlarmComment> implements AlarmCommentDao {
private final SqlPartitioningRepository partitioningRepository; private final SqlPartitioningRepository partitioningRepository;
@Value("${sql.alarm_comments.partition_size:168}") @Value("${sql.alarm_comments.partition_size:168}")
private int partitionSizeInHours; private int partitionSizeInHours;
@ -54,21 +53,7 @@ public class JpaAlarmCommentDao extends JpaAbstractDao<AlarmCommentEntity, Alarm
private AlarmCommentRepository alarmCommentRepository; private AlarmCommentRepository alarmCommentRepository;
@Override @Override
public AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment){ public PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink) {
log.trace("Saving entity {}", alarmComment);
partitioningRepository.createPartitionIfNotExists(ALARM_COMMENT_TABLE_NAME, alarmComment.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
AlarmCommentEntity saved = alarmCommentRepository.save(new AlarmCommentEntity(alarmComment));
return DaoUtil.getData(saved);
}
@Override
public void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId){
log.trace("Try to delete entity alarm comment by id using [{}]", alarmCommentId);
alarmCommentRepository.deleteById(alarmCommentId.getId());
}
@Override
public PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink){
log.trace("Try to find alarm comments by alarm id using [{}]", id); log.trace("Try to find alarm comments by alarm id using [{}]", id);
return DaoUtil.toPageData( return DaoUtil.toPageData(
alarmCommentRepository.findAllByAlarmId(id.getId(), DaoUtil.toPageable(pageLink))); alarmCommentRepository.findAllByAlarmId(id.getId(), DaoUtil.toPageable(pageLink)));
@ -86,6 +71,11 @@ public class JpaAlarmCommentDao extends JpaAbstractDao<AlarmCommentEntity, Alarm
return findByIdAsync(tenantId, key); return findByIdAsync(tenantId, key);
} }
@Override
public void createPartition(AlarmCommentEntity entity) {
partitioningRepository.createPartitionIfNotExists(ALARM_COMMENT_TABLE_NAME, entity.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
@Override @Override
protected Class<AlarmCommentEntity> getEntityClass() { protected Class<AlarmCommentEntity> getEntityClass() {
return AlarmCommentEntity.class; return AlarmCommentEntity.class;

33
dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java

@ -15,8 +15,6 @@
*/ */
package org.thingsboard.server.dao.sql.audit; package org.thingsboard.server.dao.sql.audit;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -25,10 +23,8 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.id.AuditLogId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
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.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
@ -36,12 +32,11 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.audit.AuditLogDao; import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.sql.AuditLogEntity; import org.thingsboard.server.dao.model.sql.AuditLogEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -49,7 +44,7 @@ import java.util.concurrent.TimeUnit;
@SqlDao @SqlDao
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> implements AuditLogDao { public class JpaAuditLogDao extends JpaPartitionedAbstractDao<AuditLogEntity, AuditLog> implements AuditLogDao {
private final AuditLogRepository auditLogRepository; private final AuditLogRepository auditLogRepository;
private final SqlPartitioningRepository partitioningRepository; private final SqlPartitioningRepository partitioningRepository;
@ -72,25 +67,6 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
return auditLogRepository; return auditLogRepository;
} }
@Override
public ListenableFuture<Void> saveByTenantId(AuditLog auditLog) {
return service.submit(() -> {
save(auditLog.getTenantId(), auditLog);
return null;
});
}
@Override
public AuditLog save(TenantId tenantId, AuditLog auditLog) {
if (auditLog.getId() == null) {
UUID uuid = Uuids.timeBased();
auditLog.setId(new AuditLogId(uuid));
auditLog.setCreatedTime(Uuids.unixTimestamp(uuid));
}
partitioningRepository.createPartitionIfNotExists(TABLE_NAME, auditLog.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
return super.save(tenantId, auditLog);
}
@Override @Override
public PageData<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink) { public PageData<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink) {
return DaoUtil.toPageData( return DaoUtil.toPageData(
@ -182,4 +158,9 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
jdbcTemplate.update("CALL migrate_audit_logs(?, ?, ?)", startTime, endTime, partitionSizeInMs); jdbcTemplate.update("CALL migrate_audit_logs(?, ?, ?)", startTime, endTime, partitionSizeInMs);
} }
@Override
public void createPartition(AuditLogEntity entity) {
partitioningRepository.createPartitionIfNotExists(TABLE_NAME, entity.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
} }

16
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java

@ -35,7 +35,7 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.edge.EdgeEventDao; import org.thingsboard.server.dao.edge.EdgeEventDao;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.sql.EdgeEventEntity; import org.thingsboard.server.dao.model.sql.EdgeEventEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
@ -47,7 +47,6 @@ import javax.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
@ -58,7 +57,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@SqlDao @SqlDao
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEvent> implements EdgeEventDao { public class JpaBaseEdgeEventDao extends JpaPartitionedAbstractDao<EdgeEventEntity, EdgeEvent> implements EdgeEventDao {
private final UUID systemTenantId = NULL_UUID; private final UUID systemTenantId = NULL_UUID;
@ -151,8 +150,9 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
if (StringUtils.isEmpty(edgeEvent.getUid())) { if (StringUtils.isEmpty(edgeEvent.getUid())) {
edgeEvent.setUid(edgeEvent.getId().toString()); edgeEvent.setUid(edgeEvent.getId().toString());
} }
partitioningRepository.createPartitionIfNotExists(TABLE_NAME, edgeEvent.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours)); EdgeEventEntity entity = new EdgeEventEntity(edgeEvent);
return save(new EdgeEventEntity(edgeEvent)); createPartition(entity);
return save(entity);
} }
private ListenableFuture<Void> save(EdgeEventEntity entity) { private ListenableFuture<Void> save(EdgeEventEntity entity) {
@ -227,4 +227,10 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
private void callMigrationFunction(long startTime, long endTime, long partitionSIzeInMs) { private void callMigrationFunction(long startTime, long endTime, long partitionSIzeInMs) {
jdbcTemplate.update("CALL migrate_edge_event(?, ?, ?)", startTime, endTime, partitionSIzeInMs); jdbcTemplate.update("CALL migrate_edge_event(?, ?, ?)", startTime, endTime, partitionSIzeInMs);
} }
@Override
public void createPartition(EdgeEventEntity entity) {
partitioningRepository.createPartitionIfNotExists(TABLE_NAME, entity.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
} }

24
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java

@ -15,8 +15,6 @@
*/ */
package org.thingsboard.server.dao.sql.notification; package org.thingsboard.server.dao.sql.notification;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.base.Strings;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
@ -34,7 +32,7 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.sql.NotificationEntity; import org.thingsboard.server.dao.model.sql.NotificationEntity;
import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.notification.NotificationDao;
import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
@ -44,7 +42,7 @@ import java.util.concurrent.TimeUnit;
@Component @Component
@SqlDao @SqlDao
@RequiredArgsConstructor @RequiredArgsConstructor
public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notification> implements NotificationDao { public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEntity, Notification> implements NotificationDao {
private final NotificationRepository notificationRepository; private final NotificationRepository notificationRepository;
private final SqlPartitioningRepository partitioningRepository; private final SqlPartitioningRepository partitioningRepository;
@ -52,18 +50,6 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
@Value("${sql.notifications.partition_size:168}") @Value("${sql.notifications.partition_size:168}")
private int partitionSizeInHours; private int partitionSizeInHours;
@Override
public Notification save(TenantId tenantId, Notification notification) {
if (notification.getId() == null) {
UUID uuid = Uuids.timeBased();
notification.setId(new NotificationId(uuid));
notification.setCreatedTime(Uuids.unixTimestamp(uuid));
partitioningRepository.createPartitionIfNotExists(ModelConstants.NOTIFICATION_TABLE_NAME,
notification.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
return super.save(tenantId, notification);
}
@Override @Override
public PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) { public PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ, return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ,
@ -114,6 +100,12 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
notificationRepository.deleteByRecipientId(recipientId.getId()); notificationRepository.deleteByRecipientId(recipientId.getId());
} }
@Override
public void createPartition(NotificationEntity entity) {
partitioningRepository.createPartitionIfNotExists(ModelConstants.NOTIFICATION_TABLE_NAME,
entity.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
@Override @Override
protected Class<NotificationEntity> getEntityClass() { protected Class<NotificationEntity> getEntityClass() {
return NotificationEntity.class; return NotificationEntity.class;

2
dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageInfoDao.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.OtaPackageId;
@ -58,6 +59,7 @@ public class JpaOtaPackageInfoDao extends JpaAbstractDao<OtaPackageInfoEntity, O
return DaoUtil.getData(otaPackageInfoRepository.findOtaPackageInfoById(id)); return DaoUtil.getData(otaPackageInfoRepository.findOtaPackageInfoById(id));
} }
@Transactional
@Override @Override
public OtaPackageInfo save(TenantId tenantId, OtaPackageInfo otaPackageInfo) { public OtaPackageInfo save(TenantId tenantId, OtaPackageInfo otaPackageInfo) {
OtaPackageInfo savedOtaPackage = super.save(tenantId, otaPackageInfo); OtaPackageInfo savedOtaPackage = super.save(tenantId, otaPackageInfo);

2
dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java

@ -85,6 +85,6 @@ public class JpaAlarmCommentDaoTest extends AbstractJpaDaoTest {
alarmComment.setUserId(new UserId(userId)); alarmComment.setUserId(new UserId(userId));
alarmComment.setType(type); alarmComment.setType(type);
alarmComment.setComment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))); alarmComment.setComment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10)));
alarmCommentDao.createAlarmComment(TenantId.fromUUID(UUID.randomUUID()), alarmComment); alarmCommentDao.save(TenantId.fromUUID(UUID.randomUUID()), alarmComment);
} }
} }

8
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -92,7 +92,7 @@ import {
BarChartWithLabelsBasicConfigComponent BarChartWithLabelsBasicConfigComponent
} from '@home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component'; } from '@home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component';
import { import {
SingSwitchBasicConfigComponent SingleSwitchBasicConfigComponent
} from '@home/components/widget/config/basic/rpc/single-switch-basic-config.component'; } from '@home/components/widget/config/basic/rpc/single-switch-basic-config.component';
@NgModule({ @NgModule({
@ -123,7 +123,7 @@ import {
DoughnutBasicConfigComponent, DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent, RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingSwitchBasicConfigComponent SingleSwitchBasicConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -157,7 +157,7 @@ import {
DoughnutBasicConfigComponent, DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent, RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingSwitchBasicConfigComponent SingleSwitchBasicConfigComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
@ -185,5 +185,5 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-doughnut-basic-config': DoughnutBasicConfigComponent, 'tb-doughnut-basic-config': DoughnutBasicConfigComponent,
'tb-range-chart-basic-config': RangeChartBasicConfigComponent, 'tb-range-chart-basic-config': RangeChartBasicConfigComponent,
'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent, 'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent,
'tb-single-switch-basic-config': SingSwitchBasicConfigComponent 'tb-single-switch-basic-config': SingleSwitchBasicConfigComponent
}; };

33
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html

@ -20,30 +20,31 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div> <div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div> <div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.initial-state-hint' | translate}}" translate>widgets.value-action.initial-state</div>
<tb-rpc-initial-state-settings fxFlex <tb-get-value-action-settings fxFlex
[stateValueType]="valueType.BOOLEAN" panelTitle="widgets.value-action.initial-state"
[aliasController]="aliasController" [valueType]="valueType.BOOLEAN"
[targetDevice]="targetDevice" [aliasController]="aliasController"
formControlName="initialState"></tb-rpc-initial-state-settings> [targetDevice]="targetDevice"
formControlName="initialState"></tb-get-value-action-settings>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-on-hint' | translate}}" translate>widgets.rpc-state.turn-on</div> <div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-on-hint' | translate}}" translate>widgets.value-action.turn-on</div>
<tb-rpc-update-state-settings fxFlex <tb-set-value-action-settings fxFlex
panelTitle="widgets.rpc-state.turn-on" panelTitle="widgets.value-action.turn-on"
[stateValueType]="valueType.BOOLEAN" [valueType]="valueType.BOOLEAN"
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
formControlName="onUpdateState"></tb-rpc-update-state-settings> formControlName="onUpdateState"></tb-set-value-action-settings>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-off-hint' | translate}}" translate>widgets.rpc-state.turn-off</div> <div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-off-hint' | translate}}" translate>widgets.value-action.turn-off</div>
<tb-rpc-update-state-settings fxFlex <tb-set-value-action-settings fxFlex
panelTitle="widgets.rpc-state.turn-off" panelTitle="widgets.value-action.turn-off"
[stateValueType]="valueType.BOOLEAN" [valueType]="valueType.BOOLEAN"
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
formControlName="offUpdateState"></tb-rpc-update-state-settings> formControlName="offUpdateState"></tb-set-value-action-settings>
</div> </div>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">

2
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts

@ -37,7 +37,7 @@ import { ValueType } from '@shared/models/constants';
templateUrl: './single-switch-basic-config.component.html', templateUrl: './single-switch-basic-config.component.html',
styleUrls: ['../basic-config.scss'] styleUrls: ['../basic-config.scss']
}) })
export class SingSwitchBasicConfigComponent extends BasicWidgetConfigComponent { export class SingleSwitchBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice { get targetDevice(): TargetDevice {
return this.singleSwitchWidgetConfigForm.get('targetDevice').value; return this.singleSwitchWidgetConfigForm.get('targetDevice').value;

659
ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts

@ -0,0 +1,659 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AttributeData,
AttributeScope,
LatestTelemetry,
TelemetrySubscriber,
TelemetryType,
telemetryTypeTranslationsShort
} from '@shared/models/telemetry/telemetry.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { BehaviorSubject, forkJoin, Observable, Observer, of, throwError } from 'rxjs';
import { catchError, delay, map, share, take } from 'rxjs/operators';
import { UtilsService } from '@core/services/utils.service';
import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import {
DataToValueSettings,
DataToValueType,
GetAttributeValueSettings,
GetTelemetryValueSettings,
GetValueAction,
GetValueSettings,
RpcSettings,
SetAttributeValueSettings,
SetValueAction,
SetValueSettings,
TelemetryValueSettings,
ValueActionSettings,
ValueToDataSettings,
ValueToDataType
} from '@shared/models/action-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
@Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export abstract class BasicActionWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
private loadingSubject = new BehaviorSubject(false);
private valueGetters: ValueGetter<any>[] = [];
private valueActions: ValueAction[] = [];
loading$ = this.loadingSubject.asObservable().pipe(share());
error = '';
protected constructor(protected cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.actionWidget = this;
}
ngAfterViewInit(): void {
const getValueObservables: Array<Observable<any>> = [];
this.valueGetters.forEach(valueGetter => {
getValueObservables.push(valueGetter.getValue());
});
this.loadingSubject.next(true);
forkJoin(getValueObservables).subscribe(
{
next: () => {
this.loadingSubject.next(false);
},
error: () => {
this.loadingSubject.next(false);
}
}
);
}
ngOnDestroy() {
this.valueActions.forEach(v => v.destroy());
}
public onInit() {
}
public clearError() {
this.error = '';
this.cd.markForCheck();
}
protected createValueGetter<V>(getValueSettings: GetValueSettings<V>,
valueType: ValueType,
valueObserver?: Partial<Observer<V>>): ValueGetter<V> {
const observer: Partial<Observer<V>> = {
next: (value: V) => {
if (valueObserver?.next) {
valueObserver.next(value);
}
},
error: (err: any) => {
this.onError(err);
if (valueObserver?.error) {
valueObserver.error(err);
}
}
};
const valueGetter = ValueGetter.fromSettings(this.ctx, getValueSettings, valueType, observer);
this.valueGetters.push(valueGetter);
this.valueActions.push(valueGetter);
return valueGetter;
}
protected createValueSetter<V>(setValueSettings: SetValueSettings): ValueSetter<V> {
const valueSetter = ValueSetter.fromSettings<V>(this.ctx, setValueSettings);
this.valueActions.push(valueSetter);
return valueSetter;
}
private onError(error: string) {
this.error = error;
this.cd.markForCheck();
}
protected updateValue<V>(valueSetter: ValueSetter<V>,
value: V,
setValueObserver?: Partial<Observer<void>>): void {
this.clearError();
this.loadingSubject.next(true);
valueSetter.setValue(value).subscribe({
next: () => {
if (setValueObserver?.next) {
setValueObserver.next();
}
this.loadingSubject.next(false);
},
error: (err) => {
this.loadingSubject.next(false);
if (setValueObserver?.error) {
setValueObserver.error(err);
}
const message = parseError(this.ctx, err);
this.onError(message);
}
});
}
}
type DataToValueFunction<V> = (data: any) => V;
export class DataToValueConverter<V> {
private readonly dataToValueFunction: DataToValueFunction<V>;
private readonly compareToValue: any;
constructor(private settings: DataToValueSettings,
private valueType: ValueType) {
this.compareToValue = settings.compareToValue;
switch (settings.type) {
case DataToValueType.FUNCTION:
try {
this.dataToValueFunction = new Function('data', settings.dataToValueFunction) as DataToValueFunction<V>;
} catch (e) {
this.dataToValueFunction = (data) => data;
}
break;
case DataToValueType.NONE:
break;
}
}
dataToValue(data: any): V {
let result: V;
switch (this.settings.type) {
case DataToValueType.FUNCTION:
result = data;
try {
result = this.dataToValueFunction(!!data ? JSON.parse(data) : data);
} catch (e) {}
break;
case DataToValueType.NONE:
result = data;
break;
}
if (this.valueType === ValueType.BOOLEAN) {
result = (result === this.compareToValue) as any;
}
return result;
}
}
export abstract class ValueAction {
protected constructor(protected ctx: WidgetContext,
protected settings: ValueActionSettings) {}
protected handleError(err: any): Error {
const reason = parseError(this.ctx, err);
let errorMessage = this.ctx.translate.instant('widgets.value-action.error.failed-to-perform-action',
{actionLabel: this.settings.actionLabel});
if (reason) {
errorMessage += '<br>' + reason;
}
return new Error(errorMessage);
}
destroy(): void {}
}
export abstract class ValueGetter<V> extends ValueAction {
static fromSettings<V>(ctx: WidgetContext,
settings: GetValueSettings<V>,
valueType: ValueType,
valueObserver: Partial<Observer<V>>): ValueGetter<V> {
switch (settings.action) {
case GetValueAction.DO_NOTHING:
return new DefaultValueGetter<V>(ctx, settings, valueType, valueObserver);
case GetValueAction.EXECUTE_RPC:
return new ExecuteRpcValueGetter<V>(ctx, settings, valueType, valueObserver);
case GetValueAction.GET_ATTRIBUTE:
return new AttributeValueGetter<V>(ctx, settings, valueType, valueObserver);
case GetValueAction.GET_TIME_SERIES:
return new TimeSeriesValueGetter<V>(ctx, settings, valueType, valueObserver);
}
}
private readonly isSimulated: boolean;
private readonly dataConverter: DataToValueConverter<V>;
protected constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
if (this.settings.action !== GetValueAction.DO_NOTHING) {
this.dataConverter = new DataToValueConverter<V>(settings.dataToValue, valueType);
}
}
getValue(): Observable<V> {
const valueObservable: Observable<V> = (this.isSimulated ? of(null).pipe(delay(500)) : this.doGetValue()).pipe(
map((data) => {
if (this.dataConverter) {
return this.dataConverter.dataToValue(data);
} else {
return data;
}
}),
catchError(err => {
throw this.handleError(err);
})
);
valueObservable.subscribe({
next: (value) => {
this.valueObserver.next(value);
},
error: (err) => {
this.valueObserver.error(err);
}
});
return valueObservable.pipe(
take(1)
);
}
destroy() {
super.destroy();
}
protected abstract doGetValue(): Observable<any>;
}
type ValueToDataFunction<V> = (value: V) => any;
export class ValueToDataConverter<V> {
private readonly constantValue: any;
private readonly valueToDataFunction: ValueToDataFunction<V>;
constructor(protected settings: ValueToDataSettings) {
switch (settings.type) {
case ValueToDataType.CONSTANT:
this.constantValue = this.settings.constantValue;
break;
case ValueToDataType.FUNCTION:
try {
this.valueToDataFunction = new Function('value', settings.valueToDataFunction) as ValueToDataFunction<V>;
} catch (e) {
this.valueToDataFunction = (data) => data;
}
break;
case ValueToDataType.NONE:
break;
}
}
valueToData(value: V): any {
switch (this.settings.type) {
case ValueToDataType.CONSTANT:
return this.constantValue;
case ValueToDataType.FUNCTION:
let result = value;
try {
result = this.valueToDataFunction(value);
} catch (e) {}
return result;
case ValueToDataType.NONE:
return null;
}
}
}
export abstract class ValueSetter<V> extends ValueAction {
static fromSettings<V>(ctx: WidgetContext,
settings: SetValueSettings): ValueSetter<V> {
switch (settings.action) {
case SetValueAction.EXECUTE_RPC:
return new ExecuteRpcValueSetter<V>(ctx, settings);
case SetValueAction.SET_ATTRIBUTE:
return new AttributeValueSetter<V>(ctx, settings);
case SetValueAction.ADD_TIME_SERIES:
return new TimeSeriesValueSetter<V>(ctx, settings);
}
}
private readonly isSimulated: boolean;
private readonly valueToDataConverter: ValueToDataConverter<V>;
protected constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
this.valueToDataConverter = new ValueToDataConverter<V>(settings.valueToData);
}
setValue(value: V): Observable<any> {
if (this.isSimulated) {
return of(null).pipe(delay(500));
} else {
return this.doSetValue(this.valueToDataConverter.valueToData(value)).pipe(
catchError(err => {
throw this.handleError(err);
})
);
}
}
protected abstract doSetValue(data: any): Observable<any>;
}
export class DefaultValueGetter<V> extends ValueGetter<V> {
private readonly defaultValue: V;
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
this.defaultValue = settings.defaultValue;
}
protected doGetValue(): Observable<V> {
return of(this.defaultValue);
}
}
export class ExecuteRpcValueGetter<V> extends ValueGetter<V> {
private readonly executeRpcSettings: RpcSettings;
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
this.executeRpcSettings = settings.executeRpc;
}
protected doGetValue(): Observable<V> {
return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null,
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
catchError((err) => {
throw handleRpcError(this.ctx, err);
})
);
}
}
export abstract class TelemetryValueGetter<V, S extends GetTelemetryValueSettings> extends ValueGetter<V> {
protected targetEntityId: EntityId;
private telemetrySubscriber: TelemetrySubscriber;
protected constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
this.targetEntityId = entityInfo?.entityId;
}
protected doGetValue(): Observable<V> {
if (!this.targetEntityId && !this.ctx.defaultSubscription.rpcEnabled) {
return throwError(() => new Error(this.ctx.translate.instant('widgets.value-action.error.target-entity-is-not-set')));
}
if (this.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.targetEntityId, this.scope());
if (err) {
return throwError(() => err);
}
if (this.getTelemetryValueSettings().subscribeForUpdates) {
return this.subscribeForTelemetryValue();
} else {
return this.doGetTelemetryValue();
}
} else {
return of(null);
}
}
private subscribeForTelemetryValue(): Observable<V> {
this.telemetrySubscriber =
TelemetrySubscriber.createEntityAttributesSubscription(this.ctx.telemetryWsService, this.targetEntityId,
this.scope(), this.ctx.ngZone, [this.getTelemetryValueSettings().key]);
this.telemetrySubscriber.subscribe();
return this.telemetrySubscriber.attributeData$().pipe(
map((data) => {
let value: V = null;
const entry = data.find(attr => attr.key === this.getTelemetryValueSettings().key);
if (entry) {
value = entry.value;
try {
value = JSON.parse(entry.value);
} catch (_e) {}
}
return value;
})
);
}
protected scope(): TelemetryType {
return LatestTelemetry.LATEST_TELEMETRY;
}
protected abstract getTelemetryValueSettings(): S;
protected abstract doGetTelemetryValue(): Observable<V>;
destroy() {
if (this.telemetrySubscriber) {
this.telemetrySubscriber.unsubscribe();
this.telemetrySubscriber = null;
}
super.destroy();
}
}
export class AttributeValueGetter<V> extends TelemetryValueGetter<V, GetAttributeValueSettings> {
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
}
protected getTelemetryValueSettings(): GetAttributeValueSettings {
return this.settings.getAttribute;
}
protected scope(): TelemetryType {
return this.getTelemetryValueSettings().scope;
}
protected doGetTelemetryValue(): Observable<V> {
const getAttributeValueSettings = this.getTelemetryValueSettings();
return this.ctx.attributeService.getEntityAttributes(this.targetEntityId,
getAttributeValueSettings.scope, [getAttributeValueSettings.key], {ignoreLoading: true, ignoreErrors: true}).pipe(
map((data) => data.find(attr => attr.key === getAttributeValueSettings.key)?.value)
);
}
}
export class TimeSeriesValueGetter<V> extends TelemetryValueGetter<V, GetTelemetryValueSettings> {
constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>,
protected valueType: ValueType,
protected valueObserver: Partial<Observer<V>>) {
super(ctx, settings, valueType, valueObserver);
}
protected getTelemetryValueSettings(): GetTelemetryValueSettings {
return this.settings.getTimeSeries;
}
protected doGetTelemetryValue(): Observable<V> {
const getTelemetryValueSettings = this.getTelemetryValueSettings();
return this.ctx.attributeService.getEntityTimeseriesLatest(this.targetEntityId,
[getTelemetryValueSettings.key], true, {ignoreLoading: true, ignoreErrors: true})
.pipe(
map((data) => {
let value: any = null;
if (data[getTelemetryValueSettings.key]) {
const dataSet = data[getTelemetryValueSettings.key];
if (dataSet.length) {
value = dataSet[0].value;
}
}
return value;
})
);
}
}
export class ExecuteRpcValueSetter<V> extends ValueSetter<V> {
private readonly executeRpcSettings: RpcSettings;
constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
this.executeRpcSettings = settings.executeRpc;
}
protected doSetValue(data: any): Observable<any> {
return this.ctx.controlApi.sendOneWayCommand(this.executeRpcSettings.method, data,
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
catchError((err) => {
throw handleRpcError(this.ctx, err);
})
);
}
}
export abstract class TelemetryValueSetter<V> extends ValueSetter<V> {
protected targetEntityId: EntityId;
protected constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
this.targetEntityId = entityInfo?.entityId;
}
protected doSetValue(data: any): Observable<V> {
if (!this.targetEntityId && !this.ctx.defaultSubscription.rpcEnabled) {
return throwError(() => new Error(this.ctx.translate.instant('widgets.value-action.error.target-entity-is-not-set')));
}
if (this.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.targetEntityId, this.scope());
if (err) {
return throwError(() => err);
}
return this.doSetTelemetryValue(data);
} else {
return of(null);
}
}
protected scope(): TelemetryType {
return LatestTelemetry.LATEST_TELEMETRY;
}
protected abstract doSetTelemetryValue(data: any): Observable<V>;
}
export class AttributeValueSetter<V> extends TelemetryValueSetter<V> {
private readonly setAttributeValueSettings: SetAttributeValueSettings;
constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
this.setAttributeValueSettings = settings.setAttribute;
}
protected doSetTelemetryValue(data: any): Observable<any> {
const attributes: Array<AttributeData> = [{key: this.setAttributeValueSettings.key, value: data}];
return this.ctx.attributeService.saveEntityAttributes(this.targetEntityId,
this.setAttributeValueSettings.scope, attributes, {ignoreLoading: true, ignoreErrors: true});
}
protected scope(): TelemetryType {
return this.setAttributeValueSettings.scope;
}
}
export class TimeSeriesValueSetter<V> extends TelemetryValueSetter<V> {
private readonly putTimeSeriesValueSettings: TelemetryValueSettings;
constructor(protected ctx: WidgetContext,
protected settings: SetValueSettings) {
super(ctx, settings);
this.putTimeSeriesValueSettings = settings.putTimeSeries;
}
protected doSetTelemetryValue(data: any): Observable<any> {
const timeSeries: Array<AttributeData> = [{key: this.putTimeSeriesValueSettings.key, value: data}];
return this.ctx.attributeService.saveEntityTimeseries(this.targetEntityId,
LatestTelemetry.LATEST_TELEMETRY, timeSeries, {ignoreLoading: true, ignoreErrors: true});
}
}
const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
const handleRpcError = (ctx: WidgetContext, err: any): Error => {
let reason: string;
if (ctx.defaultSubscription.rpcErrorText) {
reason = ctx.defaultSubscription.rpcErrorText;
} else {
reason = parseError(ctx, err);
}
return new Error(reason);
};
const validateAttributeScope = (ctx: WidgetContext, targetEntityId: EntityId, scope?: TelemetryType): Error | null => {
if (targetEntityId.entityType !== EntityType.DEVICE && scope &&
![AttributeScope.SERVER_SCOPE, LatestTelemetry.LATEST_TELEMETRY].includes(scope)) {
const scopeStr = ctx.translate.instant(telemetryTypeTranslationsShort.get(scope));
const entityType =
ctx.translate.instant(entityTypeTranslations.get(targetEntityId.entityType).type);
const errorMessage =
ctx.translate.instant('widgets.value-action.error.invalid-attribute-scope', {scope: scopeStr, entityType});
return new Error(errorMessage);
} else {
return null;
}
};

626
ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widget.models.ts

@ -1,626 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AttributeData,
AttributeScope,
LatestTelemetry,
telemetryTypeTranslationsShort
} from '@shared/models/telemetry/telemetry.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, delay, map, share } from 'rxjs/operators';
import { UtilsService } from '@core/services/utils.service';
import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnInit, TemplateRef } from '@angular/core';
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import {
RpcActionSettings,
RpcGetAttributeSettings,
RpcSetAttributeSettings,
RpcSettings,
RpcStateToParamsSettings,
RpcStateToParamsType,
RpcStateWidgetSettings,
RpcTelemetrySettings,
RpcUpdateStateAction
} from '@shared/models/rpc-widget-settings.models';
import {
RpcDataToStateSettings,
RpcDataToStateType,
RpcInitialStateAction,
RpcInitialStateSettings,
RpcStateBehaviourSettings,
RpcUpdateStateSettings
} from '@app/shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
@Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export abstract class BasicRpcStateWidgetComponent<V, S extends RpcStateWidgetSettings<V>> implements OnInit, AfterViewInit {
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
settings: S;
behaviorApi: RpcStateBehaviorApi<V>;
loading$: Observable<boolean>;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
value: V;
error = '';
protected constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
protected cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.rpcWidget = this;
this.settings = {...this.defaultSettings(), ...this.ctx.settings};
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
const behaviourSettings: RpcStateBehaviourSettings<V> = {
initialState: this.initialState(),
updateStateByValue: val => this.getUpdateStateSettingsForValue(val)
};
const callbacks: RpcStateCallbacks<V> = {
onStateValue: val => this.setValue(val),
onError: err => this.onError(err),
validateStateValue: val => this.validateValue(val)
};
this.behaviorApi = new RpcStateBehaviorApi<V>(this.defaultValue(), this.ctx,
behaviourSettings, callbacks, this.stateValueType());
this.loading$ = this.behaviorApi.loading$.pipe(share());
}
ngAfterViewInit(): void {
this.behaviorApi.initState();
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public clearError() {
this.error = '';
}
public updateValue() {
this.behaviorApi.updateState(this.value);
}
private onError(error: string) {
this.error = error;
this.ctx.detectChanges();
}
protected abstract defaultSettings(): S;
protected abstract initialState(): RpcInitialStateSettings<V>;
protected abstract getUpdateStateSettingsForValue(value: V): RpcUpdateStateSettings;
protected stateValueType(): ValueType {
return ValueType.BOOLEAN;
}
protected defaultValue(): V {
return null;
}
protected setValue(value: V) {
this.value = value;
}
protected validateValue(value: any): V {
return value;
}
}
export abstract class RpcHasLoading {
public get loading$() {
return this.loadingSubject.asObservable();
}
protected loadingSubject = new BehaviorSubject(false);
}
export interface RpcStateCallbacks<V> {
onStateValue: (value: V) => void;
validateStateValue: (value: any) => V;
onError: (error: string) => void;
}
export class RpcStateBehaviorApi<V> extends RpcHasLoading {
private readonly initialStateGetter: RpcInitialStateGetter<V>;
private readonly stateUpdatersMap: Map<RpcUpdateStateSettings, RpcStateUpdater<V>>;
constructor(private state: V,
private ctx: WidgetContext,
private settings: RpcStateBehaviourSettings<V>,
private callbacks: RpcStateCallbacks<V>,
stateValueType: ValueType) {
super();
this.initialStateGetter = RpcInitialStateGetter.fromSettings(ctx, settings.initialState, stateValueType, callbacks);
this.stateUpdatersMap = new Map<RpcUpdateStateSettings, RpcStateUpdater<V>>();
}
initState() {
if (this.ctx.defaultSubscription.targetEntityId || this.ctx.defaultSubscription.rpcEnabled) {
this.loadingSubject.next(true);
this.initialStateGetter.initState().subscribe(
{
next: (value) => {
this.state = value;
this.loadingSubject.next(false);
this.callbacks.onStateValue(value);
this.ctx.detectChanges();
},
error: (err: any) => {
this.loadingSubject.next(false);
const message = parseError(this.ctx, err);
this.callbacks.onError(message);
}
}
);
} else {
this.callbacks.onError(this.ctx.translate.instant('widgets.rpc-state.error.target-entity-is-not-set'));
}
}
updateState(value: V) {
this.callbacks.onError(null);
let updater: RpcStateUpdater<V>;
const updateStateSettings = this.settings.updateStateByValue(value);
if (updateStateSettings) {
updater = this.stateUpdatersMap.get(updateStateSettings);
if (!updater) {
updater = RpcStateUpdater.fromSettings(this.ctx, updateStateSettings);
this.stateUpdatersMap.set(updateStateSettings, updater);
}
}
if (updater) {
this.loadingSubject.next(true);
updater.updateState(value).subscribe(
{
next: () => {
this.state = value;
this.loadingSubject.next(false);
this.ctx.detectChanges();
},
error: (err: any) => {
this.loadingSubject.next(false);
const message = parseError(this.ctx, err);
this.callbacks.onStateValue(this.state);
this.callbacks.onError(message);
this.ctx.detectChanges();
}
});
}
}
}
type RpcDataToStateFunction<V> = (data: any) => V;
export class RpcDataToStateConverter<V> {
private readonly dataToStateFunction: RpcDataToStateFunction<V>;
private readonly compareToValue: any;
constructor(private settings: RpcDataToStateSettings,
private stateValueType: ValueType,
private callbacks: RpcStateCallbacks<V>) {
this.compareToValue = settings.compareToValue;
switch (settings.type) {
case RpcDataToStateType.FUNCTION:
try {
this.dataToStateFunction = new Function('data', settings.dataToStateFunction) as RpcDataToStateFunction<V>;
} catch (e) {
this.dataToStateFunction = (data) => data;
}
break;
case RpcDataToStateType.NONE:
break;
}
}
dataToStateValue(data: any): V {
let result: V;
switch (this.settings.type) {
case RpcDataToStateType.FUNCTION:
result = data;
try {
result = this.dataToStateFunction(!!data ? JSON.parse(data) : data);
} catch (e) {}
break;
case RpcDataToStateType.NONE:
result = data;
break;
}
if (this.stateValueType === ValueType.BOOLEAN) {
result = (result === this.compareToValue) as any;
}
result = this.callbacks.validateStateValue(result);
return result;
}
}
export abstract class RpcAction {
protected constructor(protected ctx: WidgetContext,
protected settings: RpcActionSettings) {}
handleError(err: any): Error {
const reason = parseError(this.ctx, err);
let errorMessage = this.ctx.translate.instant('widgets.rpc-state.error.failed-to-perform-action',
{actionLabel: this.settings.actionLabel});
if (reason) {
errorMessage += '<br>' + reason;
}
return new Error(errorMessage);
}
}
export abstract class RpcInitialStateGetter<V> extends RpcAction {
static fromSettings<V>(ctx: WidgetContext,
settings: RpcInitialStateSettings<V>,
stateValueType: ValueType,
callbacks: RpcStateCallbacks<V>): RpcInitialStateGetter<V> {
switch (settings.action) {
case RpcInitialStateAction.DO_NOTHING:
return new RpcDefaultStateGetter<V>(ctx, settings, stateValueType, callbacks);
case RpcInitialStateAction.EXECUTE_RPC:
return new ExecuteRpcStateGetter<V>(ctx, settings, stateValueType, callbacks);
case RpcInitialStateAction.GET_ATTRIBUTE:
return new RpcAttributeStateGetter<V>(ctx, settings, stateValueType, callbacks);
case RpcInitialStateAction.GET_TIME_SERIES:
return new RpcTimeSeriesStateGetter<V>(ctx, settings, stateValueType, callbacks);
}
}
private readonly isSimulated: boolean;
private readonly dataConverter: RpcDataToStateConverter<V>;
protected constructor(protected ctx: WidgetContext,
protected settings: RpcInitialStateSettings<V>,
protected stateValueType: ValueType,
protected callbacks: RpcStateCallbacks<V>) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
if (this.settings.action !== RpcInitialStateAction.DO_NOTHING) {
this.dataConverter = new RpcDataToStateConverter<V>(settings.dataToState, stateValueType, this.callbacks);
}
}
initState(): Observable<V> {
const stateObservable: Observable<V> = this.isSimulated ? of(null).pipe(delay(500)) : this.doGetState();
return stateObservable.pipe(
map((data) => {
if (this.dataConverter) {
return this.dataConverter.dataToStateValue(data);
} else {
return data;
}
}),
catchError(err => {
throw this.handleError(err);
})
);
}
protected abstract doGetState(): Observable<any>;
}
type RpcStateToParamsFunction<V> = (state: V) => any;
export class RpcStateToParamsConverter<V> {
private readonly constantValue: any;
private readonly stateToParamsFunction: RpcStateToParamsFunction<V>;
constructor(protected settings: RpcStateToParamsSettings) {
switch (settings.type) {
case RpcStateToParamsType.CONSTANT:
this.constantValue = this.settings.constantValue;
break;
case RpcStateToParamsType.FUNCTION:
try {
this.stateToParamsFunction = new Function('value', settings.stateToParamsFunction) as RpcStateToParamsFunction<V>;
} catch (e) {
this.stateToParamsFunction = (data) => data;
}
break;
case RpcStateToParamsType.NONE:
break;
}
}
stateToParams(state: V): any {
switch (this.settings.type) {
case RpcStateToParamsType.CONSTANT:
return this.constantValue;
case RpcStateToParamsType.FUNCTION:
let result = state;
try {
result = this.stateToParamsFunction(state);
} catch (e) {}
return result;
case RpcStateToParamsType.NONE:
return null;
}
}
}
export abstract class RpcStateUpdater<V> extends RpcAction {
static fromSettings<V>(ctx: WidgetContext,
settings: RpcUpdateStateSettings): RpcStateUpdater<V> {
switch (settings.action) {
case RpcUpdateStateAction.EXECUTE_RPC:
return new ExecuteRpcStateUpdater<V>(ctx, settings);
case RpcUpdateStateAction.SET_ATTRIBUTE:
return new RpcAttributeStateUpdater<V>(ctx, settings);
case RpcUpdateStateAction.ADD_TIME_SERIES:
return new RpcTimeSeriesStateUpdater<V>(ctx, settings);
}
}
private readonly isSimulated: boolean;
private readonly paramsConverter: RpcStateToParamsConverter<V>;
protected constructor(protected ctx: WidgetContext,
protected settings: RpcUpdateStateSettings) {
super(ctx, settings);
this.isSimulated = this.ctx.$injector.get(UtilsService).widgetEditMode;
this.paramsConverter = new RpcStateToParamsConverter<V>(settings.stateToParams);
}
updateState(state: V): Observable<any> {
if (this.isSimulated) {
return of(null).pipe(delay(500));
} else {
return this.doUpdateState(this.paramsConverter.stateToParams(state)).pipe(
catchError(err => {
throw this.handleError(err);
})
);
}
}
protected abstract doUpdateState(params: any): Observable<any>;
}
export class RpcDefaultStateGetter<V> extends RpcInitialStateGetter<V> {
private readonly defaultValue: V;
constructor(protected ctx: WidgetContext,
protected settings: RpcInitialStateSettings<V>,
protected stateValueType: ValueType,
protected callbacks: RpcStateCallbacks<V>) {
super(ctx, settings, stateValueType, callbacks);
this.defaultValue = settings.defaultValue;
}
protected doGetState(): Observable<V> {
return of(this.defaultValue);
}
}
export class ExecuteRpcStateGetter<V> extends RpcInitialStateGetter<V> {
private readonly executeRpcSettings: RpcSettings;
constructor(protected ctx: WidgetContext,
protected settings: RpcInitialStateSettings<V>,
protected stateValueType: ValueType,
protected callbacks: RpcStateCallbacks<V>) {
super(ctx, settings, stateValueType, callbacks);
this.executeRpcSettings = settings.executeRpc;
}
protected doGetState(): Observable<V> {
return this.ctx.controlApi.sendTwoWayCommand(this.executeRpcSettings.method, null,
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
catchError((err) => {
throw handleRpcError(this.ctx, err);
})
);
}
}
export class RpcAttributeStateGetter<V> extends RpcInitialStateGetter<V> {
private readonly getAttributeSettings: RpcGetAttributeSettings;
constructor(protected ctx: WidgetContext,
protected settings: RpcInitialStateSettings<V>,
protected stateValueType: ValueType,
protected callbacks: RpcStateCallbacks<V>) {
super(ctx, settings, stateValueType, callbacks);
this.getAttributeSettings = settings.getAttribute;
}
protected doGetState(): Observable<V> {
if (this.ctx.defaultSubscription.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.getAttributeSettings.scope);
if (err) {
return throwError(() => err);
}
return this.ctx.attributeService.getEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
this.getAttributeSettings.scope, [this.getAttributeSettings.key], {ignoreLoading: true, ignoreErrors: true})
.pipe(
map((data) => data.find(attr => attr.key === this.getAttributeSettings.key)?.value)
);
} else {
return of(null);
}
}
}
export class RpcTimeSeriesStateGetter<V> extends RpcInitialStateGetter<V> {
private readonly getTimeSeriesSettings: RpcTelemetrySettings;
constructor(protected ctx: WidgetContext,
protected settings: RpcInitialStateSettings<V>,
protected stateValueType: ValueType,
protected callbacks: RpcStateCallbacks<V>) {
super(ctx, settings, stateValueType, callbacks);
this.getTimeSeriesSettings = settings.getTimeSeries;
}
protected doGetState(): Observable<V> {
if (this.ctx.defaultSubscription.targetEntityId) {
return this.ctx.attributeService.getEntityTimeseriesLatest(this.ctx.defaultSubscription.targetEntityId,
[this.getTimeSeriesSettings.key], true, {ignoreLoading: true, ignoreErrors: true})
.pipe(
map((data) => {
let value: any = null;
if (data[this.getTimeSeriesSettings.key]) {
const dataSet = data[this.getTimeSeriesSettings.key];
if (dataSet.length) {
value = dataSet[0].value;
}
}
return value;
})
);
} else {
return of(null);
}
}
}
export class ExecuteRpcStateUpdater<V> extends RpcStateUpdater<V> {
private readonly executeRpcSettings: RpcSettings;
constructor(protected ctx: WidgetContext,
protected settings: RpcUpdateStateSettings) {
super(ctx, settings);
this.executeRpcSettings = settings.executeRpc;
}
protected doUpdateState(params: any): Observable<any> {
return this.ctx.controlApi.sendOneWayCommand(this.executeRpcSettings.method, params,
this.executeRpcSettings.requestTimeout,
this.executeRpcSettings.requestPersistent,
this.executeRpcSettings.persistentPollingInterval).pipe(
catchError((err) => {
throw handleRpcError(this.ctx, err);
})
);
}
}
export class RpcAttributeStateUpdater<V> extends RpcStateUpdater<V> {
private readonly setAttributeSettings: RpcSetAttributeSettings;
constructor(protected ctx: WidgetContext,
protected settings: RpcUpdateStateSettings) {
super(ctx, settings);
this.setAttributeSettings = settings.setAttribute;
}
protected doUpdateState(params: any): Observable<any> {
if (this.ctx.defaultSubscription.targetEntityId) {
const err = validateAttributeScope(this.ctx, this.setAttributeSettings.scope);
if (err) {
return throwError(() => err);
}
const attributes: Array<AttributeData> = [{key: this.setAttributeSettings.key, value: params}];
return this.ctx.attributeService.saveEntityAttributes(this.ctx.defaultSubscription.targetEntityId,
this.setAttributeSettings.scope, attributes, {ignoreLoading: true, ignoreErrors: true});
} else {
return of(null);
}
}
}
export class RpcTimeSeriesStateUpdater<V> extends RpcStateUpdater<V> {
private readonly putTimeSeriesSettings: RpcTelemetrySettings;
constructor(protected ctx: WidgetContext,
protected settings: RpcUpdateStateSettings) {
super(ctx, settings);
this.putTimeSeriesSettings = settings.putTimeSeries;
}
protected doUpdateState(params: any): Observable<any> {
if (this.ctx.defaultSubscription.targetEntityId) {
const timeSeries: Array<AttributeData> = [{key: this.putTimeSeriesSettings.key, value: params}];
return this.ctx.attributeService.saveEntityTimeseries(this.ctx.defaultSubscription.targetEntityId,
LatestTelemetry.LATEST_TELEMETRY, timeSeries, {ignoreLoading: true, ignoreErrors: true});
} else {
return of(null);
}
}
}
const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
const handleRpcError = (ctx: WidgetContext, err: any): Error => {
let reason: string;
if (ctx.defaultSubscription.rpcErrorText) {
reason = ctx.defaultSubscription.rpcErrorText;
} else {
reason = parseError(ctx, err);
}
return new Error(reason);
};
const validateAttributeScope = (ctx: WidgetContext, scope?: AttributeScope): Error | null => {
if (ctx.defaultSubscription.targetEntityId.entityType !== EntityType.DEVICE && scope && scope !== AttributeScope.SERVER_SCOPE) {
const scopeStr = ctx.translate.instant(telemetryTypeTranslationsShort.get(scope));
const entityType =
ctx.translate.instant(entityTypeTranslations.get(ctx.defaultSubscription.targetEntityId.entityType).type);
const errorMessage =
ctx.translate.instant('widgets.rpc-state.error.invalid-attribute-scope', {scope: scopeStr, entityType});
return new Error(errorMessage);
} else {
return null;
}
};

2
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html

@ -27,7 +27,7 @@
</div> </div>
<div #singleSwitchToggleRow class="tb-single-switch-slide-toggle-row"> <div #singleSwitchToggleRow class="tb-single-switch-slide-toggle-row">
<div *ngIf="showOffLabel" [style]="offLabelStyle">{{ offLabel }}</div> <div *ngIf="showOffLabel" [style]="offLabelStyle">{{ offLabel }}</div>
<mat-slide-toggle class="tb-single-switch-toggle" [disabled]="loading$ | async" [(ngModel)]="value" (change)="updateValue()"> <mat-slide-toggle class="tb-single-switch-toggle" [disabled]="loading$ | async" [(ngModel)]="value" (click)="onToggleChange($event)">
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="showOnLabel" [style]="onLabelStyle">{{ onLabel }}</div> <div *ngIf="showOnLabel" [style]="onLabelStyle">{{ onLabel }}</div>
</div> </div>

81
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts

@ -25,20 +25,25 @@ import {
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { BasicRpcStateWidgetComponent } from '@home/components/widget/lib/rpc/rpc-widget.models'; import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { import {
singleSwitchDefaultSettings, singleSwitchDefaultSettings,
SingleSwitchLayout, SingleSwitchLayout,
SingleSwitchWidgetSettings SingleSwitchWidgetSettings
} from '@home/components/widget/lib/rpc/single-switch-widget.models'; } from '@home/components/widget/lib/rpc/single-switch-widget.models';
import { ComponentStyle, iconStyle, textStyle } from '@shared/models/widget-settings.models'; import {
backgroundStyle,
ComponentStyle,
iconStyle,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { hashCode } from '@core/utils'; import { hashCode } from '@core/utils';
import { RpcInitialStateSettings, RpcUpdateStateSettings } from '@shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
const horizontalLayoutPadding = 48; const horizontalLayoutPadding = 48;
@ -51,7 +56,7 @@ const verticalLayoutPadding = 36;
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class SingleSwitchWidgetComponent extends export class SingleSwitchWidgetComponent extends
BasicRpcStateWidgetComponent<boolean, SingleSwitchWidgetSettings> implements OnInit, AfterViewInit, OnDestroy { BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('singleSwitchPanel', {static: false}) @ViewChild('singleSwitchPanel', {static: false})
singleSwitchPanel: ElementRef<HTMLElement>; singleSwitchPanel: ElementRef<HTMLElement>;
@ -65,6 +70,13 @@ export class SingleSwitchWidgetComponent extends
@ViewChild('singleSwitchToggleRow', {static: false}) @ViewChild('singleSwitchToggleRow', {static: false})
singleSwitchToggleRow: ElementRef<HTMLElement>; singleSwitchToggleRow: ElementRef<HTMLElement>;
settings: SingleSwitchWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
value = false;
layout: SingleSwitchLayout; layout: SingleSwitchLayout;
showIcon = false; showIcon = false;
@ -87,16 +99,24 @@ export class SingleSwitchWidgetComponent extends
private panelResize$: ResizeObserver; private panelResize$: ResizeObserver;
private onValueSetter: ValueSetter<boolean>;
private offValueSetter: ValueSetter<boolean>;
constructor(protected imagePipe: ImagePipe, constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer, protected sanitizer: DomSanitizer,
private renderer: Renderer2, private renderer: Renderer2,
protected cd: ChangeDetectorRef, protected cd: ChangeDetectorRef,
private elementRef: ElementRef) { private elementRef: ElementRef) {
super(imagePipe, sanitizer, cd); super(cd);
} }
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.settings = {...singleSwitchDefaultSettings, ...this.ctx.settings};
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.layout = this.settings.layout; this.layout = this.settings.layout;
this.autoScale = this.settings.autoScale; this.autoScale = this.settings.autoScale;
@ -134,6 +154,20 @@ export class SingleSwitchWidgetComponent extends
cssParser.cssPreviewNamespace = namespace; cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, switchVariablesCss); cssParser.createStyleElement(namespace, switchVariablesCss);
this.renderer.addClass(this.elementRef.nativeElement, namespace); this.renderer.addClass(this.elementRef.nativeElement, namespace);
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.value-action.initial-state')};
this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onValue(value)
});
const onUpdateStateSettings = {...this.settings.onUpdateState,
actionLabel: this.ctx.translate.instant('widgets.value-action.turn-on')};
this.onValueSetter = this.createValueSetter(onUpdateStateSettings);
const offUpdateStateSettings = {...this.settings.offUpdateState,
actionLabel: this.ctx.translate.instant('widgets.value-action.turn-off')};
this.offValueSetter = this.createValueSetter(offUpdateStateSettings);
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -156,31 +190,30 @@ export class SingleSwitchWidgetComponent extends
if (this.panelResize$) { if (this.panelResize$) {
this.panelResize$.disconnect(); this.panelResize$.disconnect();
} }
super.ngOnDestroy();
} }
protected stateValueType(): ValueType { public onInit() {
return ValueType.BOOLEAN; super.onInit();
} const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
protected defaultValue(): boolean { this.cd.detectChanges();
return false;
}
protected defaultSettings(): SingleSwitchWidgetSettings {
return {...singleSwitchDefaultSettings};
}
protected initialState(): RpcInitialStateSettings<boolean> {
return {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
} }
protected getUpdateStateSettingsForValue(value: boolean): RpcUpdateStateSettings { public onToggleChange(event: MouseEvent) {
const targetSettings = value ? this.settings.onUpdateState : this.settings.offUpdateState; event.preventDefault();
return {...targetSettings, actionLabel: this.ctx.translate.instant(value ? 'widgets.rpc-state.turn-on' : 'widgets.rpc-state.turn-off')}; const targetValue = this.value;
const targetSetter = targetValue ? this.onValueSetter : this.offValueSetter;
this.updateValue(targetSetter, targetValue, {
next: () => this.onValue(targetValue),
error: () => this.onValue(!targetValue)
});
} }
protected validateValue(value: any): boolean { private onValue(value: boolean): void {
return !!value; console.log(`onValue: ${value}`);
this.value = !!value;
this.cd.markForCheck();
} }
private onResize() { private onResize() {

54
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts

@ -14,16 +14,16 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { import {
RpcDataToStateType, DataToValueType,
RpcInitialStateAction, GetValueAction,
RpcStateToParamsType, GetValueSettings,
RpcStateWidgetSettings, SetValueAction,
RpcUpdateStateAction, SetValueSettings,
RpcUpdateStateSettings ValueToDataType
} from '@shared/models/rpc-widget-settings.models'; } from '@shared/models/action-widget-settings.models';
export enum SingleSwitchLayout { export enum SingleSwitchLayout {
right = 'right', right = 'right',
@ -49,9 +49,10 @@ export const singleSwitchLayoutImages = new Map<SingleSwitchLayout, string>(
] ]
); );
export interface SingleSwitchWidgetSettings extends RpcStateWidgetSettings<boolean> { export interface SingleSwitchWidgetSettings {
onUpdateState: RpcUpdateStateSettings; initialState: GetValueSettings<boolean>;
offUpdateState: RpcUpdateStateSettings; onUpdateState: SetValueSettings;
offUpdateState: SetValueSettings;
layout: SingleSwitchLayout; layout: SingleSwitchLayout;
autoScale: boolean; autoScale: boolean;
showLabel: boolean; showLabel: boolean;
@ -77,11 +78,12 @@ export interface SingleSwitchWidgetSettings extends RpcStateWidgetSettings<boole
offLabel: string; offLabel: string;
offLabelFont: Font; offLabelFont: Font;
offLabelColor: string; offLabelColor: string;
background: BackgroundSettings;
} }
export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = { export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
initialState: { initialState: {
action: RpcInitialStateAction.EXECUTE_RPC, action: GetValueAction.EXECUTE_RPC,
defaultValue: false, defaultValue: false,
executeRpc: { executeRpc: {
method: 'getState', method: 'getState',
@ -91,19 +93,21 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
}, },
getAttribute: { getAttribute: {
key: 'state', key: 'state',
scope: null scope: null,
subscribeForUpdates: false
}, },
getTimeSeries: { getTimeSeries: {
key: 'state' key: 'state',
subscribeForUpdates: false
}, },
dataToState: { dataToValue: {
type: RpcDataToStateType.NONE, type: DataToValueType.NONE,
compareToValue: true, compareToValue: true,
dataToStateFunction: '/* Should return boolean value */\nreturn data;' dataToValueFunction: '/* Should return boolean value */\nreturn data;'
} }
}, },
onUpdateState: { onUpdateState: {
action: RpcUpdateStateAction.EXECUTE_RPC, action: SetValueAction.EXECUTE_RPC,
executeRpc: { executeRpc: {
method: 'setState', method: 'setState',
requestTimeout: 5000, requestTimeout: 5000,
@ -117,14 +121,14 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
putTimeSeries: { putTimeSeries: {
key: 'state' key: 'state'
}, },
stateToParams: { valueToData: {
type: RpcStateToParamsType.CONSTANT, type: ValueToDataType.CONSTANT,
constantValue: true, constantValue: true,
stateToParamsFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;' valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;'
} }
}, },
offUpdateState: { offUpdateState: {
action: RpcUpdateStateAction.EXECUTE_RPC, action: SetValueAction.EXECUTE_RPC,
executeRpc: { executeRpc: {
method: 'setState', method: 'setState',
requestTimeout: 5000, requestTimeout: 5000,
@ -138,10 +142,10 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
putTimeSeries: { putTimeSeries: {
key: 'state' key: 'state'
}, },
stateToParams: { valueToData: {
type: RpcStateToParamsType.CONSTANT, type: ValueToDataType.CONSTANT,
constantValue: false, constantValue: false,
stateToParamsFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */ \n return value;' valueToDataFunction: '/* Convert input boolean value to RPC parameters or attribute/time-series value */ \n return value;'
} }
}, },
layout: SingleSwitchLayout.right, layout: SingleSwitchLayout.right,

114
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html

@ -15,50 +15,50 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-rpc-state-settings-panel" [formGroup]="initialStateSettingsFormGroup"> <div class="tb-value-action-settings-panel" [formGroup]="getValueSettingsFormGroup">
<div class="tb-rpc-state-settings-title" translate>widgets.rpc-state.initial-state</div> <div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-rpc-state-settings-panel-content"> <div class="tb-value-action-settings-panel-content">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.action' | translate }}</div> <div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="action"> <mat-select formControlName="action">
<mat-option *ngFor="let action of rpcInitialStateActions" [value]="action"> <mat-option *ngFor="let action of getValueActions" [value]="action">
{{ rpcInitialStateTranslationsMap.get(action) | translate }} {{ getValueActionTranslationsMap.get(action) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<ng-container [ngSwitch]="initialStateSettingsFormGroup.get('action').value"> <ng-container [ngSwitch]="getValueSettingsFormGroup.get('action').value">
<ng-template [ngSwitchCase]="rpcInitialStateAction.DO_NOTHING"> <ng-template [ngSwitchCase]="getValueAction.DO_NOTHING">
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{'widgets.rpc-state.init-value-hint' | translate}}" translate>widgets.rpc-state.value</div> <div tb-hint-tooltip-icon="{{'widgets.value-action.init-value-hint' | translate}}" translate>widgets.value-action.value</div>
<tb-value-input [valueType]="stateValueType" <tb-value-input [valueType]="valueType"
trueLabel="widgets.rpc-state.on" trueLabel="widgets.value-action.on"
falseLabel="widgets.rpc-state.off" falseLabel="widgets.value-action.off"
formControlName="defaultValue"></tb-value-input> formControlName="defaultValue"></tb-value-input>
</div> </div>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="rpcInitialStateAction.EXECUTE_RPC"> <ng-template [ngSwitchCase]="getValueAction.EXECUTE_RPC">
<div class="tb-form-row" formGroupName="executeRpc"> <div class="tb-form-row" formGroupName="executeRpc">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.method' | translate }}*</div> <div class="fixed-title-width" >{{ 'widgets.value-action.method' | translate }}*</div>
<mat-form-field class="flex tb-suffix-absolute" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex tb-suffix-absolute" appearance="outline" subscriptSizing="dynamic">
<input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'widgets.rpc-state.method-name-required' | translate" [matTooltip]="'widgets.value-action.method-name-required' | translate"
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('method').hasError('required') *ngIf="getValueSettingsFormGroup.get('executeRpc').get('method').hasError('required')
&& initialStateSettingsFormGroup.get('executeRpc').get('method').touched" && getValueSettingsFormGroup.get('executeRpc').get('method').touched"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="rpcInitialStateAction.GET_ATTRIBUTE"> <ng-template [ngSwitchCase]="getValueAction.GET_ATTRIBUTE">
<ng-container formGroupName="getAttribute"> <ng-container formGroupName="getAttribute">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width">{{ 'widgets.rpc-state.attribute-scope' | translate }}</div> <div class="fixed-title-width">{{ 'widgets.value-action.attribute-scope' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="scope" placeholder="{{ 'attribute.scope-any' | translate }}"> <mat-select formControlName="scope" placeholder="{{ 'attribute.scope-any' | translate }}">
<mat-option [value]="null"> <mat-option [value]="null">
@ -71,58 +71,68 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.attribute-key' | translate }}*</div> <div class="fixed-title-width" >{{ 'widgets.value-action.attribute-key' | translate }}*</div>
<tb-device-key-autocomplete <tb-device-key-autocomplete
fxFlex fxFlex
formControlName="key" formControlName="key"
required required
requiredText="widgets.rpc-state.attribute-key-required" requiredText="widgets.value-action.attribute-key-required"
inlineField inlineField
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
[keyType]="dataKeyType.attribute" [keyType]="dataKeyType.attribute"
[attributeScope]="initialStateSettingsFormGroup.get('getAttribute').get('scope').value"> [attributeScope]="getValueSettingsFormGroup.get('getAttribute').get('scope').value">
</tb-device-key-autocomplete> </tb-device-key-autocomplete>
</div> </div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="subscribeForUpdates">
<span tb-hint-tooltip-icon="{{'widgets.value-action.subscribe-for-updates-hint' | translate}}">{{ 'widgets.value-action.subscribe-for-updates' | translate }}</span>
</mat-slide-toggle>
</div>
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="rpcInitialStateAction.GET_TIME_SERIES"> <ng-template [ngSwitchCase]="getValueAction.GET_TIME_SERIES">
<ng-container formGroupName="getTimeSeries"> <ng-container formGroupName="getTimeSeries">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.time-series-key' | translate }}*</div> <div class="fixed-title-width" >{{ 'widgets.value-action.time-series-key' | translate }}*</div>
<tb-device-key-autocomplete <tb-device-key-autocomplete
fxFlex fxFlex
formControlName="key" formControlName="key"
required required
requiredText="widgets.rpc-state.time-series-key-required" requiredText="widgets.value-action.time-series-key-required"
inlineField inlineField
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
[keyType]="dataKeyType.timeseries"> [keyType]="dataKeyType.timeseries">
</tb-device-key-autocomplete> </tb-device-key-autocomplete>
</div> </div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="subscribeForUpdates">
<span tb-hint-tooltip-icon="{{'widgets.value-action.subscribe-for-updates-hint' | translate}}">{{ 'widgets.value-action.subscribe-for-updates' | translate }}</span>
</mat-slide-toggle>
</div>
</ng-container> </ng-container>
</ng-template> </ng-template>
</ng-container> </ng-container>
<div *ngIf="initialStateSettingsFormGroup.get('action').value !== rpcInitialStateAction.DO_NOTHING" <div *ngIf="getValueSettingsFormGroup.get('action').value !== getValueAction.DO_NOTHING"
class="tb-form-panel stroked" formGroupName="dataToState"> class="tb-form-panel stroked" formGroupName="dataToValue">
<div class="tb-form-row no-padding no-border column-xs"> <div class="tb-form-row no-padding no-border column-xs">
<div class="fixed-title-width" translate>widgets.rpc-state.action-result-converter</div> <div class="fixed-title-width" translate>widgets.value-action.action-result-converter</div>
<tb-toggle-select fxFlex formControlName="type"> <tb-toggle-select fxFlex formControlName="type">
<tb-toggle-option [value]="dataToStateType.NONE">{{ 'widgets.rpc-state.converter-none' | translate }}</tb-toggle-option> <tb-toggle-option [value]="dataToValueType.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="dataToStateType.FUNCTION">{{ 'widgets.rpc-state.converter-function' | translate }}</tb-toggle-option> <tb-toggle-option [value]="dataToValueType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>
</div> </div>
<tb-js-func *ngIf="initialStateSettingsFormGroup.get('dataToState').get('type').value === dataToStateType.FUNCTION" <tb-js-func *ngIf="getValueSettingsFormGroup.get('dataToValue').get('type').value === dataToValueType.FUNCTION"
formControlName="dataToStateFunction" formControlName="dataToValueFunction"
required required
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['data']" [functionArgs]="['data']"
functionTitle="{{ 'widgets.rpc-state.parse-value-function' | translate }}" functionTitle="{{ 'widgets.value-action.parse-value-function' | translate }}"
helpId="widget/lib/rpc/parse_value_fn"> helpId="widget/lib/rpc/parse_value_fn">
</tb-js-func> </tb-js-func>
<div *ngIf="stateValueType === valueType.BOOLEAN" class="tb-form-row align-start no-gap column-xs"> <div *ngIf="valueType === ValueType.BOOLEAN" class="tb-form-row align-start no-gap column-xs">
<div class="fixed-title-width fixed-title-height" translate>widgets.rpc-state.on-when-result-is</div> <div class="fixed-title-width fixed-title-height" translate>widgets.value-action.on-when-result-is</div>
<tb-value-input <tb-value-input
fxFlex fxFlex
layout="column" layout="column"
@ -130,7 +140,7 @@
</tb-value-input> </tb-value-input>
</div> </div>
</div> </div>
<div *ngIf="initialStateSettingsFormGroup.get('action').value === rpcInitialStateAction.EXECUTE_RPC" <div *ngIf="getValueSettingsFormGroup.get('action').value === getValueAction.EXECUTE_RPC"
class="tb-form-panel no-border tb-slide-toggle" formGroupName="executeRpc"> class="tb-form-panel no-border tb-slide-toggle" formGroupName="executeRpc">
<mat-expansion-panel class="tb-settings no-padding"> <mat-expansion-panel class="tb-settings no-padding">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
@ -142,16 +152,16 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div>{{ 'widgets.rpc-state.request-timeout-ms' | translate }}</div> <div>{{ 'widgets.value-action.request-timeout-ms' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="(initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required') [matTooltip]="(getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required')
? 'widgets.rpc-state.request-timeout-required' : 'widgets.rpc-state.min-request-timeout-error') | translate" ? 'widgets.value-action.request-timeout-required' : 'widgets.value-action.min-request-timeout-error') | translate"
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && *ngIf="getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched &&
initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid" getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -159,28 +169,28 @@
</div> </div>
<div class="tb-form-panel stroked tb-slide-toggle"> <div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" <mat-expansion-panel class="tb-settings"
[expanded]="initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value" [expanded]="getValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value"
[disabled]="!initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value"> [disabled]="!getValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.rpc-state.request-persistent' | translate }} {{ 'widgets.value-action.request-persistent' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{'widgets.rpc-state.persistent-polling-interval-hint' | translate}}">{{ 'widgets.rpc-state.persistent-polling-interval' | translate }}</div> <div tb-hint-tooltip-icon="{{'widgets.value-action.persistent-polling-interval-hint' | translate}}">{{ 'widgets.value-action.persistent-polling-interval' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="(initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required') [matTooltip]="(getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required')
? 'widgets.rpc-state.persistent-polling-interval-required' : 'widgets.rpc-state.min-persistent-polling-interval-error') | translate" ? 'widgets.value-action.persistent-polling-interval-required' : 'widgets.value-action.min-persistent-polling-interval-error') | translate"
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && *ngIf="getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched &&
initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid" getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -193,7 +203,7 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</div> </div>
<div class="tb-rpc-state-settings-panel-buttons"> <div class="tb-value-action-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"
@ -203,8 +213,8 @@
<button mat-raised-button <button mat-raised-button
color="primary" color="primary"
type="button" type="button"
(click)="applyInitialStateSettings()" (click)="applyGetValueSettings()"
[disabled]="initialStateSettingsFormGroup.invalid || !initialStateSettingsFormGroup.dirty"> [disabled]="getValueSettingsFormGroup.invalid || !getValueSettingsFormGroup.dirty">
{{ 'action.apply' | translate }} {{ 'action.apply' | translate }}
</button> </button>
</div> </div>

182
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts

@ -0,0 +1,182 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import {
DataToValueType,
GetValueAction,
getValueActions,
getValueActionTranslations,
GetValueSettings
} from '@shared/models/action-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { TargetDevice } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-get-value-action-settings-panel',
templateUrl: './get-value-action-settings-panel.component.html',
providers: [],
styleUrls: ['./value-action-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class GetValueActionSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
getValueSettings: GetValueSettings<any>;
@Input()
panelTitle: string;
@Input()
valueType: ValueType;
@Input()
aliasController: IAliasController;
@Input()
targetDevice: TargetDevice;
@Input()
popover: TbPopoverComponent<GetValueActionSettingsPanelComponent>;
@Output()
getValueSettingsApplied = new EventEmitter<GetValueSettings<any>>();
getValueAction = GetValueAction;
getValueActions = getValueActions;
getValueActionTranslationsMap = getValueActionTranslations;
telemetryTypeTranslationsMap = telemetryTypeTranslationsShort;
attributeScopes = Object.keys(AttributeScope) as AttributeScope[];
dataKeyType = DataKeyType;
dataToValueType = DataToValueType;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
ValueType = ValueType;
getValueSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.getValueSettingsFormGroup = this.fb.group(
{
action: [this.getValueSettings?.action, []],
defaultValue: [this.getValueSettings?.defaultValue, [Validators.required]],
executeRpc: this.fb.group({
method: [this.getValueSettings?.executeRpc?.method, [Validators.required]],
requestTimeout: [this.getValueSettings?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]],
requestPersistent: [this.getValueSettings?.executeRpc?.requestPersistent, []],
persistentPollingInterval:
[this.getValueSettings?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]]
}),
getAttribute: this.fb.group({
scope: [this.getValueSettings?.getAttribute?.scope, []],
key: [this.getValueSettings?.getAttribute?.key, [Validators.required]],
subscribeForUpdates: [this.getValueSettings?.getAttribute?.subscribeForUpdates, []]
}),
getTimeSeries: this.fb.group({
key: [this.getValueSettings?.getTimeSeries?.key, [Validators.required]],
subscribeForUpdates: [this.getValueSettings?.getTimeSeries?.subscribeForUpdates, []]
}),
dataToValue: this.fb.group({
type: [this.getValueSettings?.dataToValue?.type, [Validators.required]],
dataToValueFunction: [this.getValueSettings?.dataToValue?.dataToValueFunction, [Validators.required]],
}),
}
);
if (this.valueType === ValueType.BOOLEAN) {
(this.getValueSettingsFormGroup.get('dataToValue') as UntypedFormGroup).addControl(
'compareToValue', this.fb.control(this.getValueSettings?.dataToValue?.compareToValue, [Validators.required])
);
}
merge(this.getValueSettingsFormGroup.get('action').valueChanges,
this.getValueSettingsFormGroup.get('dataToValue').get('type').valueChanges,
this.getValueSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => {
this.updateValidators();
});
this.updateValidators();
}
cancel() {
this.popover?.hide();
}
applyGetValueSettings() {
const getValueSettings: GetValueSettings<any> = this.getValueSettingsFormGroup.getRawValue();
this.getValueSettingsApplied.emit(getValueSettings);
}
private updateValidators() {
const action: GetValueAction = this.getValueSettingsFormGroup.get('action').value;
const dataToValueType: DataToValueType = this.getValueSettingsFormGroup.get('dataToValue').get('type').value;
this.getValueSettingsFormGroup.get('defaultValue').disable({emitEvent: false});
this.getValueSettingsFormGroup.get('executeRpc').disable({emitEvent: false});
this.getValueSettingsFormGroup.get('getAttribute').disable({emitEvent: false});
this.getValueSettingsFormGroup.get('getTimeSeries').disable({emitEvent: false});
switch (action) {
case GetValueAction.DO_NOTHING:
this.getValueSettingsFormGroup.get('defaultValue').enable({emitEvent: false});
break;
case GetValueAction.EXECUTE_RPC:
this.getValueSettingsFormGroup.get('executeRpc').enable({emitEvent: false});
const requestPersistent: boolean = this.getValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value;
if (requestPersistent) {
this.getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false});
} else {
this.getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false});
}
break;
case GetValueAction.GET_ATTRIBUTE:
this.getValueSettingsFormGroup.get('getAttribute').enable({emitEvent: false});
break;
case GetValueAction.GET_TIME_SERIES:
this.getValueSettingsFormGroup.get('getTimeSeries').enable({emitEvent: false});
break;
}
if (action === GetValueAction.DO_NOTHING) {
this.getValueSettingsFormGroup.get('dataToValue').disable({emitEvent: false});
} else {
this.getValueSettingsFormGroup.get('dataToValue').enable({emitEvent: false});
if (dataToValueType === DataToValueType.FUNCTION) {
this.getValueSettingsFormGroup.get('dataToValue').get('dataToValueFunction').enable({emitEvent: false});
} else {
this.getValueSettingsFormGroup.get('dataToValue').get('dataToValueFunction').disable({emitEvent: false});
}
}
}
}

62
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts

@ -28,35 +28,38 @@ import {
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { RpcInitialStateAction, RpcInitialStateSettings } from '@shared/models/rpc-widget-settings.models'; import { GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { import {
RpcInitialStateSettingsPanelComponent GetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component'; } from '@home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice } from '@shared/models/widget.models'; import { TargetDevice } from '@shared/models/widget.models';
@Component({ @Component({
selector: 'tb-rpc-initial-state-settings', selector: 'tb-get-value-action-settings',
templateUrl: './rpc-state-settings-button.component.html', templateUrl: './value-action-settings-button.component.html',
styleUrls: ['./rpc-state-settings-button.scss'], styleUrls: ['./value-action-settings-button.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RpcInitialStateSettingsComponent), useExisting: forwardRef(() => GetValueActionSettingsComponent),
multi: true multi: true
} }
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAccessor { export class GetValueActionSettingsComponent implements OnInit, ControlValueAccessor {
@HostBinding('style.overflow') @HostBinding('style.overflow')
overflow = 'hidden'; overflow = 'hidden';
@Input() @Input()
stateValueType: ValueType; panelTitle: string;
@Input()
valueType: ValueType;
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@ -67,7 +70,7 @@ export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAcc
@Input() @Input()
disabled = false; disabled = false;
modelValue: RpcInitialStateSettings<any>; modelValue: GetValueSettings<any>;
displayValue: string; displayValue: string;
@ -95,12 +98,12 @@ export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAcc
} }
} }
writeValue(value: RpcInitialStateSettings<any>): void { writeValue(value: GetValueSettings<any>): void {
this.modelValue = value; this.modelValue = value;
this.updateDisplayValue(); this.updateDisplayValue();
} }
openRpcStateSettingsPopup($event: Event, matButton: MatButton) { openValueActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -109,21 +112,22 @@ export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAcc
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const ctx: any = { const ctx: any = {
initialState: this.modelValue, getValueSettings: this.modelValue,
stateValueType: this.stateValueType, panelTitle: this.panelTitle,
valueType: this.valueType,
aliasController: this.aliasController, aliasController: this.aliasController,
targetDevice: this.targetDevice targetDevice: this.targetDevice
}; };
const initialStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const getValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, RpcInitialStateSettingsPanelComponent, this.viewContainerRef, GetValueActionSettingsPanelComponent,
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
ctx, ctx,
{}, {},
{}, {}, true); {}, {}, true);
initialStateSettingsPanelPopover.tbComponentRef.instance.popover = initialStateSettingsPanelPopover; getValueSettingsPanelPopover.tbComponentRef.instance.popover = getValueSettingsPanelPopover;
initialStateSettingsPanelPopover.tbComponentRef.instance.initialStateSettingsApplied.subscribe((initialState) => { getValueSettingsPanelPopover.tbComponentRef.instance.getValueSettingsApplied.subscribe((getValueSettings) => {
initialStateSettingsPanelPopover.hide(); getValueSettingsPanelPopover.hide();
this.modelValue = initialState; this.modelValue = getValueSettings;
this.updateDisplayValue(); this.updateDisplayValue();
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
}); });
@ -132,22 +136,22 @@ export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAcc
private updateDisplayValue() { private updateDisplayValue() {
switch (this.modelValue.action) { switch (this.modelValue.action) {
case RpcInitialStateAction.DO_NOTHING: case GetValueAction.DO_NOTHING:
if (this.stateValueType === ValueType.BOOLEAN) { if (this.valueType === ValueType.BOOLEAN) {
this.displayValue = this.translate.instant(!!this.modelValue.defaultValue ? 'widgets.rpc-state.on' : 'widgets.rpc-state.off'); this.displayValue = this.translate.instant(!!this.modelValue.defaultValue ? 'widgets.value-action.on' : 'widgets.value-action.off');
} else { } else {
this.displayValue = this.modelValue.defaultValue + ''; this.displayValue = this.modelValue.defaultValue + '';
} }
break; break;
case RpcInitialStateAction.EXECUTE_RPC: case GetValueAction.EXECUTE_RPC:
const methodName = this.modelValue.executeRpc.method; const methodName = this.modelValue.executeRpc.method;
this.displayValue = this.translate.instant('widgets.rpc-state.execute-rpc-text', {methodName}); this.displayValue = this.translate.instant('widgets.value-action.execute-rpc-text', {methodName});
break; break;
case RpcInitialStateAction.GET_ATTRIBUTE: case GetValueAction.GET_ATTRIBUTE:
this.displayValue = this.translate.instant('widgets.rpc-state.get-attribute-text', {key: this.modelValue.getAttribute.key}); this.displayValue = this.translate.instant('widgets.value-action.get-attribute-text', {key: this.modelValue.getAttribute.key});
break; break;
case RpcInitialStateAction.GET_TIME_SERIES: case GetValueAction.GET_TIME_SERIES:
this.displayValue = this.translate.instant('widgets.rpc-state.get-time-series-text', {key: this.modelValue.getTimeSeries.key}); this.displayValue = this.translate.instant('widgets.value-action.get-time-series-text', {key: this.modelValue.getTimeSeries.key});
break; break;
} }
this.cd.markForCheck(); this.cd.markForCheck();

96
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html

@ -15,41 +15,41 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-rpc-state-settings-panel" [formGroup]="updateStateSettingsFormGroup"> <div class="tb-value-action-settings-panel" [formGroup]="setValueSettingsFormGroup">
<div class="tb-rpc-state-settings-title">{{ panelTitle | translate }}</div> <div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-rpc-state-settings-panel-content"> <div class="tb-value-action-settings-panel-content">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.action' | translate }}</div> <div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="action"> <mat-select formControlName="action">
<mat-option *ngFor="let action of rpcUpdateStateActions" [value]="action"> <mat-option *ngFor="let action of setValueActions" [value]="action">
{{ rpcUpdateStateTranslationsMap.get(action) | translate }} {{ setValueActionTranslationsMap.get(action) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<ng-container [ngSwitch]="updateStateSettingsFormGroup.get('action').value"> <ng-container [ngSwitch]="setValueSettingsFormGroup.get('action').value">
<ng-template [ngSwitchCase]="rpcUpdateStateAction.EXECUTE_RPC"> <ng-template [ngSwitchCase]="setValueAction.EXECUTE_RPC">
<div class="tb-form-row" formGroupName="executeRpc"> <div class="tb-form-row" formGroupName="executeRpc">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.method' | translate }}*</div> <div class="fixed-title-width" >{{ 'widgets.value-action.method' | translate }}*</div>
<mat-form-field class="flex tb-suffix-absolute" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex tb-suffix-absolute" appearance="outline" subscriptSizing="dynamic">
<input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'widgets.rpc-state.method-name-required' | translate" [matTooltip]="'widgets.value-action.method-name-required' | translate"
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('method').hasError('required') *ngIf="setValueSettingsFormGroup.get('executeRpc').get('method').hasError('required')
&& updateStateSettingsFormGroup.get('executeRpc').get('method').touched" && setValueSettingsFormGroup.get('executeRpc').get('method').touched"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="rpcUpdateStateAction.SET_ATTRIBUTE"> <ng-template [ngSwitchCase]="setValueAction.SET_ATTRIBUTE">
<ng-container formGroupName="setAttribute"> <ng-container formGroupName="setAttribute">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width">{{ 'widgets.rpc-state.attribute-scope' | translate }}</div> <div class="fixed-title-width">{{ 'widgets.value-action.attribute-scope' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="scope"> <mat-select formControlName="scope">
<mat-option *ngFor="let scope of attributeScopes" [value]="scope"> <mat-option *ngFor="let scope of attributeScopes" [value]="scope">
@ -59,30 +59,30 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.attribute-key' | translate }}*</div> <div class="fixed-title-width" >{{ 'widgets.value-action.attribute-key' | translate }}*</div>
<tb-device-key-autocomplete <tb-device-key-autocomplete
fxFlex fxFlex
formControlName="key" formControlName="key"
required required
requiredText="widgets.rpc-state.attribute-key-required" requiredText="widgets.value-action.attribute-key-required"
inlineField inlineField
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
[keyType]="dataKeyType.attribute" [keyType]="dataKeyType.attribute"
[attributeScope]="updateStateSettingsFormGroup.get('setAttribute').get('scope').value"> [attributeScope]="setValueSettingsFormGroup.get('setAttribute').get('scope').value">
</tb-device-key-autocomplete> </tb-device-key-autocomplete>
</div> </div>
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="rpcUpdateStateAction.ADD_TIME_SERIES"> <ng-template [ngSwitchCase]="setValueAction.ADD_TIME_SERIES">
<ng-container formGroupName="putTimeSeries"> <ng-container formGroupName="putTimeSeries">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.rpc-state.time-series-key' | translate }}*</div> <div class="fixed-title-width" >{{ 'widgets.value-action.time-series-key' | translate }}*</div>
<tb-device-key-autocomplete <tb-device-key-autocomplete
fxFlex fxFlex
formControlName="key" formControlName="key"
required required
requiredText="widgets.rpc-state.time-series-key-required" requiredText="widgets.value-action.time-series-key-required"
inlineField inlineField
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
@ -92,18 +92,18 @@
</ng-container> </ng-container>
</ng-template> </ng-template>
</ng-container> </ng-container>
<div class="tb-form-panel stroked" formGroupName="stateToParams"> <div class="tb-form-panel stroked" formGroupName="valueToData">
<div class="tb-form-row no-padding no-border column-xs"> <div class="tb-form-row no-padding no-border column-xs">
<div class="fixed-title-width">{{ (updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC ? <div class="fixed-title-width">{{ (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC ?
'widgets.rpc-state.parameters' : 'widgets.rpc-state.value') | translate }}</div> 'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}</div>
<tb-toggle-select fxFlex formControlName="type"> <tb-toggle-select fxFlex formControlName="type">
<tb-toggle-option [value]="stateToParamsType.CONSTANT">{{ 'widgets.rpc-state.converter-constant' | translate }}</tb-toggle-option> <tb-toggle-option [value]="valueToDataType.CONSTANT">{{ 'widgets.value-action.converter-constant' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="stateToParamsType.FUNCTION">{{ 'widgets.rpc-state.converter-function' | translate }}</tb-toggle-option> <tb-toggle-option [value]="valueToDataType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC" <tb-toggle-option *ngIf="setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC"
[value]="stateToParamsType.NONE">{{ 'widgets.rpc-state.converter-none' | translate }}</tb-toggle-option> [value]="valueToDataType.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>
</div> </div>
<tb-value-input *ngIf="updateStateSettingsFormGroup.get('stateToParams').get('type').value === stateToParamsType.CONSTANT" <tb-value-input *ngIf="setValueSettingsFormGroup.get('valueToData').get('type').value === valueToDataType.CONSTANT"
fxFlex fxFlex
[layout]="{ [layout]="{
layout: 'row', layout: 'row',
@ -111,16 +111,16 @@
}" }"
formControlName="constantValue"> formControlName="constantValue">
</tb-value-input> </tb-value-input>
<tb-js-func *ngIf="updateStateSettingsFormGroup.get('stateToParams').get('type').value === stateToParamsType.FUNCTION" <tb-js-func *ngIf="setValueSettingsFormGroup.get('valueToData').get('type').value === valueToDataType.FUNCTION"
formControlName="stateToParamsFunction" formControlName="valueToDataFunction"
required required
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value']" [functionArgs]="['value']"
functionTitle="{{ 'widgets.rpc-state.convert-value-function' | translate }}" functionTitle="{{ 'widgets.value-action.convert-value-function' | translate }}"
helpId="widget/lib/rpc/convert_value_fn"> helpId="widget/lib/rpc/convert_value_fn">
</tb-js-func> </tb-js-func>
</div> </div>
<div *ngIf="updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC" <div *ngIf="setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC"
class="tb-form-panel no-border tb-slide-toggle" formGroupName="executeRpc"> class="tb-form-panel no-border tb-slide-toggle" formGroupName="executeRpc">
<mat-expansion-panel class="tb-settings no-padding"> <mat-expansion-panel class="tb-settings no-padding">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
@ -132,16 +132,16 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div>{{ 'widgets.rpc-state.request-timeout-ms' | translate }}</div> <div>{{ 'widgets.value-action.request-timeout-ms' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="(updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required') [matTooltip]="(setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required')
? 'widgets.rpc-state.request-timeout-required' : 'widgets.rpc-state.min-request-timeout-error') | translate" ? 'widgets.value-action.request-timeout-required' : 'widgets.value-action.min-request-timeout-error') | translate"
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').touched && *ngIf="setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched &&
updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid" setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -149,28 +149,28 @@
</div> </div>
<div class="tb-form-panel stroked tb-slide-toggle"> <div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" <mat-expansion-panel class="tb-settings"
[expanded]="updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value" [expanded]="setValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value"
[disabled]="!updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value"> [disabled]="!setValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.rpc-state.request-persistent' | translate }} {{ 'widgets.value-action.request-persistent' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{'widgets.rpc-state.persistent-polling-interval-hint' | translate}}">{{ 'widgets.rpc-state.persistent-polling-interval' | translate }}</div> <div tb-hint-tooltip-icon="{{'widgets.value-action.persistent-polling-interval-hint' | translate}}">{{ 'widgets.value-action.persistent-polling-interval' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="(updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required') [matTooltip]="(setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required')
? 'widgets.rpc-state.persistent-polling-interval-required' : 'widgets.rpc-state.min-persistent-polling-interval-error') | translate" ? 'widgets.value-action.persistent-polling-interval-required' : 'widgets.value-action.min-persistent-polling-interval-error') | translate"
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched && *ngIf="setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched &&
updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid" setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
@ -183,7 +183,7 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</div> </div>
<div class="tb-rpc-state-settings-panel-buttons"> <div class="tb-value-action-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"
@ -193,8 +193,8 @@
<button mat-raised-button <button mat-raised-button
color="primary" color="primary"
type="button" type="button"
(click)="applyUpdateStateSettings()" (click)="applySetValueSettings()"
[disabled]="updateStateSettingsFormGroup.invalid || !updateStateSettingsFormGroup.dirty"> [disabled]="setValueSettingsFormGroup.invalid || !setValueSettingsFormGroup.dirty">
{{ 'action.apply' | translate }} {{ 'action.apply' | translate }}
</button> </button>
</div> </div>

182
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts

@ -0,0 +1,182 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import {
getValueActions,
SetValueAction,
setValueActions,
setValueActionTranslations,
SetValueSettings,
ValueToDataType
} from '@shared/models/action-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { TargetDevice } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-set-value-action-settings-panel',
templateUrl: './set-value-action-settings-panel.component.html',
providers: [],
styleUrls: ['./value-action-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SetValueActionSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
panelTitle: string;
@Input()
setValueSettings: SetValueSettings;
@Input()
valueType: ValueType;
@Input()
aliasController: IAliasController;
@Input()
targetDevice: TargetDevice;
@Input()
popover: TbPopoverComponent<SetValueActionSettingsPanelComponent>;
@Output()
setValueSettingsApplied = new EventEmitter<SetValueSettings>();
setValueAction = SetValueAction;
setValueActions = setValueActions;
setValueActionTranslationsMap = setValueActionTranslations;
telemetryTypeTranslationsMap = telemetryTypeTranslationsShort;
attributeScopes = [AttributeScope.SERVER_SCOPE, AttributeScope.SHARED_SCOPE];
dataKeyType = DataKeyType;
valueToDataType = ValueToDataType;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
ValueType = ValueType;
setValueSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.setValueSettingsFormGroup = this.fb.group(
{
action: [this.setValueSettings?.action, []],
executeRpc: this.fb.group({
method: [this.setValueSettings?.executeRpc?.method, [Validators.required]],
requestTimeout: [this.setValueSettings?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]],
requestPersistent: [this.setValueSettings?.executeRpc?.requestPersistent, []],
persistentPollingInterval:
[this.setValueSettings?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]]
}),
setAttribute: this.fb.group({
scope: [this.setValueSettings?.setAttribute?.scope, []],
key: [this.setValueSettings?.setAttribute?.key, [Validators.required]],
}),
putTimeSeries: this.fb.group({
key: [this.setValueSettings?.putTimeSeries?.key, [Validators.required]],
}),
valueToData: this.fb.group({
type: [this.setValueSettings?.valueToData?.type, [Validators.required]],
constantValue: [this.setValueSettings?.valueToData?.constantValue, [Validators.required]],
valueToDataFunction: [this.setValueSettings?.valueToData?.valueToDataFunction, [Validators.required]],
}),
}
);
merge(this.setValueSettingsFormGroup.get('action').valueChanges,
this.setValueSettingsFormGroup.get('valueToData').get('type').valueChanges,
this.setValueSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => {
this.updateValidators();
});
this.updateValidators();
}
cancel() {
this.popover?.hide();
}
applySetValueSettings() {
const setValueSettings: SetValueSettings = this.setValueSettingsFormGroup.getRawValue();
this.setValueSettingsApplied.emit(setValueSettings);
}
private updateValidators() {
const action: SetValueAction = this.setValueSettingsFormGroup.get('action').value;
let valueToDataType: ValueToDataType = this.setValueSettingsFormGroup.get('valueToData').get('type').value;
this.setValueSettingsFormGroup.get('executeRpc').disable({emitEvent: false});
this.setValueSettingsFormGroup.get('setAttribute').disable({emitEvent: false});
this.setValueSettingsFormGroup.get('putTimeSeries').disable({emitEvent: false});
switch (action) {
case SetValueAction.EXECUTE_RPC:
this.setValueSettingsFormGroup.get('executeRpc').enable({emitEvent: false});
const requestPersistent: boolean = this.setValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value;
if (requestPersistent) {
this.setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false});
} else {
this.setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false});
}
break;
case SetValueAction.SET_ATTRIBUTE:
case SetValueAction.ADD_TIME_SERIES:
if (valueToDataType === ValueToDataType.NONE) {
valueToDataType = ValueToDataType.CONSTANT;
this.setValueSettingsFormGroup.get('valueToData').get('type').patchValue(valueToDataType, {emitEvent: false});
}
if (action === SetValueAction.SET_ATTRIBUTE) {
this.setValueSettingsFormGroup.get('setAttribute').enable({emitEvent: false});
} else {
this.setValueSettingsFormGroup.get('putTimeSeries').enable({emitEvent: false});
}
break;
}
switch (valueToDataType) {
case ValueToDataType.CONSTANT:
this.setValueSettingsFormGroup.get('valueToData').get('constantValue').enable({emitEvent: false});
this.setValueSettingsFormGroup.get('valueToData').get('valueToDataFunction').disable({emitEvent: false});
break;
case ValueToDataType.FUNCTION:
this.setValueSettingsFormGroup.get('valueToData').get('constantValue').disable({emitEvent: false});
this.setValueSettingsFormGroup.get('valueToData').get('valueToDataFunction').enable({emitEvent: false});
break;
case ValueToDataType.NONE:
this.setValueSettingsFormGroup.get('valueToData').get('constantValue').disable({emitEvent: false});
this.setValueSettingsFormGroup.get('valueToData').get('valueToDataFunction').disable({emitEvent: false});
break;
}
}
}

68
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts

@ -28,34 +28,30 @@ import {
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { import { SetValueAction, SetValueSettings, ValueToDataType } from '@shared/models/action-widget-settings.models';
RpcStateToParamsType,
RpcUpdateStateAction,
RpcUpdateStateSettings
} from '@shared/models/rpc-widget-settings.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice } from '@shared/models/widget.models'; import { TargetDevice } from '@shared/models/widget.models';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { import {
RpcUpdateStateSettingsPanelComponent SetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component'; } from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component';
@Component({ @Component({
selector: 'tb-rpc-update-state-settings', selector: 'tb-set-value-action-settings',
templateUrl: './rpc-state-settings-button.component.html', templateUrl: './value-action-settings-button.component.html',
styleUrls: ['./rpc-state-settings-button.scss'], styleUrls: ['./value-action-settings-button.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RpcUpdateStateSettingsComponent), useExisting: forwardRef(() => SetValueActionSettingsComponent),
multi: true multi: true
} }
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAccessor { export class SetValueActionSettingsComponent implements OnInit, ControlValueAccessor {
@HostBinding('style.overflow') @HostBinding('style.overflow')
overflow = 'hidden'; overflow = 'hidden';
@ -64,7 +60,7 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
panelTitle: string; panelTitle: string;
@Input() @Input()
stateValueType: ValueType; valueType: ValueType;
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@ -75,7 +71,7 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
@Input() @Input()
disabled = false; disabled = false;
modelValue: RpcUpdateStateSettings; modelValue: SetValueSettings;
displayValue: string; displayValue: string;
@ -103,12 +99,12 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
} }
} }
writeValue(value: RpcUpdateStateSettings): void { writeValue(value: SetValueSettings): void {
this.modelValue = value; this.modelValue = value;
this.updateDisplayValue(); this.updateDisplayValue();
} }
openRpcStateSettingsPopup($event: Event, matButton: MatButton) { openValueActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -117,22 +113,22 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const ctx: any = { const ctx: any = {
updateState: this.modelValue, setValueSettings: this.modelValue,
panelTitle: this.panelTitle, panelTitle: this.panelTitle,
stateValueType: this.stateValueType, valueType: this.valueType,
aliasController: this.aliasController, aliasController: this.aliasController,
targetDevice: this.targetDevice targetDevice: this.targetDevice
}; };
const updateStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const setValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, RpcUpdateStateSettingsPanelComponent, this.viewContainerRef, SetValueActionSettingsPanelComponent,
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
ctx, ctx,
{}, {},
{}, {}, true); {}, {}, true);
updateStateSettingsPanelPopover.tbComponentRef.instance.popover = updateStateSettingsPanelPopover; setValueSettingsPanelPopover.tbComponentRef.instance.popover = setValueSettingsPanelPopover;
updateStateSettingsPanelPopover.tbComponentRef.instance.updateStateSettingsApplied.subscribe((updateState) => { setValueSettingsPanelPopover.tbComponentRef.instance.setValueSettingsApplied.subscribe((setValueSettings) => {
updateStateSettingsPanelPopover.hide(); setValueSettingsPanelPopover.hide();
this.modelValue = updateState; this.modelValue = setValueSettings;
this.updateDisplayValue(); this.updateDisplayValue();
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
}); });
@ -141,31 +137,31 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
private updateDisplayValue() { private updateDisplayValue() {
let value: any; let value: any;
switch (this.modelValue.stateToParams.type) { switch (this.modelValue.valueToData.type) {
case RpcStateToParamsType.CONSTANT: case ValueToDataType.CONSTANT:
value = this.modelValue.stateToParams.constantValue; value = this.modelValue.valueToData.constantValue;
break; break;
case RpcStateToParamsType.FUNCTION: case ValueToDataType.FUNCTION:
value = 'f(value)'; value = 'f(value)';
break; break;
case RpcStateToParamsType.NONE: case ValueToDataType.NONE:
break; break;
} }
switch (this.modelValue.action) { switch (this.modelValue.action) {
case RpcUpdateStateAction.EXECUTE_RPC: case SetValueAction.EXECUTE_RPC:
let methodName = this.modelValue.executeRpc.method; let methodName = this.modelValue.executeRpc.method;
if (isDefinedAndNotNull(value)) { if (isDefinedAndNotNull(value)) {
methodName = `${methodName}(${value})`; methodName = `${methodName}(${value})`;
} }
this.displayValue = this.translate.instant('widgets.rpc-state.execute-rpc-text', {methodName}); this.displayValue = this.translate.instant('widgets.value-action.execute-rpc-text', {methodName});
break; break;
case RpcUpdateStateAction.SET_ATTRIBUTE: case SetValueAction.SET_ATTRIBUTE:
this.displayValue = this.translate.instant('widgets.rpc-state.set-attribute-to-value-text', this.displayValue = this.translate.instant('widgets.value-action.set-attribute-to-value-text',
{key: this.modelValue.setAttribute.key, value}); {key: this.modelValue.setAttribute.key, value});
break; break;
case RpcUpdateStateAction.ADD_TIME_SERIES: case SetValueAction.ADD_TIME_SERIES:
this.displayValue = this.translate.instant('widgets.rpc-state.add-time-series-value-text', this.displayValue = this.translate.instant('widgets.value-action.add-time-series-value-text',
{key: this.modelValue.setAttribute.key, value}); {key: this.modelValue.putTimeSeries.key, value});
break; break;
} }
this.cd.markForCheck(); this.cd.markForCheck();

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-button.component.html

@ -16,9 +16,9 @@
--> -->
<button #matButton [disabled]="disabled" <button #matButton [disabled]="disabled"
class="tb-rpc-state-settings" class="tb-value-action-settings"
mat-stroked-button color="primary" mat-stroked-button color="primary"
(click)="openRpcStateSettingsPopup($event, matButton)"> (click)="openValueActionSettingsPopup($event, matButton)">
<div> <div>
<span>{{ displayValue }}</span> <span>{{ displayValue }}</span>
<mat-icon class="tb-mat-20">edit</mat-icon> <mat-icon class="tb-mat-20">edit</mat-icon>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-button.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-button.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.mdc-button.tb-rpc-state-settings { .mdc-button.tb-value-action-settings {
width: 100%; width: 100%;
.mdc-button__label { .mdc-button__label {
width: 100%; width: 100%;

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-state-settings-panel.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-panel.component.scss

@ -15,7 +15,7 @@
*/ */
@import '../../../../../../../../../scss/constants'; @import '../../../../../../../../../scss/constants';
.tb-rpc-state-settings-panel { .tb-value-action-settings-panel {
width: 530px; width: 530px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -23,20 +23,20 @@
@media #{$mat-lt-md} { @media #{$mat-lt-md} {
width: 90vw; width: 90vw;
} }
.tb-rpc-state-settings-panel-content { .tb-value-action-settings-panel-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
overflow: auto; overflow: auto;
} }
.tb-rpc-state-settings-title { .tb-value-action-settings-title {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 24px; line-height: 24px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-rpc-state-settings-panel-buttons { .tb-value-action-settings-panel-buttons {
height: 40px; height: 40px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;

176
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component.ts

@ -1,176 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import {
RpcDataToStateType,
RpcInitialStateAction,
rpcInitialStateActions,
RpcInitialStateSettings,
rpcInitialStateTranslations
} from '@shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { TargetDevice } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-rpc-initial-state-settings-panel',
templateUrl: './rpc-initial-state-settings-panel.component.html',
providers: [],
styleUrls: ['./rpc-state-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class RpcInitialStateSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
initialState: RpcInitialStateSettings<any>;
@Input()
stateValueType: ValueType;
@Input()
aliasController: IAliasController;
@Input()
targetDevice: TargetDevice;
@Input()
popover: TbPopoverComponent<RpcInitialStateSettingsPanelComponent>;
@Output()
initialStateSettingsApplied = new EventEmitter<RpcInitialStateSettings<any>>();
rpcInitialStateAction = RpcInitialStateAction;
rpcInitialStateActions = rpcInitialStateActions;
rpcInitialStateTranslationsMap = rpcInitialStateTranslations;
telemetryTypeTranslationsMap = telemetryTypeTranslationsShort;
attributeScopes = Object.keys(AttributeScope) as AttributeScope[];
dataKeyType = DataKeyType;
dataToStateType = RpcDataToStateType;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
valueType = ValueType;
initialStateSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.initialStateSettingsFormGroup = this.fb.group(
{
action: [this.initialState?.action, []],
defaultValue: [this.initialState?.defaultValue, [Validators.required]],
executeRpc: this.fb.group({
method: [this.initialState?.executeRpc?.method, [Validators.required]],
requestTimeout: [this.initialState?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]],
requestPersistent: [this.initialState?.executeRpc?.requestPersistent, []],
persistentPollingInterval: [this.initialState?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]]
}),
getAttribute: this.fb.group({
scope: [this.initialState?.getAttribute?.scope, []],
key: [this.initialState?.getAttribute?.key, [Validators.required]],
}),
getTimeSeries: this.fb.group({
key: [this.initialState?.getTimeSeries?.key, [Validators.required]],
}),
dataToState: this.fb.group({
type: [this.initialState?.dataToState?.type, [Validators.required]],
dataToStateFunction: [this.initialState?.dataToState?.dataToStateFunction, [Validators.required]],
}),
}
);
if (this.stateValueType === ValueType.BOOLEAN) {
(this.initialStateSettingsFormGroup.get('dataToState') as UntypedFormGroup).addControl(
'compareToValue', this.fb.control(this.initialState?.dataToState?.compareToValue, [Validators.required])
);
}
merge(this.initialStateSettingsFormGroup.get('action').valueChanges,
this.initialStateSettingsFormGroup.get('dataToState').get('type').valueChanges,
this.initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => {
this.updateValidators();
});
this.updateValidators();
}
cancel() {
this.popover?.hide();
}
applyInitialStateSettings() {
const initialStateSettings: RpcInitialStateSettings<any> = this.initialStateSettingsFormGroup.getRawValue();
this.initialStateSettingsApplied.emit(initialStateSettings);
}
private updateValidators() {
const action: RpcInitialStateAction = this.initialStateSettingsFormGroup.get('action').value;
const dataToStateType: RpcDataToStateType = this.initialStateSettingsFormGroup.get('dataToState').get('type').value;
this.initialStateSettingsFormGroup.get('defaultValue').disable({emitEvent: false});
this.initialStateSettingsFormGroup.get('executeRpc').disable({emitEvent: false});
this.initialStateSettingsFormGroup.get('getAttribute').disable({emitEvent: false});
this.initialStateSettingsFormGroup.get('getTimeSeries').disable({emitEvent: false});
switch (action) {
case RpcInitialStateAction.DO_NOTHING:
this.initialStateSettingsFormGroup.get('defaultValue').enable({emitEvent: false});
break;
case RpcInitialStateAction.EXECUTE_RPC:
this.initialStateSettingsFormGroup.get('executeRpc').enable({emitEvent: false});
const requestPersistent: boolean = this.initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value;
if (requestPersistent) {
this.initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false});
} else {
this.initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false});
}
break;
case RpcInitialStateAction.GET_ATTRIBUTE:
this.initialStateSettingsFormGroup.get('getAttribute').enable({emitEvent: false});
break;
case RpcInitialStateAction.GET_TIME_SERIES:
this.initialStateSettingsFormGroup.get('getTimeSeries').enable({emitEvent: false});
break;
}
if (action === RpcInitialStateAction.DO_NOTHING) {
this.initialStateSettingsFormGroup.get('dataToState').disable({emitEvent: false});
} else {
this.initialStateSettingsFormGroup.get('dataToState').enable({emitEvent: false});
if (dataToStateType === RpcDataToStateType.FUNCTION) {
this.initialStateSettingsFormGroup.get('dataToState').get('dataToStateFunction').enable({emitEvent: false});
} else {
this.initialStateSettingsFormGroup.get('dataToState').get('dataToStateFunction').disable({emitEvent: false});
}
}
}
}

180
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component.ts

@ -1,180 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import {
RpcStateToParamsType,
RpcUpdateStateAction,
rpcUpdateStateActions,
RpcUpdateStateSettings,
rpcUpdateStateTranslations
} from '@shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { TargetDevice } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-rpc-update-state-settings-panel',
templateUrl: './rpc-update-state-settings-panel.component.html',
providers: [],
styleUrls: ['./rpc-state-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class RpcUpdateStateSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
panelTitle: string;
@Input()
updateState: RpcUpdateStateSettings;
@Input()
stateValueType: ValueType;
@Input()
aliasController: IAliasController;
@Input()
targetDevice: TargetDevice;
@Input()
popover: TbPopoverComponent<RpcUpdateStateSettingsPanelComponent>;
@Output()
updateStateSettingsApplied = new EventEmitter<RpcUpdateStateSettings>();
rpcUpdateStateAction = RpcUpdateStateAction;
rpcUpdateStateActions = rpcUpdateStateActions;
rpcUpdateStateTranslationsMap = rpcUpdateStateTranslations;
telemetryTypeTranslationsMap = telemetryTypeTranslationsShort;
attributeScopes = [AttributeScope.SERVER_SCOPE, AttributeScope.SHARED_SCOPE];
dataKeyType = DataKeyType;
stateToParamsType = RpcStateToParamsType;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
valueType = ValueType;
updateStateSettingsFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.updateStateSettingsFormGroup = this.fb.group(
{
action: [this.updateState?.action, []],
executeRpc: this.fb.group({
method: [this.updateState?.executeRpc?.method, [Validators.required]],
requestTimeout: [this.updateState?.executeRpc?.requestTimeout, [Validators.required, Validators.min(5000)]],
requestPersistent: [this.updateState?.executeRpc?.requestPersistent, []],
persistentPollingInterval: [this.updateState?.executeRpc?.persistentPollingInterval, [Validators.required, Validators.min(1000)]]
}),
setAttribute: this.fb.group({
scope: [this.updateState?.setAttribute?.scope, []],
key: [this.updateState?.setAttribute?.key, [Validators.required]],
}),
putTimeSeries: this.fb.group({
key: [this.updateState?.putTimeSeries?.key, [Validators.required]],
}),
stateToParams: this.fb.group({
type: [this.updateState?.stateToParams?.type, [Validators.required]],
constantValue: [this.updateState?.stateToParams?.constantValue, [Validators.required]],
stateToParamsFunction: [this.updateState?.stateToParams?.stateToParamsFunction, [Validators.required]],
}),
}
);
merge(this.updateStateSettingsFormGroup.get('action').valueChanges,
this.updateStateSettingsFormGroup.get('stateToParams').get('type').valueChanges,
this.updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').valueChanges).subscribe(() => {
this.updateValidators();
});
this.updateValidators();
}
cancel() {
this.popover?.hide();
}
applyUpdateStateSettings() {
const updateStateSettings: RpcUpdateStateSettings = this.updateStateSettingsFormGroup.getRawValue();
this.updateStateSettingsApplied.emit(updateStateSettings);
}
private updateValidators() {
const action: RpcUpdateStateAction = this.updateStateSettingsFormGroup.get('action').value;
let stateToParamsType: RpcStateToParamsType = this.updateStateSettingsFormGroup.get('stateToParams').get('type').value;
this.updateStateSettingsFormGroup.get('executeRpc').disable({emitEvent: false});
this.updateStateSettingsFormGroup.get('setAttribute').disable({emitEvent: false});
this.updateStateSettingsFormGroup.get('putTimeSeries').disable({emitEvent: false});
switch (action) {
case RpcUpdateStateAction.EXECUTE_RPC:
this.updateStateSettingsFormGroup.get('executeRpc').enable({emitEvent: false});
const requestPersistent: boolean = this.updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value;
if (requestPersistent) {
this.updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').enable({emitEvent: false});
} else {
this.updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').disable({emitEvent: false});
}
break;
case RpcUpdateStateAction.SET_ATTRIBUTE:
case RpcUpdateStateAction.ADD_TIME_SERIES:
if (stateToParamsType === RpcStateToParamsType.NONE) {
stateToParamsType = RpcStateToParamsType.CONSTANT;
this.updateStateSettingsFormGroup.get('stateToParams').get('type').patchValue(stateToParamsType, {emitEvent: false});
}
if (action === RpcUpdateStateAction.SET_ATTRIBUTE) {
this.updateStateSettingsFormGroup.get('setAttribute').enable({emitEvent: false});
} else {
this.updateStateSettingsFormGroup.get('putTimeSeries').enable({emitEvent: false});
}
break;
}
switch (stateToParamsType) {
case RpcStateToParamsType.CONSTANT:
this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').enable({emitEvent: false});
this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').disable({emitEvent: false});
break;
case RpcStateToParamsType.FUNCTION:
this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').disable({emitEvent: false});
this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').enable({emitEvent: false});
break;
case RpcStateToParamsType.NONE:
this.updateStateSettingsFormGroup.get('stateToParams').get('constantValue').disable({emitEvent: false});
this.updateStateSettingsFormGroup.get('stateToParams').get('stateToParamsFunction').disable({emitEvent: false});
break;
}
}
}

32
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -52,20 +52,20 @@ import {
ColorRangeSettingsComponent, ColorRangeSettingsComponentService ColorRangeSettingsComponent, ColorRangeSettingsComponentService
} from '@home/components/widget/lib/settings/common/color-range-settings.component'; } from '@home/components/widget/lib/settings/common/color-range-settings.component';
import { import {
RpcInitialStateSettingsComponent GetValueActionSettingsComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings.component'; } from '@home/components/widget/lib/settings/common/action/get-value-action-settings.component';
import { import {
RpcInitialStateSettingsPanelComponent GetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component'; } from '@home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component';
import { import {
DeviceKeyAutocompleteComponent DeviceKeyAutocompleteComponent
} from '@home/components/widget/lib/settings/control/device-key-autocomplete.component'; } from '@home/components/widget/lib/settings/control/device-key-autocomplete.component';
import { import {
RpcUpdateStateSettingsComponent SetValueActionSettingsComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings.component'; } from '@home/components/widget/lib/settings/common/action/set-value-action-settings.component';
import { import {
RpcUpdateStateSettingsPanelComponent SetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component'; } from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component';
import { CssSizeInputComponent } from '@home/components/widget/lib/settings/common/css-size-input.component'; import { CssSizeInputComponent } from '@home/components/widget/lib/settings/common/css-size-input.component';
@NgModule({ @NgModule({
@ -89,11 +89,11 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
ColorRangeListComponent, ColorRangeListComponent,
ColorRangePanelComponent, ColorRangePanelComponent,
ColorRangeSettingsComponent, ColorRangeSettingsComponent,
RpcInitialStateSettingsComponent, GetValueActionSettingsComponent,
RpcInitialStateSettingsPanelComponent, GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent, DeviceKeyAutocompleteComponent,
RpcUpdateStateSettingsComponent, SetValueActionSettingsComponent,
RpcUpdateStateSettingsPanelComponent SetValueActionSettingsPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -120,11 +120,11 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
ColorRangeListComponent, ColorRangeListComponent,
ColorRangePanelComponent, ColorRangePanelComponent,
ColorRangeSettingsComponent, ColorRangeSettingsComponent,
RpcInitialStateSettingsComponent, GetValueActionSettingsComponent,
RpcInitialStateSettingsPanelComponent, GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent, DeviceKeyAutocompleteComponent,
RpcUpdateStateSettingsComponent, SetValueActionSettingsComponent,
RpcUpdateStateSettingsPanelComponent SetValueActionSettingsPanelComponent
], ],
providers: [ providers: [
ColorSettingsComponentService, ColorSettingsComponentService,

33
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html

@ -19,30 +19,31 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div> <div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div> <div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.initial-state-hint' | translate}}" translate>widgets.value-action.initial-state</div>
<tb-rpc-initial-state-settings fxFlex <tb-get-value-action-settings fxFlex
[stateValueType]="valueType.BOOLEAN" panelTitle="widgets.value-action.initial-state"
[aliasController]="aliasController" [valueType]="valueType.BOOLEAN"
[targetDevice]="targetDevice" [aliasController]="aliasController"
formControlName="initialState"></tb-rpc-initial-state-settings> [targetDevice]="targetDevice"
formControlName="initialState"></tb-get-value-action-settings>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-on-hint' | translate}}" translate>widgets.rpc-state.turn-on</div> <div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-on-hint' | translate}}" translate>widgets.value-action.turn-on</div>
<tb-rpc-update-state-settings fxFlex <tb-set-value-action-settings fxFlex
panelTitle="widgets.rpc-state.turn-on" panelTitle="widgets.value-action.turn-on"
[stateValueType]="valueType.BOOLEAN" [valueType]="valueType.BOOLEAN"
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
formControlName="onUpdateState"></tb-rpc-update-state-settings> formControlName="onUpdateState"></tb-set-value-action-settings>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-off-hint' | translate}}" translate>widgets.rpc-state.turn-off</div> <div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-off-hint' | translate}}" translate>widgets.value-action.turn-off</div>
<tb-rpc-update-state-settings fxFlex <tb-set-value-action-settings fxFlex
panelTitle="widgets.rpc-state.turn-off" panelTitle="widgets.value-action.turn-off"
[stateValueType]="valueType.BOOLEAN" [valueType]="valueType.BOOLEAN"
[aliasController]="aliasController" [aliasController]="aliasController"
[targetDevice]="targetDevice" [targetDevice]="targetDevice"
formControlName="offUpdateState"></tb-rpc-update-state-settings> formControlName="offUpdateState"></tb-set-value-action-settings>
</div> </div>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">

2
ui-ngx/src/app/shared/components/image/image-dialog.component.ts

@ -154,7 +154,7 @@ export class ImageDialogComponent extends
this.imageChanged = true; this.imageChanged = true;
this.image = result; this.image = result;
this.imagePreviewData = { this.imagePreviewData = {
url: this.image.public ? this.image.publicLink : this.image.link url: this.image.public ? `${this.image.publicLink}?ts=${new Date().getTime()}` : this.image.link
}; };
} }
}); });

128
ui-ngx/src/app/shared/models/action-widget-settings.models.ts

@ -0,0 +1,128 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
export enum GetValueAction {
DO_NOTHING = 'DO_NOTHING',
EXECUTE_RPC = 'EXECUTE_RPC',
GET_ATTRIBUTE = 'GET_ATTRIBUTE',
GET_TIME_SERIES = 'GET_TIME_SERIES'
}
export const getValueActions = Object.keys(GetValueAction) as GetValueAction[];
export const getValueActionTranslations = new Map<GetValueAction, string>(
[
[GetValueAction.DO_NOTHING, 'widgets.value-action.do-nothing'],
[GetValueAction.EXECUTE_RPC, 'widgets.value-action.execute-rpc'],
[GetValueAction.GET_ATTRIBUTE, 'widgets.value-action.get-attribute'],
[GetValueAction.GET_TIME_SERIES, 'widgets.value-action.get-time-series']
]
);
export interface RpcSettings {
method: string;
requestTimeout: number;
requestPersistent: boolean;
persistentPollingInterval: number;
}
export interface TelemetryValueSettings {
key: string;
}
export interface GetTelemetryValueSettings extends TelemetryValueSettings {
subscribeForUpdates: boolean;
}
export interface GetAttributeValueSettings extends GetTelemetryValueSettings {
scope: AttributeScope | null;
}
export interface SetAttributeValueSettings extends TelemetryValueSettings {
scope: AttributeScope.SERVER_SCOPE | AttributeScope.SHARED_SCOPE;
}
export enum DataToValueType {
NONE = 'NONE',
FUNCTION = 'FUNCTION'
}
export interface DataToValueSettings {
type: DataToValueType;
dataToValueFunction: string;
compareToValue?: any;
}
export interface ValueActionSettings {
actionLabel?: string;
}
export interface GetValueSettings<V> extends ValueActionSettings {
action: GetValueAction;
defaultValue: V;
executeRpc: RpcSettings;
getAttribute: GetAttributeValueSettings;
getTimeSeries: GetTelemetryValueSettings;
dataToValue: DataToValueSettings;
}
export enum SetValueAction {
EXECUTE_RPC = 'EXECUTE_RPC',
SET_ATTRIBUTE = 'SET_ATTRIBUTE',
ADD_TIME_SERIES = 'ADD_TIME_SERIES'
}
export const setValueActions = Object.keys(SetValueAction) as SetValueAction[];
export const setValueActionTranslations = new Map<SetValueAction, string>(
[
[SetValueAction.EXECUTE_RPC, 'widgets.value-action.execute-rpc'],
[SetValueAction.SET_ATTRIBUTE, 'widgets.value-action.set-attribute'],
[SetValueAction.ADD_TIME_SERIES, 'widgets.value-action.add-time-series']
]
);
export enum ValueToDataType {
CONSTANT = 'CONSTANT',
FUNCTION = 'FUNCTION',
NONE = 'NONE'
}
export interface ValueToDataSettings {
type: ValueToDataType;
constantValue: any;
valueToDataFunction: string;
}
export interface SetValueSettings extends ValueActionSettings {
action: SetValueAction;
executeRpc: RpcSettings;
setAttribute: SetAttributeValueSettings;
putTimeSeries: TelemetryValueSettings;
valueToData: ValueToDataSettings;
}
/*export interface RpcStateBehaviourSettings<V> {
initialState: RpcInitialStateSettings<V>;
updateStateByValue: (value: V) => RpcUpdateStateSettings;
}
export interface RpcStateWidgetSettings<V> {
initialState: RpcInitialStateSettings<V>;
background: BackgroundSettings;
}*/

125
ui-ngx/src/app/shared/models/rpc-widget-settings.models.ts

@ -1,125 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { BackgroundSettings } from '@shared/models/widget-settings.models';
export enum RpcInitialStateAction {
DO_NOTHING = 'DO_NOTHING',
EXECUTE_RPC = 'EXECUTE_RPC',
GET_ATTRIBUTE = 'GET_ATTRIBUTE',
GET_TIME_SERIES = 'GET_TIME_SERIES'
}
export const rpcInitialStateActions = Object.keys(RpcInitialStateAction) as RpcInitialStateAction[];
export const rpcInitialStateTranslations = new Map<RpcInitialStateAction, string>(
[
[RpcInitialStateAction.DO_NOTHING, 'widgets.rpc-state.do-nothing'],
[RpcInitialStateAction.EXECUTE_RPC, 'widgets.rpc-state.execute-rpc'],
[RpcInitialStateAction.GET_ATTRIBUTE, 'widgets.rpc-state.get-attribute'],
[RpcInitialStateAction.GET_TIME_SERIES, 'widgets.rpc-state.get-time-series']
]
);
export interface RpcSettings {
method: string;
requestTimeout: number;
requestPersistent: boolean;
persistentPollingInterval: number;
}
export interface RpcTelemetrySettings {
key: string;
}
export interface RpcGetAttributeSettings extends RpcTelemetrySettings {
scope: AttributeScope | null;
}
export interface RpcSetAttributeSettings extends RpcTelemetrySettings {
scope: AttributeScope.SERVER_SCOPE | AttributeScope.SHARED_SCOPE;
}
export enum RpcDataToStateType {
NONE = 'NONE',
FUNCTION = 'FUNCTION'
}
export interface RpcDataToStateSettings {
type: RpcDataToStateType;
dataToStateFunction: string;
compareToValue?: any;
}
export interface RpcActionSettings {
actionLabel?: string;
}
export interface RpcInitialStateSettings<V> extends RpcActionSettings {
action: RpcInitialStateAction;
defaultValue: V;
executeRpc: RpcSettings;
getAttribute: RpcGetAttributeSettings;
getTimeSeries: RpcTelemetrySettings;
dataToState: RpcDataToStateSettings;
}
export enum RpcUpdateStateAction {
EXECUTE_RPC = 'EXECUTE_RPC',
SET_ATTRIBUTE = 'SET_ATTRIBUTE',
ADD_TIME_SERIES = 'ADD_TIME_SERIES'
}
export const rpcUpdateStateActions = Object.keys(RpcUpdateStateAction) as RpcUpdateStateAction[];
export const rpcUpdateStateTranslations = new Map<RpcUpdateStateAction, string>(
[
[RpcUpdateStateAction.EXECUTE_RPC, 'widgets.rpc-state.execute-rpc'],
[RpcUpdateStateAction.SET_ATTRIBUTE, 'widgets.rpc-state.set-attribute'],
[RpcUpdateStateAction.ADD_TIME_SERIES, 'widgets.rpc-state.add-time-series']
]
);
export enum RpcStateToParamsType {
CONSTANT = 'CONSTANT',
FUNCTION = 'FUNCTION',
NONE = 'NONE'
}
export interface RpcStateToParamsSettings {
type: RpcStateToParamsType;
constantValue: any;
stateToParamsFunction: string;
}
export interface RpcUpdateStateSettings extends RpcActionSettings {
action: RpcUpdateStateAction;
executeRpc: RpcSettings;
setAttribute: RpcSetAttributeSettings;
putTimeSeries: RpcTelemetrySettings;
stateToParams: RpcStateToParamsSettings;
}
export interface RpcStateBehaviourSettings<V> {
initialState: RpcInitialStateSettings<V>;
updateStateByValue: (value: V) => RpcUpdateStateSettings;
}
export interface RpcStateWidgetSettings<V> {
initialState: RpcInitialStateSettings<V>;
background: BackgroundSettings;
}

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

@ -6102,60 +6102,6 @@
"min-value": "Minimum value", "min-value": "Minimum value",
"max-value": "Maximum value" "max-value": "Maximum value"
}, },
"rpc-state": {
"initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value of the component.",
"turn-on": "Turn 'On'",
"turn-on-hint": "Action performed to turn ON the component.",
"turn-off": "Turn 'Off'",
"turn-off-hint": "Action performed to turn OFF the component.",
"on": "On",
"off": "Off",
"do-nothing": "Do nothing",
"execute-rpc": "Execute RPC",
"get-attribute": "Get attribute",
"set-attribute": "Set attribute",
"get-time-series": "Get time-series",
"add-time-series": "Add time-series",
"execute-rpc-text": "Execute RPC method '{{methodName}}'",
"get-attribute-text": "Use attribute '{{key}}'",
"get-time-series-text": "Use time-series '{{key}}'",
"set-attribute-to-value-text": "Set '{{key}}' attribute to: {{value}}",
"add-time-series-value-text": "Add '{{key}}' time-series value: {{value}}",
"set-attribute-text": "Set '{{key}}' attribute",
"add-time-series-text": "Add '{{key}}' time-series",
"action": "Action",
"value": "Value",
"init-value-hint": "Value that will be set until device sends data.",
"method": "Method",
"method-name-required": "Method name is required.",
"request-timeout-ms": "RPC request timeout (ms)",
"request-timeout-required": "Request timeout is required.",
"min-request-timeout-error": "Request timeout value should be greater or equal 5000 ms (5 seconds).",
"request-persistent": "RPC request persistent",
"persistent-polling-interval": "Persistent polling interval (ms)",
"persistent-polling-interval-hint": "Polling interval (ms) to get persistent RPC command response",
"persistent-polling-interval-required": "Persistent polling interval is required.",
"min-persistent-polling-interval-error": "Persistent polling interval value should be greater or equal 1000 ms (1 second).",
"attribute-scope": "Attribute scope",
"attribute-key": "Attribute key",
"attribute-key-required": "Attribute key is required.",
"time-series-key": "Time-series key",
"time-series-key-required": "Time-series key is required.",
"action-result-converter": "Action result converter",
"converter-none": "None",
"converter-function": "Function",
"converter-constant": "Constant",
"parse-value-function": "Parse value function",
"on-when-result-is": "'On' when result is",
"parameters": "Parameters",
"convert-value-function": "Convert value function",
"error": {
"target-entity-is-not-set": "Target entity is not set!",
"failed-to-perform-action": "Failed to perform the <b>{{ actionLabel }}</b> action.",
"invalid-attribute-scope": "{{scope}} attribute scope is not supported by <b>{{entityType}}</b> entity."
}
},
"maps": { "maps": {
"select-entity": "Select entity", "select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position", "select-entity-hint": "Hint: after selection click at the map to set position",
@ -6628,6 +6574,62 @@
"source-entity-alias": "Source entity alias", "source-entity-alias": "Source entity alias",
"source-entity-attribute": "Source entity attribute" "source-entity-attribute": "Source entity attribute"
}, },
"value-action": {
"initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value of the component.",
"turn-on": "Turn 'On'",
"turn-on-hint": "Action performed to turn ON the component.",
"turn-off": "Turn 'Off'",
"turn-off-hint": "Action performed to turn OFF the component.",
"on": "On",
"off": "Off",
"do-nothing": "Do nothing",
"execute-rpc": "Execute RPC",
"get-attribute": "Get attribute",
"set-attribute": "Set attribute",
"get-time-series": "Get time-series",
"add-time-series": "Add time-series",
"execute-rpc-text": "Execute RPC method '{{methodName}}'",
"get-attribute-text": "Use attribute '{{key}}'",
"get-time-series-text": "Use time-series '{{key}}'",
"set-attribute-to-value-text": "Set '{{key}}' attribute to: {{value}}",
"add-time-series-value-text": "Add '{{key}}' time-series value: {{value}}",
"set-attribute-text": "Set '{{key}}' attribute",
"add-time-series-text": "Add '{{key}}' time-series",
"action": "Action",
"value": "Value",
"init-value-hint": "Value that will be set until device sends data.",
"method": "Method",
"method-name-required": "Method name is required.",
"request-timeout-ms": "RPC request timeout (ms)",
"request-timeout-required": "Request timeout is required.",
"min-request-timeout-error": "Request timeout value should be greater or equal 5000 ms (5 seconds).",
"request-persistent": "RPC request persistent",
"persistent-polling-interval": "Persistent polling interval (ms)",
"persistent-polling-interval-hint": "Polling interval (ms) to get persistent RPC command response",
"persistent-polling-interval-required": "Persistent polling interval is required.",
"min-persistent-polling-interval-error": "Persistent polling interval value should be greater or equal 1000 ms (1 second).",
"attribute-scope": "Attribute scope",
"attribute-key": "Attribute key",
"attribute-key-required": "Attribute key is required.",
"time-series-key": "Time-series key",
"time-series-key-required": "Time-series key is required.",
"subscribe-for-updates": "Subscribe for updates",
"subscribe-for-updates-hint": "Subscribe for updates",
"action-result-converter": "Action result converter",
"converter-none": "None",
"converter-function": "Function",
"converter-constant": "Constant",
"parse-value-function": "Parse value function",
"on-when-result-is": "'On' when result is",
"parameters": "Parameters",
"convert-value-function": "Convert value function",
"error": {
"target-entity-is-not-set": "Target entity is not set!",
"failed-to-perform-action": "Failed to perform the <b>{{ actionLabel }}</b> action.",
"invalid-attribute-scope": "{{scope}} attribute scope is not supported by <b>{{entityType}}</b> entity."
}
},
"widget-font": { "widget-font": {
"font-settings": "Font settings", "font-settings": "Font settings",
"font-family": "Font family", "font-family": "Font family",

Loading…
Cancel
Save