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": {
"type": "CONSTANT",
"constantValue": false,
"constantValue": true,
"valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
}
},
@ -127,7 +127,7 @@
},
"valueToData": {
"type": "CONSTANT",
"constantValue": true,
"constantValue": false,
"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;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
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.web.client.RestTemplate;
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.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
@ -56,17 +59,93 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
@Override
public void sendNotification(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
Message message = new Message();
message.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor()));
if (targetConfig.getUseOldApi() == null || Boolean.TRUE.equals(targetConfig.getUseOldApi())) {
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())) {
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 {
message.setSummary(processedTemplate.getSubject());
Message.Section section = new Message.Section();
TeamsAdaptiveCard.TextBlock subjectTextBlock = new TeamsAdaptiveCard.TextBlock();
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.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();
if (button != null && button.isEnabled()) {
String uri;
@ -99,17 +178,9 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
} else {
uri = button.getLink();
}
if (StringUtils.isNotBlank(uri) && button.getText() != null) {
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 uri;
}
restTemplate.postForEntity(targetConfig.getWebhookUrl(), message, String.class);
return null;
}
@Override
@ -121,74 +192,4 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
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()),
new ProvisionDeviceProfileCredentials(
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceKey(),
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret())));
requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()),
requestMsg.getGateway()));
} catch (ProvisionFailedException e) {
return getTransportApiResponseMsg(new DeviceCredentials(), TransportProtos.ResponseStatus.valueOf(e.getMessage()));
}
@ -665,7 +666,7 @@ public class DefaultTransportApiService implements TransportApiService {
private ProvisionRequest createProvisionRequest(String certificateValue) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
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) {
return new ProvisionRequest(null, DeviceCredentialsType.X509_CERTIFICATE,
new ProvisionDeviceCredentialsData(null, null, null, null, certificateValue),
null);
null, null);
}
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpEntity;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.web.client.RestTemplate;
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.service.DaoSqlTest;
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 java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -752,7 +757,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
}
@Test
public void testMicrosoftTeamsNotifications() throws Exception {
public void testMicrosoftTeamsNotificationsWithOfficeConnector() throws URISyntaxException {
RestTemplate restTemplate = mock(RestTemplate.class);
microsoftTeamsNotificationChannel.setRestTemplate(restTemplate);
@ -760,6 +765,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
var targetConfig = new MicrosoftTeamsNotificationTargetConfig();
targetConfig.setWebhookUrl(webhookUrl);
targetConfig.setChannelName("My channel");
targetConfig.setUseOldApi(true);
NotificationTarget target = new NotificationTarget();
target.setName("Microsoft Teams channel");
target.setConfiguration(targetConfig);
@ -770,7 +776,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams);
template.setThemeColor("ff0000");
template.setThemeColor("#ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true);
button.setText("Button: " + templateParams);
@ -803,11 +809,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
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);
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";
assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor());
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);
}
@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
public void testMobileAppNotifications() throws Exception {
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.thingsboard.common.util.JacksonUtil;
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.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -67,6 +68,11 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
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 {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -222,16 +259,30 @@ public class CoapProvisionJsonDeviceTest extends AbstractCoapIntegrationTest {
}
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 {
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);
return client.postMethod(provisionRequestMsg.getBytes()).getPayload();
}
private String createTestProvisionMessage(String deviceCredentials) {
return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}";
protected String createTestProvisionMessage(String deviceCredentials, boolean isGateway) {
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;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResponse;
import org.junit.After;
@ -22,6 +23,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -74,6 +76,11 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
@ -124,6 +131,35 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
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 {
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -233,6 +269,10 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
}
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()
.setDeviceName("Test Provision device")
.setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN)
@ -241,7 +281,9 @@ public class CoapProvisionProtoDeviceTest extends AbstractCoapIntegrationTest {
ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey("testProvisionKey")
.setProvisionDeviceSecret("testProvisionSecret")
).build()
)
.setGateway(isGateway)
.build()
.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.springframework.beans.factory.annotation.Autowired;
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.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -68,6 +69,11 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
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 {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -264,11 +301,19 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
}
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 {
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();
client.connectAndWait("provision");
MqttTestCallback onProvisionCallback = new MqttTestSubscribeOnTopicCallback(DEVICE_PROVISION_RESPONSE_TOPIC);
@ -280,7 +325,12 @@ public class MqttProvisionJsonDeviceTest extends AbstractMqttIntegrationTest {
return onProvisionCallback.getPayloadBytes();
}
protected String createTestProvisionMessage(String deviceCredentials) {
return "{\"deviceName\":\"Test Provision device\",\"provisionDeviceKey\":\"testProvisionKey\", \"provisionDeviceSecret\":\"testProvisionSecret\"" + deviceCredentials + "}";
protected String createTestProvisionMessage(String deviceCredentials, boolean isGateway) {
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;
import com.fasterxml.jackson.databind.JsonNode;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.TransportPayloadType;
@ -76,6 +78,11 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
processTestProvisioningCreateNewDeviceWithoutCredentials();
}
@Test
public void testProvisioningCreateNewGatewayDevice() throws Exception {
processTestProvisioningCreateNewGatewayDevice();
}
@Test
public void testProvisioningCreateNewDeviceWithAccessToken() throws Exception {
processTestProvisioningCreateNewDeviceWithAccessToken();
@ -130,6 +137,36 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
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 {
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Provision device3")
@ -280,6 +317,10 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
}
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()
.setDeviceName("Test Provision device")
.setCredentialsType(credentialsType != null ? credentialsType : CredentialsType.ACCESS_TOKEN)
@ -288,7 +329,9 @@ public class MqttProvisionProtoDeviceTest extends AbstractMqttIntegrationTest {
ProvisionDeviceCredentialsMsg.newBuilder()
.setProvisionDeviceKey("testProvisionKey")
.setProvisionDeviceSecret("testProvisionSecret")
).build()
)
.setGateway(isGateway)
.build()
.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 ProvisionDeviceCredentialsData credentialsData;
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;
@NotEmpty
private String channelName;
private Boolean useOldApi = Boolean.TRUE;
@Override
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(
getStrValue(jo, DataConstants.PROVISION_KEY, true),
getStrValue(jo, DataConstants.PROVISION_SECRET, true)))
.setGateway(jo.has(DataConstants.GATEWAY_PARAMETER) && jo.get(DataConstants.GATEWAY_PARAMETER).getAsBoolean())
.build();
}

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

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

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

@ -15,6 +15,7 @@
*/
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.ListenableFuture;
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.server.cache.device.DeviceCacheEvictEvent;
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.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo;
@ -554,6 +556,11 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
device.setName(provisionRequest.getDeviceName());
device.setType(profile.getName());
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);
if (!StringUtils.isEmpty(provisionRequest.getCredentialsData().getToken()) ||
!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 javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ -265,6 +267,94 @@ public class ImageUtils {
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
@AllArgsConstructor
@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.BeforeMethod;
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.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
@ -156,6 +157,46 @@ public class HttpClientTest extends AbstractContainerTest {
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
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 historyApiFallback from 'connect-history-api-fallback';
import { Socket } from 'net';
import { RequestHandler } from 'serve-static';
const logger = _logger('main');
@ -99,7 +100,19 @@ let connections: Socket[] = [];
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, () => {
logger.info('==> 🌎 Listening on port %s.', bindPort);

2
packaging/js/scripts/init/template

@ -183,7 +183,7 @@ restart() {
stop && start
}
orce_reload() {
force_reload() {
working_dir=$(dirname "$mainfile")
pushd "$working_dir" > /dev/null
[[ -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;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.TbContext;
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.TbNodeConnectionType;
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.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.HashMap;
import java.util.Map;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "delay (deprecated)",
version = 1,
configClazz = TbMsgDelayNodeConfiguration.class,
nodeDescription = "Delays incoming message (deprecated)",
nodeDetails = "Delays messages for a configurable period. " +
@ -50,13 +60,22 @@ import java.util.concurrent.TimeUnit;
)
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 Map<UUID, TbMsg> pendingMsgs;
private ConcurrentMap<UUID, TbMsg> pendingMsgs;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
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
@ -67,7 +86,7 @@ public class TbMsgDelayNode implements TbNode {
ctx.enqueueForTellNext(
TbMsg.newMsg(
pendingMsg.getQueueName(),
pendingMsg.getType(),
pendingMsg.getInternalType(),
pendingMsg.getOriginator(),
pendingMsg.getCustomerId(),
pendingMsg.getMetaData(),
@ -89,25 +108,69 @@ public class TbMsgDelayNode implements TbNode {
}
private long getDelay(TbMsg msg) {
int periodInSeconds;
if (config.isUseMetadataPeriodInSecondsPatterns()) {
if (isParsable(msg, config.getPeriodInSecondsPattern())) {
periodInSeconds = Integer.parseInt(TbNodeUtils.processPattern(config.getPeriodInSecondsPattern(), msg));
} else {
throw new RuntimeException("Can't parse period in seconds from metadata using pattern: " + config.getPeriodInSecondsPattern());
String timeUnitPattern = TbNodeUtils.processPattern(config.getTimeUnit(), msg);
String periodPattern = TbNodeUtils.processPattern(config.getPeriod(), msg);
try {
TimeUnit timeUnit = TimeUnit.valueOf(timeUnitPattern.toUpperCase());
if (!supportedTimeUnits.contains(timeUnit)) {
throw new RuntimeException("Time unit '" + timeUnit + "' is not supported! " +
"Only " + supportedTimeUnitsStr + " are supported.");
}
} else {
periodInSeconds = config.getPeriodInSeconds();
int period = Integer.parseInt(periodPattern);
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
public void destroy() {
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;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.concurrent.TimeUnit;
@Data
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 String periodInSecondsPattern;
private boolean useMetadataPeriodInSecondsPatterns;
@Override
public TbMsgDelayNodeConfiguration defaultConfiguration() {
TbMsgDelayNodeConfiguration configuration = new TbMsgDelayNodeConfiguration();
configuration.setPeriodInSeconds(60);
configuration.setPeriod("60");
configuration.setTimeUnit(TimeUnit.SECONDS.name());
configuration.setMaxPendingMsgs(1000);
configuration.setUseMetadataPeriodInSecondsPatterns(false);
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.TbMsgMetaData;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.transport.ProxyProvider;
import javax.net.ssl.SSLException;
@ -95,7 +96,12 @@ public class TbHttpClient {
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))
.doOnConnected(c ->
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 {
int systemMaxInMemoryBufferSizeInKb = 25000;
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> {
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> {

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

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

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

@ -19,13 +19,18 @@ import {
AggregationType,
DAY,
defaultTimeIntervals,
defaultTimewindow, Interval, IntervalMath,
defaultTimewindow,
getDefaultTimezoneInfo,
Interval,
IntervalMath,
SECOND,
TimeInterval,
Timewindow
Timewindow,
TimezoneInfo
} from '@shared/models/time/time.models';
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 MAX_INTERVAL = 365 * 20 * DAY;
@ -41,8 +46,11 @@ export class TimeService {
private maxDatapointsLimit = MAX_DATAPOINTS_LIMIT;
private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
constructor(
private http: HttpClient
private http: HttpClient,
private translate: TranslateService
) {}
public setMaxDatapointsLimit(limit: number) {
@ -161,4 +169,13 @@ export class TimeService {
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 { 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;
@ -466,6 +469,9 @@ class ModulesMap implements IModulesMap {
'@shared/components/time/datetime-period.component': DatetimePeriodComponent,
'@shared/components/time/datetime.component': DatetimeComponent,
'@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/dashboard-autocomplete.component': DashboardAutocompleteComponent,
'@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(
map(value => this.parseBreakpointsResponse(value.breakpoints)),
tap((value) => {
this.dashboardCtx.breakpoint = value.id;
this.changeMobileSize.next(this.isMobileSize(value));
this.dashboardCtx.breakpoint = value ? value.id : 'default';
this.changeMobileSize.next(value ? this.isMobileSize(value) : false);
}),
distinctUntilChanged((_, next) => {
if (this.layouts.right.show || this.isEdit) {
return true;
}
let nextBreakpointConfiguration: BreakpointId = 'default';
if (!!this.layouts.main.layoutCtx.layoutData?.[next.id]) {
if (next && !!this.layouts.main.layoutCtx.layoutData?.[next.id]) {
nextBreakpointConfiguration = next.id;
}
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 {
ConfigurationModes,
GatewayConnector, LocalLogsConfigs, LogSavingPeriod,
GatewayConnector,
LocalLogsConfigs,
LogSavingPeriod,
SecurityTypes,
StorageTypes
} 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 {
DeviceInfoType,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
SourceTypes,
OPCUaSourceType,
SourceType,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@ -81,7 +81,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
required = false;
@Input()
sourceTypes: Array<SourceTypes | OPCUaSourceTypes> = Object.values(SourceTypes);
sourceTypes: Array<SourceType | OPCUaSourceType> = Object.values(SourceType);
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,
mappingValueTypesMap,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
OPCUaSourceType,
RpcMethodsMapping,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -73,7 +73,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
keysType: MappingKeysType;
@Input()
valueTypeKeys: Array<MappingValueType | OPCUaSourceTypes> = Object.values(MappingValueType);
valueTypeKeys: Array<MappingValueType | OPCUaSourceType> = Object.values(MappingValueType);
@Input()
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,
} from '@angular/forms';
import {
AttributeUpdate,
ConnectorMapping,
ConnectRequest,
ConverterConnectorMapping,
ConvertorTypeTranslationsMap,
DeviceConnectorMapping,
DisconnectRequest,
MappingInfo,
MappingType,
MappingTypeTranslationsMap,
MappingValue,
RequestMappingData,
RequestMappingValue,
RequestType,
RequestTypesTranslationsMap
RequestTypesTranslationsMap,
ServerSideRpc
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
@ -259,16 +263,17 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
};
case MappingType.REQUESTS:
let details: string;
if ((value as RequestMappingData).requestType === RequestType.ATTRIBUTE_UPDATE) {
details = (value as RequestMappingData).requestValue.attributeFilter;
} else if ((value as RequestMappingData).requestType === RequestType.SERVER_SIDE_RPC) {
details = (value as RequestMappingData).requestValue.methodFilter;
const requestValue = value as RequestMappingValue;
if (requestValue.requestType === RequestType.ATTRIBUTE_UPDATE) {
details = (requestValue.requestValue as AttributeUpdate).attributeFilter;
} else if (requestValue.requestType === RequestType.SERVER_SIDE_RPC) {
details = (requestValue.requestValue as ServerSideRpc).methodFilter;
} else {
details = (value as RequestMappingData).requestValue.topicFilter;
details = (requestValue.requestValue as ConnectRequest | DisconnectRequest).topicFilter;
}
return {
requestType: (value as RequestMappingData).requestType,
type: this.translate.instant(RequestTypesTranslationsMap.get((value as RequestMappingData).requestType)),
requestType: (value as RequestMappingValue).requestType,
type: this.translate.instant(RequestTypesTranslationsMap.get((value as RequestMappingValue).requestType)),
details
};
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.
///
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 {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
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';
ModbusBasicConfig_v3_5_2,
ModbusMasterConfig,
ModbusSlave
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
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 { 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({
selector: 'tb-modbus-basic-config',
@ -63,80 +56,19 @@ import { isEqual } from '@core/utils';
],
styleUrls: ['./modbus-basic-config.component.scss'],
})
export class ModbusBasicConfigComponent extends ModbusBasicConfigDirective<ModbusBasicConfig_v3_5_2> {
export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>;
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 ?? {},
protected override mapConfigToFormValue({ master, slave }: ModbusBasicConfig_v3_5_2): ModbusBasicConfig_v3_5_2 {
return {
master: master?.slaves ? master : { slaves: [] } as ModbusMasterConfig,
slave: slave ?? {} as ModbusSlave,
};
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 {
if (isEnabled) {
this.basicFormGroup.get('slave').enable({emitEvent: false});
} else {
this.basicFormGroup.get('slave').disable({emitEvent: false});
}
protected override getMappedValue(value: ModbusBasicConfig_v3_5_2): ModbusBasicConfig_v3_5_2 {
return {
master: value.master,
slave: value.slave,
};
}
}

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>
<ng-template #tagName>
<div class="tb-flex">
<div class="title-container" tbTruncateWithTooltip>{{ 'gateway.key' | translate }}: {{ keyControl.get('tag').value }}</div>
<div class="title-container">{{ 'gateway.address' | translate }}: {{ keyControl.get('address').value }}</div>
<div class="title-container">{{ 'gateway.type' | translate }}: {{ keyControl.get('type').value }}</div>
<div class="title-container tb-flex">{{ 'gateway.key' | translate }}:
<span class="key-label" tbTruncateWithTooltip>{{ keyControl.get('tag').value }}</span>
</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>
</ng-template>
</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;
}
.key-label {
font-weight: 400;
}
.key-panel {
height: 500px;
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>
</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">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<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]],
sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG],
wordOrder: [ModbusOrderType.BIG],
security: [],
identity: this.fb.group({
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
@ -243,6 +244,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
pollPeriod = 5000,
sendDataToThingsBoard = false,
byteOrder = ModbusOrderType.BIG,
wordOrder = ModbusOrderType.BIG,
security = {},
identity = {
vendorName: '',
@ -266,6 +268,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
pollPeriod,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder,
wordOrder,
security,
identity,
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>
</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="tb-flex no-gap">
<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';
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>();

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
color="primary"
[disabled]="disabled"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesButton
(click)="manageKeys($event, attributesButton, ModbusValueKey.ATTRIBUTES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -69,6 +71,8 @@
mat-icon-button
color="primary"
[disabled]="disabled"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#telemetryButton
(click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -90,6 +94,8 @@
mat-icon-button
[disabled]="disabled"
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, ModbusValueKey.ATTRIBUTES_UPDATES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -111,6 +117,8 @@
mat-icon-button
color="primary"
[disabled]="disabled"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#rpcRequestsButton
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)">
<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 label="{{ 'gateway.data-mapping' | translate }}*">
<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>
</mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<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>
</mat-tab>
<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 { Subject } from 'rxjs';
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({
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>
</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 tbTruncateWithTooltip>{{ 'gateway.poll-period' | translate }}</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.
///
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -39,6 +39,7 @@ import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import { HOUR } from '@shared/models/time/time.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-opc-server-config',
@ -64,7 +65,11 @@ import { HOUR } from '@shared/models/time/time.models';
SecurityConfigComponent,
]
})
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy {
@Input()
@coerceBoolean()
hideNewFields: boolean = false;
securityPolicyTypes = SecurityPolicyTypes;
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 {
this.destroy$.next();
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>
</mat-tab>
<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 label="{{ 'gateway.data-mapping' | translate }}*">
<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,
ConnectorType,
CreatedConnectorConfigData,
GatewayConnector,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
getDefaultConfig,
GatewayVersion,
GatewayVersionedDefaultConfig,
noLeadTrailSpacesRegex
} 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 { takeUntil, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants';
@ -42,7 +44,8 @@ import { helpBaseUrl } from '@shared/models/constants';
styleUrls: ['./add-connector-dialog.component.scss'],
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;
@ -95,8 +98,15 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
this.submitted = true;
const value = this.connectorForm.getRawValue();
if (value.useDefaults) {
getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => {
value.configurationJson = defaultConfig;
this.getDefaultConfig(value.type).subscribe((defaultConfig: GatewayVersionedDefaultConfig) => {
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) {
this.dialogRef.close(value);
}
@ -130,4 +140,8 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
takeUntil(this.destroy$),
).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"
mat-icon-button
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesButton
(click)="manageKeys($event, attributesButton, MappingKeysType.ATTRIBUTES)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -130,6 +132,8 @@
<button type="button"
mat-icon-button
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#telemetryButton
(click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -175,6 +179,8 @@
<button type="button"
mat-icon-button
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#keysButton
(click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -666,6 +672,8 @@
<button type="button"
mat-icon-button
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#opcAttributesButton
(click)="manageKeys($event, opcAttributesButton, MappingKeysType.ATTRIBUTES)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -686,6 +694,8 @@
<button type="button"
mat-icon-button
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#opcTelemetryButton
(click)="manageKeys($event, opcTelemetryButton, MappingKeysType.TIMESERIES)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -706,6 +716,8 @@
<button type="button"
mat-icon-button
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, MappingKeysType.ATTRIBUTES_UPDATES)">
<tb-icon matButtonIcon>edit</tb-icon>
@ -726,6 +738,8 @@
<button type="button"
mat-icon-button
color="primary"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#rpcMethodsButton
(click)="manageKeys($event, rpcMethodsButton, MappingKeysType.RPC_METHODS)">
<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,
MappingTypeTranslationsMap,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
OPCUaSourceType,
QualityTypes,
QualityTypeTranslationsMap,
RequestMappingData,
RequestMappingFormValue,
RequestType,
RequestTypesTranslationsMap,
RpcMethod,
ServerSideRPCType,
SourceTypes,
SourceType,
SourceTypeTranslationsMap,
Timeseries
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -82,10 +83,10 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
ConvertorTypeEnum = ConvertorType;
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap;
sourceTypes: SourceTypes[] = Object.values(SourceTypes);
OPCUaSourceTypes = Object.values(OPCUaSourceTypes) as Array<OPCUaSourceTypes>;
OPCUaSourceTypesEnum = OPCUaSourceTypes;
sourceTypesEnum = SourceTypes;
sourceTypes: SourceType[] = Object.values(SourceType);
OPCUaSourceTypes = Object.values(OPCUaSourceType) as Array<OPCUaSourceType>;
OPCUaSourceTypesEnum = OPCUaSourceType;
sourceTypesEnum = SourceType;
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
requestTypes: RequestType[] = Object.values(RequestType);
@ -230,8 +231,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType)
};
if (this.data.mappingType === MappingType.OPCUA) {
ctx.valueTypeKeys = Object.values(OPCUaSourceTypes);
ctx.valueTypeEnum = OPCUaSourceTypes;
ctx.valueTypeKeys = Object.values(OPCUaSourceType);
ctx.valueTypeEnum = OPCUaSourceType;
ctx.valueTypes = SourceTypeTranslationsMap;
}
this.keysPopupClosed = false;
@ -293,8 +294,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
requestType: this.data.value.requestType,
requestValue: {
[this.data.value.requestType]: this.data.value.requestValue
}
} as RequestMappingFormValue;
} as Record<RequestType, RequestMappingData>
};
default:
return this.data.value as DeviceConnectorMapping;
}
@ -349,10 +350,10 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
attributeRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: this.fb.group({
deviceNameExpressionSource: [SourceTypes.MSG, []],
deviceNameExpressionSource: [SourceType.MSG, []],
deviceNameExpression: ['', [Validators.required]],
}),
attributeNameExpressionSource: [SourceTypes.MSG, []],
attributeNameExpressionSource: [SourceType.MSG, []],
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -407,7 +408,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
private createOPCUAMappingForm(): void {
this.mappingForm = this.fb.group({
deviceNodeSource: [OPCUaSourceTypes.PATH, []],
deviceNodeSource: [OPCUaSourceType.PATH, []],
deviceNodePattern: ['', [Validators.required]],
deviceInfo: [{}, []],
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">
{{ initialConnector?.type ? GatewayConnectorTypesTranslatesMap.get(initialConnector.type) : '' }}
{{ 'gateway.configuration' | translate }}
<span class="version-placeholder" *ngIf="connectorForm.get('configVersion').value">v{{connectorForm.get('configVersion').value}}</span>
</div>
<tb-toggle-select *ngIf="initialConnector && allowBasicConfig.has(initialConnector.type)"
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">
<ng-container *ngIf="connectorForm.get('mode')?.value === ConnectorConfigurationModes.BASIC else defaultConfig">
<ng-container [ngSwitch]="initialConnector.type">
<tb-mqtt-basic-config *ngSwitchCase="ConnectorType.MQTT"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-mqtt-basic-config>
<tb-opc-ua-basic-config *ngSwitchCase="ConnectorType.OPCUA"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-opc-ua-basic-config>
<tb-modbus-basic-config *ngSwitchCase="ConnectorType.MODBUS"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-modbus-basic-config>
<ng-container *ngSwitchCase="ConnectorType.MQTT">
<tb-mqtt-basic-config
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
<ng-template #legacy>
<tb-mqtt-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.OPCUA">
<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-template #defaultConfig>

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

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

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

@ -59,12 +59,16 @@ import {
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
noLeadTrailSpacesRegex,
GatewayVersion,
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { debounceTime, filter, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core';
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 {
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 GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
readonly ConnectorConfigurationModes = ConfigurationModes;
readonly GatewayVersion = GatewayVersion;
pageLink: PageLink;
dataSource: MatTableDataSource<GatewayAttributeData>;
@ -106,6 +111,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
private gatewayVersion: string;
private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource;
@ -239,6 +245,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
delete value.class;
}
if (this.gatewayVersion && !value.configVersion) {
value.configVersion = this.gatewayVersion;
}
value.ts = Date.now();
return value;
@ -263,7 +273,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
isConnectorSynced(attribute: GatewayAttributeData) {
isConnectorSynced(attribute: GatewayAttributeData): boolean {
const connectorData = attribute.value;
if (!connectorData.ts || attribute.skipSync) {
return false;
@ -340,7 +350,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
class: '',
configuration: '',
configurationJson: {},
basicConfig: {}
basicConfig: {},
configVersion: ''
}, {emitEvent: false});
this.connectorForm.markAsPristine();
}
@ -483,7 +494,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { dataSourceData: this.dataSource.data }
data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed();
}
@ -526,7 +540,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]],
basicConfig: [{}]
basicConfig: [{}],
configVersion: [''],
});
this.connectorForm.disable();
}
@ -560,10 +575,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
forkJoin([
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 => {
this.activeConnectors = this.parseConnectors(attributes[0]);
this.inactiveConnectors = this.parseConnectors(attributes[1]);
this.gatewayVersion = attributes[2][0]?.value;
this.updateData(true);
});
@ -721,12 +738,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.enable();
}
const connectorState = {
const connectorState = GatewayConnectorVersionMappingUtil.getConfig({
configuration: '',
key: 'auto',
configurationJson: {} as ConnectorBaseConfig,
...connector,
};
}, this.gatewayVersion);
connectorState.basicConfig = connectorState.configurationJson;
this.initialConnector = connectorState;
@ -739,6 +756,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
case ConnectorType.OPCUA:
case ConnectorType.MODBUS:
this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
setTimeout(() => {
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
@ -760,7 +778,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
JSON.parse(clientConnectorData.value) : clientConnectorData.value;
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.
///
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { helpBaseUrl, ValueTypeData } from '@shared/models/constants';
import { AttributeData } from '@shared/models/telemetry/telemetry.models';
@ -115,21 +113,30 @@ export interface GatewayAttributeData extends AttributeData {
skipSync?: boolean;
}
export interface GatewayConnector {
export interface GatewayConnectorBase {
name: string;
type: ConnectorType;
configuration?: string;
configurationJson: ConnectorBaseConfig;
basicConfig?: ConnectorBaseConfig;
logLevel: string;
key?: string;
class?: string;
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 {
topicFilter: string;
QoS: string;
QoS: string | number;
converter: Converter;
}
@ -181,11 +188,20 @@ export interface ConnectorSecurity {
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 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 {
name: string;
@ -194,33 +210,64 @@ export interface ConnectorBaseInfo {
logLevel: GatewayLogLevel;
}
export interface MQTTBasicConfig {
dataMapping: ConverterConnectorMapping[];
requestsMapping: Record<RequestType, RequestMappingData[]> | RequestMappingData[];
export type MQTTBasicConfig = MQTTBasicConfig_v3_5_2 | MQTTLegacyBasicConfig;
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;
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[];
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;
slave: ModbusSlave;
}
export interface ModbusLegacyBasicConfig {
master: ModbusMasterConfig;
slave: ModbusLegacySlave;
}
export interface WorkersConfig {
maxNumberOfWorkers: number;
maxMessageNumberPerWorker: number;
}
interface DeviceInfo {
export interface ConnectorDeviceInfo {
deviceNameExpression: string;
deviceNameExpressionSource: string;
deviceNameExpressionSource: SourceType | OPCUaSourceType;
deviceProfileExpression: string;
deviceProfileExpressionSource: string;
deviceProfileExpressionSource: SourceType | OPCUaSourceType;
}
export interface Attribute {
@ -229,13 +276,23 @@ export interface Attribute {
value: string;
}
export interface LegacyAttribute {
key: string;
path: string;
}
export interface Timeseries {
key: string;
type: string;
value: string;
}
interface RpcArgument {
export interface LegacyTimeseries {
key: string;
path: string;
}
export interface RpcArgument {
type: string;
value: number;
}
@ -245,28 +302,59 @@ export interface RpcMethod {
arguments: RpcArgument[];
}
export interface LegacyRpcMethod {
method: string;
arguments: unknown[];
}
export interface AttributesUpdate {
key: string;
type: string;
value: string;
}
export interface LegacyDeviceAttributeUpdate {
attributeOnThingsBoard: string;
attributeOnDevice: string;
}
export interface Converter {
type: ConvertorType;
deviceNameJsonExpression: string;
deviceTypeJsonExpression: string;
deviceInfo?: ConnectorDeviceInfo;
sendDataOnlyOnChange: boolean;
timeout: number;
attributes: Attribute[];
timeseries: Timeseries[];
attributes?: Attribute[];
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 {
topicFilter: string;
subscriptionQos?: string;
subscriptionQos?: string | number;
converter: Converter;
}
export interface LegacyConverterConnectorMapping {
topicFilter: string;
subscriptionQos?: string | number;
converter: LegacyConverter;
}
export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'converter'> & {
converter: {
type: ConvertorType;
@ -275,14 +363,24 @@ export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'convert
export interface DeviceConnectorMapping {
deviceNodePattern: string;
deviceNodeSource: string;
deviceInfo: DeviceInfo;
deviceNodeSource: OPCUaSourceType;
deviceInfo: ConnectorDeviceInfo;
attributes?: Attribute[];
timeseries?: Timeseries[];
rpc_methods?: RpcMethod[];
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 {
MQTT = 'mqtt',
MODBUS = 'modbus',
@ -461,6 +559,7 @@ export interface GatewayLogData {
export interface AddConnectorConfigData {
dataSourceData: Array<any>;
gatewayVersion: string;
}
export interface CreatedConnectorConfigData {
@ -591,13 +690,13 @@ export const ConvertorTypeTranslationsMap = new Map<ConvertorType, string>(
]
);
export enum SourceTypes {
export enum SourceType {
MSG = 'message',
TOPIC = 'topic',
CONST = 'constant'
}
export enum OPCUaSourceTypes {
export enum OPCUaSourceType {
PATH = 'path',
IDENTIFIER = 'identifier',
CONST = 'constant'
@ -608,33 +707,116 @@ export enum DeviceInfoType {
PARTIAL = 'partial'
}
export const SourceTypeTranslationsMap = new Map<SourceTypes | OPCUaSourceTypes, string>(
export const SourceTypeTranslationsMap = new Map<SourceType | OPCUaSourceType, string>(
[
[SourceTypes.MSG, 'gateway.source-type.msg'],
[SourceTypes.TOPIC, 'gateway.source-type.topic'],
[SourceTypes.CONST, 'gateway.source-type.const'],
[OPCUaSourceTypes.PATH, 'gateway.source-type.path'],
[OPCUaSourceTypes.IDENTIFIER, 'gateway.source-type.identifier'],
[OPCUaSourceTypes.CONST, 'gateway.source-type.const']
[SourceType.MSG, 'gateway.source-type.msg'],
[SourceType.TOPIC, 'gateway.source-type.topic'],
[SourceType.CONST, 'gateway.source-type.const'],
[OPCUaSourceType.PATH, 'gateway.source-type.path'],
[OPCUaSourceType.IDENTIFIER, 'gateway.source-type.identifier'],
[OPCUaSourceType.CONST, 'gateway.source-type.const']
]
);
export interface RequestMappingData {
export interface RequestMappingValue {
requestType: RequestType;
requestValue: RequestDataItem;
requestValue: RequestMappingData;
}
export type RequestMappingFormValue = Omit<RequestMappingData, 'requestValue'> & {
requestValue: Record<RequestType, RequestDataItem>;
};
export interface RequestDataItem {
type: string;
details: string;
export interface RequestMappingFormValue {
requestType: RequestType;
methodFilter?: string;
attributeFilter?: string;
topicFilter?: string;
requestValue: Record<RequestType, RequestMappingData>;
}
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 {
@ -708,9 +890,6 @@ export enum ServerSideRPCType {
TWO_WAY = 'twoWay'
}
export const getDefaultConfig = (resourcesService: ResourcesService, type: string): Observable<any> =>
resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
export enum MappingValueType {
STRING = 'string',
INTEGER = 'integer',
@ -938,7 +1117,7 @@ export interface SlaveConfig {
pollPeriod: number;
unitId: number;
deviceName: string;
deviceType: string;
deviceType?: string;
sendDataOnlyOnChange: boolean;
connectAttemptTimeMs: number;
connectAttemptCount: number;
@ -984,14 +1163,26 @@ export interface ModbusSlave {
pollPeriod: number;
sendDataToThingsBoard: boolean;
byteOrder: ModbusOrderType;
wordOrder: ModbusOrderType;
identity: ModbusIdentity;
values: ModbusValuesState;
values?: ModbusValuesState;
port: string | number;
security: ModbusSecurity;
}
export interface ModbusLegacySlave extends Omit<ModbusSlave, 'values'> {
values?: ModbusLegacyRegisterValues;
}
export type ModbusValuesState = ModbusRegisterValues | ModbusValues;
export interface ModbusLegacyRegisterValues {
holding_registers: ModbusValues[];
coils_initializer: ModbusValues[];
input_registers: ModbusValues[];
discrete_inputs: ModbusValues[];
}
export interface ModbusRegisterValues {
holding_registers: 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 {
MappingValueType,
OPCUaSourceTypes,
SourceTypes
OPCUaSourceType,
SourceType
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Pipe({
@ -26,9 +26,9 @@ import {
standalone: true,
})
export class GatewayHelpLinkPipe implements PipeTransform {
transform(field: string, sourceType: SourceTypes | OPCUaSourceTypes, sourceTypes?: Array<SourceTypes | OPCUaSourceTypes | MappingValueType> ): string {
if (!sourceTypes || sourceTypes?.includes(OPCUaSourceTypes.PATH)) {
if (sourceType !== OPCUaSourceTypes.CONST) {
transform(field: string, sourceType: SourceType | OPCUaSourceType, sourceTypes?: Array<SourceType | OPCUaSourceType | MappingValueType> ): string {
if (!sourceTypes || sourceTypes?.includes(OPCUaSourceType.PATH)) {
if (sourceType !== OPCUaSourceType.CONST) {
return `widget/lib/gateway/${field}-${sourceType}_fn`;
} else {
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()
).filter(className => className.length > 0);
fontSetClasses.forEach(className => textElement.addClass(className));
textElement.font({size});
textElement.font({size: `${size}px`});
textElement.attr({
'text-anchor': 'start',
'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 {
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 {
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 {
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 {
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 {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
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 {
ModbusBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component';
@ -145,12 +145,21 @@ import {
ModbusRpcParametersComponent
} 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 {
MqttLegacyBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-legacy-basic-config.component';
import {
GatewayBasicConfigurationComponent
} from '@home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component';
import {
GatewayAdvancedConfigurationComponent
} 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({
declarations: [
@ -239,8 +248,11 @@ import {
ModbusBasicConfigComponent,
EllipsisChipListDirective,
ModbusRpcParametersComponent,
MqttLegacyBasicConfigComponent,
GatewayBasicConfigurationComponent,
GatewayAdvancedConfigurationComponent,
OpcUaLegacyBasicConfigComponent,
ModbusLegacyBasicConfigComponent,
],
exports: [
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);
if (index > -1) {
const prevDashboardWidget = this.dashboardWidgets[index];
if (!isEqual(prevDashboardWidget.widget, record.widget) ||
!isEqual(prevDashboardWidget.widgetLayout, record.widgetLayout)) {
if (!isEqual(prevDashboardWidget.widget, record.widget)) {
this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout,
this.parentDashboard, this.popoverComponent);
this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted;
@ -391,7 +390,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>();
private gridsterItemComponentValue: GridsterItemComponentInterface;
private readonly aspectRatio: number;
private aspectRatio: number;
private heightValue: number;
private widthValue: number;
@ -419,85 +418,97 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.gridsterItemComponentSubject.complete();
}
private preserveAspectRatioApplied = false;
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.colsValue = $item.cols;
this.rowsValue = $item.rows;
this.colsValue = $item.cols;
Object.defineProperty($item, 'rows', {
get: () => this.rowsValue,
set: v => {
if (this.rowsValue !== v) {
if (this.preserveAspectRatio) {
this.colsValue = v * this.aspectRatio;
Object.defineProperty($item, 'rows', {
get: () => this.rowsValue,
set: v => {
if (this.rowsValue !== v) {
if (this.preserveAspectRatio) {
this.colsValue = v * this.aspectRatio;
}
this.rowsValue = v;
}
this.rowsValue = v;
}
}
});
});
Object.defineProperty($item, 'cols', {
get: () => this.colsValue,
set: v => {
if (this.colsValue !== v) {
if (this.preserveAspectRatio) {
this.rowsValue = v / this.aspectRatio;
Object.defineProperty($item, 'cols', {
get: () => this.colsValue,
set: v => {
if (this.colsValue !== v) {
if (this.preserveAspectRatio) {
this.rowsValue = v / this.aspectRatio;
}
this.colsValue = v;
}
this.colsValue = v;
}
}
});
});
const resizable = item.resize;
const resizable = item.resize;
this.heightValue = resizable.height;
this.widthValue = resizable.width;
this.heightValue = resizable.height;
this.widthValue = resizable.width;
const setItemHeight = resizable.setItemHeight.bind(resizable);
const setItemWidth = resizable.setItemWidth.bind(resizable);
resizable.setItemHeight = (height) => {
setItemHeight(height);
this.heightValue = height;
if (this.preserveAspectRatio) {
setItemWidth(height * this.aspectRatio);
}
};
resizable.setItemWidth = (width) => {
setItemWidth(width);
this.widthValue = width;
if (this.preserveAspectRatio) {
setItemHeight(width / this.aspectRatio);
}
};
Object.defineProperty(resizable, 'height', {
get: () => this.heightValue,
set: v => {
if (this.heightValue !== v) {
if (this.preserveAspectRatio) {
this.widthValue = v * this.aspectRatio;
const setItemHeight = resizable.setItemHeight.bind(resizable);
const setItemWidth = resizable.setItemWidth.bind(resizable);
resizable.setItemHeight = (height) => {
setItemHeight(height);
this.heightValue = height;
if (this.preserveAspectRatio) {
setItemWidth(height * this.aspectRatio);
}
};
resizable.setItemWidth = (width) => {
setItemWidth(width);
this.widthValue = width;
if (this.preserveAspectRatio) {
setItemHeight(width / this.aspectRatio);
}
};
Object.defineProperty(resizable, 'height', {
get: () => this.heightValue,
set: v => {
if (this.heightValue !== v) {
if (this.preserveAspectRatio) {
this.widthValue = v * this.aspectRatio;
}
this.heightValue = v;
}
this.heightValue = v;
}
}
});
});
Object.defineProperty(resizable, 'width', {
get: () => this.widthValue,
set: v => {
if (this.widthValue !== v) {
if (this.preserveAspectRatio) {
this.heightValue = v / this.aspectRatio;
Object.defineProperty(resizable, 'width', {
get: () => this.widthValue,
set: v => {
if (this.widthValue !== v) {
if (this.preserveAspectRatio) {
this.heightValue = v / this.aspectRatio;
}
this.widthValue = v;
}
this.widthValue = v;
}
}
});
});
this.preserveAspectRatioApplied = true;
}
}
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(
private dashboard: IDashboardComponent,
public widget: Widget,
public widgetLayout?: WidgetLayout,
private widgetLayoutValue?: WidgetLayout,
private parentDashboard?: IDashboardComponent,
private popoverComponent?: TbPopoverComponent) {
if (isDefined(widgetLayout?.resizable)) {
this.resizeEnabled = widgetLayout.resizable;
}
if (widgetLayout?.preserveAspectRatio) {
this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY;
}
this.widgetLayout = widgetLayoutValue;
if (!widget.id) {
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 { 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 { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum';
import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-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 { HomeSettingsComponent } from '@home/pages/admin/home-settings.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 { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { MenuId } from '@core/services/menu.models';
import { catchError } from 'rxjs/operators';
export const scadaSymbolResolver: ResolveFn<ScadaSymbolData> =
(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
router = inject(Router),
imageService = inject(ImageService)) => {
const type: ImageResourceType = route.params.type;
const key = decodeURIComponent(route.params.key);
return forkJoin({
imageResource: imageService.getImageInfo(type, 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>

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

@ -22,7 +22,7 @@ import {
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
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 { NULL_UUID } from '@shared/models/id/has-uuid';
import { DatePipe } from '@angular/common';
@ -118,7 +118,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (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;
}
@ -149,6 +149,13 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
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 {
if (authority === Authority.TENANT_ADMIN) {
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 class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd">
<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')">
{{ 'resource.title-required' | translate }}
</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 resourceTypes: ResourceType[] = Object.values(this.resourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
maxResourceSize = getCurrentAuthState(this.store).maxResourceSize;
readonly maxResourceSize = getCurrentAuthState(this.store).maxResourceSize;
private destroy$ = new Subject<void>();
@ -57,20 +56,20 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
ngOnInit() {
ngOnInit(): void {
super.ngOnInit();
if (this.isAdd) {
this.observeResourceTypeChange();
}
}
ngOnDestroy() {
ngOnDestroy(): void {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
hideDelete() {
hideDelete(): boolean {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
@ -87,19 +86,18 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
});
}
updateForm(entity: Resource) {
if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false});
if (entity.resourceType !== ResourceType.JS_MODULE) {
this.entityForm.get('fileName').disable({emitEvent: false});
updateForm(entity: Resource): void {
this.entityForm.patchValue(entity);
}
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 {
@ -109,7 +107,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
return super.prepareFormValue(formValue);
}
getAllowedExtensions() {
getAllowedExtensions(): string {
try {
return ResourceTypeExtension.get(this.entityForm.get('resourceType').value);
} catch (e) {
@ -117,7 +115,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
}
}
getAcceptType() {
getAcceptType(): string {
try {
return ResourceTypeMIMETypes.get(this.entityForm.get('resourceType').value);
} catch (e) {
@ -129,7 +127,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
return window.btoa(data);
}
onResourceIdCopied() {
onResourceIdCopied(): void {
this.store.dispatch(new ActionNotificationShow(
{
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>
</section>
<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-label translate>notification.webhook-url</mat-label>
<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 { NotificationService } from '@core/http/notification.service';
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 { takeUntil } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum';
@ -100,6 +100,7 @@ export class RecipientNotificationDialogComponent extends
conversation: [{value: '', disabled: true}, Validators.required],
webhookUrl: [{value: '', disabled: true}, Validators.required],
channelName: [{value: '', disabled: true}, Validators.required],
useOldApi: [{value: !this.isAdd, disabled: true}],
description: [null]
})
});
@ -120,6 +121,7 @@ export class RecipientNotificationDialogComponent extends
case NotificationTargetType.MICROSOFT_TEAMS:
this.targetNotificationForm.get('configuration.webhookUrl').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.channelName').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.useOldApi').enable({emitEvent: false});
break;
}
this.targetNotificationForm.get('configuration.type').enable({emitEvent: false});
@ -169,6 +171,10 @@ export class RecipientNotificationDialogComponent extends
this.targetNotificationForm.get('configuration.usersFilter.filterByTenants')
.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"
headerTitle="{{symbolData?.imageResource?.title}}">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div tb-help="scadaSymbolDev"></div>
<button mat-stroked-button
[disabled]="scadaSymbolFormGroup.invalid"
(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}">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<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"
fxLayout="row"
fxLayoutAlign="start center">
@ -72,7 +75,7 @@
matTooltipPosition="above">
<tb-icon>mdi:file-import</tb-icon>
</button>
<button fxHide fxShow.lt-lg
<button [fxHide]="!(dialogMode && isScada)" fxShow.lt-lg
[disabled]="isLoading$ | async"
mat-icon-button
color="primary"
@ -82,7 +85,7 @@
<mat-icon>add</mat-icon>
</button>
</section>
<button fxHide fxShow.gt-md
<button fxHide [fxShow.gt-md]="!(dialogMode && isScada)"
[disabled]="isLoading$ | async"
mat-flat-button
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 {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
}
.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.
-->
<section fxLayout="column" fxLayoutAlign="start stretch">
<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-from</mat-label>
<mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDatePicker type="date" openOnFocus="true"></mat-datetimepicker>
<section class="tb-form-row column-xs no-border no-padding tb-standard-fields">
<mat-form-field class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>datetime.from</mat-label>
<mat-datetimepicker-toggle [for]="startDatePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field>
<mat-form-field>
<mat-label translate>datetime.time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" 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>
<mat-form-field class="flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label translate>datetime.to</mat-label>
<mat-datetimepicker-toggle [for]="endDatePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input matInput [disabled]="disabled" [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()">
</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>

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
* limitations under the License.
*/
@import '../../../../scss/constants';
:host ::ng-deep {
.mat-mdc-form-field-infix {
width: 100px;
@media #{$mat-xs} {
width: 100%;
}
:host {
.tb-form-row {
gap: 8px;
}
}

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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FixedWindow } from '@shared/models/time/time.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-datetime-period',
@ -34,6 +35,12 @@ export class DatetimePeriodComponent implements OnInit, ControlValueAccessor {
@Input() disabled: boolean;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
modelValue: FixedWindow;
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>
<mat-form-field fxFlex>
<mat-label translate>timewindow.interval</mat-label>
<mat-form-field fxFlex [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<mat-label *ngIf="displayLabel" translate>timewindow.interval</mat-label>
<mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()">
<mat-option *ngFor="let interval of intervals" [value]="interval">
{{ 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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-quick-time-interval',
@ -39,10 +41,20 @@ export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor
rendered = false;
@Input()
@coerceBoolean()
displayLabel = true;
@Input() disabled: boolean;
@Input() onlyCurrentInterval = false;
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
appearance: MatFormFieldAppearance = 'fill';
private propagateChange = (_: any) => {};
constructor() {

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

@ -15,43 +15,40 @@
limitations under the License.
-->
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="8px">
<section fxLayout="column" fxLayoutAlign="start center" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [(ngModel)]="hideFlag" (ngModelChange)="onHideFlagChange()"></mat-checkbox>
<section class="tb-form-panel no-border no-padding" [formGroup]="timeintervalFormGroup">
<section class="tb-form-row column-xs no-border no-padding tb-standard-fields">
<ng-content></ng-content>
<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 class="interval-section" fxLayout="column" fxFlex [fxShow]="advanced && (isEdit || !hideFlag)">
<section fxLayout="row wrap" fxLayoutAlign="start start" fxFlex fxLayoutGap="6px">
<mat-form-field class="number-input">
<section class="tb-form-row column-xs no-border no-padding tb-standard-fields advanced-input"
formGroupName="customInterval"
[fxShow]="advanced">
<div class="tb-flex">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<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 class="number-input">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<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 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>
<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 class="number-input">
<mat-form-field class="number-input flex" [subscriptSizing]="subscriptSizing" [appearance]="appearance">
<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>
</section>
</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>
</div>
</section>
</section>

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

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

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

Loading…
Cancel
Save