Browse Source

Merged with master

pull/10011/head
Ivan Raznatovskyi 2 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"
about: Create a report to help us improve
title: "[Bug] "
labels: bug
assignees: ashvayka, vvlladd28
title: "Your title here"
labels: ['bug', 'unconfirmed']
assignees: AndriichnekoDm
---
@ -12,11 +12,13 @@ A clear and concise description of what the bug is.
**Your Server Environment**
<!-- 🔅🔅🔅🔅🔅🔅🔅 Choose one of the following or write your own 🔅🔅🔅🔅🔅🔅🔅-->
* demo.thingsboard.io
* cloud.thingsboard.io
* [https://demo.thingsboard.io](demo.thingsboard.io)
* [https://thingsboard.cloud](thingsboard.cloud)
* own setup
* cloud or local infrastructure or docker deployment
* Deployment: monolith or microservices
* Deployment type: deb, rpm, exe, docker-compose, k8s, ami
* ThingsBoard Version
* Community or Professional Edition
* OS Name and Version
**Your Client Environment**
@ -54,7 +56,7 @@ Steps to reproduce the behavior:
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
If applicable, please add screenshots to help explain your problem.
**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
about: Suggest an idea for this project
title: "[Feature Request]"
labels: ['Feature']
title: "Your title here"
labels: ['feature']
assignees: 'AndriichnekoDm'
---

10
.github/ISSUE_TEMPLATE/question.md

@ -1,9 +1,9 @@
---
name: Question
about: Describe your questions in details
title: "[Question] your title here"
labels: question
assignees: ashvayka
about: Describe your questions in detail
title: "Your title here"
labels: ['question']
assignees: 'AndriichnekoDm'
---
@ -16,7 +16,7 @@ assignees: ashvayka
* Generic
**Description**
A clear and concise details.
Clear and concise details.
**Environment**
<!-- 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": [],
"templateHtml": "<tb-single-switch-widget\n [ctx]='ctx'\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-single-switch-widget>",
"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": "",
"dataKeySettingsSchema": "{}\n",
"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;
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.MoreExecutors;
import lombok.Getter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
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);
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) {
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 org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
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.TbCallback;
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.queue.TbQueueProducer;
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.TbApplicationEventListener;
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.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
@ -154,7 +154,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
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);
<E extends HasName, I extends EntityId> ListenableFuture<List<Void>> logEntityAction(
<E extends HasName, I extends EntityId> ListenableFuture<Void> logEntityAction(
TenantId tenantId,
CustomerId customerId,
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
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) {
@ -376,7 +381,12 @@ public class HashPartitionService implements PartitionService {
.collect(Collectors.toList()))
.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

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();
}
if (validUpdate && filterTbApplicationEvent(event)) {
onTbApplicationEvent(event);
try {
onTbApplicationEvent(event);
} catch (Exception e) {
log.error("Failed to handle partition change event: {}", event, e);
}
} else {
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
protected void onTbApplicationEvent(PartitionChangeEvent event) {
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);
lock.lock();
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 org.thingsboard.server.common.data.alarm.AlarmComment;
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.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -29,13 +28,10 @@ import java.util.UUID;
public interface AlarmCommentDao extends Dao<AlarmComment> {
AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment);
void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId);
AlarmComment findAlarmCommentById(TenantId tenantId, UUID key);
PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink);
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;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.service.DataValidator;
import java.util.UUID;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service
@ -101,12 +98,7 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
if (alarmComment.getType() == null) {
alarmComment.setType(AlarmCommentType.OTHER);
}
if (alarmComment.getId() == null) {
UUID uuid = Uuids.timeBased();
alarmComment.setId(new AlarmCommentId(uuid));
alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid));
}
return alarmCommentDao.createAlarmComment(tenantId, alarmComment);
return alarmCommentDao.save(tenantId, alarmComment);
}
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> {
ListenableFuture<Void> saveByTenantId(AuditLog auditLog);
PageData<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, 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;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
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.ListenableFuture;
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.ActionType;
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.EntityId;
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.entity.EntityService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
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 {
private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private static final int INSERTS_PER_ENTRY = 3;
@Autowired
private AuditLogLevelFilter auditLogLevelFilter;
@ -80,6 +76,9 @@ public class AuditLogServiceImpl implements AuditLogService {
@Autowired
private AuditLogSink auditLogSink;
@Autowired
private JpaExecutorService executor;
@Autowired
private DataValidator<AuditLog> auditLogValidator;
@ -115,7 +114,7 @@ public class AuditLogServiceImpl implements AuditLogService {
}
@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,
ActionType actionType, Exception e, Object... additionalInfo) {
if (canLog(entityId.getEntityType(), actionType)) {
@ -370,9 +369,6 @@ public class AuditLogServiceImpl implements AuditLogService {
ActionStatus actionStatus,
String actionFailureDetails) {
AuditLog result = new AuditLog();
UUID id = Uuids.timeBased();
result.setId(new AuditLogId(id));
result.setCreatedTime(Uuids.unixTimestamp(id));
result.setTenantId(tenantId);
result.setEntityId(entityId);
result.setEntityName(entityName);
@ -386,16 +382,16 @@ public class AuditLogServiceImpl implements AuditLogService {
return result;
}
private ListenableFuture<List<Void>> logAction(TenantId tenantId,
EntityId entityId,
String entityName,
CustomerId customerId,
UserId userId,
String userName,
ActionType actionType,
JsonNode actionData,
ActionStatus actionStatus,
String actionFailureDetails) {
private ListenableFuture<Void> logAction(TenantId tenantId,
EntityId entityId,
String entityName,
CustomerId customerId,
UserId userId,
String userName,
ActionType actionType,
JsonNode actionData,
ActionStatus actionStatus,
String actionFailureDetails) {
AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName,
actionType, actionData, actionStatus, actionFailureDetails);
log.trace("Executing logAction [{}]", auditLogEntry);
@ -408,12 +404,12 @@ public class AuditLogServiceImpl implements AuditLogService {
return Futures.immediateFailedFuture(e);
}
}
List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
futures.add(auditLogDao.saveByTenantId(auditLogEntry));
auditLogSink.logAction(auditLogEntry);
return Futures.allAsList(futures);
return executor.submit(() -> {
AuditLog auditLog = auditLogDao.save(tenantId, auditLogEntry);
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
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;
}

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.stereotype.Component;
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.audit.AuditLog;
import org.thingsboard.server.common.data.id.TenantId;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Component
@ConditionalOnProperty(prefix = "audit-log.sink", value = "type", havingValue = "elasticsearch")
@ -68,6 +72,7 @@ public class ElasticsearchAuditLogSink implements AuditLogSink {
private String dateFormat;
private RestClient restClient;
private ExecutorService executor;
@PostConstruct
public void init() {
@ -87,14 +92,32 @@ public class ElasticsearchAuditLogSink implements AuditLogSink {
}
this.restClient = builder.build();
this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("elasticsearch-audit-log"));
} catch (Exception e) {
log.error("Sink init failed!", e);
throw new RuntimeException(e.getMessage(), e);
}
}
@PreDestroy
private void destroy() {
if (executor != null) {
executor.shutdownNow();
}
}
@Override
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);
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.util.SqlDao;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@ -45,9 +47,6 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
protected abstract JpaRepository<E, UUID> getRepository();
protected void setSearchText(E entity) {
}
@Override
@Transactional
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);
throw new IllegalArgumentException("Can't create entity for domain object {" + domain + "}", e);
}
setSearchText(entity);
log.debug("Saving entity {}", entity);
if (entity.getUuid() == null) {
boolean isNew = entity.getUuid() == null;
if (isNew) {
UUID uuid = Uuids.timeBased();
entity.setUuid(uuid);
entity.setCreatedTime(Uuids.unixTimestamp(uuid));
}
entity = getRepository().save(entity);
entity = doSave(entity, isNew);
return DaoUtil.getData(entity);
}
protected E doSave(E entity, boolean isNew) {
return getRepository().save(entity);
}
@Override
@Transactional
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());
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.thingsboard.server.common.data.alarm.AlarmComment;
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.TenantId;
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.alarm.AlarmCommentDao;
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.util.SqlDao;
@ -45,7 +44,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_TABL
@Component
@SqlDao
@RequiredArgsConstructor
public class JpaAlarmCommentDao extends JpaAbstractDao<AlarmCommentEntity, AlarmComment> implements AlarmCommentDao {
public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao<AlarmCommentEntity, AlarmComment> implements AlarmCommentDao {
private final SqlPartitioningRepository partitioningRepository;
@Value("${sql.alarm_comments.partition_size:168}")
private int partitionSizeInHours;
@ -54,21 +53,7 @@ public class JpaAlarmCommentDao extends JpaAbstractDao<AlarmCommentEntity, Alarm
private AlarmCommentRepository alarmCommentRepository;
@Override
public AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment){
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){
public PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink) {
log.trace("Try to find alarm comments by alarm id using [{}]", id);
return DaoUtil.toPageData(
alarmCommentRepository.findAllByAlarmId(id.getId(), DaoUtil.toPageable(pageLink)));
@ -86,6 +71,11 @@ public class JpaAlarmCommentDao extends JpaAbstractDao<AlarmCommentEntity, Alarm
return findByIdAsync(tenantId, key);
}
@Override
public void createPartition(AlarmCommentEntity entity) {
partitioningRepository.createPartitionIfNotExists(ALARM_COMMENT_TABLE_NAME, entity.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
@Override
protected Class<AlarmCommentEntity> getEntityClass() {
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;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -25,10 +23,8 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.audit.ActionType;
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.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
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.model.ModelConstants;
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.util.SqlDao;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -49,7 +44,7 @@ import java.util.concurrent.TimeUnit;
@SqlDao
@RequiredArgsConstructor
@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 SqlPartitioningRepository partitioningRepository;
@ -72,25 +67,6 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
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
public PageData<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, List<ActionType> actionTypes, TimePageLink pageLink) {
return DaoUtil.toPageData(
@ -182,4 +158,9 @@ public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> imp
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.model.ModelConstants;
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.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
@ -47,7 +47,6 @@ import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@ -58,7 +57,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@SqlDao
@RequiredArgsConstructor
@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;
@ -151,8 +150,9 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
if (StringUtils.isEmpty(edgeEvent.getUid())) {
edgeEvent.setUid(edgeEvent.getId().toString());
}
partitioningRepository.createPartitionIfNotExists(TABLE_NAME, edgeEvent.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
return save(new EdgeEventEntity(edgeEvent));
EdgeEventEntity entity = new EdgeEventEntity(edgeEvent);
createPartition(entity);
return save(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) {
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;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.base.Strings;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
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.sql.NotificationEntity;
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.util.SqlDao;
@ -44,7 +42,7 @@ import java.util.concurrent.TimeUnit;
@Component
@SqlDao
@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 SqlPartitioningRepository partitioningRepository;
@ -52,18 +50,6 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
@Value("${sql.notifications.partition_size:168}")
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
public PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ,
@ -114,6 +100,12 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
notificationRepository.deleteByRecipientId(recipientId.getId());
}
@Override
public void createPartition(NotificationEntity entity) {
partitioningRepository.createPartitionIfNotExists(ModelConstants.NOTIFICATION_TABLE_NAME,
entity.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
@Override
protected Class<NotificationEntity> getEntityClass() {
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.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.DeviceProfileId;
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));
}
@Transactional
@Override
public OtaPackageInfo save(TenantId tenantId, OtaPackageInfo 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.setType(type);
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
} from '@home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component';
import {
SingSwitchBasicConfigComponent
SingleSwitchBasicConfigComponent
} from '@home/components/widget/config/basic/rpc/single-switch-basic-config.component';
@NgModule({
@ -123,7 +123,7 @@ import {
DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent,
SingSwitchBasicConfigComponent
SingleSwitchBasicConfigComponent
],
imports: [
CommonModule,
@ -157,7 +157,7 @@ import {
DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent,
SingSwitchBasicConfigComponent
SingleSwitchBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -185,5 +185,5 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-doughnut-basic-config': DoughnutBasicConfigComponent,
'tb-range-chart-basic-config': RangeChartBasicConfigComponent,
'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-title" translate>widgets.single-switch.behavior</div>
<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>
<tb-rpc-initial-state-settings fxFlex
[stateValueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="initialState"></tb-rpc-initial-state-settings>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.initial-state-hint' | translate}}" translate>widgets.value-action.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.value-action.initial-state"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<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>
<tb-rpc-update-state-settings fxFlex
panelTitle="widgets.rpc-state.turn-on"
[stateValueType]="valueType.BOOLEAN"
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-on-hint' | translate}}" translate>widgets.value-action.turn-on</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-on"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="onUpdateState"></tb-rpc-update-state-settings>
formControlName="onUpdateState"></tb-set-value-action-settings>
</div>
<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>
<tb-rpc-update-state-settings fxFlex
panelTitle="widgets.rpc-state.turn-off"
[stateValueType]="valueType.BOOLEAN"
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-off-hint' | translate}}" translate>widgets.value-action.turn-off</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-off"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="offUpdateState"></tb-rpc-update-state-settings>
formControlName="offUpdateState"></tb-set-value-action-settings>
</div>
</div>
<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',
styleUrls: ['../basic-config.scss']
})
export class SingSwitchBasicConfigComponent extends BasicWidgetConfigComponent {
export class SingleSwitchBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
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 #singleSwitchToggleRow class="tb-single-switch-slide-toggle-row">
<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>
<div *ngIf="showOnLabel" [style]="onLabelStyle">{{ onLabel }}</div>
</div>

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

@ -25,20 +25,25 @@ import {
ViewChild,
ViewEncapsulation
} 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 {
singleSwitchDefaultSettings,
SingleSwitchLayout,
SingleSwitchWidgetSettings
} 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 { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import cssjs from '@core/css/css';
import { hashCode } from '@core/utils';
import { RpcInitialStateSettings, RpcUpdateStateSettings } from '@shared/models/rpc-widget-settings.models';
import { ValueType } from '@shared/models/constants';
const horizontalLayoutPadding = 48;
@ -51,7 +56,7 @@ const verticalLayoutPadding = 36;
encapsulation: ViewEncapsulation.None
})
export class SingleSwitchWidgetComponent extends
BasicRpcStateWidgetComponent<boolean, SingleSwitchWidgetSettings> implements OnInit, AfterViewInit, OnDestroy {
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('singleSwitchPanel', {static: false})
singleSwitchPanel: ElementRef<HTMLElement>;
@ -65,6 +70,13 @@ export class SingleSwitchWidgetComponent extends
@ViewChild('singleSwitchToggleRow', {static: false})
singleSwitchToggleRow: ElementRef<HTMLElement>;
settings: SingleSwitchWidgetSettings;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
value = false;
layout: SingleSwitchLayout;
showIcon = false;
@ -87,16 +99,24 @@ export class SingleSwitchWidgetComponent extends
private panelResize$: ResizeObserver;
private onValueSetter: ValueSetter<boolean>;
private offValueSetter: ValueSetter<boolean>;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
protected cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(imagePipe, sanitizer, cd);
super(cd);
}
ngOnInit(): void {
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.autoScale = this.settings.autoScale;
@ -134,6 +154,20 @@ export class SingleSwitchWidgetComponent extends
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, switchVariablesCss);
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 {
@ -156,31 +190,30 @@ export class SingleSwitchWidgetComponent extends
if (this.panelResize$) {
this.panelResize$.disconnect();
}
super.ngOnDestroy();
}
protected stateValueType(): ValueType {
return ValueType.BOOLEAN;
}
protected defaultValue(): boolean {
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')};
public onInit() {
super.onInit();
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
protected getUpdateStateSettingsForValue(value: boolean): RpcUpdateStateSettings {
const targetSettings = value ? this.settings.onUpdateState : this.settings.offUpdateState;
return {...targetSettings, actionLabel: this.ctx.translate.instant(value ? 'widgets.rpc-state.turn-on' : 'widgets.rpc-state.turn-off')};
public onToggleChange(event: MouseEvent) {
event.preventDefault();
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 {
return !!value;
private onValue(value: boolean): void {
console.log(`onValue: ${value}`);
this.value = !!value;
this.cd.markForCheck();
}
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.
///
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 {
RpcDataToStateType,
RpcInitialStateAction,
RpcStateToParamsType,
RpcStateWidgetSettings,
RpcUpdateStateAction,
RpcUpdateStateSettings
} from '@shared/models/rpc-widget-settings.models';
DataToValueType,
GetValueAction,
GetValueSettings,
SetValueAction,
SetValueSettings,
ValueToDataType
} from '@shared/models/action-widget-settings.models';
export enum SingleSwitchLayout {
right = 'right',
@ -49,9 +49,10 @@ export const singleSwitchLayoutImages = new Map<SingleSwitchLayout, string>(
]
);
export interface SingleSwitchWidgetSettings extends RpcStateWidgetSettings<boolean> {
onUpdateState: RpcUpdateStateSettings;
offUpdateState: RpcUpdateStateSettings;
export interface SingleSwitchWidgetSettings {
initialState: GetValueSettings<boolean>;
onUpdateState: SetValueSettings;
offUpdateState: SetValueSettings;
layout: SingleSwitchLayout;
autoScale: boolean;
showLabel: boolean;
@ -77,11 +78,12 @@ export interface SingleSwitchWidgetSettings extends RpcStateWidgetSettings<boole
offLabel: string;
offLabelFont: Font;
offLabelColor: string;
background: BackgroundSettings;
}
export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
initialState: {
action: RpcInitialStateAction.EXECUTE_RPC,
action: GetValueAction.EXECUTE_RPC,
defaultValue: false,
executeRpc: {
method: 'getState',
@ -91,19 +93,21 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
},
getAttribute: {
key: 'state',
scope: null
scope: null,
subscribeForUpdates: false
},
getTimeSeries: {
key: 'state'
key: 'state',
subscribeForUpdates: false
},
dataToState: {
type: RpcDataToStateType.NONE,
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToStateFunction: '/* Should return boolean value */\nreturn data;'
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
onUpdateState: {
action: RpcUpdateStateAction.EXECUTE_RPC,
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
@ -117,14 +121,14 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
putTimeSeries: {
key: 'state'
},
stateToParams: {
type: RpcStateToParamsType.CONSTANT,
valueToData: {
type: ValueToDataType.CONSTANT,
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: {
action: RpcUpdateStateAction.EXECUTE_RPC,
action: SetValueAction.EXECUTE_RPC,
executeRpc: {
method: 'setState',
requestTimeout: 5000,
@ -138,10 +142,10 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
putTimeSeries: {
key: 'state'
},
stateToParams: {
type: RpcStateToParamsType.CONSTANT,
valueToData: {
type: ValueToDataType.CONSTANT,
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,

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.
-->
<div class="tb-rpc-state-settings-panel" [formGroup]="initialStateSettingsFormGroup">
<div class="tb-rpc-state-settings-title" translate>widgets.rpc-state.initial-state</div>
<div class="tb-rpc-state-settings-panel-content">
<div class="tb-value-action-settings-panel" [formGroup]="getValueSettingsFormGroup">
<div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-value-action-settings-panel-content">
<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-select formControlName="action">
<mat-option *ngFor="let action of rpcInitialStateActions" [value]="action">
{{ rpcInitialStateTranslationsMap.get(action) | translate }}
<mat-option *ngFor="let action of getValueActions" [value]="action">
{{ getValueActionTranslationsMap.get(action) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container [ngSwitch]="initialStateSettingsFormGroup.get('action').value">
<ng-template [ngSwitchCase]="rpcInitialStateAction.DO_NOTHING">
<ng-container [ngSwitch]="getValueSettingsFormGroup.get('action').value">
<ng-template [ngSwitchCase]="getValueAction.DO_NOTHING">
<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>
<tb-value-input [valueType]="stateValueType"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
<div tb-hint-tooltip-icon="{{'widgets.value-action.init-value-hint' | translate}}" translate>widgets.value-action.value</div>
<tb-value-input [valueType]="valueType"
trueLabel="widgets.value-action.on"
falseLabel="widgets.value-action.off"
formControlName="defaultValue"></tb-value-input>
</div>
</ng-template>
<ng-template [ngSwitchCase]="rpcInitialStateAction.EXECUTE_RPC">
<ng-template [ngSwitchCase]="getValueAction.EXECUTE_RPC">
<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">
<input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widgets.rpc-state.method-name-required' | translate"
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('method').hasError('required')
&& initialStateSettingsFormGroup.get('executeRpc').get('method').touched"
[matTooltip]="'widgets.value-action.method-name-required' | translate"
*ngIf="getValueSettingsFormGroup.get('executeRpc').get('method').hasError('required')
&& getValueSettingsFormGroup.get('executeRpc').get('method').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="rpcInitialStateAction.GET_ATTRIBUTE">
<ng-template [ngSwitchCase]="getValueAction.GET_ATTRIBUTE">
<ng-container formGroupName="getAttribute">
<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-select formControlName="scope" placeholder="{{ 'attribute.scope-any' | translate }}">
<mat-option [value]="null">
@ -71,58 +71,68 @@
</mat-form-field>
</div>
<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
fxFlex
formControlName="key"
required
requiredText="widgets.rpc-state.attribute-key-required"
requiredText="widgets.value-action.attribute-key-required"
inlineField
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[keyType]="dataKeyType.attribute"
[attributeScope]="initialStateSettingsFormGroup.get('getAttribute').get('scope').value">
[attributeScope]="getValueSettingsFormGroup.get('getAttribute').get('scope').value">
</tb-device-key-autocomplete>
</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-template>
<ng-template [ngSwitchCase]="rpcInitialStateAction.GET_TIME_SERIES">
<ng-template [ngSwitchCase]="getValueAction.GET_TIME_SERIES">
<ng-container formGroupName="getTimeSeries">
<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
fxFlex
formControlName="key"
required
requiredText="widgets.rpc-state.time-series-key-required"
requiredText="widgets.value-action.time-series-key-required"
inlineField
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[keyType]="dataKeyType.timeseries">
</tb-device-key-autocomplete>
</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-template>
</ng-container>
<div *ngIf="initialStateSettingsFormGroup.get('action').value !== rpcInitialStateAction.DO_NOTHING"
class="tb-form-panel stroked" formGroupName="dataToState">
<div *ngIf="getValueSettingsFormGroup.get('action').value !== getValueAction.DO_NOTHING"
class="tb-form-panel stroked" formGroupName="dataToValue">
<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-option [value]="dataToStateType.NONE">{{ 'widgets.rpc-state.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.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="dataToValueType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-js-func *ngIf="initialStateSettingsFormGroup.get('dataToState').get('type').value === dataToStateType.FUNCTION"
formControlName="dataToStateFunction"
<tb-js-func *ngIf="getValueSettingsFormGroup.get('dataToValue').get('type').value === dataToValueType.FUNCTION"
formControlName="dataToValueFunction"
required
[globalVariables]="functionScopeVariables"
[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">
</tb-js-func>
<div *ngIf="stateValueType === 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 *ngIf="valueType === ValueType.BOOLEAN" class="tb-form-row align-start no-gap column-xs">
<div class="fixed-title-width fixed-title-height" translate>widgets.value-action.on-when-result-is</div>
<tb-value-input
fxFlex
layout="column"
@ -130,7 +140,7 @@
</tb-value-input>
</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">
<mat-expansion-panel class="tb-settings no-padding">
<mat-expansion-panel-header fxLayout="row wrap">
@ -142,16 +152,16 @@
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<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">
<input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required')
? 'widgets.rpc-state.request-timeout-required' : 'widgets.rpc-state.min-request-timeout-error') | translate"
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').touched &&
initialStateSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid"
[matTooltip]="(getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required')
? 'widgets.value-action.request-timeout-required' : 'widgets.value-action.min-request-timeout-error') | translate"
*ngIf="getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched &&
getValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid"
class="tb-error">
warning
</mat-icon>
@ -159,28 +169,28 @@
</div>
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings"
[expanded]="initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value"
[disabled]="!initialStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value">
[expanded]="getValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value"
[disabled]="!getValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.rpc-state.request-persistent' | translate }}
{{ 'widgets.value-action.request-persistent' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<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">
<input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required')
? 'widgets.rpc-state.persistent-polling-interval-required' : 'widgets.rpc-state.min-persistent-polling-interval-error') | translate"
*ngIf="initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched &&
initialStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid"
[matTooltip]="(getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required')
? 'widgets.value-action.persistent-polling-interval-required' : 'widgets.value-action.min-persistent-polling-interval-error') | translate"
*ngIf="getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched &&
getValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid"
class="tb-error">
warning
</mat-icon>
@ -193,7 +203,7 @@
</mat-expansion-panel>
</div>
</div>
<div class="tb-rpc-state-settings-panel-buttons">
<div class="tb-value-action-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
@ -203,8 +213,8 @@
<button mat-raised-button
color="primary"
type="button"
(click)="applyInitialStateSettings()"
[disabled]="initialStateSettingsFormGroup.invalid || !initialStateSettingsFormGroup.dirty">
(click)="applyGetValueSettings()"
[disabled]="getValueSettingsFormGroup.invalid || !getValueSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</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 { MatButton } from '@angular/material/button';
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 { ValueType } from '@shared/models/constants';
import {
RpcInitialStateSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component';
GetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice } from '@shared/models/widget.models';
@Component({
selector: 'tb-rpc-initial-state-settings',
templateUrl: './rpc-state-settings-button.component.html',
styleUrls: ['./rpc-state-settings-button.scss'],
selector: 'tb-get-value-action-settings',
templateUrl: './value-action-settings-button.component.html',
styleUrls: ['./value-action-settings-button.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RpcInitialStateSettingsComponent),
useExisting: forwardRef(() => GetValueActionSettingsComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAccessor {
export class GetValueActionSettingsComponent implements OnInit, ControlValueAccessor {
@HostBinding('style.overflow')
overflow = 'hidden';
@Input()
stateValueType: ValueType;
panelTitle: string;
@Input()
valueType: ValueType;
@Input()
aliasController: IAliasController;
@ -67,7 +70,7 @@ export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAcc
@Input()
disabled = false;
modelValue: RpcInitialStateSettings<any>;
modelValue: GetValueSettings<any>;
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.updateDisplayValue();
}
openRpcStateSettingsPopup($event: Event, matButton: MatButton) {
openValueActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
@ -109,21 +112,22 @@ export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAcc
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
initialState: this.modelValue,
stateValueType: this.stateValueType,
getValueSettings: this.modelValue,
panelTitle: this.panelTitle,
valueType: this.valueType,
aliasController: this.aliasController,
targetDevice: this.targetDevice
};
const initialStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, RpcInitialStateSettingsPanelComponent,
const getValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, GetValueActionSettingsPanelComponent,
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
initialStateSettingsPanelPopover.tbComponentRef.instance.popover = initialStateSettingsPanelPopover;
initialStateSettingsPanelPopover.tbComponentRef.instance.initialStateSettingsApplied.subscribe((initialState) => {
initialStateSettingsPanelPopover.hide();
this.modelValue = initialState;
getValueSettingsPanelPopover.tbComponentRef.instance.popover = getValueSettingsPanelPopover;
getValueSettingsPanelPopover.tbComponentRef.instance.getValueSettingsApplied.subscribe((getValueSettings) => {
getValueSettingsPanelPopover.hide();
this.modelValue = getValueSettings;
this.updateDisplayValue();
this.propagateChange(this.modelValue);
});
@ -132,22 +136,22 @@ export class RpcInitialStateSettingsComponent implements OnInit, ControlValueAcc
private updateDisplayValue() {
switch (this.modelValue.action) {
case RpcInitialStateAction.DO_NOTHING:
if (this.stateValueType === ValueType.BOOLEAN) {
this.displayValue = this.translate.instant(!!this.modelValue.defaultValue ? 'widgets.rpc-state.on' : 'widgets.rpc-state.off');
case GetValueAction.DO_NOTHING:
if (this.valueType === ValueType.BOOLEAN) {
this.displayValue = this.translate.instant(!!this.modelValue.defaultValue ? 'widgets.value-action.on' : 'widgets.value-action.off');
} else {
this.displayValue = this.modelValue.defaultValue + '';
}
break;
case RpcInitialStateAction.EXECUTE_RPC:
case GetValueAction.EXECUTE_RPC:
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;
case RpcInitialStateAction.GET_ATTRIBUTE:
this.displayValue = this.translate.instant('widgets.rpc-state.get-attribute-text', {key: this.modelValue.getAttribute.key});
case GetValueAction.GET_ATTRIBUTE:
this.displayValue = this.translate.instant('widgets.value-action.get-attribute-text', {key: this.modelValue.getAttribute.key});
break;
case RpcInitialStateAction.GET_TIME_SERIES:
this.displayValue = this.translate.instant('widgets.rpc-state.get-time-series-text', {key: this.modelValue.getTimeSeries.key});
case GetValueAction.GET_TIME_SERIES:
this.displayValue = this.translate.instant('widgets.value-action.get-time-series-text', {key: this.modelValue.getTimeSeries.key});
break;
}
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.
-->
<div class="tb-rpc-state-settings-panel" [formGroup]="updateStateSettingsFormGroup">
<div class="tb-rpc-state-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-rpc-state-settings-panel-content">
<div class="tb-value-action-settings-panel" [formGroup]="setValueSettingsFormGroup">
<div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-value-action-settings-panel-content">
<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-select formControlName="action">
<mat-option *ngFor="let action of rpcUpdateStateActions" [value]="action">
{{ rpcUpdateStateTranslationsMap.get(action) | translate }}
<mat-option *ngFor="let action of setValueActions" [value]="action">
{{ setValueActionTranslationsMap.get(action) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container [ngSwitch]="updateStateSettingsFormGroup.get('action').value">
<ng-template [ngSwitchCase]="rpcUpdateStateAction.EXECUTE_RPC">
<ng-container [ngSwitch]="setValueSettingsFormGroup.get('action').value">
<ng-template [ngSwitchCase]="setValueAction.EXECUTE_RPC">
<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">
<input matInput required formControlName="method" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widgets.rpc-state.method-name-required' | translate"
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('method').hasError('required')
&& updateStateSettingsFormGroup.get('executeRpc').get('method').touched"
[matTooltip]="'widgets.value-action.method-name-required' | translate"
*ngIf="setValueSettingsFormGroup.get('executeRpc').get('method').hasError('required')
&& setValueSettingsFormGroup.get('executeRpc').get('method').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="rpcUpdateStateAction.SET_ATTRIBUTE">
<ng-template [ngSwitchCase]="setValueAction.SET_ATTRIBUTE">
<ng-container formGroupName="setAttribute">
<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-select formControlName="scope">
<mat-option *ngFor="let scope of attributeScopes" [value]="scope">
@ -59,30 +59,30 @@
</mat-form-field>
</div>
<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
fxFlex
formControlName="key"
required
requiredText="widgets.rpc-state.attribute-key-required"
requiredText="widgets.value-action.attribute-key-required"
inlineField
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[keyType]="dataKeyType.attribute"
[attributeScope]="updateStateSettingsFormGroup.get('setAttribute').get('scope').value">
[attributeScope]="setValueSettingsFormGroup.get('setAttribute').get('scope').value">
</tb-device-key-autocomplete>
</div>
</ng-container>
</ng-template>
<ng-template [ngSwitchCase]="rpcUpdateStateAction.ADD_TIME_SERIES">
<ng-template [ngSwitchCase]="setValueAction.ADD_TIME_SERIES">
<ng-container formGroupName="putTimeSeries">
<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
fxFlex
formControlName="key"
required
requiredText="widgets.rpc-state.time-series-key-required"
requiredText="widgets.value-action.time-series-key-required"
inlineField
[aliasController]="aliasController"
[targetDevice]="targetDevice"
@ -92,18 +92,18 @@
</ng-container>
</ng-template>
</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="fixed-title-width">{{ (updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC ?
'widgets.rpc-state.parameters' : 'widgets.rpc-state.value') | translate }}</div>
<div class="fixed-title-width">{{ (setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC ?
'widgets.value-action.parameters' : 'widgets.value-action.value') | translate }}</div>
<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]="stateToParamsType.FUNCTION">{{ 'widgets.rpc-state.converter-function' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="updateStateSettingsFormGroup.get('action').value === rpcUpdateStateAction.EXECUTE_RPC"
[value]="stateToParamsType.NONE">{{ 'widgets.rpc-state.converter-none' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="valueToDataType.CONSTANT">{{ 'widgets.value-action.converter-constant' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="valueToDataType.FUNCTION">{{ 'widgets.value-action.converter-function' | translate }}</tb-toggle-option>
<tb-toggle-option *ngIf="setValueSettingsFormGroup.get('action').value === setValueAction.EXECUTE_RPC"
[value]="valueToDataType.NONE">{{ 'widgets.value-action.converter-none' | translate }}</tb-toggle-option>
</tb-toggle-select>
</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
[layout]="{
layout: 'row',
@ -111,16 +111,16 @@
}"
formControlName="constantValue">
</tb-value-input>
<tb-js-func *ngIf="updateStateSettingsFormGroup.get('stateToParams').get('type').value === stateToParamsType.FUNCTION"
formControlName="stateToParamsFunction"
<tb-js-func *ngIf="setValueSettingsFormGroup.get('valueToData').get('type').value === valueToDataType.FUNCTION"
formControlName="valueToDataFunction"
required
[globalVariables]="functionScopeVariables"
[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">
</tb-js-func>
</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">
<mat-expansion-panel class="tb-settings no-padding">
<mat-expansion-panel-header fxLayout="row wrap">
@ -132,16 +132,16 @@
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<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">
<input matInput formControlName="requestTimeout" type="number" required min="5000" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required')
? 'widgets.rpc-state.request-timeout-required' : 'widgets.rpc-state.min-request-timeout-error') | translate"
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').touched &&
updateStateSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid"
[matTooltip]="(setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').hasError('required')
? 'widgets.value-action.request-timeout-required' : 'widgets.value-action.min-request-timeout-error') | translate"
*ngIf="setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').touched &&
setValueSettingsFormGroup.get('executeRpc').get('requestTimeout').invalid"
class="tb-error">
warning
</mat-icon>
@ -149,28 +149,28 @@
</div>
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings"
[expanded]="updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value"
[disabled]="!updateStateSettingsFormGroup.get('executeRpc').get('requestPersistent').value">
[expanded]="setValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value"
[disabled]="!setValueSettingsFormGroup.get('executeRpc').get('requestPersistent').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="requestPersistent" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.rpc-state.request-persistent' | translate }}
{{ 'widgets.value-action.request-persistent' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<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">
<input matInput formControlName="persistentPollingInterval" type="number" required min="1000" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required')
? 'widgets.rpc-state.persistent-polling-interval-required' : 'widgets.rpc-state.min-persistent-polling-interval-error') | translate"
*ngIf="updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched &&
updateStateSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid"
[matTooltip]="(setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').hasError('required')
? 'widgets.value-action.persistent-polling-interval-required' : 'widgets.value-action.min-persistent-polling-interval-error') | translate"
*ngIf="setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').touched &&
setValueSettingsFormGroup.get('executeRpc').get('persistentPollingInterval').invalid"
class="tb-error">
warning
</mat-icon>
@ -183,7 +183,7 @@
</mat-expansion-panel>
</div>
</div>
<div class="tb-rpc-state-settings-panel-buttons">
<div class="tb-value-action-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
@ -193,8 +193,8 @@
<button mat-raised-button
color="primary"
type="button"
(click)="applyUpdateStateSettings()"
[disabled]="updateStateSettingsFormGroup.invalid || !updateStateSettingsFormGroup.dirty">
(click)="applySetValueSettings()"
[disabled]="setValueSettingsFormGroup.invalid || !setValueSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</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 { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import {
RpcStateToParamsType,
RpcUpdateStateAction,
RpcUpdateStateSettings
} from '@shared/models/rpc-widget-settings.models';
import { SetValueAction, SetValueSettings, ValueToDataType } from '@shared/models/action-widget-settings.models';
import { TranslateService } from '@ngx-translate/core';
import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice } from '@shared/models/widget.models';
import { isDefinedAndNotNull } from '@core/utils';
import {
RpcUpdateStateSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component';
SetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component';
@Component({
selector: 'tb-rpc-update-state-settings',
templateUrl: './rpc-state-settings-button.component.html',
styleUrls: ['./rpc-state-settings-button.scss'],
selector: 'tb-set-value-action-settings',
templateUrl: './value-action-settings-button.component.html',
styleUrls: ['./value-action-settings-button.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RpcUpdateStateSettingsComponent),
useExisting: forwardRef(() => SetValueActionSettingsComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAccessor {
export class SetValueActionSettingsComponent implements OnInit, ControlValueAccessor {
@HostBinding('style.overflow')
overflow = 'hidden';
@ -64,7 +60,7 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
panelTitle: string;
@Input()
stateValueType: ValueType;
valueType: ValueType;
@Input()
aliasController: IAliasController;
@ -75,7 +71,7 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
@Input()
disabled = false;
modelValue: RpcUpdateStateSettings;
modelValue: SetValueSettings;
displayValue: string;
@ -103,12 +99,12 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
}
}
writeValue(value: RpcUpdateStateSettings): void {
writeValue(value: SetValueSettings): void {
this.modelValue = value;
this.updateDisplayValue();
}
openRpcStateSettingsPopup($event: Event, matButton: MatButton) {
openValueActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
@ -117,22 +113,22 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
updateState: this.modelValue,
setValueSettings: this.modelValue,
panelTitle: this.panelTitle,
stateValueType: this.stateValueType,
valueType: this.valueType,
aliasController: this.aliasController,
targetDevice: this.targetDevice
};
const updateStateSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, RpcUpdateStateSettingsPanelComponent,
const setValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, SetValueActionSettingsPanelComponent,
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
updateStateSettingsPanelPopover.tbComponentRef.instance.popover = updateStateSettingsPanelPopover;
updateStateSettingsPanelPopover.tbComponentRef.instance.updateStateSettingsApplied.subscribe((updateState) => {
updateStateSettingsPanelPopover.hide();
this.modelValue = updateState;
setValueSettingsPanelPopover.tbComponentRef.instance.popover = setValueSettingsPanelPopover;
setValueSettingsPanelPopover.tbComponentRef.instance.setValueSettingsApplied.subscribe((setValueSettings) => {
setValueSettingsPanelPopover.hide();
this.modelValue = setValueSettings;
this.updateDisplayValue();
this.propagateChange(this.modelValue);
});
@ -141,31 +137,31 @@ export class RpcUpdateStateSettingsComponent implements OnInit, ControlValueAcce
private updateDisplayValue() {
let value: any;
switch (this.modelValue.stateToParams.type) {
case RpcStateToParamsType.CONSTANT:
value = this.modelValue.stateToParams.constantValue;
switch (this.modelValue.valueToData.type) {
case ValueToDataType.CONSTANT:
value = this.modelValue.valueToData.constantValue;
break;
case RpcStateToParamsType.FUNCTION:
case ValueToDataType.FUNCTION:
value = 'f(value)';
break;
case RpcStateToParamsType.NONE:
case ValueToDataType.NONE:
break;
}
switch (this.modelValue.action) {
case RpcUpdateStateAction.EXECUTE_RPC:
case SetValueAction.EXECUTE_RPC:
let methodName = this.modelValue.executeRpc.method;
if (isDefinedAndNotNull(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;
case RpcUpdateStateAction.SET_ATTRIBUTE:
this.displayValue = this.translate.instant('widgets.rpc-state.set-attribute-to-value-text',
case SetValueAction.SET_ATTRIBUTE:
this.displayValue = this.translate.instant('widgets.value-action.set-attribute-to-value-text',
{key: this.modelValue.setAttribute.key, value});
break;
case RpcUpdateStateAction.ADD_TIME_SERIES:
this.displayValue = this.translate.instant('widgets.rpc-state.add-time-series-value-text',
{key: this.modelValue.setAttribute.key, value});
case SetValueAction.ADD_TIME_SERIES:
this.displayValue = this.translate.instant('widgets.value-action.add-time-series-value-text',
{key: this.modelValue.putTimeSeries.key, value});
break;
}
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"
class="tb-rpc-state-settings"
class="tb-value-action-settings"
mat-stroked-button color="primary"
(click)="openRpcStateSettingsPopup($event, matButton)">
(click)="openValueActionSettingsPopup($event, matButton)">
<div>
<span>{{ displayValue }}</span>
<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
* limitations under the License.
*/
.mdc-button.tb-rpc-state-settings {
.mdc-button.tb-value-action-settings {
width: 100%;
.mdc-button__label {
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';
.tb-rpc-state-settings-panel {
.tb-value-action-settings-panel {
width: 530px;
display: flex;
flex-direction: column;
@ -23,20 +23,20 @@
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-rpc-state-settings-panel-content {
.tb-value-action-settings-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
}
.tb-rpc-state-settings-title {
.tb-value-action-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-rpc-state-settings-panel-buttons {
.tb-value-action-settings-panel-buttons {
height: 40px;
display: flex;
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
} from '@home/components/widget/lib/settings/common/color-range-settings.component';
import {
RpcInitialStateSettingsComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings.component';
GetValueActionSettingsComponent
} from '@home/components/widget/lib/settings/common/action/get-value-action-settings.component';
import {
RpcInitialStateSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-initial-state-settings-panel.component';
GetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component';
import {
DeviceKeyAutocompleteComponent
} from '@home/components/widget/lib/settings/control/device-key-autocomplete.component';
import {
RpcUpdateStateSettingsComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings.component';
SetValueActionSettingsComponent
} from '@home/components/widget/lib/settings/common/action/set-value-action-settings.component';
import {
RpcUpdateStateSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/rpc/rpc-update-state-settings-panel.component';
SetValueActionSettingsPanelComponent
} 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';
@NgModule({
@ -89,11 +89,11 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
ColorRangeListComponent,
ColorRangePanelComponent,
ColorRangeSettingsComponent,
RpcInitialStateSettingsComponent,
RpcInitialStateSettingsPanelComponent,
GetValueActionSettingsComponent,
GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent,
RpcUpdateStateSettingsComponent,
RpcUpdateStateSettingsPanelComponent
SetValueActionSettingsComponent,
SetValueActionSettingsPanelComponent
],
imports: [
CommonModule,
@ -120,11 +120,11 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
ColorRangeListComponent,
ColorRangePanelComponent,
ColorRangeSettingsComponent,
RpcInitialStateSettingsComponent,
RpcInitialStateSettingsPanelComponent,
GetValueActionSettingsComponent,
GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent,
RpcUpdateStateSettingsComponent,
RpcUpdateStateSettingsPanelComponent
SetValueActionSettingsComponent,
SetValueActionSettingsPanelComponent
],
providers: [
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-title" translate>widgets.single-switch.behavior</div>
<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>
<tb-rpc-initial-state-settings fxFlex
[stateValueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="initialState"></tb-rpc-initial-state-settings>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.initial-state-hint' | translate}}" translate>widgets.value-action.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.value-action.initial-state"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<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>
<tb-rpc-update-state-settings fxFlex
panelTitle="widgets.rpc-state.turn-on"
[stateValueType]="valueType.BOOLEAN"
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-on-hint' | translate}}" translate>widgets.value-action.turn-on</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-on"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="onUpdateState"></tb-rpc-update-state-settings>
formControlName="onUpdateState"></tb-set-value-action-settings>
</div>
<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>
<tb-rpc-update-state-settings fxFlex
panelTitle="widgets.rpc-state.turn-off"
[stateValueType]="valueType.BOOLEAN"
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-off-hint' | translate}}" translate>widgets.value-action.turn-off</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-off"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
formControlName="offUpdateState"></tb-rpc-update-state-settings>
formControlName="offUpdateState"></tb-set-value-action-settings>
</div>
</div>
<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.image = result;
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",
"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": {
"select-entity": "Select entity",
"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-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": {
"font-settings": "Font settings",
"font-family": "Font family",

Loading…
Cancel
Save