Browse Source

Merge branch 'thingsboard:master' into oauth2-custom-add-none-auth-method

pull/11647/head
cbecker 2 years ago
committed by GitHub
parent
commit
ab74ce2e1d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg
  2. 179
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  3. 93
      application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsAdaptiveCard.java
  4. 92
      application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsMessageCard.java
  5. 5
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  6. 2
      application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java
  7. 86
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  8. 59
      application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionJsonDeviceTest.java
  9. 44
      application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionProtoDeviceTest.java
  10. 58
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java
  11. 45
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java
  12. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java
  13. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java
  14. 1
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java
  15. 1
      common/proto/src/main/proto/queue.proto
  16. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  17. 90
      dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java
  18. 41
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java
  19. 15
      msa/web-ui/server.ts
  20. 2
      packaging/js/scripts/init/template
  21. 101
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java
  22. 18
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeConfiguration.java
  23. 20
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  24. 343
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeTest.java
  25. 2
      ui-ngx/src/app/core/http/ota-package.service.ts
  26. 2
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  27. 25
      ui-ngx/src/app/core/services/time.service.ts
  28. 6
      ui-ngx/src/app/modules/common/modules-map.ts
  29. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  30. 72
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract.ts
  31. 61
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts
  32. 67
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract.ts
  33. 101
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract.ts
  34. 56
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/opc-version-processor.abstract.ts
  35. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts
  36. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts
  37. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.ts
  38. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  39. 76
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.abstract.ts
  40. 110
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts
  41. 78
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-legacy-basic-config.component.ts
  42. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  43. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss
  44. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  45. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  46. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  47. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  48. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html
  49. 191
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts
  50. 82
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract.ts
  51. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.html
  52. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.scss
  53. 103
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.ts
  54. 124
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-legacy-basic-config.component.ts
  55. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component.html
  56. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component.ts
  57. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component.html
  58. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component.ts
  59. 134
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts
  60. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.html
  61. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.scss
  62. 15
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.ts
  63. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.html
  64. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.scss
  65. 88
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.ts
  66. 88
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-legacy-basic-config.component.ts
  67. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  68. 14
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  69. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  70. 43
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  71. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  72. 34
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  73. 291
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  74. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts
  75. 42
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts
  76. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/modbus-version-mapping.util.ts
  77. 217
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/mqtt-version-mapping.util.ts
  78. 134
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts
  79. 2
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  80. 22
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  81. 172
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  82. 21
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  83. 11
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  84. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  85. 36
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  86. 13
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  87. 8
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts
  88. 1
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html
  89. 9
      ui-ngx/src/app/shared/components/image/image-gallery.component.html
  90. 5
      ui-ngx/src/app/shared/components/image/image-gallery.component.scss
  91. 30
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.html
  92. 38
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss
  93. 160
      ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts
  94. 36
      ui-ngx/src/app/shared/components/time/datetime-period.component.html
  95. 11
      ui-ngx/src/app/shared/components/time/datetime-period.component.scss
  96. 7
      ui-ngx/src/app/shared/components/time/datetime-period.component.ts
  97. 4
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.html
  98. 12
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  99. 55
      ui-ngx/src/app/shared/components/time/timeinterval.component.html
  100. 15
      ui-ngx/src/app/shared/components/time/timeinterval.component.scss

4
application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg

@ -93,7 +93,7 @@
}, },
"valueToData": { "valueToData": {
"type": "CONSTANT", "type": "CONSTANT",
"constantValue": false, "constantValue": true,
"valueToDataFunction": "/* 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;"
} }
}, },
@ -127,7 +127,7 @@
}, },
"valueToData": { "valueToData": {
"type": "CONSTANT", "type": "CONSTANT",
"constantValue": true, "constantValue": false,
"valueToDataFunction": "/* 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;"
} }
}, },

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

179
application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java

@ -15,16 +15,17 @@
*/ */
package org.thingsboard.server.service.notification.channels; package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDe
import org.thingsboard.server.service.notification.NotificationProcessingContext; import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration; import java.time.Duration;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
@ -56,17 +59,93 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
@Override @Override
public void sendNotification(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { public void sendNotification(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
Message message = new Message(); if (targetConfig.getUseOldApi() == null || Boolean.TRUE.equals(targetConfig.getUseOldApi())) {
message.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor())); sendTeamsMessageCard(targetConfig, processedTemplate, ctx);
} else {
sendTeamsAdaptiveCard(targetConfig, processedTemplate, ctx);
}
}
private void sendTeamsAdaptiveCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws URISyntaxException, JsonProcessingException {
TeamsAdaptiveCard teamsAdaptiveCard = new TeamsAdaptiveCard();
TeamsAdaptiveCard.Attachment attachment = new TeamsAdaptiveCard.Attachment();
teamsAdaptiveCard.setAttachments(List.of(attachment));
TeamsAdaptiveCard.AdaptiveCard adaptiveCard = new TeamsAdaptiveCard.AdaptiveCard();
attachment.setContent(adaptiveCard);
TeamsAdaptiveCard.BackgroundImage backgroundImage = new TeamsAdaptiveCard.BackgroundImage(processedTemplate.getThemeColor());
adaptiveCard.setBackgroundImage(backgroundImage);
if (StringUtils.isEmpty(processedTemplate.getSubject())) { if (StringUtils.isEmpty(processedTemplate.getSubject())) {
message.setText(processedTemplate.getBody()); TeamsAdaptiveCard.TextBlock textBlock = new TeamsAdaptiveCard.TextBlock();
textBlock.setText(processedTemplate.getBody());
textBlock.setWeight("Normal");
textBlock.setSize("Medium");
textBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(textBlock);
} else { } else {
message.setSummary(processedTemplate.getSubject()); TeamsAdaptiveCard.TextBlock subjectTextBlock = new TeamsAdaptiveCard.TextBlock();
Message.Section section = new Message.Section(); subjectTextBlock.setText(processedTemplate.getSubject());
subjectTextBlock.setWeight("Bolder");
subjectTextBlock.setSize("Large");
subjectTextBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(subjectTextBlock);
TeamsAdaptiveCard.TextBlock bodyTextBlock = new TeamsAdaptiveCard.TextBlock();
bodyTextBlock.setText(processedTemplate.getBody());
bodyTextBlock.setWeight("Lighter");
bodyTextBlock.setSize("Medium");
bodyTextBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(bodyTextBlock);
}
String uri = getButtonUri(processedTemplate, ctx);
if (StringUtils.isNotBlank(uri) && processedTemplate.getButton().getText() != null) {
TeamsAdaptiveCard.ActionOpenUrl actionOpenUrl = new TeamsAdaptiveCard.ActionOpenUrl();
actionOpenUrl.setTitle(processedTemplate.getButton().getText());
actionOpenUrl.setUrl(uri);
adaptiveCard.getActions().add(actionOpenUrl);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsAdaptiveCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
}
private void sendTeamsMessageCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException, URISyntaxException {
TeamsMessageCard teamsMessageCard = new TeamsMessageCard();
teamsMessageCard.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor()));
if (StringUtils.isEmpty(processedTemplate.getSubject())) {
teamsMessageCard.setText(processedTemplate.getBody());
} else {
teamsMessageCard.setSummary(processedTemplate.getSubject());
TeamsMessageCard.Section section = new TeamsMessageCard.Section();
section.setActivityTitle(processedTemplate.getSubject()); section.setActivityTitle(processedTemplate.getSubject());
section.setActivitySubtitle(processedTemplate.getBody()); section.setActivitySubtitle(processedTemplate.getBody());
message.setSections(List.of(section)); teamsMessageCard.setSections(List.of(section));
} }
var button = processedTemplate.getButton();
String uri = getButtonUri(processedTemplate, ctx);
if (StringUtils.isNotBlank(uri) && button.getText() != null) {
TeamsMessageCard.ActionCard actionCard = new TeamsMessageCard.ActionCard();
actionCard.setType("OpenUri");
actionCard.setName(button.getText());
var target = new TeamsMessageCard.ActionCard.Target("default", uri);
actionCard.setTargets(List.of(target));
teamsMessageCard.setPotentialAction(List.of(actionCard));
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsMessageCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
}
private String getButtonUri(MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException {
var button = processedTemplate.getButton(); var button = processedTemplate.getButton();
if (button != null && button.isEnabled()) { if (button != null && button.isEnabled()) {
String uri; String uri;
@ -99,17 +178,9 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
} else { } else {
uri = button.getLink(); uri = button.getLink();
} }
if (StringUtils.isNotBlank(uri) && button.getText() != null) { return uri;
Message.ActionCard actionCard = new Message.ActionCard();
actionCard.setType("OpenUri");
actionCard.setName(button.getText());
var target = new Message.ActionCard.Target("default", uri);
actionCard.setTargets(List.of(target));
message.setPotentialAction(List.of(actionCard));
}
} }
return null;
restTemplate.postForEntity(targetConfig.getWebhookUrl(), message, String.class);
} }
@Override @Override
@ -121,74 +192,4 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
return NotificationDeliveryMethod.MICROSOFT_TEAMS; return NotificationDeliveryMethod.MICROSOFT_TEAMS;
} }
@Data
public static class Message {
@JsonProperty("@type")
private final String type = "MessageCard";
@JsonProperty("@context")
private final String context = "http://schema.org/extensions";
private String themeColor;
private String summary;
private String text;
private List<Section> sections;
private List<ActionCard> potentialAction;
@Data
public static class Section {
private String activityTitle;
private String activitySubtitle;
private String activityImage;
private List<Fact> facts;
private boolean markdown;
@Data
public static class Fact {
private final String name;
private final String value;
}
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ActionCard {
@JsonProperty("@type")
private String type; // ActionCard, OpenUri
private String name;
private List<Input> inputs; // for ActionCard
private List<Action> actions; // for ActionCard
private List<Target> targets;
@Data
public static class Input {
@JsonProperty("@type")
private String type; // TextInput, DateInput, MultichoiceInput
private String id;
private boolean isMultiple;
private String title;
private boolean isMultiSelect;
@Data
public static class Choice {
private final String display;
private final String value;
}
}
@Data
public static class Action {
@JsonProperty("@type")
private final String type; // HttpPOST
private final String name;
private final String target; // url
}
@Data
public static class Target {
private final String os;
private final String uri;
}
}
}
} }

93
application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsAdaptiveCard.java

@ -0,0 +1,93 @@
/**
* 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.
*/
package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.dao.util.ImageUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @link <a href="https://adaptivecards.io/designer/">AdaptiveCard Designer</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TeamsAdaptiveCard {
private String type = "message";
private List<Attachment> attachments;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Attachment {
private String contentType = "application/vnd.microsoft.card.adaptive";
private AdaptiveCard content;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AdaptiveCard {
@JsonProperty("$schema")
private final String schema = "http://adaptivecards.io/schemas/adaptive-card.json";
private final String type = "AdaptiveCard";
private BackgroundImage backgroundImage;
@JsonProperty("body")
private List<TextBlock> textBlocks = new ArrayList<>();
private List<ActionOpenUrl> actions = new ArrayList<>();
}
@Data
@NoArgsConstructor
public static class BackgroundImage {
private String url;
private final String fillMode = "repeat";
public BackgroundImage(String color) {
// This is the only one way how to specify color the custom color for the card
url = ImageUtils.getEmbeddedBase64EncodedImg(color);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TextBlock {
private final String type = "TextBlock";
private String text;
private String weight = "Normal";
private String size = "Medium";
private String spacing = "None";
private String color = "#FFFFFF";
private final boolean wrap = true;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ActionOpenUrl {
private final String type = "Action.OpenUrl";
private String title;
private String url;
}
}

92
application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsMessageCard.java

@ -0,0 +1,92 @@
/**
* 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.
*/
package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class TeamsMessageCard {
@JsonProperty("@type")
private final String type = "MessageCard";
@JsonProperty("@context")
private final String context = "http://schema.org/extensions";
private String themeColor;
private String summary;
private String text;
private List<Section> sections;
private List<ActionCard> potentialAction;
@Data
public static class Section {
private String activityTitle;
private String activitySubtitle;
private String activityImage;
private List<Fact> facts;
private boolean markdown;
@Data
public static class Fact {
private final String name;
private final String value;
}
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ActionCard {
@JsonProperty("@type")
private String type; // ActionCard, OpenUri
private String name;
private List<Input> inputs; // for ActionCard
private List<Action> actions; // for ActionCard
private List<Target> targets;
@Data
public static class Input {
@JsonProperty("@type")
private String type; // TextInput, DateInput, MultichoiceInput
private String id;
private boolean isMultiple;
private String title;
private boolean isMultiSelect;
@Data
public static class Choice {
private final String display;
private final String value;
}
}
@Data
public static class Action {
@JsonProperty("@type")
private final String type; // HttpPOST
private final String name;
private final String target; // url
}
@Data
public static class Target {
private final String os;
private final String uri;
}
}
}

5
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -417,7 +417,8 @@ public class DefaultTransportApiService implements TransportApiService {
requestMsg.getCredentialsDataProto().getValidateDeviceX509CertRequestMsg().getHash()), requestMsg.getCredentialsDataProto().getValidateDeviceX509CertRequestMsg().getHash()),
new ProvisionDeviceProfileCredentials( new ProvisionDeviceProfileCredentials(
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(), requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(),
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()))); requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()),
requestMsg.getGateway()));
} catch (ProvisionFailedException e) { } catch (ProvisionFailedException e) {
return getTransportApiResponseMsg(new DeviceCredentials(), TransportProtos.ResponseStatus.valueOf(e.getMessage())); return getTransportApiResponseMsg(new DeviceCredentials(), TransportProtos.ResponseStatus.valueOf(e.getMessage()));
} }
@ -665,7 +666,7 @@ public class DefaultTransportApiService implements TransportApiService {
private ProvisionRequest createProvisionRequest(String certificateValue) { private ProvisionRequest createProvisionRequest(String certificateValue) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue), new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue),
null); null, null);
} }
} }

2
application/src/test/java/org/thingsboard/server/service/device/provision/DeviceProvisionServiceTest.java

@ -236,7 +236,7 @@ public class DeviceProvisionServiceTest {
private ProvisionRequest createProvisionRequest(String certificateValue) { private ProvisionRequest createProvisionRequest(String certificateValue) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE, return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue), new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue),
null); null, null);
} }
public static String certTrimNewLinesForChainInDeviceProfile(String input) { public static String certTrimNewLinesForChainInDeviceProfile(String input) {

86
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -25,6 +25,7 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpEntity;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -85,8 +86,12 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications; import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.notification.channels.TeamsAdaptiveCard;
import org.thingsboard.server.service.notification.channels.TeamsMessageCard;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -752,7 +757,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
} }
@Test @Test
public void testMicrosoftTeamsNotifications() throws Exception { public void testMicrosoftTeamsNotificationsWithOfficeConnector() throws URISyntaxException {
RestTemplate restTemplate = mock(RestTemplate.class); RestTemplate restTemplate = mock(RestTemplate.class);
microsoftTeamsNotificationChannel.setRestTemplate(restTemplate); microsoftTeamsNotificationChannel.setRestTemplate(restTemplate);
@ -760,6 +765,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
var targetConfig = new MicrosoftTeamsNotificationTargetConfig(); var targetConfig = new MicrosoftTeamsNotificationTargetConfig();
targetConfig.setWebhookUrl(webhookUrl); targetConfig.setWebhookUrl(webhookUrl);
targetConfig.setChannelName("My channel"); targetConfig.setChannelName("My channel");
targetConfig.setUseOldApi(true);
NotificationTarget target = new NotificationTarget(); NotificationTarget target = new NotificationTarget();
target.setName("Microsoft Teams channel"); target.setName("Microsoft Teams channel");
target.setConfiguration(targetConfig); target.setConfiguration(targetConfig);
@ -770,7 +776,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
String templateParams = "${recipientTitle} - ${entityType}"; String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams); template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams); template.setBody("Body: " + templateParams);
template.setThemeColor("ff0000"); template.setThemeColor("#ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button(); var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true); button.setEnabled(true);
button.setText("Button: " + templateParams); button.setText("Button: " + templateParams);
@ -803,11 +809,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1); assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName()); assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName());
var messageCaptor = ArgumentCaptor.forClass(MicrosoftTeamsNotificationChannel.Message.class); ArgumentCaptor<HttpEntity<String>> messageCaptor = ArgumentCaptor.forClass(HttpEntity.class);
notificationCenter.processNotificationRequest(tenantId, notificationRequest, null); notificationCenter.processNotificationRequest(tenantId, notificationRequest, null);
verify(restTemplate, timeout(20000)).postForEntity(eq(webhookUrl), messageCaptor.capture(), any()); verify(restTemplate, timeout(20000)).postForEntity(eq(new URI(webhookUrl)), messageCaptor.capture(), any());
HttpEntity<String> value = messageCaptor.getValue();
TeamsMessageCard message = JacksonUtil.fromString(value.getBody(), TeamsMessageCard.class);
var message = messageCaptor.getValue();
String expectedParams = "My channel - Device"; String expectedParams = "My channel - Device";
assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor()); assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor());
assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo("Subject: " + expectedParams); assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo("Subject: " + expectedParams);
@ -816,6 +824,74 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams); assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams);
} }
@Test
public void testMicrosoftTeamsNotificationsWithWorkflow() throws Exception {
RestTemplate restTemplate = mock(RestTemplate.class);
microsoftTeamsNotificationChannel.setRestTemplate(restTemplate);
String webhookUrl = "https://webhook.com/webhookb2/9628fa60-d873-11ed-913c-a196b1f9b445";
var targetConfig = new MicrosoftTeamsNotificationTargetConfig();
targetConfig.setWebhookUrl(webhookUrl);
targetConfig.setChannelName("My channel");
targetConfig.setUseOldApi(false);
NotificationTarget target = new NotificationTarget();
target.setName("Microsoft Teams channel");
target.setConfiguration(targetConfig);
target = saveNotificationTarget(target);
var template = new MicrosoftTeamsDeliveryMethodNotificationTemplate();
template.setEnabled(true);
String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams);
template.setThemeColor("#ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true);
button.setText("Button: " + templateParams);
button.setLinkType(LinkType.LINK);
button.setLink("https://" + templateParams);
template.setButton(button);
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setName("Notification to Teams");
notificationTemplate.setNotificationType(NotificationType.GENERAL);
NotificationTemplateConfig templateConfig = new NotificationTemplateConfig();
templateConfig.setDeliveryMethodsTemplates(Map.of(
NotificationDeliveryMethod.MICROSOFT_TEAMS, template
));
notificationTemplate.setConfiguration(templateConfig);
notificationTemplate = saveNotificationTemplate(notificationTemplate);
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId)
.originatorEntityId(tenantAdminUserId)
.templateId(notificationTemplate.getId())
.targets(List.of(target.getUuidId()))
.info(EntityActionNotificationInfo.builder()
.entityId(new DeviceId(UUID.randomUUID()))
.actionType(ActionType.ADDED)
.userId(tenantAdminUserId.getId())
.build())
.build();
NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class);
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName());
ArgumentCaptor<HttpEntity<String>> messageCaptor = ArgumentCaptor.forClass(HttpEntity.class);
notificationCenter.processNotificationRequest(tenantId, notificationRequest, null);
verify(restTemplate, timeout(20000)).postForEntity(eq(new URI(webhookUrl)), messageCaptor.capture(), any());
HttpEntity<String> value = messageCaptor.getValue();
TeamsAdaptiveCard message = JacksonUtil.fromString(value.getBody(), TeamsAdaptiveCard.class);
String expectedParams = "My channel - Device";
assertThat(message).isNotNull();
assertThat(message.getAttachments().get(0).getContent().getBackgroundImage().getUrl()).isNotEmpty();
assertThat(message.getAttachments().get(0).getContent().getTextBlocks().get(0).getText()).isEqualTo("Subject: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getTextBlocks().get(1).getText()).isEqualTo("Body: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getActions().get(0).getTitle()).isEqualTo("Button: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getActions().get(0).getUrl()).isEqualTo("https://" + expectedParams);
}
@Test @Test
public void testMobileAppNotifications() throws Exception { public void testMobileAppNotifications() throws Exception {
loginCustomerUser(); loginCustomerUser();

59
application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionJsonDeviceTest.java

@ -23,6 +23,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.TransportPayloadType;
@ -67,6 +68,11 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials(); processTestProvisioningCreateNewDeviceWithoutCredentials();
} }
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test @Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception { public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken(); processTestProvisioningCreateNewDeviceWithAccessToken();
@ -123,6 +129,37 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
} }
private void processTestProvisioningCreateNewGatewayDevice() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
.coapDeviceType(CoapDeviceType.DEFAULT)
.transportPayloadType(TransportPayloadType.JSON)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
processBeforeTest(configProperties);
JsonNode response = JacksonUtil.fromBytes(createCoapClientAndPublish(true));
Assert.assertTrue(response.hasNonNull("credentialsType"));
Assert.assertTrue(response.hasNonNull("status"));
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText());
}
private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3") .deviceName("Test Provision device3")
@ -222,16 +259,30 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
} }
private byte[] createCoapClientAndPublish() throws Exception { private byte[] createCoapClientAndPublish() throws Exception {
return createCoapClientAndPublish(""); return createCoapClientAndPublish(false);
}
private byte[] createCoapClientAndPublish(boolean isGateway) throws Exception {
return createCoapClientAndPublish("", isGateway);
} }
private byte[] createCoapClientAndPublish(String deviceCredentials) throws Exception { private byte[] createCoapClientAndPublish(String deviceCredentials) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials); return createCoapClientAndPublish(deviceCredentials, false);
}
private byte[] createCoapClientAndPublish(String deviceCredentials, boolean isGateway) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials, isGateway);
client = new CoapTestClient(accessToken, FeatureType.PROVISION); client = new CoapTestClient(accessToken, FeatureType.PROVISION);
return client.postMethod(provisionRequestMsg.getBytes()).getPayload(); return client.postMethod(provisionRequestMsg.getBytes()).getPayload();
} }
private String createTestProvisionMessage(String deviceCredentials) { protected String createTestProvisionMessage(String deviceCredentials, boolean isGateway) {
return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}"; String request = "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\""
+ deviceCredentials;
if (isGateway) {
request += ",\"gateway\":true";
}
request += "}";
return request;
} }
} }

44
application/src/test/java/org/thingsboard/server/transport/coap/provision/CoapProvisionProtoDeviceTest.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.transport.coap.provision; package org.thingsboard.server.transport.coap.provision;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.CoapResponse;
import org.junit.After; import org.junit.After;
@ -22,6 +23,7 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.CoapDeviceType; import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.TransportPayloadType;
@ -74,6 +76,11 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials(); processTestProvisioningCreateNewDeviceWithoutCredentials();
} }
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test @Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception { public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken(); processTestProvisioningCreateNewDeviceWithAccessToken();
@ -124,6 +131,35 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name()); Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
} }
private void processTestProvisioningCreateNewGatewayDevice() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
.coapDeviceType(CoapDeviceType.DEFAULT)
.transportPayloadType(TransportPayloadType.PROTOBUF)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
processBeforeTest(configProperties);
byte[] testsProvisionMessage = createTestsProvisionMessage(null, null, true);
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createCoapClientAndPublish(testsProvisionMessage));
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().name());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
}
private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { private void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3") .deviceName("Test Provision device3")
@ -233,6 +269,10 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
} }
private byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception { private byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception {
return createTestsProvisionMessage(credentialsType, credentialsData, false);
}
private byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData, boolean isGateway) throws Exception {
return ProvisionDeviceRequestMsg.newBuilder() return ProvisionDeviceRequestMsg.newBuilder()
.setDeviceName("Test Provision device") .setDeviceName("Test Provision device")
.setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN) .setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN)
@ -241,7 +281,9 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
ProvisionDeviceCredentialsMsg.newBuilder() ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey("testProvisionKey") .setProvisionDeviceKey("testProvisionKey")
.setProvisionDeviceSecret("testProvisionSecret") .setProvisionDeviceSecret("testProvisionSecret")
).build() )
.setGateway(isGateway)
.build()
.toByteArray(); .toByteArray();
} }

58
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionJsonDeviceTest.java

@ -22,6 +22,7 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.TransportPayloadType;
@ -68,6 +69,11 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials(); processTestProvisioningCreateNewDeviceWithoutCredentials();
} }
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test @Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception { public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken(); processTestProvisioningCreateNewDeviceWithAccessToken();
@ -130,6 +136,37 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
} }
protected void processTestProvisioningCreateNewGatewayDevice() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
.transportPayloadType(TransportPayloadType.JSON)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
super.processBeforeTest(configProperties);
byte[] result = createMqttClientAndPublish(true);
JsonNode response = JacksonUtil.fromBytes(result);
Assert.assertTrue(response.hasNonNull("credentialsType"));
Assert.assertTrue(response.hasNonNull("status"));
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.get("credentialsType").asText());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.get("status").asText());
}
protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3") .deviceName("Test Provision device3")
@ -264,11 +301,19 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
} }
protected byte[] createMqttClientAndPublish() throws Exception { protected byte[] createMqttClientAndPublish() throws Exception {
return createMqttClientAndPublish(""); return createMqttClientAndPublish(false);
}
protected byte[] createMqttClientAndPublish(boolean isGateway) throws Exception {
return createMqttClientAndPublish("", isGateway);
} }
protected byte[] createMqttClientAndPublish(String deviceCredentials) throws Exception { protected byte[] createMqttClientAndPublish(String deviceCredentials) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials); return createMqttClientAndPublish(deviceCredentials, false);
}
protected byte[] createMqttClientAndPublish(String deviceCredentials, boolean isGateway) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceCredentials, isGateway);
MqttTestClient client = new MqttTestClient(); MqttTestClient client = new MqttTestClient();
client.connectAndWait("provision"); client.connectAndWait("provision");
MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC); MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC);
@ -280,7 +325,12 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
return onProvisionCallback.getPayloadBytes(); return onProvisionCallback.getPayloadBytes();
} }
protected String createTestProvisionMessage(String deviceCredentials) { protected String createTestProvisionMessage(String deviceCredentials, boolean isGateway) {
return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}"; String request = "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials;
if (isGateway) {
request += ",\"gateway\":true";
}
request += "}";
return request;
} }
} }

45
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/provision/MqttProvisionProtoDeviceTest.java

@ -15,12 +15,14 @@
*/ */
package org.thingsboard.server.transport.mqtt.mqttv3.provision; package org.thingsboard.server.transport.mqtt.mqttv3.provision;
import com.fasterxml.jackson.databind.JsonNode;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.TransportPayloadType;
@ -76,6 +78,11 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials(); processTestProvisioningCreateNewDeviceWithoutCredentials();
} }
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test @Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception { public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken(); processTestProvisioningCreateNewDeviceWithAccessToken();
@ -130,6 +137,36 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name()); Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
} }
protected void processTestProvisioningCreateNewGatewayDevice() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
.transportPayloadType(TransportPayloadType.PROTOBUF)
.provisionType(DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES)
.provisionKey("testProvisionKey")
.provisionSecret("testProvisionSecret")
.build();
processBeforeTest(configProperties);
byte[] provisionRequestMsg = createTestsProvisionMessage(null, null, true);
byte[] responseBytesMsg = createMqttClientAndPublish(provisionRequestMsg);
ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(responseBytesMsg);
Device createdDevice = deviceService.findDeviceByTenantIdAndName(tenantId, "Test Provision device");
Assert.assertNotNull(createdDevice);
JsonNode additionalInfo = createdDevice.getAdditionalInfo();
Assert.assertNotNull(additionalInfo);
Assert.assertTrue(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean());
Assert.assertTrue(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean());
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, createdDevice.getId());
Assert.assertEquals(deviceCredentials.getCredentialsType().name(), response.getCredentialsType().name());
Assert.assertEquals(ProvisionResponseStatus.SUCCESS.name(), response.getStatus().name());
}
protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3") .deviceName("Test Provision device3")
@ -280,6 +317,10 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
} }
protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception { protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData) throws Exception {
return createTestsProvisionMessage(credentialsType, credentialsData, false);
}
protected byte[] createTestsProvisionMessage(CredentialsType credentialsType, CredentialsDataProto credentialsData, boolean isGateway) throws Exception {
return ProvisionDeviceRequestMsg.newBuilder() return ProvisionDeviceRequestMsg.newBuilder()
.setDeviceName("Test Provision device") .setDeviceName("Test Provision device")
.setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN) .setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN)
@ -288,7 +329,9 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
ProvisionDeviceCredentialsMsg.newBuilder() ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey("testProvisionKey") .setProvisionDeviceKey("testProvisionKey")
.setProvisionDeviceSecret("testProvisionSecret") .setProvisionDeviceSecret("testProvisionSecret")
).build() )
.setGateway(isGateway)
.build()
.toByteArray(); .toByteArray();
} }

1
common/dao-api/src/main/java/org/thingsboard/server/dao/device/provision/ProvisionRequest.java

@ -28,4 +28,5 @@ public class ProvisionRequest {
private DeviceCredentialsType credentialsType; private DeviceCredentialsType credentialsType;
private ProvisionDeviceCredentialsData credentialsData; private ProvisionDeviceCredentialsData credentialsData;
private ProvisionDeviceProfileCredentials credentials; private ProvisionDeviceProfileCredentials credentials;
private Boolean gateway;
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java

@ -28,6 +28,7 @@ public class MicrosoftTeamsNotificationTargetConfig extends NotificationTargetCo
private String webhookUrl; private String webhookUrl;
@NotEmpty @NotEmpty
private String channelName; private String channelName;
private Boolean useOldApi = Boolean.TRUE;
@Override @Override
public NotificationTargetType getType() { public NotificationTargetType getType() {

1
common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java

@ -635,6 +635,7 @@ public class JsonConverter {
.setProvisionDeviceCredentialsMsg(buildProvisionDeviceCredentialsMsg( .setProvisionDeviceCredentialsMsg(buildProvisionDeviceCredentialsMsg(
getStrValue(jo, DataConstants.PROVISION_KEY, true), getStrValue(jo, DataConstants.PROVISION_KEY, true),
getStrValue(jo, DataConstants.PROVISION_SECRET, true))) getStrValue(jo, DataConstants.PROVISION_SECRET, true)))
.setGateway(jo.has(DataConstants.GATEWAY_PARAMETER) && jo.get(DataConstants.GATEWAY_PARAMETER).getAsBoolean())
.build(); .build();
} }

1
common/proto/src/main/proto/queue.proto

@ -697,6 +697,7 @@ message ProvisionDeviceRequestMsg {
CredentialsType credentialsType = 2; CredentialsType credentialsType = 2;
ProvisionDeviceCredentialsMsg provisionDeviceCredentialsMsg = 3; ProvisionDeviceCredentialsMsg provisionDeviceCredentialsMsg = 3;
CredentialsDataProto credentialsDataProto = 4; CredentialsDataProto credentialsDataProto = 4;
bool gateway = 5;
} }
message ProvisionDeviceCredentialsMsg { message ProvisionDeviceCredentialsMsg {

7
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -27,6 +28,7 @@ import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.device.DeviceCacheEvictEvent; import org.thingsboard.server.cache.device.DeviceCacheEvictEvent;
import org.thingsboard.server.cache.device.DeviceCacheKey; import org.thingsboard.server.cache.device.DeviceCacheKey;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
@ -554,6 +556,11 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
device.setName(provisionRequest.getDeviceName()); device.setName(provisionRequest.getDeviceName());
device.setType(profile.getName()); device.setType(profile.getName());
device.setTenantId(profile.getTenantId()); device.setTenantId(profile.getTenantId());
if (provisionRequest.getGateway() != null && provisionRequest.getGateway()) {
ObjectNode additionalInfoNode = JacksonUtil.newObjectNode();
additionalInfoNode.put(DataConstants.GATEWAY_PARAMETER, true);
device.setAdditionalInfo(additionalInfoNode);
}
Device savedDevice = saveDevice(device); Device savedDevice = saveDevice(device);
if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) || if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) ||
!StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) || !StringUtils.isEmpty(provisionRequest.getCredentialsData().getX509CertHash()) ||

90
dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java

@ -46,9 +46,11 @@ import org.thingsboard.server.common.data.StringUtils;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.Map; import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
@ -265,6 +267,94 @@ public class ImageUtils {
return new int[]{thumbnailWidth, thumbnailHeight}; return new int[]{thumbnailWidth, thumbnailHeight};
} }
public static String getEmbeddedBase64EncodedImg(String colorStr) {
try {
Color color = parseColor(colorStr); // Support for hex, rgb, hsla
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, color.getRGB());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", outputStream);
byte[] imageBytes = outputStream.toByteArray();
String base64String = Base64.getEncoder().encodeToString(imageBytes);
return "data:image/png;base64," + base64String;
} catch (Exception e) {
log.warn("Failed to generate embedded image for color: {}", colorStr, e);
return null;
}
}
private static Color parseColor(String colorStr) {
if (colorStr.startsWith("#")) {
return Color.decode(colorStr);
}
if (colorStr.startsWith("rgb")) {
return parseRgbColor(colorStr);
}
if (colorStr.startsWith("hsl")) {
return parseHslaColor(colorStr);
}
throw new IllegalArgumentException("Unsupported color format: " + colorStr);
}
private static Color parseRgbColor(String rgb) {
String[] rgbValues = rgb.replaceAll("[^0-9,]", "").split(",");
int r = Integer.parseInt(rgbValues[0]);
int g = Integer.parseInt(rgbValues[1]);
int b = Integer.parseInt(rgbValues[2]);
return new Color(r, g, b);
}
private static Color parseHslaColor(String hsla) {
String[] hslaValues = hsla.replaceAll("[^0-9.,]", "").split(",");
float h = Float.parseFloat(hslaValues[0]);
float s = Float.parseFloat(hslaValues[1]) / 100;
float l = Float.parseFloat(hslaValues[2]) / 100;
float a = hslaValues.length > 3 ? Float.parseFloat(hslaValues[3]) : 1.0f;
return hslaToColor(h, s, l, a);
}
private static Color hslaToColor(float h, float s, float l, float alpha) {
float c = (1 - Math.abs(2 * l - 1)) * s;
float x = c * (1 - Math.abs((h / 60) % 2 - 1));
float m = l - c / 2;
float r = 0, g = 0, b = 0;
if (h < 60) {
r = c;
g = x;
} else if (h < 120) {
r = x;
g = c;
} else if (h < 180) {
g = c;
b = x;
} else if (h < 240) {
g = x;
b = c;
} else if (h < 300) {
r = x;
b = c;
} else {
r = c;
b = x;
}
r += m;
g += m;
b += m;
return new Color(clamp(r), clamp(g), clamp(b), clamp(alpha));
}
private static float clamp(float value) {
return Math.max(0, Math.min(1, value));
}
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor

41
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java

@ -21,6 +21,7 @@ import io.restassured.path.json.JsonPath;
import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@ -156,6 +157,46 @@ public class HttpClientTest extends AbstractContainerTest {
updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.DISABLED); updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.DISABLED);
} }
@Test
public void provisionRequestForGatewayDeviceWithAllowToCreateNewDevicesStrategy() throws Exception {
String testDeviceName = "test_provision_device";
DeviceProfile deviceProfile = testRestClient.getDeviceProfileById(device.getDeviceProfileId());
deviceProfile = updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES);
JsonObject provisionRequest = new JsonObject();
provisionRequest.addProperty("provisionDeviceKey", TEST_PROVISION_DEVICE_KEY);
provisionRequest.addProperty("provisionDeviceSecret", TEST_PROVISION_DEVICE_SECRET);
provisionRequest.addProperty("deviceName", testDeviceName);
provisionRequest.addProperty("gateway", true);
JsonPath provisionResponse = testRestClient.postProvisionRequest(provisionRequest.toString());
String credentialsType = provisionResponse.get("credentialsType");
String credentialsValue = provisionResponse.get("credentialsValue");
String status = provisionResponse.get("status");
testRestClient.deleteDeviceIfExists(device.getId());
device = testRestClient.getDeviceByName(testDeviceName);
JsonNode additionalInfo = device.getAdditionalInfo();
assertThat(additionalInfo).isNotNull();
assertThat(additionalInfo.has(DataConstants.GATEWAY_PARAMETER)
&& additionalInfo.get(DataConstants.GATEWAY_PARAMETER).isBoolean()).isTrue();
assertThat(additionalInfo.get(DataConstants.GATEWAY_PARAMETER).asBoolean()).isTrue();
DeviceCredentials expectedDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
assertThat(credentialsType).isEqualTo(expectedDeviceCredentials.getCredentialsType().name());
assertThat(credentialsValue).isEqualTo(expectedDeviceCredentials.getCredentialsId());
assertThat(status).isEqualTo("SUCCESS");
updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.DISABLED);
}
@Test @Test
public void provisionRequestForDeviceWithDisabledProvisioningStrategy() throws Exception { public void provisionRequestForDeviceWithDisabledProvisioningStrategy() throws Exception {

15
msa/web-ui/server.ts

@ -23,6 +23,7 @@ import httpProxy from 'http-proxy';
import compression from 'compression'; import compression from 'compression';
import historyApiFallback from 'connect-history-api-fallback'; import historyApiFallback from 'connect-history-api-fallback';
import { Socket } from 'net'; import { Socket } from 'net';
import { RequestHandler } from 'serve-static';
const logger = _logger('main'); const logger = _logger('main');
@ -99,7 +100,19 @@ let connections: Socket[] = [];
const root = path.join(webDir, 'public'); const root = path.join(webDir, 'public');
app.use(express.static(root)); const staticServe = express.static(root);
const indexHtmlFallback: RequestHandler<any> = (req, res, next) => {
res.sendFile(path.join(root, 'index.html'));
};
app.use(staticServe);
const indexHtmlFallbackPaths = [
/^\/resources\/scada-symbols\/(?:system|tenant)\/[^/]+$/
];
app.use(indexHtmlFallbackPaths, indexHtmlFallback);
server.listen(bindPort, bindAddress, () => { server.listen(bindPort, bindAddress, () => {
logger.info('==> 🌎 Listening on port %s.', bindPort); logger.info('==> 🌎 Listening on port %s.', bindPort);

2
packaging/js/scripts/init/template

@ -183,7 +183,7 @@ restart() {
stop && start stop && start
} }
orce_reload() { force_reload() {
working_dir=$(dirname "$mainfile") working_dir=$(dirname "$mainfile")
pushd "$working_dir" > /dev/null pushd "$working_dir" > /dev/null
[[ -f $pid_file ]] || { echoRed "Not running (pidfile not found)"; return 7; } [[ -f $pid_file ]] || { echoRed "Not running (pidfile not found)"; return 7; }

101
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java

@ -15,8 +15,9 @@
*/ */
package org.thingsboard.rule.engine.delay; package org.thingsboard.rule.engine.delay;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNode;
@ -26,18 +27,27 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.HashMap; import java.util.EnumSet;
import java.util.Map; import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
name = "delay (deprecated)", name = "delay (deprecated)",
version = 1,
configClazz = TbMsgDelayNodeConfiguration.class, configClazz = TbMsgDelayNodeConfiguration.class,
nodeDescription = "Delays incoming message (deprecated)", nodeDescription = "Delays incoming message (deprecated)",
nodeDetails = "Delays messages for a configurable period. " + nodeDetails = "Delays messages for a configurable period. " +
@ -50,13 +60,22 @@ import java.util.concurrent.TimeUnit;
) )
public class TbMsgDelayNode implements TbNode { public class TbMsgDelayNode implements TbNode {
private static final Set<TimeUnit> supportedTimeUnits = EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS);
private static final String supportedTimeUnitsStr = supportedTimeUnits.stream().map(TimeUnit::name).collect(Collectors.joining(", "));
private TbMsgDelayNodeConfiguration config; private TbMsgDelayNodeConfiguration config;
private Map<UUID, TbMsg> pendingMsgs; private ConcurrentMap<UUID, TbMsg> pendingMsgs;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgDelayNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbMsgDelayNodeConfiguration.class);
this.pendingMsgs = new HashMap<>(); String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: ";
try {
validateFields(config, errorPrefix);
} catch (DataValidationException e) {
throw new TbNodeException(e, true);
}
this.pendingMsgs = new ConcurrentHashMap<>();
} }
@Override @Override
@ -67,7 +86,7 @@ public class TbMsgDelayNode implements TbNode {
ctx.enqueueForTellNext( ctx.enqueueForTellNext(
TbMsg.newMsg( TbMsg.newMsg(
pendingMsg.getQueueName(), pendingMsg.getQueueName(),
pendingMsg.getType(), pendingMsg.getInternalType(),
pendingMsg.getOriginator(), pendingMsg.getOriginator(),
pendingMsg.getCustomerId(), pendingMsg.getCustomerId(),
pendingMsg.getMetaData(), pendingMsg.getMetaData(),
@ -89,25 +108,69 @@ public class TbMsgDelayNode implements TbNode {
} }
private long getDelay(TbMsg msg) { private long getDelay(TbMsg msg) {
int periodInSeconds; String timeUnitPattern = TbNodeUtils.processPattern(config.getTimeUnit(), msg);
if (config.isUseMetadataPeriodInSecondsPatterns()) { String periodPattern = TbNodeUtils.processPattern(config.getPeriod(), msg);
if (isParsable(msg, config.getPeriodInSecondsPattern())) { try {
periodInSeconds = Integer.parseInt(TbNodeUtils.processPattern(config.getPeriodInSecondsPattern(), msg)); TimeUnit timeUnit = TimeUnit.valueOf(timeUnitPattern.toUpperCase());
} else { if (!supportedTimeUnits.contains(timeUnit)) {
throw new RuntimeException("Can't parse period in seconds from metadata using pattern: " + config.getPeriodInSecondsPattern()); throw new RuntimeException("Time unit '" + timeUnit + "' is not supported! " +
"Only " + supportedTimeUnitsStr + " are supported.");
} }
} else { int period = Integer.parseInt(periodPattern);
periodInSeconds = config.getPeriodInSeconds(); return timeUnit.toMillis(period);
} catch (NumberFormatException e) {
throw new NumberFormatException("Can't parse period value : " + periodPattern);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid value for period time unit : " + timeUnitPattern);
} }
return TimeUnit.SECONDS.toMillis(periodInSeconds);
}
private boolean isParsable(TbMsg msg, String pattern) {
return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg));
} }
@Override @Override
public void destroy() { public void destroy() {
pendingMsgs.clear(); pendingMsgs.clear();
} }
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
switch (fromVersion) {
case 0:
var periodInSeconds = "periodInSeconds";
var periodInSecondsPattern = "periodInSecondsPattern";
var useMetadataPeriodInSecondsPatterns = "useMetadataPeriodInSecondsPatterns";
var period = "period";
if (oldConfiguration.has(useMetadataPeriodInSecondsPatterns)) {
var isUsedPattern = oldConfiguration.get(useMetadataPeriodInSecondsPatterns).booleanValue();
if (isUsedPattern) {
if (!oldConfiguration.has(periodInSecondsPattern)) {
throw new TbNodeException("Property to update: '" + periodInSecondsPattern + "' does not exist in configuration.");
}
((ObjectNode) oldConfiguration).set(period, oldConfiguration.get(periodInSecondsPattern));
} else {
if (!oldConfiguration.has(periodInSeconds)) {
throw new TbNodeException("Property to update: '" + periodInSeconds + "' does not exist in configuration.");
}
((ObjectNode) oldConfiguration).put(period, oldConfiguration.get(periodInSeconds).asText());
}
hasChanges = true;
} else if (oldConfiguration.has(periodInSeconds)) {
((ObjectNode) oldConfiguration).put(period, oldConfiguration.get(periodInSeconds).asText());
hasChanges = true;
}
if (!oldConfiguration.has(period)) {
((ObjectNode) oldConfiguration).put(period, "60");
hasChanges = true;
}
var timeUnit = "timeUnit";
if (!oldConfiguration.has(timeUnit)) {
((ObjectNode) oldConfiguration).put(timeUnit, TimeUnit.SECONDS.name());
hasChanges = true;
}
((ObjectNode) oldConfiguration).remove(List.of(periodInSeconds, periodInSecondsPattern, useMetadataPeriodInSecondsPatterns));
break;
default:
break;
}
return new TbPair<>(hasChanges, oldConfiguration);
}
} }

18
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeConfiguration.java

@ -15,23 +15,31 @@
*/ */
package org.thingsboard.rule.engine.delay; package org.thingsboard.rule.engine.delay;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.concurrent.TimeUnit;
@Data @Data
public class TbMsgDelayNodeConfiguration implements NodeConfiguration<TbMsgDelayNodeConfiguration> { public class TbMsgDelayNodeConfiguration implements NodeConfiguration<TbMsgDelayNodeConfiguration> {
private int periodInSeconds; @NotNull
private String period;
@NotNull
private String timeUnit;
@Min(1)
@Max(100000)
private int maxPendingMsgs; private int maxPendingMsgs;
private String periodInSecondsPattern;
private boolean useMetadataPeriodInSecondsPatterns;
@Override @Override
public TbMsgDelayNodeConfiguration defaultConfiguration() { public TbMsgDelayNodeConfiguration defaultConfiguration() {
TbMsgDelayNodeConfiguration configuration = new TbMsgDelayNodeConfiguration(); TbMsgDelayNodeConfiguration configuration = new TbMsgDelayNodeConfiguration();
configuration.setPeriodInSeconds(60); configuration.setPeriod("60");
configuration.setTimeUnit(TimeUnit.SECONDS.name());
configuration.setMaxPendingMsgs(1000); configuration.setMaxPendingMsgs(1000);
configuration.setUseMetadataPeriodInSecondsPatterns(false);
return configuration; return configuration;
} }
} }

20
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.transport.ProxyProvider; import reactor.netty.transport.ProxyProvider;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
@ -95,7 +96,12 @@ public class TbHttpClient {
semaphore = new Semaphore(config.getMaxParallelRequestsCount()); semaphore = new Semaphore(config.getMaxParallelRequestsCount());
} }
HttpClient httpClient = HttpClient.create() ConnectionProvider connectionProvider = ConnectionProvider
.builder("rule-engine-http-client")
.maxConnections(getPoolMaxConnections())
.build();
HttpClient httpClient = HttpClient.create(connectionProvider)
.runOn(getSharedOrCreateEventLoopGroup(eventLoopGroupShared)) .runOn(getSharedOrCreateEventLoopGroup(eventLoopGroupShared))
.doOnConnected(c -> .doOnConnected(c ->
c.addHandlerLast(new ReadTimeoutHandler(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS))); c.addHandlerLast(new ReadTimeoutHandler(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)));
@ -143,6 +149,18 @@ public class TbHttpClient {
} }
} }
private int getPoolMaxConnections() {
String poolMaxConnectionsEnv = System.getenv("TB_RE_HTTP_CLIENT_POOL_MAX_CONNECTIONS");
int poolMaxConnections;
if (poolMaxConnectionsEnv != null) {
poolMaxConnections = Integer.parseInt(poolMaxConnectionsEnv);
} else {
poolMaxConnections = ConnectionProvider.DEFAULT_POOL_MAX_CONNECTIONS;
}
return poolMaxConnections;
}
private void validateMaxInMemoryBufferSize(TbRestApiCallNodeConfiguration config) throws TbNodeException { private void validateMaxInMemoryBufferSize(TbRestApiCallNodeConfiguration config) throws TbNodeException {
int systemMaxInMemoryBufferSizeInKb = 25000; int systemMaxInMemoryBufferSizeInKb = 25000;
try { try {

343
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/delay/TbMsgDelayNodeTest.java

@ -0,0 +1,343 @@
/**
* 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.
*/
package org.thingsboard.rule.engine.delay;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.spy;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willAnswer;
@ExtendWith(MockitoExtension.class)
public class TbMsgDelayNodeTest extends AbstractRuleNodeUpgradeTest {
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("20107cf0-1c5e-4ac4-8131-7c466c955a7c"));
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("1be24225-b669-4b26-ab7e-083aaa82d0a0"));
private final Set<TimeUnit> supportedTimeUnits = EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS);
private final String supportedTimeUnitsStr = supportedTimeUnits.stream().map(TimeUnit::name).collect(Collectors.joining(", "));
private TbMsgDelayNode node;
private TbMsgDelayNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private RuleNode ruleNodeMock;
@BeforeEach
public void setUp() {
node = spy(new TbMsgDelayNode());
config = new TbMsgDelayNodeConfiguration().defaultConfiguration();
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getPeriod()).isEqualTo("60");
assertThat(config.getMaxPendingMsgs()).isEqualTo(1000);
assertThat(config.getTimeUnit()).isEqualTo(TimeUnit.SECONDS.name());
}
@Test
public void givenDefaultConfig_whenInit_thenOk() {
given(ctxMock.getSelf()).willReturn(ruleNodeMock);
assertThatNoException().isThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))));
}
@ParameterizedTest
@ValueSource(ints = {-1, 0, 5000000})
public void givenInvalidMaxPendingMsgsValue_whenInit_thenThrowsException(int maxPendingMsgs) {
config.setMaxPendingMsgs(maxPendingMsgs);
verifyValidationExceptionOnInit();
}
@Test
public void givenPeriodIsNull_whenInit_thenThrowsException() {
config.setPeriod(null);
verifyValidationExceptionOnInit();
}
@Test
public void givenTimeUnitIsNull_whenInit_thenThrowsException() {
config.setTimeUnit(null);
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@MethodSource
public void givenPeriodValueAndPeriodTimeUnitPatterns_whenOnMsg_thenTellSelfTickMsgAndEnqueueForTellNext(
String periodPattern, String timeUnitPattern, TbMsgMetaData metaData, String data, long expectedDelay) throws TbNodeException {
config.setPeriod(periodPattern);
config.setTimeUnit(timeUnitPattern);
given(ctxMock.getSelf()).willReturn(ruleNodeMock);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, metaData, data);
var tickMsg = TbMsg.newMsg(TbMsgType.DELAY_TIMEOUT_SELF_MSG, RULE_NODE_ID, TbMsgMetaData.EMPTY, msg.getId().toString());
given(ctxMock.newMsg(any(), any(TbMsgType.class), any(), any(), any(), any())).willReturn(tickMsg);
given(ctxMock.getSelfId()).willReturn(RULE_NODE_ID);
willAnswer(invocation -> {
node.onMsg(ctxMock, invocation.getArgument(0));
return null;
}).given(ctxMock).tellSelf(any(TbMsg.class), any(Long.class));
node.onMsg(ctxMock, msg);
then(ctxMock).should().tellSelf(tickMsg, expectedDelay);
then(ctxMock).should().ack(msg);
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().enqueueForTellNext(actualMsg.capture(), eq(TbNodeConnectionType.SUCCESS));
assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("id", "ts").isEqualTo(msg);
}
private static Stream<Arguments> givenPeriodValueAndPeriodTimeUnitPatterns_whenOnMsg_thenTellSelfTickMsgAndEnqueueForTellNext() {
return Stream.of(
Arguments.of("1", "HOURS", TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, TimeUnit.HOURS.toMillis(1L)),
Arguments.of("${md-period}", "${md-time-unit}",
new TbMsgMetaData(Map.of(
"md-period", "5",
"md-time-unit", "MINUTES"
)), TbMsg.EMPTY_JSON_OBJECT, TimeUnit.MINUTES.toMillis(5L)),
Arguments.of("$[msg-period]", "$[msg-time-unit]", TbMsgMetaData.EMPTY,
"{\"msg-period\":10,\"msg-time-unit\":\"SECONDS\"}", TimeUnit.SECONDS.toMillis(10L))
);
}
@ParameterizedTest
@EnumSource(TimeUnit.class)
public void givenTimeUnit_whenOnMsg_thenVerify(TimeUnit timeUnit) throws TbNodeException {
config.setTimeUnit(timeUnit.name());
given(ctxMock.getSelf()).willReturn(ruleNodeMock);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
if (supportedTimeUnits.contains(timeUnit)) {
assertThatNoException().isThrownBy(() -> node.onMsg(ctxMock, msg));
} else {
assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(RuntimeException.class)
.hasMessage("Time unit '" + timeUnit + "' is not supported! Only " + supportedTimeUnitsStr + " are supported.");
}
}
@Test
public void givenPeriodIsUnparsable_whenOnMsg_thenThrowsException() throws TbNodeException {
config.setPeriod("five");
given(ctxMock.getSelf()).willReturn(ruleNodeMock);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(NumberFormatException.class)
.hasMessage("Can't parse period value : five");
}
@Test
public void givenInvalidTimeUnit_whenOnMsg_thenThrowsException() throws TbNodeException {
config.setTimeUnit("sec");
given(ctxMock.getSelf()).willReturn(ruleNodeMock);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid value for period time unit : sec");
}
@Test
public void givenMaxLimitOfPendingMsgsReached_whenOnMsg_thenTellFailure() throws TbNodeException {
config.setMaxPendingMsgs(1);
given(ctxMock.getSelf()).willReturn(ruleNodeMock);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
for (int i = 0; i < 2; i++) {
node.onMsg(ctxMock, msg);
}
ArgumentCaptor<Throwable> throwable = ArgumentCaptor.forClass(Throwable.class);
then(ctxMock).should().tellFailure(eq(msg), throwable.capture());
assertThat(throwable.getValue()).isInstanceOf(RuntimeException.class).hasMessage("Max limit of pending messages reached!");
}
@Test
public void verifyDestroyMethod() {
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
var pendingMsgs = new ConcurrentHashMap<>();
pendingMsgs.put(UUID.fromString("321f0301-9bed-4e7d-b92f-a978f53ec5d6"), msg);
ReflectionTestUtils.setField(node, "pendingMsgs", pendingMsgs);
var actualPendingMsgs = (Map<UUID, TbMsg>) ReflectionTestUtils.getField(node, "pendingMsgs");
assertThat(actualPendingMsgs).isEqualTo(pendingMsgs);
node.destroy();
assertThat(actualPendingMsgs).isEmpty();
}
private void verifyValidationExceptionOnInit() {
RuleNode ruleNode = new RuleNode();
ruleNode.setName("test");
given(ctxMock.getSelf()).willReturn(ruleNode);
String errorPrefix = "'test' node configuration is invalid: ";
assertThatThrownBy(() -> node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessageContaining(errorPrefix)
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// config for version 1 with upgrade from version 0 (useMetadataPeriodInSecondsPatterns does not exist and periodInSeconds exists)
Arguments.of(0,
"""
{
"periodInSeconds": 13,
"maxPendingMsgs": 1000,
"periodInSecondsPattern": "17"
}
""",
true,
"""
{
"period": "13",
"timeUnit": "SECONDS",
"maxPendingMsgs": 1000
}
"""
),
// config for version 1 with upgrade from version 0 (useMetadataPeriodInSecondsPatterns and periodInSeconds do not exist)
Arguments.of(0,
"""
{
"maxPendingMsgs": 1000,
"periodInSecondsPattern": "17"
}
""",
true,
"""
{
"period": "60",
"timeUnit": "SECONDS",
"maxPendingMsgs": 1000
}
"""
),
// config for version 1 with upgrade from version 0 (useMetadataPeriodInSecondsPatterns is false)
Arguments.of(0,
"""
{
"periodInSeconds": 60,
"maxPendingMsgs": 1000,
"periodInSecondsPattern": null,
"useMetadataPeriodInSecondsPatterns": false
}
""",
true,
"""
{
"period": "60",
"timeUnit": "SECONDS",
"maxPendingMsgs": 1000
}
"""
),
// config for version 1 with upgrade from version 0 (useMetadataPeriodInSecondsPattern is true)
Arguments.of(0,
"""
{
"periodInSeconds": 60,
"maxPendingMsgs": 1000,
"periodInSecondsPattern": "${period-pattern}",
"useMetadataPeriodInSecondsPatterns": true
}
""",
true,
"""
{
"period": "${period-pattern}",
"timeUnit": "SECONDS",
"maxPendingMsgs": 1000
}
"""
),
// config for version 1 with upgrade from version 0 (hasChanges is false)
Arguments.of(0,
"""
{
"period": "${period-pattern}",
"timeUnit": "SECONDS",
"maxPendingMsgs": 1000
}
""",
false,
"""
{
"period": "${period-pattern}",
"timeUnit": "SECONDS",
"maxPendingMsgs": 1000
}
"""
)
);
}
@Override
protected TbNode getTestNode() {
return node;
}
}

2
ui-ngx/src/app/core/http/ota-package.service.ts

@ -59,7 +59,7 @@ export class OtaPackageService {
} }
public getOtaPackage(otaPackageId: string, config?: RequestConfig): Observable<OtaPackage> { public getOtaPackage(otaPackageId: string, config?: RequestConfig): Observable<OtaPackage> {
return this.http.get<OtaPackage>(`/api/otaPackages/${otaPackageId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<OtaPackage>(`/api/otaPackage/${otaPackageId}`, defaultHttpOptionsFromConfig(config));
} }
public getOtaPackageInfo(otaPackageId: string, config?: RequestConfig): Observable<OtaPackageInfo> { public getOtaPackageInfo(otaPackageId: string, config?: RequestConfig): Observable<OtaPackageInfo> {

2
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -349,7 +349,7 @@ export class DashboardUtilsService {
const config = widget.config; const config = widget.config;
config.showTitle = false; config.showTitle = false;
config.dropShadow = false; config.dropShadow = false;
config.resizable = !isScada; config.resizable = true;
config.preserveAspectRatio = isScada; config.preserveAspectRatio = isScada;
config.padding = '0'; config.padding = '0';
config.margin = '0'; config.margin = '0';

25
ui-ngx/src/app/core/services/time.service.ts

@ -19,13 +19,18 @@ import {
AggregationType, AggregationType,
DAY, DAY,
defaultTimeIntervals, defaultTimeIntervals,
defaultTimewindow, Interval, IntervalMath, defaultTimewindow,
getDefaultTimezoneInfo,
Interval,
IntervalMath,
SECOND, SECOND,
TimeInterval, TimeInterval,
Timewindow Timewindow,
TimezoneInfo
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { isDefined } from '@core/utils'; import { deepClone, isDefined } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
const MIN_INTERVAL = SECOND; const MIN_INTERVAL = SECOND;
const MAX_INTERVAL = 365 * 20 * DAY; const MAX_INTERVAL = 365 * 20 * DAY;
@ -41,8 +46,11 @@ export class TimeService {
private maxDatapointsLimit = MAX_DATAPOINTS_LIMIT; private maxDatapointsLimit = MAX_DATAPOINTS_LIMIT;
private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
constructor( constructor(
private http: HttpClient private http: HttpClient,
private translate: TranslateService
) {} ) {}
public setMaxDatapointsLimit(limit: number) { public setMaxDatapointsLimit(limit: number) {
@ -161,4 +169,13 @@ export class TimeService {
return defValue; return defValue;
} }
} }
public getLocalBrowserTimezoneInfoPlaceholder(): TimezoneInfo {
if (!this.localBrowserTimezoneInfoPlaceholder) {
this.localBrowserTimezoneInfoPlaceholder = deepClone(getDefaultTimezoneInfo());
this.localBrowserTimezoneInfoPlaceholder.id = null;
this.localBrowserTimezoneInfoPlaceholder.name = this.translate.instant('timezone.browser-time');
}
return this.localBrowserTimezoneInfoPlaceholder;
}
} }

6
ui-ngx/src/app/modules/common/modules-map.ts

@ -335,6 +335,9 @@ import * as AssetProfileAutocompleteComponent from '@home/components/profile/ass
import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component'; import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component';
import { IModulesMap } from '@modules/common/modules-map.models'; import { IModulesMap } from '@modules/common/modules-map.models';
import { TimezoneComponent } from '@shared/components/time/timezone.component';
import { TimezonePanelComponent } from '@shared/components/time/timezone-panel.component';
import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component';
declare const System; declare const System;
@ -466,6 +469,9 @@ class ModulesMap implements IModulesMap {
'@shared/components/time/datetime-period.component': DatetimePeriodComponent, '@shared/components/time/datetime-period.component': DatetimePeriodComponent,
'@shared/components/time/datetime.component': DatetimeComponent, '@shared/components/time/datetime.component': DatetimeComponent,
'@shared/components/time/timezone-select.component': TimezoneSelectComponent, '@shared/components/time/timezone-select.component': TimezoneSelectComponent,
'@shared/components/time/timezone.component': TimezoneComponent,
'@shared/components/time/timezone-panel.component': TimezonePanelComponent,
'@shared/components/time/datapoints-limit': DatapointsLimitComponent,
'@shared/components/value-input.component': ValueInputComponent, '@shared/components/value-input.component': ValueInputComponent,
'@shared/components/dashboard-autocomplete.component': DashboardAutocompleteComponent, '@shared/components/dashboard-autocomplete.component': DashboardAutocompleteComponent,
'@shared/components/entity/entity-subtype-autocomplete.component': EntitySubTypeAutocompleteComponent, '@shared/components/entity/entity-subtype-autocomplete.component': EntitySubTypeAutocompleteComponent,

6
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -437,15 +437,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
).pipe( ).pipe(
map(value => this.parseBreakpointsResponse(value.breakpoints)), map(value => this.parseBreakpointsResponse(value.breakpoints)),
tap((value) => { tap((value) => {
this.dashboardCtx.breakpoint = value.id; this.dashboardCtx.breakpoint = value ? value.id : 'default';
this.changeMobileSize.next(this.isMobileSize(value)); this.changeMobileSize.next(value ? this.isMobileSize(value) : false);
}), }),
distinctUntilChanged((_, next) => { distinctUntilChanged((_, next) => {
if (this.layouts.right.show || this.isEdit) { if (this.layouts.right.show || this.isEdit) {
return true; return true;
} }
let nextBreakpointConfiguration: BreakpointId = 'default'; let nextBreakpointConfiguration: BreakpointId = 'default';
if (!!this.layouts.main.layoutCtx.layoutData?.[next.id]) { if (next && !!this.layouts.main.layoutCtx.layoutData?.[next.id]) {
nextBreakpointConfiguration = next.id; nextBreakpointConfiguration = next.id;
} }
return this.layouts.main.layoutCtx.breakpoint === nextBreakpointConfiguration; return this.layouts.main.layoutCtx.breakpoint === nextBreakpointConfiguration;

72
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract.ts

@ -0,0 +1,72 @@
///
/// 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 { Directive, inject, Input, OnDestroy, TemplateRef } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, ValidationErrors, Validator } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Directive()
export abstract class GatewayConnectorBasicConfigDirective<InputBasicConfig, OutputBasicConfig>
implements ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>;
basicFormGroup: FormGroup;
protected fb = inject(FormBuilder);
protected onChange!: (value: OutputBasicConfig) => void;
protected onTouched!: () => void;
protected destroy$ = new Subject<void>();
constructor() {
this.basicFormGroup = this.initBasicFormGroup();
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value) => this.onBasicFormGroupChange(value));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { basicFormGroup: { valid: false } };
}
registerOnChange(fn: (value: OutputBasicConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(config: OutputBasicConfig): void {
this.basicFormGroup.setValue(this.mapConfigToFormValue(config), { emitEvent: false });
}
protected onBasicFormGroupChange(value: InputBasicConfig): void {
this.onChange(this.getMappedValue(value));
this.onTouched();
}
protected abstract mapConfigToFormValue(config: OutputBasicConfig): InputBasicConfig;
protected abstract getMappedValue(config: InputBasicConfig): OutputBasicConfig;
protected abstract initBasicFormGroup(): FormGroup;
}

61
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts

@ -0,0 +1,61 @@
///
/// 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 { GatewayConnector, GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isNumber, isString } from '@core/utils';
export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
gatewayVersion: number;
configVersion: number;
protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) {
this.gatewayVersion = this.parseVersion(this.gatewayVersionIn);
this.configVersion = this.parseVersion(connector.configVersion);
}
getProcessedByVersion(): GatewayConnector<BasicConfig> {
if (this.isVersionUpdateNeeded()) {
return this.isVersionUpgradeNeeded()
? this.getUpgradedVersion()
: this.getDowngradedVersion();
}
return this.connector;
}
private isVersionUpdateNeeded(): boolean {
if (!this.gatewayVersion) {
return false;
}
return this.configVersion !== this.gatewayVersion;
}
private isVersionUpgradeNeeded(): boolean {
return this.gatewayVersionIn === GatewayVersion.Current && (!this.configVersion || this.configVersion < this.gatewayVersion);
}
private parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;
}
return isString(version) ? parseFloat((version as string).replace(/\./g, '').slice(0, 3)) / 100 : 0;
}
protected abstract getDowngradedVersion(): GatewayConnector<BasicConfig>;
protected abstract getUpgradedVersion(): GatewayConnector<BasicConfig>;
}

67
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract.ts

@ -0,0 +1,67 @@
///
/// 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 {
GatewayConnector,
ModbusBasicConfig,
ModbusBasicConfig_v3_5_2,
ModbusLegacyBasicConfig,
ModbusLegacySlave,
ModbusSlave,
} from '../gateway-widget.models';
import { GatewayConnectorVersionProcessor } from './gateway-connector-version-processor.abstract';
import { ModbusVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/modbus-version-mapping.util';
export class ModbusVersionProcessor extends GatewayConnectorVersionProcessor<any> {
constructor(
protected gatewayVersionIn: string,
protected connector: GatewayConnector<ModbusBasicConfig>
) {
super(gatewayVersionIn, connector);
}
getUpgradedVersion(): GatewayConnector<ModbusBasicConfig_v3_5_2> {
const configurationJson = this.connector.configurationJson;
return {
...this.connector,
configurationJson: {
master: configurationJson.master?.slaves
? ModbusVersionMappingUtil.mapMasterToUpgradedVersion(configurationJson.master)
: { slaves: [] },
slave: configurationJson.slave
? ModbusVersionMappingUtil.mapSlaveToUpgradedVersion(configurationJson.slave as ModbusLegacySlave)
: {} as ModbusSlave,
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<ModbusBasicConfig_v3_5_2>;
}
getDowngradedVersion(): GatewayConnector<ModbusLegacyBasicConfig> {
const configurationJson = this.connector.configurationJson;
return {
...this.connector,
configurationJson: {
...configurationJson,
slave: configurationJson.slave
? ModbusVersionMappingUtil.mapSlaveToDowngradedVersion(configurationJson.slave as ModbusSlave)
: {} as ModbusLegacySlave,
master: configurationJson.master,
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<ModbusLegacyBasicConfig>;
}
}

101
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract.ts

@ -0,0 +1,101 @@
///
/// 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 { isEqual } from '@core/utils';
import {
GatewayConnector,
MQTTBasicConfig,
MQTTBasicConfig_v3_5_2,
MQTTLegacyBasicConfig,
RequestMappingData,
RequestType,
} from '../gateway-widget.models';
import { MqttVersionMappingUtil } from '../utils/mqtt-version-mapping.util';
import { GatewayConnectorVersionProcessor } from './gateway-connector-version-processor.abstract';
export class MqttVersionProcessor extends GatewayConnectorVersionProcessor<MQTTBasicConfig> {
private readonly mqttRequestTypeKeys = Object.values(RequestType);
constructor(
protected gatewayVersionIn: string,
protected connector: GatewayConnector<MQTTBasicConfig>
) {
super(gatewayVersionIn, connector);
}
getUpgradedVersion(): GatewayConnector<MQTTBasicConfig_v3_5_2> {
const {
connectRequests,
disconnectRequests,
attributeRequests,
attributeUpdates,
serverSideRpc
} = this.connector.configurationJson as MQTTLegacyBasicConfig;
let configurationJson = {
...this.connector.configurationJson,
requestsMapping: MqttVersionMappingUtil.mapRequestsToUpgradedVersion({
connectRequests,
disconnectRequests,
attributeRequests,
attributeUpdates,
serverSideRpc
}),
mapping: MqttVersionMappingUtil.mapMappingToUpgradedVersion((this.connector.configurationJson as MQTTLegacyBasicConfig).mapping),
};
this.mqttRequestTypeKeys.forEach((key: RequestType) => {
const { [key]: removedValue, ...rest } = configurationJson as MQTTLegacyBasicConfig;
configurationJson = { ...rest } as any;
});
this.cleanUpConfigJson(configurationJson as MQTTBasicConfig_v3_5_2);
return {
...this.connector,
configurationJson,
configVersion: this.gatewayVersionIn
} as GatewayConnector<MQTTBasicConfig_v3_5_2>;
}
getDowngradedVersion(): GatewayConnector<MQTTLegacyBasicConfig> {
const { requestsMapping, mapping, ...restConfig } = this.connector.configurationJson as MQTTBasicConfig_v3_5_2;
const updatedRequestsMapping =
MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>);
const updatedMapping = MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping);
return {
...this.connector,
configurationJson: {
...restConfig,
...updatedRequestsMapping,
mapping: updatedMapping,
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<MQTTLegacyBasicConfig>;
}
private cleanUpConfigJson(configurationJson: MQTTBasicConfig_v3_5_2): void {
if (isEqual(configurationJson.requestsMapping, {})) {
delete configurationJson.requestsMapping;
}
if (isEqual(configurationJson.mapping, [])) {
delete configurationJson.mapping;
}
}
}

56
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/opc-version-processor.abstract.ts

@ -0,0 +1,56 @@
///
/// 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 {
GatewayConnector, LegacyServerConfig,
OPCBasicConfig,
OPCBasicConfig_v3_5_2,
OPCLegacyBasicConfig,
} from '../gateway-widget.models';
import { GatewayConnectorVersionProcessor } from './gateway-connector-version-processor.abstract';
import { OpcVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/opc-version-mapping.util';
export class OpcVersionProcessor extends GatewayConnectorVersionProcessor<OPCBasicConfig> {
constructor(
protected gatewayVersionIn: string,
protected connector: GatewayConnector<OPCBasicConfig>
) {
super(gatewayVersionIn, connector);
}
getUpgradedVersion(): GatewayConnector<OPCBasicConfig_v3_5_2> {
const server = this.connector.configurationJson.server as LegacyServerConfig;
return {
...this.connector,
configurationJson: {
server: server ? OpcVersionMappingUtil.mapServerToUpgradedVersion(server) : {},
mapping: server.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(server.mapping) : [],
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<OPCBasicConfig_v3_5_2>;
}
getDowngradedVersion(): GatewayConnector<OPCLegacyBasicConfig> {
return {
...this.connector,
configurationJson: {
server: OpcVersionMappingUtil.mapServerToDowngradedVersion(this.connector.configurationJson as OPCBasicConfig_v3_5_2)
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<OPCLegacyBasicConfig>;
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts

@ -16,7 +16,9 @@
import { import {
ConfigurationModes, ConfigurationModes,
GatewayConnector, LocalLogsConfigs, LogSavingPeriod, GatewayConnector,
LocalLogsConfigs,
LogSavingPeriod,
SecurityTypes, SecurityTypes,
StorageTypes StorageTypes
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts

@ -42,8 +42,8 @@ import {
import { import {
DeviceInfoType, DeviceInfoType,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
OPCUaSourceTypes, OPCUaSourceType,
SourceTypes, SourceType,
SourceTypeTranslationsMap SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@ -81,7 +81,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
required = false; required = false;
@Input() @Input()
sourceTypes: Array<SourceTypes | OPCUaSourceTypes> = Object.values(SourceTypes); sourceTypes: Array<SourceType | OPCUaSourceType> = Object.values(SourceType);
deviceInfoTypeValue: any; deviceInfoTypeValue: any;

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.ts

@ -42,7 +42,7 @@ import {
MappingValueType, MappingValueType,
mappingValueTypesMap, mappingValueTypesMap,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
OPCUaSourceTypes, OPCUaSourceType,
RpcMethodsMapping, RpcMethodsMapping,
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -73,7 +73,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
keysType: MappingKeysType; keysType: MappingKeysType;
@Input() @Input()
valueTypeKeys: Array<MappingValueType | OPCUaSourceTypes> = Object.values(MappingValueType); valueTypeKeys: Array<MappingValueType | OPCUaSourceType> = Object.values(MappingValueType);
@Input() @Input()
valueTypeEnum = MappingValueType; valueTypeEnum = MappingValueType;

23
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts

@ -40,17 +40,21 @@ import {
Validator, Validator,
} from '@angular/forms'; } from '@angular/forms';
import { import {
AttributeUpdate,
ConnectorMapping, ConnectorMapping,
ConnectRequest,
ConverterConnectorMapping, ConverterConnectorMapping,
ConvertorTypeTranslationsMap, ConvertorTypeTranslationsMap,
DeviceConnectorMapping, DeviceConnectorMapping,
DisconnectRequest,
MappingInfo, MappingInfo,
MappingType, MappingType,
MappingTypeTranslationsMap, MappingTypeTranslationsMap,
MappingValue, MappingValue,
RequestMappingData, RequestMappingValue,
RequestType, RequestType,
RequestTypesTranslationsMap RequestTypesTranslationsMap,
ServerSideRpc
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
@ -259,16 +263,17 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
}; };
case MappingType.REQUESTS: case MappingType.REQUESTS:
let details: string; let details: string;
if ((value as RequestMappingData).requestType === RequestType.ATTRIBUTE_UPDATE) { const requestValue = value as RequestMappingValue;
details = (value as RequestMappingData).requestValue.attributeFilter; if (requestValue.requestType === RequestType.ATTRIBUTE_UPDATE) {
} else if ((value as RequestMappingData).requestType === RequestType.SERVER_SIDE_RPC) { details = (requestValue.requestValue as AttributeUpdate).attributeFilter;
details = (value as RequestMappingData).requestValue.methodFilter; } else if (requestValue.requestType === RequestType.SERVER_SIDE_RPC) {
details = (requestValue.requestValue as ServerSideRpc).methodFilter;
} else { } else {
details = (value as RequestMappingData).requestValue.topicFilter; details = (requestValue.requestValue as ConnectRequest | DisconnectRequest).topicFilter;
} }
return { return {
requestType: (value as RequestMappingData).requestType, requestType: (value as RequestMappingValue).requestType,
type: this.translate.instant(RequestTypesTranslationsMap.get((value as RequestMappingData).requestType)), type: this.translate.instant(RequestTypesTranslationsMap.get((value as RequestMappingValue).requestType)),
details details
}; };
case MappingType.OPCUA: case MappingType.OPCUA:

76
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.abstract.ts

@ -0,0 +1,76 @@
///
/// 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 { Directive } from '@angular/core';
import { FormControl, FormGroup, ValidationErrors } from '@angular/forms';
import { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
import { GatewayConnectorBasicConfigDirective } from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
import {
ModbusBasicConfig,
ModbusBasicConfig_v3_5_2,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Directive()
export abstract class ModbusBasicConfigDirective<BasicConfig>
extends GatewayConnectorBasicConfigDirective<ModbusBasicConfig_v3_5_2, BasicConfig> {
enableSlaveControl: FormControl<boolean> = new FormControl(false);
constructor() {
super();
this.enableSlaveControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(enable => {
this.updateSlaveEnabling(enable);
this.basicFormGroup.get('slave').updateValueAndValidity({ emitEvent: !!this.onChange });
});
}
override writeValue(basicConfig: BasicConfig & ModbusBasicConfig): void {
super.writeValue(basicConfig);
this.onEnableSlaveControl(basicConfig);
}
override validate(): ValidationErrors | null {
const { master, slave } = this.basicFormGroup.value;
const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave);
if (!this.basicFormGroup.valid || isEmpty) {
return { basicFormGroup: { valid: false } };
}
return null;
}
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
master: [],
slave: [],
});
}
private updateSlaveEnabling(isEnabled: boolean): void {
if (isEnabled) {
this.basicFormGroup.get('slave').enable({ emitEvent: false });
} else {
this.basicFormGroup.get('slave').disable({ emitEvent: false });
}
}
private onEnableSlaveControl(basicConfig: ModbusBasicConfig): void {
this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {}));
}
}

110
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts

@ -14,28 +14,21 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, TemplateRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import {
ControlValueAccessor, ModbusBasicConfig_v3_5_2,
FormBuilder, ModbusMasterConfig,
FormControl, ModbusSlave
FormGroup, } from '@home/components/widget/lib/gateway/gateway-widget.models';
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
ValidationErrors,
Validator,
} from '@angular/forms';
import { ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators'; import { SharedModule } from '@shared/shared.module';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
import { isEqual } from '@core/utils'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import {
ModbusBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.abstract';
@Component({ @Component({
selector: 'tb-modbus-basic-config', selector: 'tb-modbus-basic-config',
@ -63,80 +56,19 @@ import { isEqual } from '@core/utils';
], ],
styleUrls: ['./modbus-basic-config.component.scss'], styleUrls: ['./modbus-basic-config.component.scss'],
}) })
export class ModbusBasicConfigComponent extends ModbusBasicConfigDirective<ModbusBasicConfig_v3_5_2> {
export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy { protected override mapConfigToFormValue({ master, slave }: ModbusBasicConfig_v3_5_2): ModbusBasicConfig_v3_5_2 {
return {
@Input() generalTabContent: TemplateRef<any>; master: master?.slaves ? master : { slaves: [] } as ModbusMasterConfig,
slave: slave ?? {} as ModbusSlave,
basicFormGroup: FormGroup;
enableSlaveControl: FormControl<boolean>;
onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.basicFormGroup = this.fb.group({
master: [],
slave: [],
});
this.enableSlaveControl = new FormControl(false);
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(({ master, slave }) => {
this.onChange({ master, slave: slave ?? {} });
this.onTouched();
});
this.enableSlaveControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(enable => {
this.updateSlaveEnabling(enable);
this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange});
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusBasicConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: ModbusBasicConfig): void {
const editedBase = {
slave: basicConfig.slave ?? {},
master: basicConfig.master ?? {},
}; };
this.basicFormGroup.setValue(editedBase, {emitEvent: false});
this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {}));
}
validate(basicFormControl: UntypedFormControl): ValidationErrors | null {
const { master, slave } = basicFormControl.value;
const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave);
if (!this.basicFormGroup.valid || isEmpty) {
return {
basicFormGroup: {valid: false}
};
}
return null;
} }
private updateSlaveEnabling(isEnabled: boolean): void { protected override getMappedValue(value: ModbusBasicConfig_v3_5_2): ModbusBasicConfig_v3_5_2 {
if (isEnabled) { return {
this.basicFormGroup.get('slave').enable({emitEvent: false}); master: value.master,
} else { slave: value.slave,
this.basicFormGroup.get('slave').disable({emitEvent: false}); };
}
} }
} }

78
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-legacy-basic-config.component.ts

@ -0,0 +1,78 @@
///
/// 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 { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
ModbusBasicConfig_v3_5_2,
ModbusLegacyBasicConfig, ModbusLegacySlave,
ModbusMasterConfig,
ModbusSlave
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/modbus-version-mapping.util';
import {
ModbusBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.abstract';
@Component({
selector: 'tb-modbus-legacy-basic-config',
templateUrl: './modbus-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusLegacyBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusLegacyBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusSlaveConfigComponent,
ModbusMasterTableComponent,
EllipsisChipListDirective,
],
styleUrls: ['./modbus-basic-config.component.scss'],
})
export class ModbusLegacyBasicConfigComponent extends ModbusBasicConfigDirective<ModbusLegacyBasicConfig> {
protected override mapConfigToFormValue(config: ModbusLegacyBasicConfig): ModbusBasicConfig_v3_5_2 {
return {
master: config.master?.slaves
? ModbusVersionMappingUtil.mapMasterToUpgradedVersion(config.master)
: { slaves: [] } as ModbusMasterConfig,
slave: config.slave ? ModbusVersionMappingUtil.mapSlaveToUpgradedVersion(config.slave) : {} as ModbusSlave,
};
}
protected override getMappedValue(value: ModbusBasicConfig_v3_5_2): ModbusLegacyBasicConfig {
return {
master: value.master,
slave: value.slave ? ModbusVersionMappingUtil.mapSlaveToDowngradedVersion(value.slave) : {} as ModbusLegacySlave,
};
}
}

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -31,9 +31,15 @@
</div> </div>
<ng-template #tagName> <ng-template #tagName>
<div class="tb-flex"> <div class="tb-flex">
<div class="title-container" tbTruncateWithTooltip>{{ 'gateway.key' | translate }}: {{ keyControl.get('tag').value }}</div> <div class="title-container tb-flex">{{ 'gateway.key' | translate }}:
<div class="title-container">{{ 'gateway.address' | translate }}: {{ keyControl.get('address').value }}</div> <span class="key-label" tbTruncateWithTooltip>{{ keyControl.get('tag').value }}</span>
<div class="title-container">{{ 'gateway.type' | translate }}: {{ keyControl.get('type').value }}</div> </div>
<div class="title-container">{{ 'gateway.address' | translate }}:
<span class="key-label">{{ keyControl.get('address').value }}</span>
</div>
<div class="title-container">{{ 'gateway.type' | translate }}:
<span class="key-label">{{ keyControl.get('type').value }}</span>
</div>
</div> </div>
</ng-template> </ng-template>
</mat-panel-title> </mat-panel-title>

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss

@ -23,6 +23,10 @@
width: 180px; width: 180px;
} }
.key-label {
font-weight: 400;
}
.key-panel { .key-panel {
height: 500px; height: 500px;
overflow: auto; overflow: auto;

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html

@ -197,6 +197,16 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.word-order' | translate }}" translate>gateway.word-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="wordOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle"> <div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value"> <mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -113,6 +113,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
pollPeriod: [5000, [Validators.required]], pollPeriod: [5000, [Validators.required]],
sendDataToThingsBoard: [false], sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG], byteOrder:[ModbusOrderType.BIG],
wordOrder: [ModbusOrderType.BIG],
security: [], security: [],
identity: this.fb.group({ identity: this.fb.group({
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
@ -243,6 +244,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
pollPeriod = 5000, pollPeriod = 5000,
sendDataToThingsBoard = false, sendDataToThingsBoard = false,
byteOrder = ModbusOrderType.BIG, byteOrder = ModbusOrderType.BIG,
wordOrder = ModbusOrderType.BIG,
security = {}, security = {},
identity = { identity = {
vendorName: '', vendorName: '',
@ -266,6 +268,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
pollPeriod, pollPeriod,
sendDataToThingsBoard: !!sendDataToThingsBoard, sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder, byteOrder,
wordOrder,
security, security,
identity, identity,
values, values,

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -260,7 +260,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.word-order' | translate }}" translate>gateway.word-order</div> <div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.word-order' | translate }}" translate>gateway.word-order</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -101,7 +101,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
helpBaseUrl + '/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters'; helpBaseUrl + '/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters';
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict']; private readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host', 'wordOrder']; private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();

8
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html

@ -48,6 +48,8 @@
mat-icon-button mat-icon-button
color="primary" color="primary"
[disabled]="disabled" [disabled]="disabled"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesButton #attributesButton
(click)="manageKeys($event, attributesButton, ModbusValueKey.ATTRIBUTES, register)"> (click)="manageKeys($event, attributesButton, ModbusValueKey.ATTRIBUTES, register)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -69,6 +71,8 @@
mat-icon-button mat-icon-button
color="primary" color="primary"
[disabled]="disabled" [disabled]="disabled"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#telemetryButton #telemetryButton
(click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)"> (click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -90,6 +94,8 @@
mat-icon-button mat-icon-button
[disabled]="disabled" [disabled]="disabled"
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesUpdatesButton #attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, ModbusValueKey.ATTRIBUTES_UPDATES, register)"> (click)="manageKeys($event, attributesUpdatesButton, ModbusValueKey.ATTRIBUTES_UPDATES, register)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -111,6 +117,8 @@
mat-icon-button mat-icon-button
color="primary" color="primary"
[disabled]="disabled" [disabled]="disabled"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#rpcRequestsButton #rpcRequestsButton
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)"> (click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>

191
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts

@ -1,191 +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 { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, TemplateRef } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
MappingType,
MQTTBasicConfig,
RequestMappingData,
RequestType,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { isObject } from 'lodash';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-mqtt-basic-config',
templateUrl: './mqtt-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
],
styleUrls: ['./mqtt-basic-config.component.scss']
})
export class MqttBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input()
generalTabContent: TemplateRef<any>;
mappingTypes = MappingType;
basicFormGroup: FormGroup;
private onChange: (value: MQTTBasicConfig) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.basicFormGroup = this.fb.group({
dataMapping: [],
requestsMapping: [],
broker: [],
workers: [],
});
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(this.getMappedMQTTConfig(value));
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: MQTTBasicConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: MQTTBasicConfig): void {
const { broker, dataMapping = [], requestsMapping } = basicConfig;
const editedBase = {
workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
} : {},
dataMapping: dataMapping || [],
broker: broker || {},
requestsMapping: Array.isArray(requestsMapping)
? requestsMapping
: this.getRequestDataArray(requestsMapping),
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});
}
private getMappedMQTTConfig(basicConfig: MQTTBasicConfig): MQTTBasicConfig {
let { broker, workers, dataMapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) {
broker = {
...broker,
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingData[]);
}
return { broker, workers, dataMapping, requestsMapping };
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
private getRequestDataArray(value: Record<RequestType, RequestMappingData[]>): RequestMappingData[] {
const mappingConfigs = [];
if (isObject(value)) {
Object.keys(value).forEach((configKey: string) => {
for (const mapping of value[configKey]) {
mappingConfigs.push({
requestType: configKey,
requestValue: mapping
});
}
});
}
return mappingConfigs;
}
private getRequestDataObject(array: RequestMappingData[]): Record<RequestType, RequestMappingData[]> {
return array.reduce((result, { requestType, requestValue }) => {
result[requestType].push(requestValue);
return result;
}, {
connectRequests: [],
disconnectRequests: [],
attributeRequests: [],
attributeUpdates: [],
serverSideRpc: [],
});
}
}

82
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract.ts

@ -0,0 +1,82 @@
///
/// 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 { Directive } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
MappingType,
MQTTBasicConfig, MQTTBasicConfig_v3_5_2,
RequestMappingData,
RequestMappingValue,
RequestType
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isObject } from '@core/utils';
import {
GatewayConnectorBasicConfigDirective
} from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
@Directive()
export abstract class MqttBasicConfigDirective<BasicConfig>
extends GatewayConnectorBasicConfigDirective<MQTTBasicConfig_v3_5_2, BasicConfig> {
MappingType = MappingType;
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
mapping: [],
requestsMapping: [],
broker: [],
workers: [],
});
}
protected getRequestDataArray(value: Record<RequestType, RequestMappingData[]>): RequestMappingData[] {
const mappingConfigs = [];
if (isObject(value)) {
Object.keys(value).forEach((configKey: string) => {
for (const mapping of value[configKey]) {
mappingConfigs.push({
requestType: configKey,
requestValue: mapping
});
}
});
}
return mappingConfigs;
}
protected getRequestDataObject(array: RequestMappingValue[]): Record<RequestType, RequestMappingValue[]> {
return array.reduce((result, { requestType, requestValue }) => {
result[requestType].push(requestValue);
return result;
}, {
connectRequests: [],
disconnectRequests: [],
attributeRequests: [],
attributeUpdates: [],
serverSideRpc: [],
});
}
writeValue(basicConfig: BasicConfig): void {
this.basicFormGroup.setValue(this.mapConfigToFormValue(basicConfig), { emitEvent: false });
}
protected abstract override mapConfigToFormValue(config: BasicConfig): MQTTBasicConfig_v3_5_2;
protected abstract override getMappedValue(config: MQTTBasicConfig): BasicConfig;
}

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.html

@ -24,12 +24,12 @@
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*"> <mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height"> <div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="dataMapping" [required]="true" [mappingType]="mappingTypes.DATA"></tb-mapping-table> <tb-mapping-table formControlName="mapping" [required]="true" [mappingType]="MappingType.DATA"></tb-mapping-table>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}"> <mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height"> <div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="requestsMapping" [mappingType]="mappingTypes.REQUESTS"></tb-mapping-table> <tb-mapping-table formControlName="requestsMapping" [mappingType]="MappingType.REQUESTS"></tb-mapping-table>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.workers-settings' | translate }}"> <mat-tab label="{{ 'gateway.workers-settings' | translate }}">

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.scss

103
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.ts

@ -0,0 +1,103 @@
///
/// 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, forwardRef, ChangeDetectionStrategy } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
import {
BrokerConfig,
MQTTBasicConfig_v3_5_2,
RequestMappingData,
RequestMappingValue,
RequestType, WorkersConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
MqttBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract';
import { isDefinedAndNotNull } from '@core/utils';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
@Component({
selector: 'tb-mqtt-basic-config',
templateUrl: './mqtt-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
}
],
styleUrls: ['./mqtt-basic-config.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
],
})
export class MqttBasicConfigComponent extends MqttBasicConfigDirective<MQTTBasicConfig_v3_5_2> {
protected override mapConfigToFormValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTBasicConfig_v3_5_2 {
const { broker, mapping = [], requestsMapping } = basicConfig;
return{
workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
} : {} as WorkersConfig,
mapping: mapping ?? [],
broker: broker ?? {} as BrokerConfig,
requestsMapping: this.getRequestDataArray(requestsMapping as Record<RequestType, RequestMappingData[]>),
};
}
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTBasicConfig_v3_5_2 {
let { broker, workers, mapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) {
broker = {
...broker,
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingValue[]);
}
return { broker, mapping, requestsMapping };
}
}

124
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-legacy-basic-config.component.ts

@ -0,0 +1,124 @@
///
/// 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, forwardRef, ChangeDetectionStrategy } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
import {
BrokerConfig,
MQTTBasicConfig_v3_5_2,
MQTTLegacyBasicConfig,
RequestMappingData,
RequestMappingValue,
RequestType, WorkersConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { MqttVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/mqtt-version-mapping.util';
import {
MqttBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract';
import { isDefinedAndNotNull } from '@core/utils';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
@Component({
selector: 'tb-mqtt-legacy-basic-config',
templateUrl: './mqtt-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttLegacyBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttLegacyBasicConfigComponent),
multi: true
}
],
styleUrls: ['./mqtt-basic-config.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
],
})
export class MqttLegacyBasicConfigComponent extends MqttBasicConfigDirective<MQTTLegacyBasicConfig> {
protected override mapConfigToFormValue(config: MQTTLegacyBasicConfig): MQTTBasicConfig_v3_5_2 {
const {
broker,
mapping = [],
connectRequests = [],
disconnectRequests = [],
attributeRequests = [],
attributeUpdates = [],
serverSideRpc = []
} = config as MQTTLegacyBasicConfig;
const updatedRequestMapping = MqttVersionMappingUtil.mapRequestsToUpgradedVersion({
connectRequests,
disconnectRequests,
attributeRequests,
attributeUpdates,
serverSideRpc
});
return {
workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
} : {} as WorkersConfig,
mapping: MqttVersionMappingUtil.mapMappingToUpgradedVersion(mapping) || [],
broker: broker || {} as BrokerConfig,
requestsMapping: this.getRequestDataArray(updatedRequestMapping),
};
}
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTLegacyBasicConfig {
let { broker, workers, mapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) {
broker = {
...broker,
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingValue[]);
}
return {
broker,
mapping: MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping),
...(MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>))
};
}
}

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component.html

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component.ts

@ -36,7 +36,7 @@ import { CommonModule } from '@angular/common';
import { generateSecret } from '@core/utils'; import { generateSecret } from '@core/utils';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { SecurityConfigComponent } from '../security-config/security-config.component'; import { SecurityConfigComponent } from '../../security-config/security-config.component';
@Component({ @Component({
selector: 'tb-broker-config-control', selector: 'tb-broker-config-control',

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component.html

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component.ts

134
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts

@ -1,134 +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 { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, TemplateRef } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ConnectorType,
MappingType,
OPCBasicConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component';
@Component({
selector: 'tb-opc-ua-basic-config',
templateUrl: './opc-ua-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
OpcServerConfigComponent,
],
styleUrls: ['./opc-ua-basic-config.component.scss']
})
export class OpcUaBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>;
mappingTypes = MappingType;
basicFormGroup: FormGroup;
onChange!: (value: string) => void;
onTouched!: () => void;
protected readonly connectorType = ConnectorType;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.basicFormGroup = this.fb.group({
mapping: [],
server: [],
});
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: OPCBasicConfig): void {
const editedBase = {
server: basicConfig.server || {},
mapping: basicConfig.mapping || [],
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.html

@ -84,7 +84,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div *ngIf="!hideNewFields" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.poll-period' | translate }}"> <div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.poll-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.poll-period' | translate }}</div> <div tbTruncateWithTooltip>{{ 'gateway.poll-period' | translate }}</div>
</div> </div>

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.scss

15
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -39,6 +39,7 @@ import {
SecurityConfigComponent SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import { HOUR } from '@shared/models/time/time.models'; import { HOUR } from '@shared/models/time/time.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-opc-server-config', selector: 'tb-opc-server-config',
@ -64,7 +65,11 @@ import { HOUR } from '@shared/models/time/time.models';
SecurityConfigComponent, SecurityConfigComponent,
] ]
}) })
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class OpcServerConfigComponent implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy {
@Input()
@coerceBoolean()
hideNewFields: boolean = false;
securityPolicyTypes = SecurityPolicyTypes; securityPolicyTypes = SecurityPolicyTypes;
serverConfigFormGroup: UntypedFormGroup; serverConfigFormGroup: UntypedFormGroup;
@ -95,6 +100,12 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
}); });
} }
ngAfterViewInit(): void {
if (this.hideNewFields) {
this.serverConfigFormGroup.get('pollPeriodInMillis').disable({emitEvent: false});
}
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.html

@ -20,7 +20,7 @@
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container> <ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.server' | translate }}*"> <mat-tab label="{{ 'gateway.server' | translate }}*">
<tb-opc-server-config formControlName="server"></tb-opc-server-config> <tb-opc-server-config formControlName="server" [hideNewFields]="isLegacy"></tb-opc-server-config>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*"> <mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height"> <div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.scss

88
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.ts

@ -0,0 +1,88 @@
///
/// 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 { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
MappingType,
OPCBasicConfig_v3_5_2,
ServerConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { MappingTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component';
import {
GatewayConnectorBasicConfigDirective
} from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
@Component({
selector: 'tb-opc-ua-basic-config',
templateUrl: './opc-ua-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
MappingTableComponent,
OpcServerConfigComponent,
],
styleUrls: ['./opc-ua-basic-config.component.scss']
})
export class OpcUaBasicConfigComponent extends GatewayConnectorBasicConfigDirective<OPCBasicConfig_v3_5_2, OPCBasicConfig_v3_5_2> {
mappingTypes = MappingType;
isLegacy = false;
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
mapping: [],
server: [],
});
}
protected override mapConfigToFormValue(config: OPCBasicConfig_v3_5_2): OPCBasicConfig_v3_5_2 {
return {
server: config.server ?? {} as ServerConfig,
mapping: config.mapping ?? [],
};
}
protected override getMappedValue(value: OPCBasicConfig_v3_5_2): OPCBasicConfig_v3_5_2 {
return {
server: value.server,
mapping: value.mapping,
};
}
}

88
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-legacy-basic-config.component.ts

@ -0,0 +1,88 @@
///
/// 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 { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
MappingType,
OPCBasicConfig_v3_5_2,
OPCLegacyBasicConfig, ServerConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { MappingTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component';
import {
GatewayConnectorBasicConfigDirective
} from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
import { OpcVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/opc-version-mapping.util';
@Component({
selector: 'tb-opc-ua-legacy-basic-config',
templateUrl: './opc-ua-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcUaLegacyBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcUaLegacyBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
MappingTableComponent,
OpcServerConfigComponent,
],
styleUrls: ['./opc-ua-basic-config.component.scss']
})
export class OpcUaLegacyBasicConfigComponent extends GatewayConnectorBasicConfigDirective<OPCBasicConfig_v3_5_2, OPCLegacyBasicConfig> {
mappingTypes = MappingType;
isLegacy = true;
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
mapping: [],
server: [],
});
}
protected override mapConfigToFormValue(config: OPCLegacyBasicConfig): OPCBasicConfig_v3_5_2 {
return {
server: config.server ? OpcVersionMappingUtil.mapServerToUpgradedVersion(config.server) : {} as ServerConfig,
mapping: config.server?.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(config.server.mapping) : [],
};
}
protected override getMappedValue(value: OPCBasicConfig_v3_5_2): OPCLegacyBasicConfig {
return {
server: OpcVersionMappingUtil.mapServerToDowngradedVersion(value),
};
}
}

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts

@ -26,12 +26,14 @@ import {
AddConnectorConfigData, AddConnectorConfigData,
ConnectorType, ConnectorType,
CreatedConnectorConfigData, CreatedConnectorConfigData,
GatewayConnector,
GatewayConnectorDefaultTypesTranslatesMap, GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel, GatewayLogLevel,
getDefaultConfig, GatewayVersion,
GatewayVersionedDefaultConfig,
noLeadTrailSpacesRegex noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from 'rxjs/operators'; import { takeUntil, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants'; import { helpBaseUrl } from '@shared/models/constants';
@ -42,7 +44,8 @@ import { helpBaseUrl } from '@shared/models/constants';
styleUrls: ['./add-connector-dialog.component.scss'], styleUrls: ['./add-connector-dialog.component.scss'],
providers: [], providers: [],
}) })
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnInit, OnDestroy { export class AddConnectorDialogComponent
extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnInit, OnDestroy {
connectorForm: UntypedFormGroup; connectorForm: UntypedFormGroup;
@ -95,8 +98,15 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
this.submitted = true; this.submitted = true;
const value = this.connectorForm.getRawValue(); const value = this.connectorForm.getRawValue();
if (value.useDefaults) { if (value.useDefaults) {
getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => { this.getDefaultConfig(value.type).subscribe((defaultConfig: GatewayVersionedDefaultConfig) => {
value.configurationJson = defaultConfig; const gatewayVersion = this.data.gatewayVersion;
if (gatewayVersion) {
value.configVersion = gatewayVersion;
}
value.configurationJson = (gatewayVersion === GatewayVersion.Current
? defaultConfig[this.data.gatewayVersion]
: defaultConfig.legacy)
?? defaultConfig;
if (this.connectorForm.valid) { if (this.connectorForm.valid) {
this.dialogRef.close(value); this.dialogRef.close(value);
} }
@ -130,4 +140,8 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
takeUntil(this.destroy$), takeUntil(this.destroy$),
).subscribe(); ).subscribe();
} }
private getDefaultConfig(type: string): Observable<GatewayVersionedDefaultConfig | GatewayConnector> {
return this.resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
};
} }

14
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html

@ -110,6 +110,8 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesButton #attributesButton
(click)="manageKeys($event, attributesButton, MappingKeysType.ATTRIBUTES)"> (click)="manageKeys($event, attributesButton, MappingKeysType.ATTRIBUTES)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -130,6 +132,8 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#telemetryButton #telemetryButton
(click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)"> (click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -175,6 +179,8 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#keysButton #keysButton
(click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)"> (click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -666,6 +672,8 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#opcAttributesButton #opcAttributesButton
(click)="manageKeys($event, opcAttributesButton, MappingKeysType.ATTRIBUTES)"> (click)="manageKeys($event, opcAttributesButton, MappingKeysType.ATTRIBUTES)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -686,6 +694,8 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#opcTelemetryButton #opcTelemetryButton
(click)="manageKeys($event, opcTelemetryButton, MappingKeysType.TIMESERIES)"> (click)="manageKeys($event, opcTelemetryButton, MappingKeysType.TIMESERIES)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -706,6 +716,8 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesUpdatesButton #attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, MappingKeysType.ATTRIBUTES_UPDATES)"> (click)="manageKeys($event, attributesUpdatesButton, MappingKeysType.ATTRIBUTES_UPDATES)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>
@ -726,6 +738,8 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
color="primary" color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#rpcMethodsButton #rpcMethodsButton
(click)="manageKeys($event, rpcMethodsButton, MappingKeysType.RPC_METHODS)"> (click)="manageKeys($event, rpcMethodsButton, MappingKeysType.RPC_METHODS)">
<tb-icon matButtonIcon>edit</tb-icon> <tb-icon matButtonIcon>edit</tb-icon>

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts

@ -43,15 +43,16 @@ import {
MappingType, MappingType,
MappingTypeTranslationsMap, MappingTypeTranslationsMap,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
OPCUaSourceTypes, OPCUaSourceType,
QualityTypes, QualityTypes,
QualityTypeTranslationsMap, QualityTypeTranslationsMap,
RequestMappingData,
RequestMappingFormValue, RequestMappingFormValue,
RequestType, RequestType,
RequestTypesTranslationsMap, RequestTypesTranslationsMap,
RpcMethod, RpcMethod,
ServerSideRPCType, ServerSideRPCType,
SourceTypes, SourceType,
SourceTypeTranslationsMap, SourceTypeTranslationsMap,
Timeseries Timeseries
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -82,10 +83,10 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
ConvertorTypeEnum = ConvertorType; ConvertorTypeEnum = ConvertorType;
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap; ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap;
sourceTypes: SourceTypes[] = Object.values(SourceTypes); sourceTypes: SourceType[] = Object.values(SourceType);
OPCUaSourceTypes = Object.values(OPCUaSourceTypes) as Array<OPCUaSourceTypes>; OPCUaSourceTypes = Object.values(OPCUaSourceType) as Array<OPCUaSourceType>;
OPCUaSourceTypesEnum = OPCUaSourceTypes; OPCUaSourceTypesEnum = OPCUaSourceType;
sourceTypesEnum = SourceTypes; sourceTypesEnum = SourceType;
SourceTypeTranslationsMap = SourceTypeTranslationsMap; SourceTypeTranslationsMap = SourceTypeTranslationsMap;
requestTypes: RequestType[] = Object.values(RequestType); requestTypes: RequestType[] = Object.values(RequestType);
@ -230,8 +231,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType) noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType)
}; };
if (this.data.mappingType === MappingType.OPCUA) { if (this.data.mappingType === MappingType.OPCUA) {
ctx.valueTypeKeys = Object.values(OPCUaSourceTypes); ctx.valueTypeKeys = Object.values(OPCUaSourceType);
ctx.valueTypeEnum = OPCUaSourceTypes; ctx.valueTypeEnum = OPCUaSourceType;
ctx.valueTypes = SourceTypeTranslationsMap; ctx.valueTypes = SourceTypeTranslationsMap;
} }
this.keysPopupClosed = false; this.keysPopupClosed = false;
@ -293,8 +294,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
requestType: this.data.value.requestType, requestType: this.data.value.requestType,
requestValue: { requestValue: {
[this.data.value.requestType]: this.data.value.requestValue [this.data.value.requestType]: this.data.value.requestValue
} } as Record<RequestType, RequestMappingData>
} as RequestMappingFormValue; };
default: default:
return this.data.value as DeviceConnectorMapping; return this.data.value as DeviceConnectorMapping;
} }
@ -349,10 +350,10 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
attributeRequests: this.fb.group({ attributeRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: this.fb.group({ deviceInfo: this.fb.group({
deviceNameExpressionSource: [SourceTypes.MSG, []], deviceNameExpressionSource: [SourceType.MSG, []],
deviceNameExpression: ['', [Validators.required]], deviceNameExpression: ['', [Validators.required]],
}), }),
attributeNameExpressionSource: [SourceTypes.MSG, []], attributeNameExpressionSource: [SourceType.MSG, []],
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -407,7 +408,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
private createOPCUAMappingForm(): void { private createOPCUAMappingForm(): void {
this.mappingForm = this.fb.group({ this.mappingForm = this.fb.group({
deviceNodeSource: [OPCUaSourceTypes.PATH, []], deviceNodeSource: [OPCUaSourceType.PATH, []],
deviceNodePattern: ['', [Validators.required]], deviceNodePattern: ['', [Validators.required]],
deviceInfo: [{}, []], deviceInfo: [{}, []],
attributes: [[], []], attributes: [[], []],

43
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -156,6 +156,7 @@
<div class="tb-form-panel-title"> <div class="tb-form-panel-title">
{{ initialConnector?.type ? GatewayConnectorTypesTranslatesMap.get(initialConnector.type) : '' }} {{ initialConnector?.type ? GatewayConnectorTypesTranslatesMap.get(initialConnector.type) : '' }}
{{ 'gateway.configuration' | translate }} {{ 'gateway.configuration' | translate }}
<span class="version-placeholder" *ngIf="connectorForm.get('configVersion').value">v{{connectorForm.get('configVersion').value}}</span>
</div> </div>
<tb-toggle-select *ngIf="initialConnector && allowBasicConfig.has(initialConnector.type)" <tb-toggle-select *ngIf="initialConnector && allowBasicConfig.has(initialConnector.type)"
formControlName="mode" appearance="fill"> formControlName="mode" appearance="fill">
@ -175,18 +176,36 @@
<section class="tb-form-panel section-container no-border no-padding tb-flex space-between" *ngIf="initialConnector"> <section class="tb-form-panel section-container no-border no-padding tb-flex space-between" *ngIf="initialConnector">
<ng-container *ngIf="connectorForm.get('mode')?.value === ConnectorConfigurationModes.BASIC else defaultConfig"> <ng-container *ngIf="connectorForm.get('mode')?.value === ConnectorConfigurationModes.BASIC else defaultConfig">
<ng-container [ngSwitch]="initialConnector.type"> <ng-container [ngSwitch]="initialConnector.type">
<tb-mqtt-basic-config *ngSwitchCase="ConnectorType.MQTT" <ng-container *ngSwitchCase="ConnectorType.MQTT">
formControlName="basicConfig" <tb-mqtt-basic-config
[generalTabContent]="generalTabContent"> *ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
</tb-mqtt-basic-config> formControlName="basicConfig"
<tb-opc-ua-basic-config *ngSwitchCase="ConnectorType.OPCUA" [generalTabContent]="generalTabContent"
formControlName="basicConfig" />
[generalTabContent]="generalTabContent"> <ng-template #legacy>
</tb-opc-ua-basic-config> <tb-mqtt-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/>
<tb-modbus-basic-config *ngSwitchCase="ConnectorType.MODBUS" </ng-template>
formControlName="basicConfig" </ng-container>
[generalTabContent]="generalTabContent"> <ng-container *ngSwitchCase="ConnectorType.OPCUA">
</tb-modbus-basic-config> <tb-opc-ua-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
<ng-template #legacy>
<tb-opc-ua-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.MODBUS">
<tb-modbus-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
<ng-template #legacy>
<tb-modbus-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/>
</ng-template>
</ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-template #defaultConfig> <ng-template #defaultConfig>

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

@ -22,6 +22,11 @@
overflow-x: auto; overflow-x: auto;
padding: 0; padding: 0;
.version-placeholder {
color: gray;
font-size: 12px
}
.connector-container { .connector-container {
height: 100%; height: 100%;
width: 100%; width: 100%;

34
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -59,12 +59,16 @@ import {
GatewayConnectorDefaultTypesTranslatesMap, GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel, GatewayLogLevel,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
GatewayVersion,
} from './gateway-widget.models'; } from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { debounceTime, filter, switchMap, take, takeUntil, tap } from 'rxjs/operators'; import { debounceTime, filter, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
export class ForceErrorStateMatcher implements ErrorStateMatcher { export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null): boolean { isErrorState(control: FormControl | null): boolean {
@ -98,6 +102,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
readonly ConnectorConfigurationModes = ConfigurationModes; readonly ConnectorConfigurationModes = ConfigurationModes;
readonly GatewayVersion = GatewayVersion;
pageLink: PageLink; pageLink: PageLink;
dataSource: MatTableDataSource<GatewayAttributeData>; dataSource: MatTableDataSource<GatewayAttributeData>;
@ -106,6 +111,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC; mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC;
initialConnector: GatewayConnector; initialConnector: GatewayConnector;
private gatewayVersion: string;
private inactiveConnectors: Array<string>; private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource; private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource; private inactiveConnectorsDataSource: AttributeDatasource;
@ -239,6 +245,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
delete value.class; delete value.class;
} }
if (this.gatewayVersion && !value.configVersion) {
value.configVersion = this.gatewayVersion;
}
value.ts = Date.now(); value.ts = Date.now();
return value; return value;
@ -263,7 +273,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}); });
} }
isConnectorSynced(attribute: GatewayAttributeData) { isConnectorSynced(attribute: GatewayAttributeData): boolean {
const connectorData = attribute.value; const connectorData = attribute.value;
if (!connectorData.ts || attribute.skipSync) { if (!connectorData.ts || attribute.skipSync) {
return false; return false;
@ -340,7 +350,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
class: '', class: '',
configuration: '', configuration: '',
configurationJson: {}, configurationJson: {},
basicConfig: {} basicConfig: {},
configVersion: ''
}, {emitEvent: false}); }, {emitEvent: false});
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
@ -483,7 +494,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, { return this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { dataSourceData: this.dataSource.data } data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed(); }).afterClosed();
} }
@ -526,7 +540,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
class: [''], class: [''],
configuration: [''], configuration: [''],
configurationJson: [{}, [Validators.required]], configurationJson: [{}, [Validators.required]],
basicConfig: [{}] basicConfig: [{}],
configVersion: [''],
}); });
this.connectorForm.disable(); this.connectorForm.disable();
} }
@ -560,10 +575,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
forkJoin([ forkJoin([
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']), this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors']) this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE, ['Version'])
]).pipe(takeUntil(this.destroy$)).subscribe(attributes => { ]).pipe(takeUntil(this.destroy$)).subscribe(attributes => {
this.activeConnectors = this.parseConnectors(attributes[0]); this.activeConnectors = this.parseConnectors(attributes[0]);
this.inactiveConnectors = this.parseConnectors(attributes[1]); this.inactiveConnectors = this.parseConnectors(attributes[1]);
this.gatewayVersion = attributes[2][0]?.value;
this.updateData(true); this.updateData(true);
}); });
@ -721,12 +738,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.enable(); this.connectorForm.enable();
} }
const connectorState = { const connectorState = GatewayConnectorVersionMappingUtil.getConfig({
configuration: '', configuration: '',
key: 'auto', key: 'auto',
configurationJson: {} as ConnectorBaseConfig, configurationJson: {} as ConnectorBaseConfig,
...connector, ...connector,
}; }, this.gatewayVersion);
connectorState.basicConfig = connectorState.configurationJson; connectorState.basicConfig = connectorState.configurationJson;
this.initialConnector = connectorState; this.initialConnector = connectorState;
@ -739,6 +756,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.MODBUS: case ConnectorType.MODBUS:
this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false}); this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
setTimeout(() => { setTimeout(() => {
this.connectorForm.patchValue(connector, {emitEvent: false}); this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
@ -760,7 +778,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
JSON.parse(clientConnectorData.value) : clientConnectorData.value; JSON.parse(clientConnectorData.value) : clientConnectorData.value;
if (this.isConnectorSynced(clientConnectorData) && clientConnectorData.value.configurationJson) { if (this.isConnectorSynced(clientConnectorData) && clientConnectorData.value.configurationJson) {
this.setFormValue(clientConnectorData.value); this.setFormValue({...clientConnectorData.value, mode: this.connectorForm.get('mode').value ?? clientConnectorData.value.mode});
} }
} }
} }

291
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -14,8 +14,6 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { helpBaseUrl, ValueTypeData } from '@shared/models/constants'; import { helpBaseUrl, ValueTypeData } from '@shared/models/constants';
import { AttributeData } from '@shared/models/telemetry/telemetry.models'; import { AttributeData } from '@shared/models/telemetry/telemetry.models';
@ -115,21 +113,30 @@ export interface GatewayAttributeData extends AttributeData {
skipSync?: boolean; skipSync?: boolean;
} }
export interface GatewayConnector { export interface GatewayConnectorBase {
name: string; name: string;
type: ConnectorType; type: ConnectorType;
configuration?: string; configuration?: string;
configurationJson: ConnectorBaseConfig;
basicConfig?: ConnectorBaseConfig;
logLevel: string; logLevel: string;
key?: string; key?: string;
class?: string; class?: string;
mode?: ConfigurationModes; mode?: ConfigurationModes;
configVersion?: string;
}
export interface GatewayConnector<BaseConfig = ConnectorBaseConfig> extends GatewayConnectorBase {
configurationJson: BaseConfig;
basicConfig?: BaseConfig;
}
export interface GatewayVersionedDefaultConfig {
legacy: GatewayConnector<ConnectorLegacyConfig>;
'3.5.2': GatewayConnector<ConnectorBaseConfig_v3_5_2>;
} }
export interface DataMapping { export interface DataMapping {
topicFilter: string; topicFilter: string;
QoS: string; QoS: string | number;
converter: Converter; converter: Converter;
} }
@ -181,11 +188,20 @@ export interface ConnectorSecurity {
mode?: ModeType; mode?: ModeType;
} }
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping; export enum GatewayVersion {
Current = '3.5.2',
Legacy = 'legacy'
}
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingValue | ConverterConnectorMapping;
export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue; export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue;
export type ConnectorBaseConfig = ConnectorBaseInfo | MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig; export type ConnectorBaseConfig = ConnectorBaseConfig_v3_5_2 | ConnectorLegacyConfig;
export type ConnectorLegacyConfig = ConnectorBaseInfo | MQTTLegacyBasicConfig | OPCLegacyBasicConfig | ModbusBasicConfig;
export type ConnectorBaseConfig_v3_5_2 = ConnectorBaseInfo | MQTTBasicConfig_v3_5_2 | OPCBasicConfig_v3_5_2;
export interface ConnectorBaseInfo { export interface ConnectorBaseInfo {
name: string; name: string;
@ -194,33 +210,64 @@ export interface ConnectorBaseInfo {
logLevel: GatewayLogLevel; logLevel: GatewayLogLevel;
} }
export interface MQTTBasicConfig { export type MQTTBasicConfig = MQTTBasicConfig_v3_5_2 | MQTTLegacyBasicConfig;
dataMapping: ConverterConnectorMapping[];
requestsMapping: Record<RequestType, RequestMappingData[]> | RequestMappingData[]; export interface MQTTBasicConfig_v3_5_2 {
mapping: ConverterConnectorMapping[];
requestsMapping: Record<RequestType, RequestMappingData[] | RequestMappingValue[]> | RequestMappingData[] | RequestMappingValue[];
broker: BrokerConfig;
workers?: WorkersConfig;
}
export interface MQTTLegacyBasicConfig {
mapping: LegacyConverterConnectorMapping[];
broker: BrokerConfig; broker: BrokerConfig;
workers?: WorkersConfig; workers?: WorkersConfig;
connectRequests: LegacyRequestMappingData[];
disconnectRequests: LegacyRequestMappingData[];
attributeRequests: LegacyRequestMappingData[];
attributeUpdates: LegacyRequestMappingData[];
serverSideRpc: LegacyRequestMappingData[];
} }
export interface OPCBasicConfig { export type OPCBasicConfig = OPCBasicConfig_v3_5_2 | OPCLegacyBasicConfig;
export interface OPCBasicConfig_v3_5_2 {
mapping: DeviceConnectorMapping[]; mapping: DeviceConnectorMapping[];
server: ServerConfig; server: ServerConfig;
} }
export interface ModbusBasicConfig { export interface OPCLegacyBasicConfig {
server: LegacyServerConfig;
}
export interface LegacyServerConfig extends Omit<ServerConfig, 'enableSubscriptions'> {
mapping: LegacyDeviceConnectorMapping[];
disableSubscriptions: boolean;
}
export type ModbusBasicConfig = ModbusBasicConfig_v3_5_2 | ModbusLegacyBasicConfig;
export interface ModbusBasicConfig_v3_5_2 {
master: ModbusMasterConfig; master: ModbusMasterConfig;
slave: ModbusSlave; slave: ModbusSlave;
} }
export interface ModbusLegacyBasicConfig {
master: ModbusMasterConfig;
slave: ModbusLegacySlave;
}
export interface WorkersConfig { export interface WorkersConfig {
maxNumberOfWorkers: number; maxNumberOfWorkers: number;
maxMessageNumberPerWorker: number; maxMessageNumberPerWorker: number;
} }
interface DeviceInfo { export interface ConnectorDeviceInfo {
deviceNameExpression: string; deviceNameExpression: string;
deviceNameExpressionSource: string; deviceNameExpressionSource: SourceType | OPCUaSourceType;
deviceProfileExpression: string; deviceProfileExpression: string;
deviceProfileExpressionSource: string; deviceProfileExpressionSource: SourceType | OPCUaSourceType;
} }
export interface Attribute { export interface Attribute {
@ -229,13 +276,23 @@ export interface Attribute {
value: string; value: string;
} }
export interface LegacyAttribute {
key: string;
path: string;
}
export interface Timeseries { export interface Timeseries {
key: string; key: string;
type: string; type: string;
value: string; value: string;
} }
interface RpcArgument { export interface LegacyTimeseries {
key: string;
path: string;
}
export interface RpcArgument {
type: string; type: string;
value: number; value: number;
} }
@ -245,28 +302,59 @@ export interface RpcMethod {
arguments: RpcArgument[]; arguments: RpcArgument[];
} }
export interface LegacyRpcMethod {
method: string;
arguments: unknown[];
}
export interface AttributesUpdate { export interface AttributesUpdate {
key: string; key: string;
type: string; type: string;
value: string; value: string;
} }
export interface LegacyDeviceAttributeUpdate {
attributeOnThingsBoard: string;
attributeOnDevice: string;
}
export interface Converter { export interface Converter {
type: ConvertorType; type: ConvertorType;
deviceNameJsonExpression: string; deviceInfo?: ConnectorDeviceInfo;
deviceTypeJsonExpression: string;
sendDataOnlyOnChange: boolean; sendDataOnlyOnChange: boolean;
timeout: number; timeout: number;
attributes: Attribute[]; attributes?: Attribute[];
timeseries: Timeseries[]; timeseries?: Timeseries[];
extension?: string;
cached?: boolean;
extensionConfig?: Record<string, number>;
}
export interface LegacyConverter extends Converter {
deviceNameJsonExpression?: string;
deviceTypeJsonExpression?: string;
deviceNameTopicExpression?: string;
deviceTypeTopicExpression?: string;
deviceNameExpression?: string;
deviceNameExpressionSource?: string;
deviceTypeExpression?: string;
deviceProfileExpression?: string;
deviceProfileExpressionSource?: string;
['extension-config']?: Record<string, unknown>;
} }
export interface ConverterConnectorMapping { export interface ConverterConnectorMapping {
topicFilter: string; topicFilter: string;
subscriptionQos?: string; subscriptionQos?: string | number;
converter: Converter; converter: Converter;
} }
export interface LegacyConverterConnectorMapping {
topicFilter: string;
subscriptionQos?: string | number;
converter: LegacyConverter;
}
export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'converter'> & { export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'converter'> & {
converter: { converter: {
type: ConvertorType; type: ConvertorType;
@ -275,14 +363,24 @@ export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'convert
export interface DeviceConnectorMapping { export interface DeviceConnectorMapping {
deviceNodePattern: string; deviceNodePattern: string;
deviceNodeSource: string; deviceNodeSource: OPCUaSourceType;
deviceInfo: DeviceInfo; deviceInfo: ConnectorDeviceInfo;
attributes?: Attribute[]; attributes?: Attribute[];
timeseries?: Timeseries[]; timeseries?: Timeseries[];
rpc_methods?: RpcMethod[]; rpc_methods?: RpcMethod[];
attributes_updates?: AttributesUpdate[]; attributes_updates?: AttributesUpdate[];
} }
export interface LegacyDeviceConnectorMapping {
deviceNamePattern: string;
deviceNodePattern: string;
deviceTypePattern: string;
attributes?: LegacyAttribute[];
timeseries?: LegacyTimeseries[];
rpc_methods?: LegacyRpcMethod[];
attributes_updates?: LegacyDeviceAttributeUpdate[];
}
export enum ConnectorType { export enum ConnectorType {
MQTT = 'mqtt', MQTT = 'mqtt',
MODBUS = 'modbus', MODBUS = 'modbus',
@ -461,6 +559,7 @@ export interface GatewayLogData {
export interface AddConnectorConfigData { export interface AddConnectorConfigData {
dataSourceData: Array<any>; dataSourceData: Array<any>;
gatewayVersion: string;
} }
export interface CreatedConnectorConfigData { export interface CreatedConnectorConfigData {
@ -591,13 +690,13 @@ export const ConvertorTypeTranslationsMap = new Map<ConvertorType, string>(
] ]
); );
export enum SourceTypes { export enum SourceType {
MSG = 'message', MSG = 'message',
TOPIC = 'topic', TOPIC = 'topic',
CONST = 'constant' CONST = 'constant'
} }
export enum OPCUaSourceTypes { export enum OPCUaSourceType {
PATH = 'path', PATH = 'path',
IDENTIFIER = 'identifier', IDENTIFIER = 'identifier',
CONST = 'constant' CONST = 'constant'
@ -608,33 +707,116 @@ export enum DeviceInfoType {
PARTIAL = 'partial' PARTIAL = 'partial'
} }
export const SourceTypeTranslationsMap = new Map<SourceTypes | OPCUaSourceTypes, string>( export const SourceTypeTranslationsMap = new Map<SourceType | OPCUaSourceType, string>(
[ [
[SourceTypes.MSG, 'gateway.source-type.msg'], [SourceType.MSG, 'gateway.source-type.msg'],
[SourceTypes.TOPIC, 'gateway.source-type.topic'], [SourceType.TOPIC, 'gateway.source-type.topic'],
[SourceTypes.CONST, 'gateway.source-type.const'], [SourceType.CONST, 'gateway.source-type.const'],
[OPCUaSourceTypes.PATH, 'gateway.source-type.path'], [OPCUaSourceType.PATH, 'gateway.source-type.path'],
[OPCUaSourceTypes.IDENTIFIER, 'gateway.source-type.identifier'], [OPCUaSourceType.IDENTIFIER, 'gateway.source-type.identifier'],
[OPCUaSourceTypes.CONST, 'gateway.source-type.const'] [OPCUaSourceType.CONST, 'gateway.source-type.const']
] ]
); );
export interface RequestMappingData { export interface RequestMappingValue {
requestType: RequestType; requestType: RequestType;
requestValue: RequestDataItem; requestValue: RequestMappingData;
} }
export type RequestMappingFormValue = Omit<RequestMappingData, 'requestValue'> & { export interface RequestMappingFormValue {
requestValue: Record<RequestType, RequestDataItem>;
};
export interface RequestDataItem {
type: string;
details: string;
requestType: RequestType; requestType: RequestType;
methodFilter?: string; requestValue: Record<RequestType, RequestMappingData>;
attributeFilter?: string; }
topicFilter?: string;
export type RequestMappingData = ConnectRequest | DisconnectRequest | AttributeRequest | AttributeUpdate | ServerSideRpc;
export type LegacyRequestMappingData =
LegacyConnectRequest
| LegacyDisconnectRequest
| LegacyAttributeRequest
| LegacyAttributeUpdate
| LegacyServerSideRpc;
export interface ConnectRequest {
topicFilter: string;
deviceInfo: ConnectorDeviceInfo;
}
export interface DisconnectRequest {
topicFilter: string;
deviceInfo: ConnectorDeviceInfo;
}
export interface AttributeRequest {
retain: boolean;
topicFilter: string;
deviceInfo: ConnectorDeviceInfo;
attributeNameExpressionSource: SourceType;
attributeNameExpression: string;
topicExpression: string;
valueExpression: string;
}
export interface AttributeUpdate {
retain: boolean;
deviceNameFilter: string;
attributeFilter: string;
topicExpression: string;
valueExpression: string;
}
export interface ServerSideRpc {
type: ServerSideRpcType;
deviceNameFilter: string;
methodFilter: string;
requestTopicExpression: string;
responseTopicExpression?: string;
responseTopicQoS?: number;
responseTimeout?: number;
valueExpression: string;
}
export enum ServerSideRpcType {
WithResponse = 'twoWay',
WithoutResponse = 'oneWay'
}
export interface LegacyConnectRequest {
topicFilter: string;
deviceNameJsonExpression?: string;
deviceNameTopicExpression?: string;
}
interface LegacyDisconnectRequest {
topicFilter: string;
deviceNameJsonExpression?: string;
deviceNameTopicExpression?: string;
}
interface LegacyAttributeRequest {
retain: boolean;
topicFilter: string;
deviceNameJsonExpression: string;
attributeNameJsonExpression: string;
topicExpression: string;
valueExpression: string;
}
interface LegacyAttributeUpdate {
retain: boolean;
deviceNameFilter: string;
attributeFilter: string;
topicExpression: string;
valueExpression: string;
}
interface LegacyServerSideRpc {
deviceNameFilter: string;
methodFilter: string;
requestTopicExpression: string;
responseTopicExpression?: string;
responseTimeout?: number;
valueExpression: string;
} }
export enum RequestType { export enum RequestType {
@ -708,9 +890,6 @@ export enum ServerSideRPCType {
TWO_WAY = 'twoWay' TWO_WAY = 'twoWay'
} }
export const getDefaultConfig = (resourcesService: ResourcesService, type: string): Observable<any> =>
resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
export enum MappingValueType { export enum MappingValueType {
STRING = 'string', STRING = 'string',
INTEGER = 'integer', INTEGER = 'integer',
@ -938,7 +1117,7 @@ export interface SlaveConfig {
pollPeriod: number; pollPeriod: number;
unitId: number; unitId: number;
deviceName: string; deviceName: string;
deviceType: string; deviceType?: string;
sendDataOnlyOnChange: boolean; sendDataOnlyOnChange: boolean;
connectAttemptTimeMs: number; connectAttemptTimeMs: number;
connectAttemptCount: number; connectAttemptCount: number;
@ -984,14 +1163,26 @@ export interface ModbusSlave {
pollPeriod: number; pollPeriod: number;
sendDataToThingsBoard: boolean; sendDataToThingsBoard: boolean;
byteOrder: ModbusOrderType; byteOrder: ModbusOrderType;
wordOrder: ModbusOrderType;
identity: ModbusIdentity; identity: ModbusIdentity;
values: ModbusValuesState; values?: ModbusValuesState;
port: string | number; port: string | number;
security: ModbusSecurity; security: ModbusSecurity;
} }
export interface ModbusLegacySlave extends Omit<ModbusSlave, 'values'> {
values?: ModbusLegacyRegisterValues;
}
export type ModbusValuesState = ModbusRegisterValues | ModbusValues; export type ModbusValuesState = ModbusRegisterValues | ModbusValues;
export interface ModbusLegacyRegisterValues {
holding_registers: ModbusValues[];
coils_initializer: ModbusValues[];
input_registers: ModbusValues[];
discrete_inputs: ModbusValues[];
}
export interface ModbusRegisterValues { export interface ModbusRegisterValues {
holding_registers: ModbusValues; holding_registers: ModbusValues;
coils_initializer: ModbusValues; coils_initializer: ModbusValues;

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts

@ -17,8 +17,8 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { import {
MappingValueType, MappingValueType,
OPCUaSourceTypes, OPCUaSourceType,
SourceTypes SourceType
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
@Pipe({ @Pipe({
@ -26,9 +26,9 @@ import {
standalone: true, standalone: true,
}) })
export class GatewayHelpLinkPipe implements PipeTransform { export class GatewayHelpLinkPipe implements PipeTransform {
transform(field: string, sourceType: SourceTypes | OPCUaSourceTypes, sourceTypes?: Array<SourceTypes | OPCUaSourceTypes | MappingValueType> ): string { transform(field: string, sourceType: SourceType | OPCUaSourceType, sourceTypes?: Array<SourceType | OPCUaSourceType | MappingValueType> ): string {
if (!sourceTypes || sourceTypes?.includes(OPCUaSourceTypes.PATH)) { if (!sourceTypes || sourceTypes?.includes(OPCUaSourceType.PATH)) {
if (sourceType !== OPCUaSourceTypes.CONST) { if (sourceType !== OPCUaSourceType.CONST) {
return `widget/lib/gateway/${field}-${sourceType}_fn`; return `widget/lib/gateway/${field}-${sourceType}_fn`;
} else { } else {
return; return;

42
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util.ts

@ -0,0 +1,42 @@
///
/// 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 {
ConnectorType,
GatewayConnector,
ModbusBasicConfig,
MQTTBasicConfig,
OPCBasicConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { MqttVersionProcessor } from '@home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract';
import { OpcVersionProcessor } from '@home/components/widget/lib/gateway/abstract/opc-version-processor.abstract';
import { ModbusVersionProcessor } from '@home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract';
export abstract class GatewayConnectorVersionMappingUtil {
static getConfig(connector: GatewayConnector, gatewayVersion: string): GatewayConnector {
switch(connector.type) {
case ConnectorType.MQTT:
return new MqttVersionProcessor(gatewayVersion, connector as GatewayConnector<MQTTBasicConfig>).getProcessedByVersion();
case ConnectorType.OPCUA:
return new OpcVersionProcessor(gatewayVersion, connector as GatewayConnector<OPCBasicConfig>).getProcessedByVersion();
case ConnectorType.MODBUS:
return new ModbusVersionProcessor(gatewayVersion, connector as GatewayConnector<ModbusBasicConfig>).getProcessedByVersion();
default:
return connector;
}
}
}

86
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/modbus-version-mapping.util.ts

@ -0,0 +1,86 @@
///
/// 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 {
ModbusDataType,
ModbusLegacyRegisterValues,
ModbusLegacySlave,
ModbusMasterConfig,
ModbusRegisterValues,
ModbusSlave,
ModbusValue,
ModbusValues,
SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
export class ModbusVersionMappingUtil {
static mapMasterToUpgradedVersion(master: ModbusMasterConfig): ModbusMasterConfig {
return {
slaves: master.slaves.map((slave: SlaveConfig) => ({
...slave,
deviceType: slave.deviceType ?? 'default',
}))
};
}
static mapSlaveToDowngradedVersion(slave: ModbusSlave): ModbusLegacySlave {
if (!slave?.values) {
return slave as Omit<ModbusLegacySlave, 'values'>;
}
const values = Object.keys(slave.values).reduce((acc, valueKey) => {
acc = {
...acc,
[valueKey]: [
slave.values[valueKey]
]
};
return acc;
}, {} as ModbusLegacyRegisterValues);
return {
...slave,
values
};
}
static mapSlaveToUpgradedVersion(slave: ModbusLegacySlave): ModbusSlave {
if (!slave?.values) {
return slave as Omit<ModbusSlave, 'values'>;
}
const values = Object.keys(slave.values).reduce((acc, valueKey) => {
acc = {
...acc,
[valueKey]: this.mapValuesToUpgradedVersion(slave.values[valueKey][0])
};
return acc;
}, {} as ModbusRegisterValues);
return {
...slave,
values
};
}
private static mapValuesToUpgradedVersion(registerValues: ModbusValues): ModbusValues {
return Object.keys(registerValues).reduce((acc, valueKey) => {
acc = {
...acc,
[valueKey]: registerValues[valueKey].map((value: ModbusValue) =>
({ ...value, type: (value.type as string) === 'int' ? ModbusDataType.INT16 : value.type }))
};
return acc;
}, {} as ModbusValues);
}
}

217
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/mqtt-version-mapping.util.ts

@ -0,0 +1,217 @@
///
/// 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 { deleteNullProperties } from '@core/utils';
import {
AttributeRequest,
ConnectorDeviceInfo,
Converter,
ConverterConnectorMapping,
ConvertorType,
LegacyConverter,
LegacyConverterConnectorMapping,
LegacyRequestMappingData,
RequestMappingData,
RequestType,
ServerSideRpc,
ServerSideRpcType,
SourceType
} from '@home/components/widget/lib/gateway/gateway-widget.models';
export class MqttVersionMappingUtil {
static readonly mqttRequestTypeKeys = Object.values(RequestType);
static readonly mqttRequestMappingOldFields =
['attributeNameJsonExpression', 'deviceNameJsonExpression', 'deviceNameTopicExpression', 'extension-config'];
static readonly mqttRequestMappingNewFields =
['attributeNameExpressionSource', 'responseTopicQoS', 'extensionConfig'];
static mapMappingToUpgradedVersion(
mapping: LegacyConverterConnectorMapping[]
): ConverterConnectorMapping[] {
return mapping?.map(({ converter, topicFilter, subscriptionQos = 1 }) => {
const deviceInfo = converter.deviceInfo ?? this.extractConverterDeviceInfo(converter);
const newConverter = {
...converter,
deviceInfo,
extensionConfig: converter.extensionConfig || converter['extension-config'] || null
};
this.cleanUpOldFields(newConverter);
return { converter: newConverter, topicFilter, subscriptionQos };
}) as ConverterConnectorMapping[];
}
static mapRequestsToUpgradedVersion(
requestMapping: Record<RequestType,
LegacyRequestMappingData[]>
): Record<RequestType, RequestMappingData[]> {
return this.mqttRequestTypeKeys.reduce((acc, key: RequestType) => {
if (!requestMapping[key]) {
return acc;
}
acc[key] = requestMapping[key].map(value => {
const newValue = this.mapRequestToUpgradedVersion(value as LegacyRequestMappingData, key);
this.cleanUpOldFields(newValue as {});
return newValue;
});
return acc;
}, {}) as Record<RequestType, RequestMappingData[]>;
}
static mapRequestsToDowngradedVersion(
requestsMapping: Record<RequestType, RequestMappingData[]>
): Record<RequestType, LegacyRequestMappingData[]> {
return this.mqttRequestTypeKeys.reduce((acc, key) => {
if (!requestsMapping[key]) {
return acc;
}
acc[key] = requestsMapping[key].map((value: RequestMappingData) => {
if (key === RequestType.SERVER_SIDE_RPC) {
delete (value as ServerSideRpc).type;
}
const { attributeNameExpression, deviceInfo, ...rest } = value as AttributeRequest;
const newValue = {
...rest,
attributeNameJsonExpression: attributeNameExpression || null,
deviceNameJsonExpression: deviceInfo?.deviceNameExpressionSource !== SourceType.TOPIC ? deviceInfo?.deviceNameExpression : null,
deviceNameTopicExpression: deviceInfo?.deviceNameExpressionSource === SourceType.TOPIC ? deviceInfo?.deviceNameExpression : null,
};
this.cleanUpNewFields(newValue);
return newValue;
});
return acc;
}, {}) as Record<RequestType, LegacyRequestMappingData[]>;
}
static mapMappingToDowngradedVersion(
mapping: ConverterConnectorMapping[]
): LegacyConverterConnectorMapping[] {
return mapping?.map((converterMapping: ConverterConnectorMapping) => {
const converter = this.mapConverterToDowngradedVersion(converterMapping.converter);
this.cleanUpNewFields(converter as {});
return { converter, topicFilter: converterMapping.topicFilter };
});
}
private static mapConverterToDowngradedVersion(converter: Converter): LegacyConverter {
const { deviceInfo, ...restConverter } = converter;
return converter.type !== ConvertorType.BYTES ? {
...restConverter,
deviceNameJsonExpression: deviceInfo?.deviceNameExpressionSource === SourceType.MSG ? deviceInfo.deviceNameExpression : null,
deviceTypeJsonExpression:
deviceInfo?.deviceProfileExpressionSource === SourceType.MSG ? deviceInfo.deviceProfileExpression : null,
deviceNameTopicExpression:
deviceInfo?.deviceNameExpressionSource !== SourceType.MSG
? deviceInfo?.deviceNameExpression
: null,
deviceTypeTopicExpression: deviceInfo?.deviceProfileExpressionSource !== SourceType.MSG
? deviceInfo?.deviceProfileExpression
: null,
} : {
...restConverter,
deviceNameExpression: deviceInfo.deviceNameExpression,
deviceTypeExpression: deviceInfo.deviceProfileExpression,
['extension-config']: converter.extensionConfig,
};
}
private static cleanUpOldFields(obj: Record<string, unknown>): void {
this.mqttRequestMappingOldFields.forEach(field => delete obj[field]);
deleteNullProperties(obj);
}
private static cleanUpNewFields(obj: Record<string, unknown>): void {
this.mqttRequestMappingNewFields.forEach(field => delete obj[field]);
deleteNullProperties(obj);
}
private static getTypeSourceByValue(value: string): SourceType {
if (value.includes('${')) {
return SourceType.MSG;
}
if (value.includes(`/`)) {
return SourceType.TOPIC;
}
return SourceType.CONST;
}
private static extractConverterDeviceInfo(converter: LegacyConverter): ConnectorDeviceInfo {
const deviceNameExpression = converter.deviceNameExpression
|| converter.deviceNameJsonExpression
|| converter.deviceNameTopicExpression
|| null;
const deviceNameExpressionSource = converter.deviceNameExpressionSource
? converter.deviceNameExpressionSource as SourceType
: deviceNameExpression ? this.getTypeSourceByValue(deviceNameExpression) : null;
const deviceProfileExpression = converter.deviceProfileExpression
|| converter.deviceTypeTopicExpression
|| converter.deviceTypeJsonExpression
|| 'default';
const deviceProfileExpressionSource = converter.deviceProfileExpressionSource
? converter.deviceProfileExpressionSource as SourceType
: deviceProfileExpression ? this.getTypeSourceByValue(deviceProfileExpression) : null;
return deviceNameExpression || deviceProfileExpression ? {
deviceNameExpression,
deviceNameExpressionSource,
deviceProfileExpression,
deviceProfileExpressionSource
} : null;
}
private static mapRequestToUpgradedVersion(value, key: RequestType): RequestMappingData {
const deviceNameExpression = value.deviceNameJsonExpression || value.deviceNameTopicExpression || null;
const deviceProfileExpression = value.deviceTypeTopicExpression || value.deviceTypeJsonExpression || 'default';
const deviceProfileExpressionSource = deviceProfileExpression ? this.getTypeSourceByValue(deviceProfileExpression) : null;
const attributeNameExpression = value.attributeNameExpressionSource || value.attributeNameJsonExpression || null;
const responseTopicQoS = key === RequestType.SERVER_SIDE_RPC ? 1 : null;
const type = key === RequestType.SERVER_SIDE_RPC
? (value as ServerSideRpc).responseTopicExpression
? ServerSideRpcType.WithResponse
: ServerSideRpcType.WithoutResponse
: null;
return {
...value,
attributeNameExpression,
attributeNameExpressionSource: attributeNameExpression ? this.getTypeSourceByValue(attributeNameExpression) : null,
deviceInfo: value.deviceInfo ? value.deviceInfo : deviceNameExpression ? {
deviceNameExpression,
deviceNameExpressionSource: this.getTypeSourceByValue(deviceNameExpression),
deviceProfileExpression,
deviceProfileExpressionSource
} : null,
responseTopicQoS,
type
};
}
}

134
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts

@ -0,0 +1,134 @@
///
/// 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 {
Attribute,
AttributesUpdate,
DeviceConnectorMapping,
LegacyAttribute,
LegacyDeviceAttributeUpdate,
LegacyDeviceConnectorMapping,
LegacyRpcMethod,
LegacyServerConfig,
LegacyTimeseries,
OPCBasicConfig_v3_5_2,
OPCUaSourceType,
RpcArgument,
RpcMethod,
ServerConfig,
Timeseries
} from '@home/components/widget/lib/gateway/gateway-widget.models';
export class OpcVersionMappingUtil {
static mapServerToUpgradedVersion(server: LegacyServerConfig): ServerConfig {
const { mapping, disableSubscriptions, ...restServer } = server;
return {
...restServer,
enableSubscriptions: !disableSubscriptions,
};
}
static mapServerToDowngradedVersion(config: OPCBasicConfig_v3_5_2): LegacyServerConfig {
const { mapping, server } = config;
const { enableSubscriptions, ...restServer } = server;
return {
...restServer,
mapping: mapping ? this.mapMappingToDowngradedVersion(mapping) : [],
disableSubscriptions: !enableSubscriptions,
};
}
static mapMappingToUpgradedVersion(mapping: LegacyDeviceConnectorMapping[]): DeviceConnectorMapping[] {
return mapping.map((legacyMapping: LegacyDeviceConnectorMapping) => ({
...legacyMapping,
deviceNodeSource: this.getTypeSourceByValue(legacyMapping.deviceNodePattern),
deviceInfo: {
deviceNameExpression: legacyMapping.deviceNamePattern,
deviceNameExpressionSource: this.getTypeSourceByValue(legacyMapping.deviceNamePattern),
deviceProfileExpression: legacyMapping.deviceTypePattern ?? 'default',
deviceProfileExpressionSource: this.getTypeSourceByValue(legacyMapping.deviceTypePattern ?? 'default'),
},
attributes: legacyMapping.attributes.map((attribute: LegacyAttribute) => ({
key: attribute.key,
type: this.getTypeSourceByValue(attribute.path),
value: attribute.path,
})),
attributes_updates: legacyMapping.attributes_updates.map((attributeUpdate: LegacyDeviceAttributeUpdate) => ({
key: attributeUpdate.attributeOnThingsBoard,
type: this.getTypeSourceByValue(attributeUpdate.attributeOnDevice),
value: attributeUpdate.attributeOnDevice,
})),
timeseries: legacyMapping.timeseries.map((timeseries: LegacyTimeseries) => ({
key: timeseries.key,
type: this.getTypeSourceByValue(timeseries.path),
value: timeseries.path,
})),
rpc_methods: legacyMapping.rpc_methods.map((rpcMethod: LegacyRpcMethod) => ({
method: rpcMethod.method,
arguments: rpcMethod.arguments.map(arg => ({
value: arg,
type: this.getArgumentType(arg),
} as RpcArgument))
}))
}));
}
static mapMappingToDowngradedVersion(mapping: DeviceConnectorMapping[]): LegacyDeviceConnectorMapping[] {
return mapping.map((upgradedMapping: DeviceConnectorMapping) => ({
...upgradedMapping,
deviceNamePattern: upgradedMapping.deviceInfo.deviceNameExpression,
deviceTypePattern: upgradedMapping.deviceInfo.deviceProfileExpression,
attributes: upgradedMapping.attributes.map((attribute: Attribute) => ({
key: attribute.key,
path: attribute.value,
})),
attributes_updates: upgradedMapping.attributes_updates.map((attributeUpdate: AttributesUpdate) => ({
attributeOnThingsBoard: attributeUpdate.key,
attributeOnDevice: attributeUpdate.value,
})),
timeseries: upgradedMapping.timeseries.map((timeseries: Timeseries) => ({
key: timeseries.key,
path: timeseries.value,
})),
rpc_methods: upgradedMapping.rpc_methods.map((rpcMethod: RpcMethod) => ({
method: rpcMethod.method,
arguments: rpcMethod.arguments.map((arg: RpcArgument) => arg.value)
}))
}));
}
private static getTypeSourceByValue(value: string): OPCUaSourceType {
if (value.includes('${')) {
return OPCUaSourceType.IDENTIFIER;
}
if (value.includes(`/`) || value.includes('\\')) {
return OPCUaSourceType.PATH;
}
return OPCUaSourceType.CONST;
}
private static getArgumentType(arg: unknown): string {
switch (typeof arg) {
case 'boolean':
return 'boolean';
case 'number':
return Number.isInteger(arg) ? 'integer' : 'float';
default:
return 'string';
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -933,7 +933,7 @@ export class ScadaSymbolObject {
this.iconRegistry.getDefaultFontSetClass() this.iconRegistry.getDefaultFontSetClass()
).filter(className => className.length > 0); ).filter(className => className.length > 0);
fontSetClasses.forEach(className => textElement.addClass(className)); fontSetClasses.forEach(className => textElement.addClass(className));
textElement.font({size}); textElement.font({size: `${size}px`});
textElement.attr({ textElement.attr({
'text-anchor': 'start', 'text-anchor': 'start',
'dominant-baseline': 'hanging', 'dominant-baseline': 'hanging',

22
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -113,22 +113,22 @@ import { GatewayHelpLinkPipe } from '@home/components/widget/lib/gateway/pipes/g
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { import {
BrokerConfigControlComponent BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component';
import { import {
WorkersConfigControlComponent WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component';
import { import {
OpcServerConfigComponent OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component';
import { import {
MqttBasicConfigComponent MqttBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component';
import { import {
MappingTableComponent MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import { import {
OpcUaBasicConfigComponent OpcUaBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component';
import { import {
ModbusBasicConfigComponent ModbusBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component';
@ -145,12 +145,21 @@ import {
ModbusRpcParametersComponent ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
import {
MqttLegacyBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-legacy-basic-config.component';
import { import {
GatewayBasicConfigurationComponent GatewayBasicConfigurationComponent
} from '@home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component'; } from '@home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component';
import { import {
GatewayAdvancedConfigurationComponent GatewayAdvancedConfigurationComponent
} from '@home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component'; } from '@home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component';
import {
OpcUaLegacyBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-legacy-basic-config.component';
import {
ModbusLegacyBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-legacy-basic-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -239,8 +248,11 @@ import {
ModbusBasicConfigComponent, ModbusBasicConfigComponent,
EllipsisChipListDirective, EllipsisChipListDirective,
ModbusRpcParametersComponent, ModbusRpcParametersComponent,
MqttLegacyBasicConfigComponent,
GatewayBasicConfigurationComponent, GatewayBasicConfigurationComponent,
GatewayAdvancedConfigurationComponent, GatewayAdvancedConfigurationComponent,
OpcUaLegacyBasicConfigComponent,
ModbusLegacyBasicConfigComponent,
], ],
exports: [ exports: [
EntitiesTableWidgetComponent, EntitiesTableWidgetComponent,

172
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -204,8 +204,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId); index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId);
if (index > -1) { if (index > -1) {
const prevDashboardWidget = this.dashboardWidgets[index]; const prevDashboardWidget = this.dashboardWidgets[index];
if (!isEqual(prevDashboardWidget.widget, record.widget) || if (!isEqual(prevDashboardWidget.widget, record.widget)) {
!isEqual(prevDashboardWidget.widgetLayout, record.widgetLayout)) {
this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout, this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout,
this.parentDashboard, this.popoverComponent); this.parentDashboard, this.popoverComponent);
this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted; this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted;
@ -391,7 +390,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>(); private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>();
private gridsterItemComponentValue: GridsterItemComponentInterface; private gridsterItemComponentValue: GridsterItemComponentInterface;
private readonly aspectRatio: number; private aspectRatio: number;
private heightValue: number; private heightValue: number;
private widthValue: number; private widthValue: number;
@ -419,85 +418,97 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.gridsterItemComponentSubject.complete(); this.gridsterItemComponentSubject.complete();
} }
private preserveAspectRatioApplied = false;
private applyPreserveAspectRatio(item: GridsterItemComponentInterface) { private applyPreserveAspectRatio(item: GridsterItemComponentInterface) {
this.resizableHandles.ne = false;
this.resizableHandles.sw = false;
this.resizableHandles.nw = false;
const $item = item.$item; if (this.widgetLayout?.preserveAspectRatio) {
this.resizableHandles.ne = false;
this.resizableHandles.sw = false;
this.resizableHandles.nw = false;
} else {
this.resizableHandles.ne = true;
this.resizableHandles.sw = true;
this.resizableHandles.nw = true;
}
if (!this.preserveAspectRatioApplied) {
const $item = item.$item;
this.rowsValue = $item.rows; this.rowsValue = $item.rows;
this.colsValue = $item.cols; this.colsValue = $item.cols;
Object.defineProperty($item, 'rows', { Object.defineProperty($item, 'rows', {
get: () => this.rowsValue, get: () => this.rowsValue,
set: v => { set: v => {
if (this.rowsValue !== v) { if (this.rowsValue !== v) {
if (this.preserveAspectRatio) { if (this.preserveAspectRatio) {
this.colsValue = v * this.aspectRatio; this.colsValue = v * this.aspectRatio;
}
this.rowsValue = v;
} }
this.rowsValue = v;
} }
} });
});
Object.defineProperty($item, 'cols', { Object.defineProperty($item, 'cols', {
get: () => this.colsValue, get: () => this.colsValue,
set: v => { set: v => {
if (this.colsValue !== v) { if (this.colsValue !== v) {
if (this.preserveAspectRatio) { if (this.preserveAspectRatio) {
this.rowsValue = v / this.aspectRatio; this.rowsValue = v / this.aspectRatio;
}
this.colsValue = v;
} }
this.colsValue = v;
} }
} });
});
const resizable = item.resize; const resizable = item.resize;
this.heightValue = resizable.height; this.heightValue = resizable.height;
this.widthValue = resizable.width; this.widthValue = resizable.width;
const setItemHeight = resizable.setItemHeight.bind(resizable); const setItemHeight = resizable.setItemHeight.bind(resizable);
const setItemWidth = resizable.setItemWidth.bind(resizable); const setItemWidth = resizable.setItemWidth.bind(resizable);
resizable.setItemHeight = (height) => { resizable.setItemHeight = (height) => {
setItemHeight(height); setItemHeight(height);
this.heightValue = height; this.heightValue = height;
if (this.preserveAspectRatio) { if (this.preserveAspectRatio) {
setItemWidth(height * this.aspectRatio); setItemWidth(height * this.aspectRatio);
} }
}; };
resizable.setItemWidth = (width) => { resizable.setItemWidth = (width) => {
setItemWidth(width); setItemWidth(width);
this.widthValue = width; this.widthValue = width;
if (this.preserveAspectRatio) { if (this.preserveAspectRatio) {
setItemHeight(width / this.aspectRatio); setItemHeight(width / this.aspectRatio);
} }
}; };
Object.defineProperty(resizable, 'height', { Object.defineProperty(resizable, 'height', {
get: () => this.heightValue, get: () => this.heightValue,
set: v => { set: v => {
if (this.heightValue !== v) { if (this.heightValue !== v) {
if (this.preserveAspectRatio) { if (this.preserveAspectRatio) {
this.widthValue = v * this.aspectRatio; this.widthValue = v * this.aspectRatio;
}
this.heightValue = v;
} }
this.heightValue = v;
} }
} });
});
Object.defineProperty(resizable, 'width', { Object.defineProperty(resizable, 'width', {
get: () => this.widthValue, get: () => this.widthValue,
set: v => { set: v => {
if (this.widthValue !== v) { if (this.widthValue !== v) {
if (this.preserveAspectRatio) { if (this.preserveAspectRatio) {
this.heightValue = v / this.aspectRatio; this.heightValue = v / this.aspectRatio;
}
this.widthValue = v;
} }
this.widthValue = v;
} }
} });
}); this.preserveAspectRatioApplied = true;
}
} }
get highlighted() { get highlighted() {
@ -527,19 +538,36 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
} }
} }
get widgetLayout(): WidgetLayout {
return this.widgetLayoutValue;
}
set widgetLayout(value: WidgetLayout) {
this.widgetLayoutValue = value;
this._widgetLayoutUpdated();
}
private _widgetLayoutUpdated() {
if (isDefined(this.widgetLayout?.resizable)) {
this.resizeEnabled = this.widgetLayout.resizable;
}
if (this.widgetLayout?.preserveAspectRatio) {
this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY;
}
if (this.gridsterItemComponentValue) {
this.applyPreserveAspectRatio(this.gridsterItemComponentValue);
}
}
constructor( constructor(
private dashboard: IDashboardComponent, private dashboard: IDashboardComponent,
public widget: Widget, public widget: Widget,
public widgetLayout?: WidgetLayout, private widgetLayoutValue?: WidgetLayout,
private parentDashboard?: IDashboardComponent, private parentDashboard?: IDashboardComponent,
private popoverComponent?: TbPopoverComponent) { private popoverComponent?: TbPopoverComponent) {
if (isDefined(widgetLayout?.resizable)) { this.widgetLayout = widgetLayoutValue;
this.resizeEnabled = widgetLayout.resizable;
}
if (widgetLayout?.preserveAspectRatio) {
this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY;
}
if (!widget.id) { if (!widget.id) {
widget.id = guid(); widget.id = guid();
} }

21
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -15,14 +15,22 @@
/// ///
import { inject, NgModule } from '@angular/core'; import { inject, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router'; import {
ActivatedRouteSnapshot,
Resolve,
ResolveFn,
Router,
RouterModule,
RouterStateSnapshot,
Routes
} from '@angular/router';
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import { forkJoin } from 'rxjs'; import { forkJoin, of } from 'rxjs';
import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
@ -45,17 +53,24 @@ import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.comp
import { ImageService } from '@core/http/image.service'; import { ImageService } from '@core/http/image.service';
import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { MenuId } from '@core/services/menu.models'; import { MenuId } from '@core/services/menu.models';
import { catchError } from 'rxjs/operators';
export const scadaSymbolResolver: ResolveFn<ScadaSymbolData> = export const scadaSymbolResolver: ResolveFn<ScadaSymbolData> =
(route: ActivatedRouteSnapshot, (route: ActivatedRouteSnapshot,
state: RouterStateSnapshot, state: RouterStateSnapshot,
router = inject(Router),
imageService = inject(ImageService)) => { imageService = inject(ImageService)) => {
const type: ImageResourceType = route.params.type; const type: ImageResourceType = route.params.type;
const key = decodeURIComponent(route.params.key); const key = decodeURIComponent(route.params.key);
return forkJoin({ return forkJoin({
imageResource: imageService.getImageInfo(type, key), imageResource: imageService.getImageInfo(type, key),
scadaSymbolContent: imageService.getImageString(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`) scadaSymbolContent: imageService.getImageString(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`)
}); }).pipe(
catchError(() => {
router.navigate(['/resources/scada-symbols']);
return of(null);
})
);
}; };
export const scadaSymbolBreadcumbLabelFunction: BreadCrumbLabelFunction<ScadaSymbolComponent> export const scadaSymbolBreadcumbLabelFunction: BreadCrumbLabelFunction<ScadaSymbolComponent>

11
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -22,7 +22,7 @@ import {
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { Resolve, Router } from '@angular/router'; import { Resolve, Router } from '@angular/router';
import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models'; import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -118,7 +118,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
const authUser = getCurrentAuthUser(this.store); const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.detailsReadonly = (resource) => !this.isResourceEditable(resource, authUser.authority); this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority);
return this.config; return this.config;
} }
@ -149,6 +149,13 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
return false; return false;
} }
private detailsReadonly(resource: ResourceInfo, authority: Authority): boolean {
if (resource?.resourceType === ResourceType.LWM2M_MODEL) {
return true;
}
return !this.isResourceEditable(resource, authority);
}
private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean { private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean {
if (authority === Authority.TENANT_ADMIN) { if (authority === Authority.TENANT_ADMIN) {
return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID; return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID;

2
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html

@ -58,7 +58,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd"> <mat-form-field class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd">
<mat-label translate>resource.title</mat-label> <mat-label translate>resource.title</mat-label>
<input matInput formControlName="title" required [readonly]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"> <input matInput formControlName="title" required>
<mat-error *ngIf="entityForm.get('title').hasError('required')"> <mat-error *ngIf="entityForm.get('title').hasError('required')">
{{ 'resource.title-required' | translate }} {{ 'resource.title-required' | translate }}
</mat-error> </mat-error>

36
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -43,8 +43,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
readonly resourceType = ResourceType; readonly resourceType = ResourceType;
readonly resourceTypes: ResourceType[] = Object.values(this.resourceType); readonly resourceTypes: ResourceType[] = Object.values(this.resourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
readonly maxResourceSize = getCurrentAuthState(this.store).maxResourceSize;
maxResourceSize = getCurrentAuthState(this.store).maxResourceSize;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -57,20 +56,20 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
super(store, fb, entityValue, entitiesTableConfigValue, cd); super(store, fb, entityValue, entitiesTableConfigValue, cd);
} }
ngOnInit() { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
if (this.isAdd) { if (this.isAdd) {
this.observeResourceTypeChange(); this.observeResourceTypeChange();
} }
} }
ngOnDestroy() { ngOnDestroy(): void {
super.ngOnDestroy(); super.ngOnDestroy();
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
hideDelete() { hideDelete(): boolean {
if (this.entitiesTableConfig) { if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity); return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else { } else {
@ -87,19 +86,18 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
}); });
} }
updateForm(entity: Resource) { updateForm(entity: Resource): void {
if (this.isEdit) { this.entityForm.patchValue(entity);
this.entityForm.get('resourceType').disable({emitEvent: false}); }
if (entity.resourceType !== ResourceType.JS_MODULE) {
this.entityForm.get('fileName').disable({emitEvent: false}); override updateFormState(): void {
super.updateFormState();
if (this.isEdit && this.entityForm) {
this.entityForm.get('resourceType').disable({ emitEvent: false });
if (this.entityForm.get('resourceType').value !== ResourceType.JS_MODULE) {
this.entityForm.get('fileName').disable({ emitEvent: false });
} }
} }
this.entityForm.patchValue({
resourceType: entity.resourceType,
fileName: entity.fileName,
title: entity.title,
data: entity.data
});
} }
prepareFormValue(formValue: Resource): Resource { prepareFormValue(formValue: Resource): Resource {
@ -109,7 +107,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
return super.prepareFormValue(formValue); return super.prepareFormValue(formValue);
} }
getAllowedExtensions() { getAllowedExtensions(): string {
try { try {
return ResourceTypeExtension.get(this.entityForm.get('resourceType').value); return ResourceTypeExtension.get(this.entityForm.get('resourceType').value);
} catch (e) { } catch (e) {
@ -117,7 +115,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
} }
} }
getAcceptType() { getAcceptType(): string {
try { try {
return ResourceTypeMIMETypes.get(this.entityForm.get('resourceType').value); return ResourceTypeMIMETypes.get(this.entityForm.get('resourceType').value);
} catch (e) { } catch (e) {
@ -129,7 +127,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
return window.btoa(data); return window.btoa(data);
} }
onResourceIdCopied() { onResourceIdCopied(): void {
this.store.dispatch(new ActionNotificationShow( this.store.dispatch(new ActionNotificationShow(
{ {
message: this.translate.instant('resource.idCopiedMessage'), message: this.translate.instant('resource.idCopiedMessage'),

13
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -121,6 +121,19 @@
</tb-slack-conversation-autocomplete> </tb-slack-conversation-autocomplete>
</section> </section>
<section *ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetType.MICROSOFT_TEAMS"> <section *ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetType.MICROSOFT_TEAMS">
<section class="tb-form-row" style="margin-bottom: 16px">
<mat-slide-toggle class="mat-slide margin" formControlName="useOldApi">
{{ "notification.use-old-api" | translate }}
</mat-slide-toggle>
<a mat-icon-button
href="https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/"
target="_blank"
matTooltip="{{ 'notification.use-deprecated-webhook-connectors' | translate }}"
matTooltipPosition="above"
class="tb-mat-20">
<mat-icon>open_in_new</mat-icon>
</a>
</section>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.webhook-url</mat-label> <mat-label translate>notification.webhook-url</mat-label>
<input matInput formControlName="webhookUrl"> <input matInput formControlName="webhookUrl">

8
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts

@ -32,7 +32,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { deepTrim, isDefinedAndNotNull } from '@core/utils'; import { deepTrim, isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
@ -100,6 +100,7 @@ export class RecipientNotificationDialogComponent extends
conversation: [{value: '', disabled: true}, Validators.required], conversation: [{value: '', disabled: true}, Validators.required],
webhookUrl: [{value: '', disabled: true}, Validators.required], webhookUrl: [{value: '', disabled: true}, Validators.required],
channelName: [{value: '', disabled: true}, Validators.required], channelName: [{value: '', disabled: true}, Validators.required],
useOldApi: [{value: !this.isAdd, disabled: true}],
description: [null] description: [null]
}) })
}); });
@ -120,6 +121,7 @@ export class RecipientNotificationDialogComponent extends
case NotificationTargetType.MICROSOFT_TEAMS: case NotificationTargetType.MICROSOFT_TEAMS:
this.targetNotificationForm.get('configuration.webhookUrl').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.webhookUrl').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.channelName').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.channelName').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.useOldApi').enable({emitEvent: false});
break; break;
} }
this.targetNotificationForm.get('configuration.type').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.type').enable({emitEvent: false});
@ -169,6 +171,10 @@ export class RecipientNotificationDialogComponent extends
this.targetNotificationForm.get('configuration.usersFilter.filterByTenants') this.targetNotificationForm.get('configuration.usersFilter.filterByTenants')
.patchValue(!Array.isArray(this.data.target.configuration.usersFilter.tenantProfilesIds), {onlySelf: true}); .patchValue(!Array.isArray(this.data.target.configuration.usersFilter.tenantProfilesIds), {onlySelf: true});
} }
if (data.target.configuration.type === NotificationTargetType.MICROSOFT_TEAMS
&& isUndefinedOrNull(this.data.target.configuration.useOldApi)) {
this.targetNotificationForm.get('configuration.useOldApi').patchValue(true, {emitEvent: false});
}
} }
} }

1
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html

@ -30,6 +30,7 @@
headerHeightPx="64" headerHeightPx="64"
headerTitle="{{symbolData?.imageResource?.title}}"> headerTitle="{{symbolData?.imageResource?.title}}">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div tb-help="scadaSymbolDev"></div>
<button mat-stroked-button <button mat-stroked-button
[disabled]="scadaSymbolFormGroup.invalid" [disabled]="scadaSymbolFormGroup.invalid"
(click)="createWidget()"> (click)="createWidget()">

9
ui-ngx/src/app/shared/components/image/image-gallery.component.html

@ -22,7 +22,10 @@
[fxShow]="!textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())" [ngClass.lt-lg]="{'multi-row': !isSysAdmin}"> [fxShow]="!textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())" [ngClass.lt-lg]="{'multi-row': !isSysAdmin}">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span fxHide fxShow.gt-sm class="tb-images-title">{{ (isScada ? 'scada.symbols' : 'image.gallery') | translate }}</span> <div fxHide fxShow.gt-sm class="tb-images-title">
<div tbTruncateWithTooltip>{{ (isScada ? 'scada.symbols' : 'image.gallery') | translate }}</div>
<div *ngIf="isScada" tb-help="scada"></div>
</div>
<div class="tb-images-view-type-toolbar" <div class="tb-images-view-type-toolbar"
fxLayout="row" fxLayout="row"
fxLayoutAlign="start center"> fxLayoutAlign="start center">
@ -72,7 +75,7 @@
matTooltipPosition="above"> matTooltipPosition="above">
<tb-icon>mdi:file-import</tb-icon> <tb-icon>mdi:file-import</tb-icon>
</button> </button>
<button fxHide fxShow.lt-lg <button [fxHide]="!(dialogMode && isScada)" fxShow.lt-lg
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
mat-icon-button mat-icon-button
color="primary" color="primary"
@ -82,7 +85,7 @@
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</section> </section>
<button fxHide fxShow.gt-md <button fxHide [fxShow.gt-md]="!(dialogMode && isScada)"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
mat-flat-button mat-flat-button
color="primary" color="primary"

5
ui-ngx/src/app/shared/components/image/image-gallery.component.scss

@ -54,9 +54,8 @@ $tb-button-selected-color: rgb(255, 110, 64) !default;
.tb-images-title { .tb-images-title {
padding-right: 20px; padding-right: 20px;
white-space: nowrap; display: flex;
overflow: hidden; align-items: center;
text-overflow: ellipsis;
} }
.tb-images-view-type-toolbar { .tb-images-view-type-toolbar {

30
ui-ngx/src/app/shared/components/time/datapoints-limit.component.html

@ -0,0 +1,30 @@
<!--
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.
-->
<div [formGroup]="datapointsLimitFormGroup" class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center">
<mat-slider fxFlex
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}">
<input matSliderThumb formControlName="limit" [value]="datapointsLimitFormGroup.get('limit').value"/>
</mat-slider>
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic" appearance="outline">
<input matInput formControlName="limit" type="number" step="1"
[value]="datapointsLimitFormGroup.get('limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
</div>

38
ui-ngx/src/app/shared/components/time/datapoints-limit.component.scss

@ -0,0 +1,38 @@
/**
* 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 "../../../../scss/constants";
.limit-slider-container {
width: 100%;
.limit-slider-value {
margin-left: 16px;
min-width: 25px;
max-width: 106px;
}
mat-form-field input[type=number] {
text-align: center;
}
}
@media #{$mat-gt-sm} {
.limit-slider-container {
> label {
margin-right: 16px;
width: min-content;
max-width: 40%;
}
}
}

160
ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts

@ -0,0 +1,160 @@
///
/// 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, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TimeService } from '@core/services/time.service';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-datapoints-limit',
templateUrl: './datapoints-limit.component.html',
styleUrls: ['./datapoints-limit.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatapointsLimitComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DatapointsLimitComponent),
multi: true
}
]
})
export class DatapointsLimitComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
datapointsLimitFormGroup: FormGroup;
modelValue: number | null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
this.updateValidators();
}
}
@Input()
disabled: boolean;
private propagateChange = (v: any) => { };
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private timeService: TimeService) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.datapointsLimitFormGroup = this.fb.group({
limit: [null, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())]]
});
this.datapointsLimitFormGroup.get('limit').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
updateValidators() {
if (this.datapointsLimitFormGroup) {
if (this.required) {
this.datapointsLimitFormGroup.get('limit').addValidators(Validators.required);
} else {
this.datapointsLimitFormGroup.get('limit').removeValidators(Validators.required);
}
this.datapointsLimitFormGroup.get('limit').updateValueAndValidity();
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.datapointsLimitFormGroup.disable({emitEvent: false});
} else {
this.datapointsLimitFormGroup.enable({emitEvent: false});
}
}
private checkLimit(limit?: number): number {
if (!limit || limit < this.minDatapointsLimit()) {
return this.minDatapointsLimit();
} else if (limit > this.maxDatapointsLimit()) {
return this.maxDatapointsLimit();
}
return limit;
}
writeValue(value: number | null): void {
this.modelValue = this.checkLimit(value);
this.datapointsLimitFormGroup.patchValue(
{ limit: this.modelValue }, {emitEvent: false}
);
}
updateView(value: number | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
validate(): ValidationErrors {
return this.datapointsLimitFormGroup.get('limit').valid ? null : {
datapointsLimitFormGroup: false,
};
}
minDatapointsLimit() {
return this.timeService.getMinDatapointsLimit();
}
maxDatapointsLimit() {
return this.timeService.getMaxDatapointsLimit();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

36
ui-ngx/src/app/shared/components/time/datetime-period.component.html

@ -15,35 +15,17 @@
limitations under the License. limitations under the License.
--> -->
<section fxLayout="column" fxLayoutAlign="start stretch"> <section class="tb-form-row column-xs no-border no-padding tb-standard-fields">
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px" <mat-form-field class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0"> <mat-label translate>datetime.from</mat-label>
<mat-form-field> <mat-datetimepicker-toggle [for]="startDatePicker" matSuffix></mat-datetimepicker-toggle>
<mat-label translate>datetime.date-from</mat-label> <mat-datetimepicker #startDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDatePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()"> <input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>datetime.time-from</mat-label> <mat-label translate>datetime.to</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="endDatePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
<mat-form-field>
<mat-label translate>datetime.date-to</mat-label>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()"> <input matInput [disabled]="disabled" [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()">
</mat-form-field> </mat-form-field>
<mat-form-field>
<mat-label translate>datetime.time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="endDate" [matDatetimepicker]="endTimePicker" (ngModelChange)="onEndDateChange()">
</mat-form-field>
</section>
</section> </section>

11
ui-ngx/src/app/shared/components/time/datetime-period.component.scss

@ -13,14 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../scss/constants';
:host ::ng-deep { :host {
.mat-mdc-form-field-infix { .tb-form-row {
width: 100px; gap: 8px;
@media #{$mat-xs} {
width: 100%;
}
} }
} }

7
ui-ngx/src/app/shared/components/time/datetime-period.component.ts

@ -17,6 +17,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FixedWindow } from '@shared/models/time/time.models'; import { FixedWindow } from '@shared/models/time/time.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({ @Component({
selector: 'tb-datetime-period', selector: 'tb-datetime-period',
@ -34,6 +35,12 @@ export class DatetimePeriodComponent implements OnInit, ControlValueAccessor {
@Input() disabled: boolean; @Input() disabled: boolean;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
modelValue: FixedWindow; modelValue: FixedWindow;
startDate: Date; startDate: Date;

4
ui-ngx/src/app/shared/components/time/quick-time-interval.component.html

@ -16,8 +16,8 @@
--> -->
<section class="interval-section" fxLayout="row" fxFlex> <section class="interval-section" fxLayout="row" fxFlex>
<mat-form-field fxFlex> <mat-form-field fxFlex [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timewindow.interval</mat-label> <mat-label *ngIf="displayLabel" translate>timewindow.interval</mat-label>
<mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()"> <mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()">
<mat-option *ngFor="let interval of intervals" [value]="interval"> <mat-option *ngFor="let interval of intervals" [value]="interval">
{{ timeIntervalTranslationMap.get(interval) | translate}} {{ timeIntervalTranslationMap.get(interval) | translate}}

12
ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts

@ -17,6 +17,8 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models'; import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-quick-time-interval', selector: 'tb-quick-time-interval',
@ -39,10 +41,20 @@ export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor
rendered = false; rendered = false;
@Input()
@coerceBoolean()
displayLabel = true;
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() onlyCurrentInterval = false; @Input() onlyCurrentInterval = false;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
private propagateChange = (_: any) => {}; private propagateChange = (_: any) => {};
constructor() { constructor() {

55
ui-ngx/src/app/shared/components/time/timeinterval.component.html

@ -15,43 +15,40 @@
limitations under the License. limitations under the License.
--> -->
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="8px"> <section class="tb-form-panel no-border no-padding" [formGroup]="timeintervalFormGroup">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit"> <section class="tb-form-row column-xs no-border no-padding tb-standard-fields">
<label class="tb-small hide-label" translate>timewindow.hide</label> <ng-content></ng-content>
<mat-checkbox [(ngModel)]="hideFlag" (ngModelChange)="onHideFlagChange()"></mat-checkbox> <mat-form-field fxFlex class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label *ngIf="predefinedName" translate>{{ predefinedName }}</mat-label>
<mat-select formControlName="interval">
<mat-option *ngFor="let interval of intervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
</section> </section>
<section class="interval-section" fxLayout="column" fxFlex [fxShow]="advanced && (isEdit || !hideFlag)"> <section class="tb-form-row column-xs no-border no-padding tb-standard-fields advanced-input"
<section fxLayout="row wrap" fxLayoutAlign="start start" fxFlex fxLayoutGap="6px"> formGroupName="customInterval"
<mat-form-field class="number-input"> [fxShow]="advanced">
<div class="tb-flex">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.days</mat-label> <mat-label translate>timeinterval.days</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" min="0" [(ngModel)]="days" (ngModelChange)="onTimeInputChange('days')"/> <input matInput type="number" step="1" min="0" formControlName="days" />
</mat-form-field> </mat-form-field>
<mat-form-field class="number-input"> <mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.hours</mat-label> <mat-label translate>timeinterval.hours</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" [(ngModel)]="hours" (ngModelChange)="onTimeInputChange('hours')"/> <input matInput type="number" step="1" formControlName="hours" />
</mat-form-field> </mat-form-field>
<mat-form-field class="number-input"> </div>
<div class="tb-flex">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.minutes</mat-label> <mat-label translate>timeinterval.minutes</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" [(ngModel)]="mins" (ngModelChange)="onTimeInputChange('mins')"/> <input matInput type="number" step="1" formControlName="mins" />
</mat-form-field> </mat-form-field>
<mat-form-field class="number-input"> <mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>timeinterval.seconds</mat-label> <mat-label translate>timeinterval.seconds</mat-label>
<input matInput [disabled]="hideFlag || disabled" type="number" step="1" [(ngModel)]="secs" (ngModelChange)="onTimeInputChange('secs')"/> <input matInput type="number" step="1" formControlName="secs" />
</mat-form-field> </mat-form-field>
</section> </div>
</section>
<section fxLayout="row" fxFlex [fxShow]="!advanced && (isEdit || !hideFlag)">
<mat-form-field fxFlex [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="predefinedName" translate>{{ predefinedName }}</mat-label>
<mat-select [disabled]="hideFlag || disabled" [(ngModel)]="interval" (ngModelChange)="onIntervalChange()" style="min-width: 150px;">
<mat-option *ngFor="let interval of intervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
<section fxLayout="column" fxLayoutAlign="center center" [fxShow]="(isEdit || !hideFlag) && !disabledAdvanced">
<label class="tb-small advanced-label" translate>timeinterval.advanced</label>
<mat-slide-toggle [disabled]="hideFlag || disabled" class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle>
</section> </section>
</section> </section>

15
ui-ngx/src/app/shared/components/time/timeinterval.component.scss

@ -16,19 +16,8 @@
@import '../../../../scss/constants'; @import '../../../../scss/constants';
:host { :host {
min-width: 355px; .advanced-input {
gap: 8px;
.advanced-switch {
margin-bottom: 16px;
}
.advanced-label {
margin: 5px 0;
}
.hide-label {
margin-bottom: 5px;
margin-right: 5px;
} }
@media #{$mat-xs} { @media #{$mat-xs} {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save