Browse Source

Merge branch 'master' into release-3.6

release-3.6 v3.6.1
Igor Kulikov 3 years ago
parent
commit
a1067866f0
  1. 22
      application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
  2. 4
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  6. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java
  7. 11
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java
  8. 117
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
  9. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  10. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java
  12. 22
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java
  13. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java
  14. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java
  15. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java
  16. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java
  18. 27
      application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java
  19. 51
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  20. 124
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  21. 4
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  22. 11
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  23. 8
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  24. 21
      application/src/main/java/org/thingsboard/server/service/queue/ProtoUtils.java
  25. 11
      application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
  26. 42
      application/src/main/java/org/thingsboard/server/utils/TbNodeUpgradeUtils.java
  27. 201
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  28. 159
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java
  29. 21
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  30. 62
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  31. 4
      application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
  32. 4
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  33. 51
      application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java
  34. 42
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java
  35. 105
      application/src/test/java/org/thingsboard/server/utils/TbNodeUpgradeUtilsTest.java
  36. 8
      common/cluster-api/src/main/proto/queue.proto
  37. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  38. 5
      common/edge-api/src/main/proto/edge.proto
  39. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java
  40. 10
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  41. 33
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  42. 3
      packaging/java/scripts/control/deb/preinst
  43. 1
      packaging/js/scripts/control/deb/preinst
  44. 2
      pom.xml
  45. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java
  46. 48
      ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts
  47. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  48. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.scss
  49. 25
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html
  50. 5
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts
  51. 2
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  52. 66
      ui-ngx/src/assets/locale/locale.constant-en_US.json

22
application/src/main/java/org/thingsboard/server/controller/AuditLogController.java

@ -99,7 +99,7 @@ public class AuditLogController extends BaseController {
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
checkParameter("CustomerId", strCustomerId);
TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink));
}
@ -133,7 +133,7 @@ public class AuditLogController extends BaseController {
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
checkParameter("UserId", strUserId);
TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink));
}
@ -171,7 +171,7 @@ public class AuditLogController extends BaseController {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink));
}
@ -202,7 +202,7 @@ public class AuditLogController extends BaseController {
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink));
}
@ -214,4 +214,18 @@ public class AuditLogController extends BaseController {
}
return result;
}
private Long getStartTime(Long startTime) {
if (startTime == null) {
return 1L;
}
return startTime;
}
private Long getEndTime(Long endTime) {
if (endTime == null) {
return System.currentTimeMillis();
}
return endTime;
}
}

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

@ -108,7 +108,7 @@ public class ThingsboardInstallService {
log.info("Migrating ThingsBoard entities data from cassandra to SQL database ...");
entitiesMigrateService.migrate();
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
systemDataLoaderService.loadSystemWidgets();
} else if ("3.0.1-cassandra".equals(upgradeFromVersion)) {
log.info("Migrating ThingsBoard latest timeseries data from cassandra to SQL database ...");
latestMigrateService.migrate();
@ -275,7 +275,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.updateSystemWidgets();
systemDataLoaderService.loadSystemWidgets();
installScripts.loadSystemLwm2mResources();
}
log.info("Upgrade finished successfully!");

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.edge;
import freemarker.template.Configuration;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@ -87,9 +86,6 @@ public class EdgeContextComponent {
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private Configuration freemarkerConfig;
@Autowired
private DeviceService deviceService;

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -641,7 +641,7 @@ public final class EdgeGrpcSession implements Closeable {
case WIDGETS_BUNDLE:
return ctx.getWidgetBundleProcessor().convertWidgetsBundleEventToDownlink(edgeEvent);
case WIDGET_TYPE:
return ctx.getWidgetTypeProcessor().convertWidgetTypeEventToDownlink(edgeEvent);
return ctx.getWidgetTypeProcessor().convertWidgetTypeEventToDownlink(edgeEvent, this.edgeVersion);
case ADMIN_SETTINGS:
return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent);
case OTA_PACKAGE:
@ -651,9 +651,9 @@ public final class EdgeGrpcSession implements Closeable {
case QUEUE:
return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent);
case TENANT:
return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent);
return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.getEdgeVersion());
case TENANT_PROFILE:
return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent);
return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.getEdgeVersion());
default:
log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent);
return null;

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -57,7 +57,7 @@ public class EdgeSyncCursor {
fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService()));
fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService()));
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));

8
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java

@ -19,10 +19,12 @@ import com.google.protobuf.ByteString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils;
@Component
@TbCoreComponent
@ -31,7 +33,9 @@ public class TenantProfileMsgConstructor {
@Autowired
private DataDecodingEncodingService dataDecodingEncodingService;
public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile) {
public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile, EdgeVersion edgeVersion) {
ByteString profileData = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_6_1) ?
ByteString.empty() : ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData()));
TenantProfileUpdateMsg.Builder builder = TenantProfileUpdateMsg.newBuilder()
.setMsgType(msgType)
.setIdMSB(tenantProfile.getId().getId().getMostSignificantBits())
@ -39,7 +43,7 @@ public class TenantProfileMsgConstructor {
.setName(tenantProfile.getName())
.setDefault(tenantProfile.isDefault())
.setIsolatedRuleChain(tenantProfile.isIsolatedTbRuleEngine())
.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData())));
.setProfileDataBytes(profileData);
if (tenantProfile.getDescription() != null) {
builder.setDescription(tenantProfile.getDescription());
}

11
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java

@ -20,9 +20,11 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils;
import java.util.Arrays;
@ -30,7 +32,7 @@ import java.util.Arrays;
@TbCoreComponent
public class WidgetTypeMsgConstructor {
public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails) {
public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion) {
WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder()
.setMsgType(msgType)
.setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits())
@ -58,7 +60,12 @@ public class WidgetTypeMsgConstructor {
builder.setImage(widgetTypeDetails.getImage());
}
if (widgetTypeDetails.getDescription() != null) {
builder.setDescription(widgetTypeDetails.getDescription());
if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_6_0) &&
widgetTypeDetails.getDescription().length() > 255) {
builder.setDescription(widgetTypeDetails.getDescription().substring(0, 254));
} else {
builder.setDescription(widgetTypeDetails.getDescription());
}
}
builder.setDeprecated(widgetTypeDetails.isDeprecated());
if (widgetTypeDetails.getTags() != null) {

117
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java

@ -15,146 +15,51 @@
*/
package org.thingsboard.server.service.edge.rpc.fetch;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.WordUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AdminSettingsId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@AllArgsConstructor
@Slf4j
public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
private final AdminSettingsService adminSettingsService;
private final Configuration freemarkerConfig;
private static final Pattern startPattern = Pattern.compile("<div class=\"content\".*?>");
private static final Pattern endPattern = Pattern.compile("<div class=\"footer\".*?>");
private static final List<String> templatesNames = Arrays.asList(
"account.activated.ftl",
"account.lockout.ftl",
"activation.ftl",
"password.was.reset.ftl",
"reset.password.ftl",
"test.ftl");
// TODO: @voba fix format of next templates
// "state.disabled.ftl",
// "state.enabled.ftl",
// "state.warning.ftl",
@Override
public PageLink getPageLink(int pageSize) {
return null;
}
@Override
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) throws Exception {
List<EdgeEvent> result = new ArrayList<>();
AdminSettings systemMailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(systemMailSettings)));
AdminSettings tenantMailSettings = convertToTenantAdminSettings(tenantId, systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(tenantMailSettings)));
AdminSettings systemMailTemplates = loadMailTemplates(tenantId);
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(systemMailTemplates)));
AdminSettings tenantMailTemplates = convertToTenantAdminSettings(tenantId, systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(tenantMailTemplates)));
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
List<EdgeEvent> result = fetchAdminSettingsForKeys(tenantId, edge.getId(), List.of("general", "mail", "connectivity", "jwt"));
// return PageData object to be in sync with other fetchers
return new PageData<>(result, 1, result.size(), false);
}
private AdminSettings loadMailTemplates(TenantId tenantId) throws Exception {
Map<String, Object> mailTemplates = new HashMap<>();
for (String templatesName : templatesNames) {
Template template = freemarkerConfig.getTemplate(templatesName);
if (template != null) {
String name = validateName(template.getName());
Map<String, String> mailTemplate = getMailTemplateFromFile(template.toString());
if (mailTemplate != null) {
mailTemplates.put(name, mailTemplate);
} else {
log.error("[{}] Can't load mail template from file {}", tenantId, template.getName());
}
}
}
AdminSettings adminSettings = new AdminSettings();
adminSettings.setId(new AdminSettingsId(Uuids.timeBased()));
adminSettings.setKey("mailTemplates");
adminSettings.setJsonValue(JacksonUtil.convertValue(mailTemplates, JsonNode.class));
return adminSettings;
}
private Map<String, String> getMailTemplateFromFile(String stringTemplate) {
Map<String, String> mailTemplate = new HashMap<>();
Matcher start = startPattern.matcher(stringTemplate);
Matcher end = endPattern.matcher(stringTemplate);
if (start.find() && end.find()) {
String body = StringUtils.substringBetween(stringTemplate, start.group(), end.group()).replaceAll("\t", "");
String subject = StringUtils.substringBetween(body, "<h2>", "</h2>");
mailTemplate.put("subject", subject);
mailTemplate.put("body", body);
} else {
return null;
}
return mailTemplate;
}
private String validateName(String name) throws Exception {
StringBuilder nameBuilder = new StringBuilder();
name = name.replace(".ftl", "");
String[] nameParts = name.split("\\.");
if (nameParts.length >= 1) {
nameBuilder.append(nameParts[0]);
for (int i = 1; i < nameParts.length; i++) {
String word = WordUtils.capitalize(nameParts[i]);
nameBuilder.append(word);
private List<EdgeEvent> fetchAdminSettingsForKeys(TenantId tenantId, EdgeId edgeId, List<String> keys) {
List<EdgeEvent> result = new ArrayList<>();
for (String key : keys) {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key);
if (adminSettings != null) {
result.add(EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(adminSettings)));
}
return nameBuilder.toString();
} else {
throw new Exception("Error during filename validation");
}
}
private AdminSettings convertToTenantAdminSettings(TenantId tenantId, String key, ObjectNode jsonValue) {
AdminSettings tenantMailSettings = new AdminSettings();
tenantMailSettings.setTenantId(tenantId);
jsonValue.put("useSystemMailSettings", true);
tenantMailSettings.setJsonValue(jsonValue);
tenantMailSettings.setKey(key);
return tenantMailSettings;
return result;
}
}

10
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -467,9 +467,9 @@ public abstract class BaseEdgeProcessor {
EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType());
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId sourceEdgeId = safeGetEdgeId(edgeNotificationMsg.getSourceEdgeIdMSB(), edgeNotificationMsg.getSourceEdgeIdLSB());
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
if (type.isAllEdgesRelated()) {
return processEntityNotificationForAllEdges(tenantId, type, actionType, entityId, sourceEdgeId);
return processEntityNotificationForAllEdges(tenantId, type, actionType, entityId, originatorEdgeId);
} else {
JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody());
EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB());
@ -481,19 +481,19 @@ public abstract class BaseEdgeProcessor {
if (edgeId != null) {
return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body);
} else {
return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, sourceEdgeId);
return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId);
}
case DELETED:
EdgeEventActionType deleted = EdgeEventActionType.DELETED;
if (edgeId != null) {
return saveEdgeEvent(tenantId, edgeId, type, deleted, entityId, body);
} else {
return Futures.transform(Futures.allAsList(processActionForAllEdgesByTenantId(tenantId, type, deleted, entityId, body, sourceEdgeId)),
return Futures.transform(Futures.allAsList(processActionForAllEdgesByTenantId(tenantId, type, deleted, entityId, body, originatorEdgeId)),
voids -> null, dbCallbackExecutorService);
}
case ASSIGNED_TO_EDGE:
case UNASSIGNED_FROM_EDGE:
if (sourceEdgeId == null) {
if (originatorEdgeId == null) {
ListenableFuture<Void> future = saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body);
return Futures.transformAsync(future, unused -> {
if (type.equals(EdgeEventType.RULE_CHAIN)) {

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java

@ -71,7 +71,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor {
public ListenableFuture<Void> processAlarmNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId sourceEdgeId = safeGetEdgeId(edgeNotificationMsg.getSourceEdgeIdMSB(), edgeNotificationMsg.getSourceEdgeIdLSB());
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
switch (actionType) {
case DELETED:
Alarm deletedAlarm = JacksonUtil.fromString(edgeNotificationMsg.getBody(), Alarm.class);
@ -79,7 +79,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor {
return Futures.immediateFuture(null);
}
List<ListenableFuture<Void>> delFutures = pushEventToAllRelatedEdges(tenantId, deletedAlarm.getOriginator(),
alarmId, actionType, JacksonUtil.valueToTree(deletedAlarm), sourceEdgeId);
alarmId, actionType, JacksonUtil.valueToTree(deletedAlarm), originatorEdgeId);
return Futures.transform(Futures.allAsList(delFutures), voids -> null, dbCallbackExecutorService);
default:
ListenableFuture<Alarm> alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId);
@ -92,7 +92,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor {
return Futures.immediateFuture(null);
}
List<ListenableFuture<Void>> futures = pushEventToAllRelatedEdges(tenantId, alarm.getOriginator(),
alarmId, actionType, null, sourceEdgeId);
alarmId, actionType, null, originatorEdgeId);
return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
}, dbCallbackExecutorService);
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java

@ -71,10 +71,12 @@ public class RelationEdgeProcessor extends BaseRelationProcessor {
if (relation == null || (relation.getFrom().getEntityType().equals(EntityType.EDGE) || relation.getTo().getEntityType().equals(EntityType.EDGE))) {
return Futures.immediateFuture(null);
}
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
Set<EdgeId> uniqueEdgeIds = new HashSet<>();
uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getTo()));
uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getFrom()));
uniqueEdgeIds.remove(originatorEdgeId);
if (uniqueEdgeIds.isEmpty()) {
return Futures.immediateFuture(null);
}

22
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java

@ -18,17 +18,20 @@ package org.thingsboard.server.service.edge.rpc.processor.resource;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
protected void saveOrUpdateTbResource(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg) {
protected boolean saveOrUpdateTbResource(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg) {
boolean resourceKeyUpdated = false;
try {
boolean created = false;
TbResource resource = resourceService.findResourceById(tenantId, tbResourceId);
@ -42,9 +45,21 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
resource.setCreatedTime(Uuids.unixTimestamp(tbResourceId.getId()));
created = true;
}
String resourceKey = resourceUpdateMsg.getResourceKey();
ResourceType resourceType = ResourceType.valueOf(resourceUpdateMsg.getResourceType());
PageDataIterable<TbResource> resourcesIterable = new PageDataIterable<>(
link -> resourceService.findTenantResourcesByResourceTypeAndPageLink(tenantId, resourceType, link), 1024);
for (TbResource tbResource : resourcesIterable) {
if (tbResource.getResourceKey().equals(resourceUpdateMsg.getResourceKey()) && !tbResourceId.equals(tbResource.getId())) {
resourceKey = StringUtils.randomAlphabetic(15) + "_" + resourceKey;
log.warn("[{}] Resource with resource type {} and key {} already exists. Renaming resource key to {}",
tenantId, resourceType, resourceUpdateMsg.getResourceKey(), resourceKey);
resourceKeyUpdated = true;
}
}
resource.setTitle(resourceUpdateMsg.getTitle());
resource.setResourceKey(resourceUpdateMsg.getResourceKey());
resource.setResourceType(ResourceType.valueOf(resourceUpdateMsg.getResourceType()));
resource.setResourceKey(resourceKey);
resource.setResourceType(resourceType);
resource.setFileName(resourceUpdateMsg.getFileName());
resource.setData(resourceUpdateMsg.hasData() ? resourceUpdateMsg.getData() : null);
resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null);
@ -57,5 +72,6 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
log.error("[{}] Failed to process resource update msg [{}]", tenantId, resourceUpdateMsg, e);
throw e;
}
return resourceKeyUpdated;
}
}

12
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java

@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -46,14 +48,12 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor {
switch (resourceUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
super.saveOrUpdateTbResource(tenantId, tbResourceId, resourceUpdateMsg);
break;
case ENTITY_DELETED_RPC_MESSAGE:
TbResource tbResourceToDelete = resourceService.findResourceById(tenantId, tbResourceId);
if (tbResourceToDelete != null) {
resourceService.deleteResource(tenantId, tbResourceId);
boolean resourceKeyUpdated = super.saveOrUpdateTbResource(tenantId, tbResourceId, resourceUpdateMsg);
if (resourceKeyUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.TB_RESOURCE, EdgeEventActionType.UPDATED, tbResourceId, null);
}
break;
case ENTITY_DELETED_RPC_MESSAGE:
case UNRECOGNIZED:
return handleUnsupportedMsgType(resourceUpdateMsg.getMsgType());
}

10
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessor.java

@ -21,6 +21,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EntityDataProto;
@ -37,6 +38,15 @@ public class TelemetryEdgeProcessor extends BaseTelemetryProcessor {
}
public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) throws JsonProcessingException {
if (edgeEvent.getBody() != null) {
String bodyStr = edgeEvent.getBody().toString();
if (bodyStr.length() > 1000) {
log.debug("[{}][{}][{}] Conversion to a DownlinkMsg telemetry event failed due to a size limit violation. " +
"Current size is {}, but the limit is 1000. {}", edgeEvent.getTenantId(), edgeEvent.getEdgeId(),
edgeEvent.getEntityId(), bodyStr.length(), StringUtils.truncate(bodyStr, 100));
return null;
}
}
EntityType entityType = EntityType.valueOf(edgeEvent.getType().name());
EntityDataProto entityDataProto = convertTelemetryEventToEntityDataProto(
edgeEvent.getTenantId(), entityType, edgeEvent.getEntityId(),

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@TbCoreComponent
public class TenantEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertTenantEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertTenantEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
TenantId tenantId = new TenantId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
@ -44,7 +45,7 @@ public class TenantEdgeProcessor extends BaseEdgeProcessor {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
TenantUpdateMsg tenantUpdateMsg = tenantMsgConstructor.constructTenantUpdateMsg(msgType, tenant);
TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId());
TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile);
TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addTenantUpdateMsg(tenantUpdateMsg)

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -33,7 +34,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@TbCoreComponent
public class TenantProfileEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertTenantProfileEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertTenantProfileEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
@ -41,7 +42,7 @@ public class TenantProfileEdgeProcessor extends BaseEdgeProcessor {
if (tenantProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
TenantProfileUpdateMsg tenantProfileUpdateMsg =
tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile);
tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addTenantProfileUpdateMsg(tenantProfileUpdateMsg)

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -32,7 +33,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@TbCoreComponent
public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertWidgetTypeEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertWidgetTypeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {
@ -42,7 +43,7 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor {
if (widgetTypeDetails != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
WidgetTypeUpdateMsg widgetTypeUpdateMsg =
widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails);
widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails, edgeVersion);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addWidgetTypeUpdateMsg(widgetTypeUpdateMsg)

27
application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.utils;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@Slf4j
public final class EdgeVersionUtils {
public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) {
return currentVersion.ordinal() < requiredVersion.ordinal();
}
}

51
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -63,7 +63,6 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
@ -84,9 +83,6 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -536,58 +532,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
installScripts.loadDashboards(demoTenant.getId(), null);
}
@Override
public void deleteSystemWidgetBundle(String bundleAlias) throws Exception {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {
PageData<WidgetTypeInfo> widgetTypes;
var pageLink = new PageLink(1024);
do {
widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), false, DeprecatedFilter.ALL, null, pageLink);
for (var widgetType : widgetTypes.getData()) {
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId());
}
pageLink.nextPageLink();
} while (widgetTypes.hasNext());
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
}
}
@Override
public void loadSystemWidgets() throws Exception {
installScripts.loadSystemWidgets();
}
@Override
public void updateSystemWidgets() throws Exception {
this.deleteSystemWidgetBundle("charts");
this.deleteSystemWidgetBundle("cards");
this.deleteSystemWidgetBundle("maps");
this.deleteSystemWidgetBundle("analogue_gauges");
this.deleteSystemWidgetBundle("digital_gauges");
this.deleteSystemWidgetBundle("gpio_widgets");
this.deleteSystemWidgetBundle("alarm_widgets");
this.deleteSystemWidgetBundle("control_widgets");
this.deleteSystemWidgetBundle("maps_v2");
this.deleteSystemWidgetBundle("gateway_widgets");
this.deleteSystemWidgetBundle("input_widgets");
this.deleteSystemWidgetBundle("date");
this.deleteSystemWidgetBundle("entity_admin_widgets");
this.deleteSystemWidgetBundle("navigation_widgets");
this.deleteSystemWidgetBundle("edge_widgets");
this.deleteSystemWidgetBundle("home_page_widgets");
this.deleteSystemWidgetBundle("entity_widgets");
this.deleteSystemWidgetBundle("html_widgets");
this.deleteSystemWidgetBundle("tables");
this.deleteSystemWidgetBundle("count_widgets");
this.deleteSystemWidgetBundle("status_indicators");
this.deleteSystemWidgetBundle("outdoor_environment");
this.deleteSystemWidgetBundle("indoor_environment");
this.deleteSystemWidgetBundle("air_quality");
this.deleteSystemWidgetBundle("liquid_level_tanks");
installScripts.loadSystemWidgets();
}
private User createUser(Authority authority,
TenantId tenantId,
CustomerId customerId,

124
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -29,9 +29,13 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -48,7 +52,9 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@ -189,6 +195,38 @@ public class InstallScripts {
}
public void loadSystemWidgets() throws Exception {
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
path -> {
JsonNode widgetsBundleDescriptorJson;
try {
widgetsBundleDescriptorJson = JacksonUtil.toJsonNode(path.toFile());
} catch (Exception e) {
log.error("Unable to parse widgets bundle from json: [{}]", path);
throw new RuntimeException("Unable to parse widgets bundle from json", e);
}
if (widgetsBundleDescriptorJson == null || !widgetsBundleDescriptorJson.has("widgetsBundle")) {
log.error("Invalid widgets bundle json: [{}]", path);
throw new RuntimeException("Invalid widgets bundle json: [" + path + "]");
}
widgetsBundlesMap.put(path, widgetsBundleDescriptorJson);
JsonNode bundleAliasNode = widgetsBundleDescriptorJson.get("widgetsBundle").get("alias");
if (bundleAliasNode == null || !bundleAliasNode.isTextual()) {
log.error("Invalid widgets bundle json: [{}]", path);
throw new RuntimeException("Invalid widgets bundle json: [" + path + "]");
}
String bundleAlias = bundleAliasNode.asText();
try {
this.deleteSystemWidgetBundle(bundleAlias);
} catch (Exception e) {
log.error("Failed to delete system widgets bundle: [{}]", bundleAlias);
throw new RuntimeException("Failed to delete system widgets bundle: [" + bundleAlias + "]", e);
}
}
);
}
Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
if (Files.exists(widgetTypesDir)) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) {
@ -206,44 +244,56 @@ public class InstallScripts {
);
}
}
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
path -> {
try {
JsonNode widgetsBundleDescriptorJson = JacksonUtil.toJsonNode(path.toFile());
JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class);
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
List<String> widgetTypeFqns = new ArrayList<>();
if (widgetsBundleDescriptorJson.has("widgetTypes")) {
JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
widgetTypesArrayJson.forEach(
widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
var savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails);
widgetTypeFqns.add(savedWidgetType.getFqn());
} catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widget type from json", e);
}
}
);
}
if (widgetsBundleDescriptorJson.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptorJson.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
for (var widgetsBundleDescriptorEntry : widgetsBundlesMap.entrySet()) {
Path path = widgetsBundleDescriptorEntry.getKey();
try {
JsonNode widgetsBundleDescriptorJson = widgetsBundleDescriptorEntry.getValue();
JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class);
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
List<String> widgetTypeFqns = new ArrayList<>();
if (widgetsBundleDescriptorJson.has("widgetTypes")) {
JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
widgetTypesArrayJson.forEach(
widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
var savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails);
widgetTypeFqns.add(savedWidgetType.getFqn());
} catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widget type from json", e);
}
}
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, savedWidgetsBundle.getId(), widgetTypeFqns);
} catch (Exception e) {
log.error("Unable to load widgets bundle from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widgets bundle from json", e);
}
}
);
);
}
if (widgetsBundleDescriptorJson.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptorJson.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
}
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, savedWidgetsBundle.getId(), widgetTypeFqns);
} catch (Exception e) {
log.error("Unable to load widgets bundle from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widgets bundle from json", e);
}
}
}
private void deleteSystemWidgetBundle(String bundleAlias) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {
PageData<WidgetTypeInfo> widgetTypes;
var pageLink = new PageLink(1024);
do {
widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), false, DeprecatedFilter.ALL, null, pageLink);
for (var widgetType : widgetTypes.getData()) {
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId());
}
pageLink.nextPageLink();
} while (widgetTypes.hasNext());
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
}
}

4
application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java

@ -31,12 +31,8 @@ public interface SystemDataLoaderService {
void loadSystemWidgets() throws Exception;
void updateSystemWidgets() throws Exception;
void loadDemoData() throws Exception;
void deleteSystemWidgetBundle(String bundleAlias) throws Exception;
void createQueues();
void createDefaultNotificationConfigs();

11
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -27,7 +27,6 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.profile.TbDeviceProfileNode;
@ -67,7 +66,6 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.audit.AuditLogDao;
@ -90,6 +88,7 @@ import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.install.SystemDataLoaderService;
import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
import java.util.Collections;
@ -291,17 +290,11 @@ public class DefaultDataUpdateService implements DataUpdateService {
continue;
}
var ruleNodeId = ruleNode.getId();
var oldConfiguration = ruleNode.getConfiguration();
int fromVersion = ruleNode.getConfigurationVersion();
log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, fromVersion, toVersion);
try {
var tbVersionedNode = (TbNode) ruleNodeClassInfo.getClazz().getDeclaredConstructor().newInstance();
TbPair<Boolean, JsonNode> upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration);
if (upgradeRuleNodeConfigurationResult.getFirst()) {
ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond());
}
ruleNode.setConfigurationVersion(toVersion);
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(ruleNode, ruleNodeClassInfo);
saveFutures.add(jpaExecutorService.submit(() -> {
ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",

8
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -486,7 +486,7 @@ public class DefaultTbClusterService implements TbClusterService {
}
@Override
public void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId sourceEdgeId) {
public void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId originatorEdgeId) {
if (!edgesEnabled) {
return;
}
@ -519,9 +519,9 @@ public class DefaultTbClusterService implements TbClusterService {
if (body != null) {
builder.setBody(body);
}
if (sourceEdgeId != null) {
builder.setSourceEdgeIdMSB(sourceEdgeId.getId().getMostSignificantBits());
builder.setSourceEdgeIdLSB(sourceEdgeId.getId().getLeastSignificantBits());
if (originatorEdgeId != null) {
builder.setOriginatorEdgeIdMSB(originatorEdgeId.getId().getMostSignificantBits());
builder.setOriginatorEdgeIdLSB(originatorEdgeId.getId().getLeastSignificantBits());
}
TransportProtos.EdgeNotificationMsgProto msg = builder.build();
log.trace("[{}] sending notification to edge service {}", tenantId.getId(), msg);

21
application/src/main/java/org/thingsboard/server/service/queue/ProtoUtils.java

@ -152,22 +152,29 @@ public class ProtoUtils {
}
private static TransportProtos.DeviceEdgeUpdateMsgProto toProto(DeviceEdgeUpdateMsg msg) {
return TransportProtos.DeviceEdgeUpdateMsgProto.newBuilder()
TransportProtos.DeviceEdgeUpdateMsgProto.Builder builder = TransportProtos.DeviceEdgeUpdateMsgProto.newBuilder()
.setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(msg.getTenantId().getId().getLeastSignificantBits())
.setDeviceIdMSB(msg.getDeviceId().getId().getMostSignificantBits())
.setDeviceIdLSB(msg.getDeviceId().getId().getLeastSignificantBits())
.setEdgeIdMSB(msg.getEdgeId().getId().getMostSignificantBits())
.setEdgeIdLSB(msg.getEdgeId().getId().getLeastSignificantBits())
.build();
.setDeviceIdLSB(msg.getDeviceId().getId().getLeastSignificantBits());
if (msg.getEdgeId() != null) {
builder.setEdgeIdMSB(msg.getEdgeId().getId().getMostSignificantBits())
.setEdgeIdLSB(msg.getEdgeId().getId().getLeastSignificantBits());
}
return builder.build();
}
private static DeviceEdgeUpdateMsg fromProto(TransportProtos.DeviceEdgeUpdateMsgProto proto) {
EdgeId edgeId = null;
if (proto.hasEdgeIdMSB() && proto.hasEdgeIdLSB()) {
edgeId = new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB()));
}
return new DeviceEdgeUpdateMsg(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
new DeviceId(new UUID(proto.getDeviceIdMSB(), proto.getDeviceIdLSB())),
new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB()))
);
edgeId);
}
private static TransportProtos.DeviceNameOrTypeUpdateMsgProto toProto(DeviceNameOrTypeUpdateMsg msg) {

11
application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java

@ -15,12 +15,10 @@
*/
package org.thingsboard.server.service.rule;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
@ -44,13 +42,13 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
import java.util.Collections;
@ -402,18 +400,13 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
var ruleNodeClass = componentDiscoveryService.getRuleNodeInfo(ruleNodeType)
.orElseThrow(() -> new RuntimeException("Rule node " + ruleNodeType + " is not supported!"));
if (ruleNodeClass.isVersioned()) {
TbNode tbVersionedNode = (TbNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance();
int fromVersion = node.getConfigurationVersion();
int toVersion = ruleNodeClass.getCurrentVersion();
if (fromVersion < toVersion) {
log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, fromVersion, toVersion);
try {
TbPair<Boolean, JsonNode> upgradeResult = tbVersionedNode.upgrade(fromVersion, node.getConfiguration());
if (upgradeResult.getFirst()) {
node.setConfiguration(upgradeResult.getSecond());
}
node.setConfigurationVersion(toVersion);
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(node, ruleNodeClass);
log.debug("Successfully upgrade rule node with id: {} type: {}, rule chain id: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, ruleChainId, fromVersion, toVersion);
} catch (TbNodeException e) {

42
application/src/main/java/org/thingsboard/server/utils/TbNodeUpgradeUtils.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
public class TbNodeUpgradeUtils {
public static void upgradeConfigurationAndVersion(RuleNode node, RuleNodeClassInfo nodeInfo) throws Exception {
JsonNode oldConfiguration = node.getConfiguration();
if (oldConfiguration == null || !oldConfiguration.isObject()) {
var configClass = nodeInfo.getAnnotation().configClazz();
node.setConfiguration(JacksonUtil.valueToTree(configClass.getDeclaredConstructor().newInstance().defaultConfiguration()));
} else {
var tbVersionedNode = (TbNode) nodeInfo.getClazz().getDeclaredConstructor().newInstance();
TbPair<Boolean, JsonNode> upgradeResult = tbVersionedNode.upgrade(node.getConfigurationVersion(), oldConfiguration);
if (upgradeResult.getFirst()) {
node.setConfiguration(upgradeResult.getSecond());
}
}
node.setConfigurationVersion(nodeInfo.getCurrentVersion());
}
}

201
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -24,7 +24,6 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeAll;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
@ -100,8 +99,6 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"C3swkUdrR3pezg==\n" +
"-----END PRIVATE KEY-----\n";
ListeningExecutorService executor;
private Tenant savedTenant;
private User tenantAdmin;
private DeviceProfileId mqttDeviceProfileId;
@ -117,8 +114,6 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
@ -208,8 +203,6 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
@After
public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId())
@ -248,19 +241,25 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP);
assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry " +
assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry " +
"-t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry" +
assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry" +
" -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER);
assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --network=host" +
" thingsboard/coap-clients coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --network=host" +
" thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
}
@Test
@ -286,10 +285,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"-t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -u \"%s\" -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
@ -330,10 +329,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"-t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
@ -378,9 +377,9 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(commands).hasSize(1);
JsonNode linuxCommands = commands.get(COAP);
assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId()));
}
@ -445,4 +444,172 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
public void testDownloadCertWithUnknownProtocol() throws Exception {
doGet("/api/device-connectivity/unknownProtocol/certificate/download").andExpect(status().isNotFound());
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDeviceIfPortsSetToDefault() throws Exception {
loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
ObjectNode http = JacksonUtil.newObjectNode();
http.put("enabled", true);
http.put("host", "");
http.put("port", 80);
config.set("http", http);
ObjectNode https = JacksonUtil.newObjectNode();
https.put("enabled", true);
https.put("host", "");
https.put("port", 443);
config.set("https", https);
ObjectNode mqtt = JacksonUtil.newObjectNode();
mqtt.put("enabled", false);
mqtt.put("host", "");
mqtt.put("port", 1883);
config.set("mqtt", mqtt);
ObjectNode mqtts = JacksonUtil.newObjectNode();
mqtts.put("enabled", false);
mqtts.put("host", "");
mqtts.put("port", 8883);
config.set("mqtts", mqtts);
ObjectNode coap = JacksonUtil.newObjectNode();
coap.put("enabled", false);
coap.put("host", "");
coap.put("port", 5683);
config.set("coap", coap);
ObjectNode coaps = JacksonUtil.newObjectNode();
coaps.put("enabled", false);
coaps.put("host", "");
coaps.put("port", 5684);
config.set("coaps", coaps);
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class);
adminSettings.setJsonValue(config);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
login("tenant2@thingsboard.org", "testPassword1");
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(1);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDeviceIfHostIsNotLocalhost() throws Exception {
loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
ObjectNode http = JacksonUtil.newObjectNode();
http.put("enabled", true);
http.put("host", "test.domain");
http.put("port", 8080);
config.set("http", http);
ObjectNode https = JacksonUtil.newObjectNode();
https.put("enabled", true);
https.put("host", "test.domain");
https.put("port", 443);
config.set("https", https);
ObjectNode mqtt = JacksonUtil.newObjectNode();
mqtt.put("enabled", true);
mqtt.put("host", "test.domain");
mqtt.put("port", 1883);
config.set("mqtt", mqtt);
ObjectNode mqtts = JacksonUtil.newObjectNode();
mqtts.put("enabled", true);
mqtts.put("host", "test.domain");
mqtts.put("port", 8883);
config.set("mqtts", mqtts);
ObjectNode coap = JacksonUtil.newObjectNode();
coap.put("enabled", true);
coap.put("host", "test.domain");
coap.put("port", 5683);
config.set("coap", coap);
ObjectNode coaps = JacksonUtil.newObjectNode();
coaps.put("enabled", true);
coaps.put("host", "test.domain");
coaps.put("port", 5684);
config.set("coaps", coaps);
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class);
adminSettings.setJsonValue(config);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
login("tenant2@thingsboard.org", "testPassword1");
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(3);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://test.domain:8080/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://test.domain/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h test.domain -p 1883 -t v1/devices/me/telemetry " +
"-u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h test.domain" +
" -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP);
assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://test.domain:5683/api/v1/%s/telemetry " +
"-t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://test.domain:5684/api/v1/%s/telemetry" +
" -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER);
assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it " +
"thingsboard/coap-clients coap-client -v 6 -m POST coap://test.domain:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it " +
"thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://test.domain:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
}
}

159
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java

@ -1,159 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
@ContextConfiguration(classes = {DeviceConnectivityControllerWithDefaultPortTest.Config.class})
@DaoSqlTest
public class DeviceConnectivityControllerWithDefaultPortTest extends AbstractControllerTest {
ListeningExecutorService executor;
private Tenant savedTenant;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
ObjectNode http = JacksonUtil.newObjectNode();
http.put("enabled", true);
http.put("host", "");
http.put("port", 80);
config.set("http", http);
ObjectNode https = JacksonUtil.newObjectNode();
https.put("enabled", true);
https.put("host", "");
https.put("port", 443);
config.set("https", https);
ObjectNode mqtt = JacksonUtil.newObjectNode();
mqtt.put("enabled", false);
mqtt.put("host", "");
mqtt.put("port", 1883);
config.set("mqtt", mqtt);
ObjectNode mqtts = JacksonUtil.newObjectNode();
mqtts.put("enabled", false);
mqtts.put("host", "");
mqtts.put("port", 8883);
config.set("mqtts", mqtts);
ObjectNode coap = JacksonUtil.newObjectNode();
coap.put("enabled", false);
coap.put("host", "");
coap.put("port", 5683);
config.set("coap", coap);
ObjectNode coaps = JacksonUtil.newObjectNode();
coaps.put("enabled", false);
coaps.put("host", "");
coaps.put("port", 5684);
config.set("coaps", coaps);
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class);
adminSettings.setJsonValue(config);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(1);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
}
}

21
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.edge.EdgeDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
@ -843,6 +844,13 @@ public class EdgeControllerTest extends AbstractControllerTest {
@Test
public void testSyncEdge() throws Exception {
loginSysAdmin();
// get jwt settings from yaml config
JwtSettings settings = doGet("/api/admin/jwtSettings", JwtSettings.class);
// save jwt settings into db
doPost("/api/admin/jwtSettings", settings).andExpect(status().isOk());
loginTenantAdmin();
Asset asset = new Asset();
asset.setName("Test Sync Edge Asset 1");
asset.setType("test");
@ -904,10 +912,10 @@ public class EdgeControllerTest extends AbstractControllerTest {
private void verifyFetchersMsgs(EdgeImitator edgeImitator) {
Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main"));
Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail", true));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail", false));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mailTemplates", true));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mailTemplates", false));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "general"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "jwt"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
@ -953,12 +961,11 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popAdminSettingsMsg(List<AbstractMessage> messages, String key, boolean isSystem) {
private boolean popAdminSettingsMsg(List<AbstractMessage> messages, String key) {
for (AbstractMessage message : messages) {
if (message instanceof AdminSettingsUpdateMsg) {
AdminSettingsUpdateMsg adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) message;
if (key.equals(adminSettingsUpdateMsg.getKey())
&& isSystem == adminSettingsUpdateMsg.getIsSystem()) {
if (key.equals(adminSettingsUpdateMsg.getKey())) {
messages.remove(message);
return true;
}

62
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -71,6 +71,7 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.edge.imitator.EdgeImitator;
@ -98,6 +99,7 @@ import java.util.List;
import java.util.Optional;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -127,6 +129,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
@Before
public void setupEdgeTest() throws Exception {
loginSysAdmin();
// get jwt settings from yaml config
JwtSettings settings = doGet("/api/admin/jwtSettings", JwtSettings.class);
// save jwt settings into db
doPost("/api/admin/jwtSettings", settings).andExpect(status().isOk());
loginTenantAdmin();
installation();
@ -173,7 +182,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
} catch (Exception ignored) {}
}
private void installation() {
private void installation() throws Exception {
thermostatDeviceProfile = this.createDeviceProfile(THERMOSTAT_DEVICE_PROFILE_NAME,
createMqttDeviceProfileTransportConfiguration(new JsonTransportPayloadConfiguration(), false));
extendDeviceProfileData(thermostatDeviceProfile);
@ -189,6 +198,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
+ "/device/" + savedDevice.getUuidId(), Device.class);
doPost("/api/edge/" + edge.getUuidId()
+ "/asset/" + savedAsset.getUuidId(), Asset.class);
// wait until assign device and asset events are fully processed by edge notification service
TimeUnit.MILLISECONDS.sleep(500);
}
protected void extendDeviceProfileData(DeviceProfile deviceProfile) {
@ -237,25 +249,23 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1);
validateRuleChainMetadataUpdates(ruleChainUUID);
// 4 messages
// - 2 from fetcher - system level ('mail', 'mailTemplates')
// - 2 from fetcher - admin level ('mail', 'mailTemplates')
// 4 messages ('general', 'mail', 'connectivity', 'jwt')
validateMsgsCnt(AdminSettingsUpdateMsg.class, 4);
validateAdminSettings();
validateAdminSettings(4);
// 4 messages
// - 1 from default profile fetcher
// - 2 from device profile fetcher (default and thermostat)
// - 1 from device fetcher
validateMsgsCnt(DeviceProfileUpdateMsg.class, 4);
validateDeviceProfiles();
validateDeviceProfiles(4);
// 3 messages
// - 1 from default profile fetcher
// - 1 message from asset profile fetcher
// - 1 message from asset fetcher
validateMsgsCnt(AssetProfileUpdateMsg.class, 3);
validateAssetProfiles();
validateAssetProfiles(3);
// 1 from device fetcher
validateMsgsCnt(DeviceUpdateMsg.class, 1);
@ -326,13 +336,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
testAutoGeneratedCodeByProtobuf(tenantProfileUpdateMsg);
}
private void validateDeviceProfiles() throws Exception {
private void validateDeviceProfiles(int expectedMsgCnt) throws Exception {
List<DeviceProfileUpdateMsg> deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class);
// default msg default device profile from fetcher
// default msg device profile from fetcher
// thermostat msg from device profile fetcher
// thermostat msg from device fetcher
Assert.assertEquals(4, deviceProfileUpdateMsgList.size());
Assert.assertEquals(expectedMsgCnt, deviceProfileUpdateMsgList.size());
Optional<DeviceProfileUpdateMsg> thermostatProfileUpdateMsgOpt =
deviceProfileUpdateMsgList.stream().filter(dfum -> THERMOSTAT_DEVICE_PROFILE_NAME.equals(dfum.getName())).findAny();
Assert.assertTrue(thermostatProfileUpdateMsgOpt.isPresent());
@ -413,20 +423,28 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID);
}
private void validateAdminSettings() {
private void validateAdminSettings(int expectedMsgCnt) {
List<AdminSettingsUpdateMsg> adminSettingsUpdateMsgs = edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class);
Assert.assertEquals(4, adminSettingsUpdateMsgs.size());
Assert.assertEquals(expectedMsgCnt, adminSettingsUpdateMsgs.size());
for (AdminSettingsUpdateMsg adminSettingsUpdateMsg : adminSettingsUpdateMsgs) {
if (adminSettingsUpdateMsg.getKey().equals("general")) {
validateGeneralAdminSettings(adminSettingsUpdateMsg);
}
if (adminSettingsUpdateMsg.getKey().equals("mail")) {
validateMailAdminSettings(adminSettingsUpdateMsg);
}
if (adminSettingsUpdateMsg.getKey().equals("mailTemplates")) {
validateMailTemplatesAdminSettings(adminSettingsUpdateMsg);
if (adminSettingsUpdateMsg.getKey().equals("connectivity")) {
validateConnectivityAdminSettings(adminSettingsUpdateMsg);
}
}
}
private void validateGeneralAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue());
Assert.assertNotNull(jsonNode.get("baseUrl"));
}
private void validateMailAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue());
Assert.assertNotNull(jsonNode.get("mailFrom"));
@ -436,19 +454,19 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Assert.assertNotNull(jsonNode.get("timeout"));
}
private void validateMailTemplatesAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
private void validateConnectivityAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue());
Assert.assertNotNull(jsonNode.get("accountActivated"));
Assert.assertNotNull(jsonNode.get("accountLockout"));
Assert.assertNotNull(jsonNode.get("activation"));
Assert.assertNotNull(jsonNode.get("passwordWasReset"));
Assert.assertNotNull(jsonNode.get("resetPassword"));
Assert.assertNotNull(jsonNode.get("test"));
Assert.assertNotNull(jsonNode.get("http"));
Assert.assertNotNull(jsonNode.get("https"));
Assert.assertNotNull(jsonNode.get("mqtt"));
Assert.assertNotNull(jsonNode.get("mqtts"));
Assert.assertNotNull(jsonNode.get("coap"));
Assert.assertNotNull(jsonNode.get("coaps"));
}
private void validateAssetProfiles() throws Exception {
private void validateAssetProfiles(int expectedMsgCnt) throws Exception {
List<AssetProfileUpdateMsg> assetProfileUpdateMsgs = edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class);
Assert.assertEquals(3, assetProfileUpdateMsgs.size());
Assert.assertEquals(expectedMsgCnt, assetProfileUpdateMsgs.size());
AssetProfileUpdateMsg assetProfileUpdateMsg = assetProfileUpdateMsgs.get(0);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType());
UUID assetProfileUUID = new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB());

4
application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java

@ -173,7 +173,7 @@ public class AssetEdgeTest extends AbstractEdgeTest {
assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
assetUpdateMsgBuilder.setName("Asset Edge 2");
assetUpdateMsgBuilder.setType("test");
assetUpdateMsgBuilder.setType("default");
assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());
@ -205,7 +205,7 @@ public class AssetEdgeTest extends AbstractEdgeTest {
assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
assetUpdateMsgBuilder.setName(assetOnCloudName);
assetUpdateMsgBuilder.setType("test");
assetUpdateMsgBuilder.setType("default");
assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());

4
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -492,7 +492,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
deviceUpdateMsgBuilder.setName(deviceOnCloudName);
deviceUpdateMsgBuilder.setType("test");
deviceUpdateMsgBuilder.setType("default");
deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder);
uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());
@ -541,7 +541,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
deviceUpdateMsgBuilder.setName("Edge Device 2");
deviceUpdateMsgBuilder.setType("test");
deviceUpdateMsgBuilder.setType("default");
deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());

51
application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -120,4 +121,54 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
Assert.assertNotNull(tbResource);
Assert.assertEquals("Edge Test Resource", tbResource.getName());
}
@Test
public void testResourceToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JKS);
resource.setTitle("Edge Test Resource");
resource.setFileName(FILE_NAME);
resource.setData(TEST_DATA);
edgeImitator.expectMessageAmount(1);
TbResource savedResource = doPost("/api/resource", resource, TbResource.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
ResourceUpdateMsg.Builder resourceUpdateMsgBuilder = ResourceUpdateMsg.newBuilder();
resourceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
resourceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
resourceUpdateMsgBuilder.setTitle("Edge Test Resource");
resourceUpdateMsgBuilder.setResourceType(ResourceType.JKS.name());
resourceUpdateMsgBuilder.setResourceKey(FILE_NAME);
resourceUpdateMsgBuilder.setFileName(FILE_NAME);
resourceUpdateMsgBuilder.setData(TEST_DATA);
resourceUpdateMsgBuilder.setIsSystem(false);
resourceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(resourceUpdateMsgBuilder);
uplinkMsgBuilder.addResourceUpdateMsg(resourceUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<ResourceUpdateMsg> resourceUpdateMsgOpt = edgeImitator.findMessageByType(ResourceUpdateMsg.class);
Assert.assertTrue(resourceUpdateMsgOpt.isPresent());
ResourceUpdateMsg latestResourceUpdateMsg = resourceUpdateMsgOpt.get();
Assert.assertNotEquals(FILE_NAME, latestResourceUpdateMsg.getResourceKey());
Assert.assertNotEquals(savedResource.getUuidId(), uuid);
TbResource tbResource = doGet("/api/resource/" + uuid, TbResource.class);
Assert.assertNotNull(tbResource);
Assert.assertNotEquals(FILE_NAME, tbResource.getName());
}
}

42
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/TelemetryEdgeProcessorTest.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.telemetry;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class TelemetryEdgeProcessorTest {
@Test
public void testConvert_maxSizeLimit() throws Exception {
EdgeEvent edgeEvent = new EdgeEvent();
ObjectNode body = JacksonUtil.newObjectNode();
body.put("value", StringUtils.randomAlphanumeric(10000));
edgeEvent.setBody(body);
DownlinkMsg downlinkMsg = new TelemetryEdgeProcessor().convertTelemetryEventToDownlink(edgeEvent);
Assert.assertNull(downlinkMsg);
}
}

105
application/src/test/java/org/thingsboard/server/utils/TbNodeUpgradeUtilsTest.java

@ -0,0 +1,105 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
import com.fasterxml.jackson.databind.node.NullNode;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TbNodeUpgradeUtilsTest {
@Test
public void testUpgradeRuleNodeConfigurationWithNullConfig() throws Exception {
// GIVEN
var node = new RuleNode();
var nodeInfo = mock(RuleNodeClassInfo.class);
var nodeConfigClazz = TbGetAttributesNodeConfiguration.class;
var annotation = mock(org.thingsboard.rule.engine.api.RuleNode.class);
var defaultConfig = JacksonUtil.valueToTree(nodeConfigClazz.getDeclaredConstructor().newInstance().defaultConfiguration());
when(nodeInfo.getClazz()).thenReturn((Class)TbGetAttributesNode.class);
when(nodeInfo.getCurrentVersion()).thenReturn(1);
when(nodeInfo.getAnnotation()).thenReturn(annotation);
when(annotation.configClazz()).thenReturn((Class) nodeConfigClazz);
// WHEN
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(node, nodeInfo);
// THEN
Assertions.assertThat(node.getConfiguration()).isEqualTo(defaultConfig);
Assertions.assertThat(node.getConfigurationVersion()).isEqualTo(1);
}
@Test
public void testUpgradeRuleNodeConfigurationWithNullNodeConfig() throws Exception {
// GIVEN
var node = new RuleNode();
node.setConfiguration(NullNode.instance);
var nodeInfo = mock(RuleNodeClassInfo.class);
var nodeConfigClazz = TbGetAttributesNodeConfiguration.class;
var annotation = mock(org.thingsboard.rule.engine.api.RuleNode.class);
var defaultConfig = JacksonUtil.valueToTree(nodeConfigClazz.getDeclaredConstructor().newInstance().defaultConfiguration());
when(nodeInfo.getClazz()).thenReturn((Class)TbGetAttributesNode.class);
when(nodeInfo.getCurrentVersion()).thenReturn(1);
when(nodeInfo.getAnnotation()).thenReturn(annotation);
when(annotation.configClazz()).thenReturn((Class) nodeConfigClazz);
// WHEN
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(node, nodeInfo);
// THEN
Assertions.assertThat(node.getConfiguration()).isEqualTo(defaultConfig);
Assertions.assertThat(node.getConfigurationVersion()).isEqualTo(1);
}
@Test
public void testUpgradeRuleNodeConfigurationWithNonNullConfig() throws Exception {
// GIVEN
var node = new RuleNode();
var nodeInfo = mock(RuleNodeClassInfo.class);
var nodeConfigClazz = TbGetAttributesNodeConfiguration.class;
var annotation = mock(org.thingsboard.rule.engine.api.RuleNode.class);
var defaultConfig = JacksonUtil.valueToTree(nodeConfigClazz.getDeclaredConstructor().newInstance().defaultConfiguration());
when(nodeInfo.getClazz()).thenReturn((Class)TbGetAttributesNode.class);
when(nodeInfo.getCurrentVersion()).thenReturn(1);
when(nodeInfo.getAnnotation()).thenReturn(annotation);
when(annotation.configClazz()).thenReturn((Class) nodeConfigClazz);
String versionZeroDefaultConfigStr = "{\"fetchToData\":false," +
"\"clientAttributeNames\":[]," +
"\"sharedAttributeNames\":[]," +
"\"serverAttributeNames\":[]," +
"\"latestTsKeyNames\":[]," +
"\"tellFailureIfAbsent\":true," +
"\"getLatestValueWithTs\":false}";
node.setConfiguration(JacksonUtil.toJsonNode(versionZeroDefaultConfigStr));
// WHEN
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(node, nodeInfo);
// THEN
Assertions.assertThat(node.getConfiguration()).isEqualTo(defaultConfig);
Assertions.assertThat(node.getConfigurationVersion()).isEqualTo(1);
}
}

8
common/cluster-api/src/main/proto/queue.proto

@ -910,8 +910,8 @@ message EdgeNotificationMsgProto {
string body = 10;
PostTelemetryMsg postTelemetryMsg = 11;
PostAttributeMsg postAttributesMsg = 12;
int64 sourceEdgeIdMSB = 13;
int64 sourceEdgeIdLSB = 14;
int64 originatorEdgeIdMSB = 13;
int64 originatorEdgeIdLSB = 14;
}
message EdgeEventUpdateMsgProto {
@ -945,8 +945,8 @@ message DeviceEdgeUpdateMsgProto {
int64 tenantIdLSB = 2;
int64 deviceIdMSB = 3;
int64 deviceIdLSB = 4;
int64 edgeIdMSB = 5;
int64 edgeIdLSB = 6;
optional int64 edgeIdMSB = 5;
optional int64 edgeIdLSB = 6;
}
message DeviceNameOrTypeUpdateMsgProto {

2
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_3_6_0)
.setEdgeVersion(EdgeVersion.V_3_6_1)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());

5
common/edge-api/src/main/proto/edge.proto

@ -35,6 +35,7 @@ enum EdgeVersion {
V_3_3_3 = 1;
V_3_4_0 = 2;
V_3_6_0 = 3;
V_3_6_1 = 4;
}
/**
@ -361,8 +362,8 @@ message WidgetsBundleUpdateMsg {
optional bytes image = 6;
bool isSystem = 7;
optional string description = 8;
optional int32 order = 9;
optional string widgets = 10;
optional string widgets = 9;
optional int32 order = 10;
}
message WidgetTypeUpdateMsg {

8
dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java

@ -91,10 +91,10 @@ public interface EdgeRepository extends JpaRepository<EdgeEntity, UUID> {
"LEFT JOIN CustomerEntity c on c.id = a.customerId " +
"WHERE a.tenantId = :tenantId " +
"AND a.customerId = :customerId " +
"AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true)")
Page<EdgeInfoEntity> findEdgeInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("searchText") String searchText,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.EdgeInfoEntity(a, c.title, c.additionalInfo) " +
@ -113,11 +113,11 @@ public interface EdgeRepository extends JpaRepository<EdgeEntity, UUID> {
@Query("SELECT ee FROM EdgeEntity ee, RelationEntity re WHERE ee.tenantId = :tenantId " +
"AND ee.id = re.fromId AND re.fromType = 'EDGE' AND re.relationTypeGroup = 'EDGE' " +
"AND re.relationType = 'Contains' AND re.toId = :entityId AND re.toType = :entityType " +
"AND (:searchText IS NULL OR ilike(ee.name, CONCAT('%', :searchText, '%')) = true)")
"AND (:textSearch IS NULL OR ilike(ee.name, CONCAT('%', :textSearch, '%')) = true)")
Page<EdgeEntity> findByTenantIdAndEntityId(@Param("tenantId") UUID tenantId,
@Param("entityId") UUID entityId,
@Param("entityType") String entityType,
@Param("searchText") String searchText,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT ee FROM EdgeEntity ee, TenantEntity te WHERE ee.tenantId = te.id AND te.tenantProfileId = :tenantProfileId ")

10
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -20,6 +20,9 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.util.Arrays;
import java.util.List;
public class DeviceConnectivityUtil {
public static final String HTTP = "http";
@ -38,6 +41,7 @@ public class DeviceConnectivityUtil {
public static final String GATEWAY_DOCKER_RUN = "docker run -it ";
public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients ";
public static final String COAP_IMAGE = "thingsboard/coap-clients ";
public static final List<String> LOCAL_HOSTS = Arrays.asList("localhost", "127.0.0.1");
public static String getHttpPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
return String.format("curl -v -X POST %s://%s%s/api/v1/%s/telemetry --header Content-Type:application/json --data " + JSON_EXAMPLE_PAYLOAD,
@ -135,7 +139,7 @@ public class DeviceConnectivityUtil {
}
StringBuilder mqttDockerCommand = new StringBuilder();
mqttDockerCommand.append(DOCKER_RUN).append(MQTT_IMAGE);
mqttDockerCommand.append(DOCKER_RUN).append(LOCAL_HOSTS.contains(host) ? "--network=host ":"").append(MQTT_IMAGE);
if (MQTTS.equals(protocol)) {
mqttDockerCommand.append("/bin/sh -c \"")
@ -158,7 +162,7 @@ public class DeviceConnectivityUtil {
switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
String client = COAPS.equals(protocol) ? "coap-client-openssl" : "coap-client";
return String.format("%s -m POST %s://%s%s/api/v1/%s/telemetry -t json -e %s",
return String.format("%s -v 6 -m POST %s://%s%s/api/v1/%s/telemetry -t json -e %s",
client, protocol, host, port, deviceCredentials.getCredentialsId(), JSON_EXAMPLE_PAYLOAD);
default:
return null;
@ -167,6 +171,6 @@ public class DeviceConnectivityUtil {
public static String getDockerCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
String coapCommand = getCoapPublishCommand(protocol, host, port, deviceCredentials);
return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN, COAP_IMAGE, coapCommand) : null;
return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN + (LOCAL_HOSTS.contains(host) ? "--network=host ":""), COAP_IMAGE, coapCommand) : null;
}
}

33
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java

@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -74,8 +75,8 @@ public class ThingsBoardDbInstaller {
new File("./../../docker/docker-compose.yml"),
new File("./../../docker/docker-compose.volumes.yml"),
IS_HYBRID_MODE
? new File("./../../docker/docker-compose.hybrid.yml")
: new File("./../../docker/docker-compose.postgres.yml"),
? new File("./../../docker/docker-compose.hybrid.yml")
: new File("./../../docker/docker-compose.postgres.yml"),
new File("./../../docker/docker-compose.postgres.volumes.yml"),
resolveRedisComposeFile(),
resolveRedisComposeVolumesFile()
@ -156,7 +157,7 @@ public class ThingsBoardDbInstaller {
return env;
}
public void createVolumes() {
public void createVolumes() {
try {
dockerCompose.withCommand("volume create " + postgresDataVolume);
@ -200,7 +201,7 @@ public class ThingsBoardDbInstaller {
}
} else if (IS_REDIS_SENTINEL) {
additionalServices.append(" redis-master");
dockerCompose.withCommand("volume create " + redisSentinelDataVolume +"-" + "master");
dockerCompose.withCommand("volume create " + redisSentinelDataVolume + "-" + "master");
dockerCompose.invokeDocker();
additionalServices.append(" redis-slave");
@ -226,7 +227,8 @@ public class ThingsBoardDbInstaller {
try {
dockerCompose.withCommand("down -v");
dockerCompose.invokeCompose();
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
}
}
@ -239,15 +241,24 @@ public class ThingsBoardDbInstaller {
copyLogs(tbSnmpTransportLogVolume, "./target/tb-snmp-transport-logs/");
copyLogs(tbVcExecutorLogVolume, "./target/tb-vc-executor-logs/");
dockerCompose.withCommand("volume rm -f " + postgresDataVolume + " " + tbLogVolume +
" " + tbCoapTransportLogVolume + " " + tbLwm2mTransportLogVolume + " " + tbHttpTransportLogVolume +
" " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume + resolveRedisComposeVolumeLog());
dockerCompose.invokeDocker();
StringJoiner rmVolumesCommand = new StringJoiner(" ")
.add("volume rm -f")
.add(postgresDataVolume)
.add(tbLogVolume)
.add(tbCoapTransportLogVolume)
.add(tbLwm2mTransportLogVolume)
.add(tbHttpTransportLogVolume)
.add(tbMqttTransportLogVolume)
.add(tbSnmpTransportLogVolume)
.add(tbVcExecutorLogVolume)
.add(resolveRedisComposeVolumeLog());
dockerCompose.withCommand(rmVolumesCommand.toString());
}
private String resolveRedisComposeVolumeLog() {
if (IS_REDIS_CLUSTER) {
return IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + "-" + i).collect(Collectors.joining());
return IntStream.range(0, 6).mapToObj(i -> redisClusterDataVolume + "-" + i).collect(Collectors.joining());
}
if (IS_REDIS_SENTINEL) {
return redisSentinelDataVolume + "-" + "master " + " " +
@ -266,7 +277,7 @@ public class ThingsBoardDbInstaller {
dockerCompose.withCommand("run -d --rm --name " + logsContainerName + " -v " + volumeName + ":/root alpine tail -f /dev/null");
dockerCompose.invokeDocker();
dockerCompose.withCommand("cp " + logsContainerName + ":/root/. "+tbLogsDir.getAbsolutePath());
dockerCompose.withCommand("cp " + logsContainerName + ":/root/. " + tbLogsDir.getAbsolutePath());
dockerCompose.invokeDocker();
dockerCompose.withCommand("rm -f " + logsContainerName);

3
packaging/java/scripts/control/deb/preinst

@ -10,7 +10,6 @@ if ! getent passwd ${pkg.user} >/dev/null; then
adduser --quiet \
--system \
--ingroup ${pkg.user} \
--quiet \
--disabled-login \
--disabled-password \
--home ${pkg.installFolder} \
@ -19,4 +18,4 @@ if ! getent passwd ${pkg.user} >/dev/null; then
${pkg.user}
fi
exit 0
exit 0

1
packaging/js/scripts/control/deb/preinst

@ -8,7 +8,6 @@ if ! getent passwd ${pkg.user} >/dev/null; then
adduser --quiet \
--system \
--ingroup ${pkg.user} \
--quiet \
--disabled-login \
--disabled-password \
--home ${pkg.installFolder} \

2
pom.xml

@ -78,7 +78,7 @@
<zookeeper.version>3.8.1</zookeeper.version>
<protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version>
<tbel.version>1.1.3</tbel.version>
<tbel.version>1.1.4</tbel.version>
<lombok.version>1.18.18</lombok.version>
<paho.client.version>1.2.4</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.api.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.JacksonUtil;
@ -85,6 +84,7 @@ public class TbNodeUtils {
}
}
@Deprecated(since = "3.6.1", forRemoval = true)
public static List<String> processPatterns(List<String> patterns, TbMsgMetaData metaData) {
if (!CollectionUtils.isEmpty(patterns)) {
return patterns.stream().map(p -> processPattern(p, metaData)).collect(Collectors.toList());

48
ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts

@ -160,26 +160,30 @@ export class LiquidLevelWidgetComponent implements OnInit {
}
private getData(): Observable<{ svg: string; volume: number; units: string }> {
const entityId: EntityId = {
entityType: this.ctx.datasources[0].entityType,
id: this.ctx.datasources[0].entityId
};
if (this.ctx.datasources?.length) {
const entityId: EntityId = {
entityType: this.ctx.datasources[0].entityType,
id: this.ctx.datasources[0].entityId
};
return this.getShape(entityId).pipe(
switchMap(shape => {
this.shape = shape;
this.svgParams = svgMapping.get(shape);
if (this.svgParams) {
return forkJoin([
this.resourcesService.loadJsonResource<string>(this.svgParams.svg),
this.getTankersParams(entityId)
]).pipe(
map(params => ({svg: params[0], ...params[1]}))
);
}
return of(null);
})
);
}
return this.getShape(entityId).pipe(
switchMap(shape => {
this.shape = shape;
this.svgParams = svgMapping.get(shape);
if (this.svgParams) {
return forkJoin([
this.resourcesService.loadJsonResource<string>(this.svgParams.svg),
this.getTankersParams(entityId)
]).pipe(
map(params => ({svg: params[0], ...params[1]}))
);
}
return of(null);
})
);
return of(null);
}
public onInit() {
@ -541,13 +545,13 @@ export class LiquidLevelWidgetComponent implements OnInit {
</div>`;
}
private convertInputData(value: number): number {
private convertInputData(value: any): number {
if (this.settings.datasourceUnits !== CapacityUnits.percent) {
return (convertLiters(value, this.settings.datasourceUnits, ConversionType.to) /
return (convertLiters(Number(value), this.settings.datasourceUnits, ConversionType.to) /
convertLiters(this.volume, this.settings.volumeUnits, ConversionType.to)) * 100;
}
return value;
return Number(value);
}
private convertOutputData(value: number): number {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts

@ -65,7 +65,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
ngOnInit(): void {
this.colorSettingsFormGroup = this.fb.group(
{
type: [this.colorSettings?.type, []],
type: [this.colorSettings?.type || ColorType.constant, []],
color: [this.colorSettings?.color, []],
rangeList: [this.colorSettings?.rangeList, []],
colorFunction: [this.colorSettings?.colorFunction, []]

1
ui-ngx/src/app/modules/home/components/widget/widget.component.scss

@ -30,6 +30,7 @@
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, .75);
z-index: 2;
span {
color: #000;

25
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html

@ -226,31 +226,6 @@
<ng-template [ngSwitchCase]="DeviceTransportType.COAP">
<div class="tb-no-data-text" translate>device.connectivity.use-following-instructions</div>
<mat-tab-group [(selectedIndex)]="coapTabIndex">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="macos"></mat-icon>
MacOS
</ng-template>
<ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body">
<div class="tb-form-panel stroked" *ngIf="commands.coap.coaps !== 'Check documentation'">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<div class="tb-install-instruction-text">
<ng-container *ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.install-coap-client',
href: 'https://thingsboard.io/docs/user-guide/ssl/coap-access-token/'}}">
</ng-container>
</div>
</div>
<ng-container
*ngTemplateOutlet="executeCommand; context: {cmd: {
noSecLabel: 'CoAP', noSec: commands.coap.coap,
secLabel: 'CoAPs', sec: commands.coap.coaps,
doc: {text: 'device.connectivity.coaps-x509-command', href: 'https://thingsboard.io/docs/user-guide/ssl/coap-x509-certificates/'}}}">
</ng-container>
</div>
</ng-template>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tabs-icon" svgIcon="linux"></mat-icon>

5
ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts

@ -199,20 +199,21 @@ export class DeviceCheckConnectivityDialogComponent extends
case 'android':
this.httpTabIndex = 2;
this.mqttTabIndex = 2;
this.coapTabIndex = 1;
break;
case 'macos':
case 'ios':
this.httpTabIndex = 1;
this.mqttTabIndex = 1;
this.coapTabIndex = 1;
break;
case 'windows':
this.httpTabIndex = 0;
this.mqttTabIndex = 0;
this.coapTabIndex = 1;
break;
default:
this.mqttTabIndex = this.commands.mqtt?.docker ? 3 : 0;
this.coapTabIndex = this.commands.coap?.docker ? 2 : 1;
this.coapTabIndex = this.commands.coap?.docker ? 1 : 0;
}
}

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

@ -161,6 +161,8 @@ export abstract class ColorProcessor {
return new RangeColorProcessor(settings);
case ColorType.function:
return new FunctionColorProcessor(settings);
default:
return new ConstantColorProcessor(settings);
}
}

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

@ -97,7 +97,7 @@
"general-settings": "General Settings",
"home-settings": "Home Settings",
"home": "Home",
"outgoing-mail": "Mail Server",
"outgoing-mail": "Mail server",
"outgoing-mail-settings": "Outgoing Mail Server Settings",
"system-settings": "System Settings",
"test-mail-sent": "Test mail was successfully sent!",
@ -621,7 +621,7 @@
"filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-entity-view-search-query": "Entity view search query",
"filter-type-entity-view-search-query-description": "Entity views with types {{entityViewTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-apiUsageState": "Api Usage State",
"filter-type-apiUsageState": "API Usage State",
"filter-type-edge-search-query": "Edge search query",
"filter-type-edge-search-query-description": "Edges with types {{edgeTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"entity-filter": "Entity filter",
@ -647,7 +647,7 @@
"assets": "Assets",
"management": "Asset management",
"view-assets": "View Assets",
"add": "Add Asset",
"add": "Add asset",
"asset-type-max-length": "Asset type should be less than 256",
"assign-to-customer": "Assign to customer",
"assign-asset-to-customer": "Assign Asset(s) To Customer",
@ -772,7 +772,7 @@
},
"api-usage": {
"api-features": "API features",
"api-usage": "Api Usage",
"api-usage": "API usage",
"alarm": "Alarm",
"alarms-created": "Alarms created",
"alarms-created-daily-activity": "Alarms created daily activity",
@ -847,10 +847,10 @@
},
"audit-log": {
"audit": "Audit",
"audit-logs": "Audit Logs",
"audit-logs": "Audit logs",
"timestamp": "Timestamp",
"entity-type": "Entity Type",
"entity-name": "Entity Name",
"entity-type": "Entity type",
"entity-name": "Entity name",
"user": "User",
"type": "Type",
"status": "Status",
@ -951,7 +951,7 @@
"public-assets": "Public Assets",
"public-edges": "Public Edges",
"public-entity-views": "Public Entity Views",
"add": "Add Customer",
"add": "Add customer",
"delete": "Delete customer",
"manage-customer-users": "Manage customer users",
"manage-customer-devices": "Manage customer devices",
@ -1011,7 +1011,7 @@
"dashboards": "Dashboards",
"management": "Dashboard management",
"view-dashboards": "View Dashboards",
"add": "Add Dashboard",
"add": "Add dashboard",
"assign-dashboard-to-customer": "Assign Dashboard(s) To Customer",
"assign-dashboard-to-customer-text": "Please select the dashboards to assign to the customer",
"assign-dashboard-to-edge-title": "Assign Dashboard(s) To Edge",
@ -1299,7 +1299,7 @@
"device-list-empty": "No devices selected.",
"device-name-filter-required": "Device name filter is required.",
"device-name-filter-no-device-matched": "No devices starting with '{{device}}' were found.",
"add": "Add Device",
"add": "Add device",
"assign-to-customer": "Assign to customer",
"assign-device-to-customer": "Assign Device(s) To Customer",
"assign-device-to-customer-text": "Please select the devices to assign to the customer",
@ -1933,7 +1933,7 @@
"type-max-length": "Type should be less than 256",
"management": "Edge management",
"no-edges-matching": "No edges matching '{{entity}}' were found.",
"add": "Add Edge",
"add": "Add edge",
"no-edges-text": "No edges found",
"edge-details": "Edge details",
"add-edge-text": "Add new edge",
@ -2192,7 +2192,7 @@
"no-entities-prompt": "No entities found",
"no-data": "No data to display",
"columns-to-display": "Columns to Display",
"type-api-usage-state": "Api Usage State",
"type-api-usage-state": "API Usage State",
"type-edge": "Edge",
"type-edges": "Edges",
"list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }",
@ -2231,9 +2231,9 @@
"label": "Label"
},
"entity-view": {
"entity-view": "Entity View",
"entity-view": "Entity view",
"entity-view-required": "Entity view is required.",
"entity-views": "Entity Views",
"entity-views": "Entity views",
"management": "Entity View management",
"view-entity-views": "View Entity Views",
"entity-view-alias": "Entity View alias",
@ -2259,7 +2259,7 @@
"entity-view-list-empty": "No entity views selected.",
"entity-view-name-filter-required": "Entity view name filter is required.",
"entity-view-name-filter-no-entity-view-matched": "No entity views starting with '{{entityView}}' were found.",
"add": "Add Entity View",
"add": "Add entity view",
"entity-view-public": "Entity view is public",
"assign-to-customer": "Assign to customer",
"assign-entity-view-to-customer": "Assign Entity View(s) To Customer",
@ -2287,14 +2287,14 @@
"unassign-entity-view": "Unassign entity view",
"unassign-entity-views-title": "Are you sure you want to unassign { count, plural, =1 {1 entity view} other {# entity views} }?",
"unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.",
"entity-view-type": "Entity View type",
"entity-view-type-required": "Entity View type is required.",
"entity-view-type": "Entity view type",
"entity-view-type-required": "Entity view type is required.",
"select-entity-view-type": "Select entity view type",
"enter-entity-view-type": "Enter entity view type",
"any-entity-view": "Any entity view",
"no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.",
"entity-view-type-list-empty": "No entity view types selected.",
"entity-view-types": "Entity View types",
"entity-view-types": "Entity view types",
"created-time": "Created time",
"name": "Name",
"name-required": "Name is required.",
@ -3457,7 +3457,7 @@
"use-external-url": "Use external URL",
"version": "Version",
"version-required": "Version is required.",
"version-tag": "Version Tag",
"version-tag": "Version tag",
"version-tag-hint": "Custom tag should match the package version reported by your device.",
"version-max-length": "Version should be less than 256",
"warning-after-save-no-edit": "Once the package is uploaded, you will not be able to modify title, version, device profile and package type."
@ -3608,7 +3608,7 @@
"no-relations-text": "No relations found"
},
"resource": {
"add": "Add Resource",
"add": "Add resource",
"all-types": "All",
"copyId": "Copy resource Id",
"delete": "Delete resource",
@ -3653,7 +3653,7 @@
"name-required": "Name is required.",
"name-max-length": "Name should be less than 256",
"description": "Description",
"add": "Add Rule Chain",
"add": "Add rule chain",
"set-root": "Make rule chain root",
"set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?",
"set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.",
@ -3685,8 +3685,8 @@
"selected-rulechains": "{ count, plural, =1 {1 rule chain} other {# rule chains} } selected",
"open-rulechain": "Open rule chain",
"assign-new-rulechain": "Assign new rulechain",
"edge-template-root": "Template Root",
"assign-to-edge": "Assign to Edge",
"edge-template-root": "Template root",
"assign-to-edge": "Assign to edge",
"edge-rulechain": "Edge rule chain",
"unassign-rulechain-from-edge-text": "After the confirmation the rulechain will be unassigned and won't be accessible by the edge.",
"unassign-rulechains-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 rulechain} other {# rulechains} }?",
@ -3896,7 +3896,7 @@
"tenant": "Tenant",
"tenants": "Tenants",
"management": "Tenant management",
"add": "Add Tenant",
"add": "Add tenant",
"admins": "Admins",
"manage-tenant-admins": "Manage tenant admins",
"delete": "Delete tenant",
@ -4623,13 +4623,13 @@
"user": {
"user": "User",
"users": "Users",
"customer-users": "Customer Users",
"tenant-admins": "Tenant Admins",
"customer-users": "Customer users",
"tenant-admins": "Tenant admins",
"sys-admin": "System administrator",
"tenant-admin": "Tenant administrator",
"customer": "Customer",
"anonymous": "Anonymous",
"add": "Add User",
"add": "Add user",
"delete": "Delete user",
"add-user-text": "Add new user",
"no-users-text": "No users found",
@ -4644,8 +4644,8 @@
"email": "Email",
"email-required": "Email is required.",
"invalid-email-format": "Invalid email format.",
"first-name": "First Name",
"last-name": "Last Name",
"first-name": "First name",
"last-name": "Last name",
"description": "Description",
"default-dashboard": "Default dashboard",
"always-fullscreen": "Always fullscreen",
@ -4764,7 +4764,7 @@
"auto-commit-settings-read-only-hint": "Auto-commit feature doesn't work with enabled read-only option in Repository settings."
},
"widget": {
"widget-library": "Widgets Library",
"widget-library": "Widgets library",
"widget-bundle": "Widgets Bundle",
"all-bundles": "All bundles",
"select-widgets-bundle": "Select widgets bundle",
@ -4849,7 +4849,7 @@
"widget-template-load-failed-error": "Failed to load widget template!",
"details": "Details",
"widget-details": "Widget details",
"add": "Add Widget",
"add": "Add widget",
"add-existing-widget": "Add existing widget",
"add-new-widget": "Add new widget",
"search-widgets": "Search widgets",
@ -4925,9 +4925,9 @@
},
"widgets-bundle": {
"current": "Current bundle",
"widgets-bundles": "Widgets Bundles",
"widgets-bundles": "Widgets bundles",
"widgets-bundle-widgets": "Widgets Bundle Widgets",
"add": "Add Widgets Bundle",
"add": "Add widgets bundle",
"delete": "Delete widgets bundle",
"title": "Title",
"title-required": "Title is required.",

Loading…
Cancel
Save