Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into bug-fix/action-navigate-to-dashboard

pull/9869/head
Dmitriymush 3 years ago
parent
commit
cd732d5b3f
  1. 4
      application/src/main/data/json/system/widget_bundles/air_quality.json
  2. 2
      application/src/main/data/json/system/widget_bundles/industial_widgets.json
  3. 3
      application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json
  4. 3
      application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json
  5. 3
      application/src/main/data/json/system/widget_types/efficiency_chart_card.json
  6. 3
      application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json
  7. 3
      application/src/main/data/json/system/widget_types/flow_rate_chart_card.json
  8. 3
      application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json
  9. 3
      application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json
  10. 3
      application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json
  11. 3
      application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json
  12. 3
      application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json
  13. 3
      application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json
  14. 3
      application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json
  15. 5
      application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json
  16. 5
      application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json
  17. 3
      application/src/main/data/json/system/widget_types/power_consumption_chart_card.json
  18. 3
      application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json
  19. 3
      application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json
  20. 3
      application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json
  21. 3
      application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json
  22. 3
      application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json
  23. 3
      application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json
  24. 3
      application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json
  25. 18
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  26. 26
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  27. 39
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
  28. 4
      application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java
  29. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  30. 16
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java
  31. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java
  32. 7
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java
  33. 5
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  34. 83
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  35. 38
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  36. 23
      application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
  37. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  38. 11
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  39. 4
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  40. 9
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  41. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  42. 4
      ui-ngx/src/app/core/http/edge.service.ts
  43. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  44. 25
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts
  45. 2
      ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts
  46. 2
      ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html
  47. 34
      ui-ngx/src/app/modules/home/pages/edge/edge.component.ts
  48. 3
      ui-ngx/src/app/shared/components/image/image-gallery.component.html
  49. 19
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  50. 29
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  51. 208
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json

4
application/src/main/data/json/system/widget_bundles/air_quality.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_bundles/industial_widgets.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/carbon_monoxide__co__chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/efficiency_chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/efficiency_chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/flow_rate_chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/flow_rate_chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/fluid_pressure_chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/fluid_pressure_chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/individual_allergy_index__iai__chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/nitrogen_dioxide__no2__chart_card_with_background.json

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/system/widget_types/ozone__o3__chart_card.json

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/system/widget_types/ozone__o3__chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/power_consumption_chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/power_consumption_chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/pump_vibration_chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/pump_vibration_chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/rotational_speed_chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/rotational_speed_chart_card_with_background.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card.json

File diff suppressed because one or more lines are too long

3
application/src/main/data/json/system/widget_types/sulfur_dioxide__so2__chart_card_with_background.json

File diff suppressed because one or more lines are too long

18
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -594,4 +594,22 @@ public class EdgeController extends BaseController {
throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL); throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL);
} }
} }
@ApiOperation(value = "Is edge upgrade enabled (isEdgeUpgradeAvailable)",
notes = "Returns 'true' if upgrade available for connected edge, 'false' - otherwise.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/upgrade/available", method = RequestMethod.GET)
@ResponseBody
public boolean isEdgeUpgradeAvailable(
@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId) throws Exception {
if (isEdgesEnabled() && edgeUpgradeServiceOpt.isPresent()) {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
edgeId = checkNotNull(edgeId);
Edge edge = checkEdgeId(edgeId, Operation.READ);
return edgeUpgradeServiceOpt.get().isUpgradeAvailable(edge.getTenantId(), edge.getId());
} else {
throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL);
}
}
} }

26
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -49,6 +51,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.ThrowingSupplier; import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ImageService;
@ -281,6 +284,19 @@ public class ImageController extends BaseController {
return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result); return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result);
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping("/api/image/specs")
public ImageSpecs getImageUploadSpecs() throws ThingsboardException {
SecurityUser user = getCurrentUser();
if (user.isSystemAdmin()) {
return ImageSpecs.DEFAULT;
}
DefaultTenantProfileConfiguration tenantProfileConfig = tenantProfileCache.get(user.getTenantId()).getDefaultProfileConfiguration();
return ImageSpecs.builder()
.maximumSize(tenantProfileConfig.getMaxResourceSize())
.build();
}
private ResponseEntity<ByteArrayResource> downloadIfChanged(String type, String key, String etag, boolean preview) throws Exception { private ResponseEntity<ByteArrayResource> downloadIfChanged(String type, String key, String etag, boolean preview) throws Exception {
ImageCacheKey cacheKey = ImageCacheKey.forImage(getTenantId(type), key, preview); ImageCacheKey cacheKey = ImageCacheKey.forImage(getTenantId(type), key, preview);
return downloadIfChanged(cacheKey, etag, () -> checkImageInfo(type, key, Operation.READ)); return downloadIfChanged(cacheKey, etag, () -> checkImageInfo(type, key, Operation.READ));
@ -343,4 +359,14 @@ public class ImageController extends BaseController {
return tenantId; return tenantId;
} }
@Data
@Builder
public static class ImageSpecs {
private final long maximumSize;
public static final ImageSpecs DEFAULT = ImageSpecs.builder()
.maximumSize(0)
.build();
}
} }

39
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java

@ -21,8 +21,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUpgradeInfo; import org.thingsboard.server.common.data.EdgeUpgradeInfo;
import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeInstructions;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.install.InstallScripts;
@ -32,6 +37,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
@Service @Service
@Slf4j @Slf4j
@ -47,6 +53,7 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc
private static final String UPGRADE_DIR = "upgrade"; private static final String UPGRADE_DIR = "upgrade";
private final InstallScripts installScripts; private final InstallScripts installScripts;
private final AttributesService attributesService;
@Value("${app.version:unknown}") @Value("${app.version:unknown}")
@Setter @Setter
@ -74,13 +81,41 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc
} }
} }
@Override
public boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception {
Optional<AttributeKvEntry> attributeKvEntryOpt = attributesService.find(tenantId, edgeId, DataConstants.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get();
if (attributeKvEntryOpt.isPresent()) {
String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString());
return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersion);
}
return false;
}
private boolean isVersionGreaterOrEqualsThan(String version1, String version2) {
String[] v1 = version1.split("\\.");
String[] v2 = version2.split("\\.");
int length = Math.max(v1.length, v2.length);
for (int i = 0; i < length; i++) {
int num1 = i < v1.length ? Integer.parseInt(v1[i]) : 0;
int num2 = i < v2.length ? Integer.parseInt(v2[i]) : 0;
if (num1 < num2) {
return false;
} else if (num1 > num2) {
return true;
}
}
return true;
}
private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) { private EdgeInstructions getDockerUpgradeInstructions(String tbVersion, String currentEdgeVersion) {
EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion);
if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) { if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) {
return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available."); return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available.");
} }
StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md"))); StringBuilder result = new StringBuilder(readFile(resolveFile("docker", "upgrade_preparing.md")));
while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { while (edgeUpgradeInfo.getNextEdgeVersion() != null && !tbVersion.equals(currentEdgeVersion)) {
String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md")); String dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md"));
if (edgeUpgradeInfo.isRequiresUpdateDb()) { if (edgeUpgradeInfo.isRequiresUpdateDb()) {
@ -109,7 +144,7 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc
String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md")); String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md"));
upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu"); upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu");
StringBuilder result = new StringBuilder(upgrade_preparing); StringBuilder result = new StringBuilder(upgrade_preparing);
while (edgeUpgradeInfo.getNextEdgeVersion() != null || !tbVersion.equals(currentEdgeVersion)) { while (edgeUpgradeInfo.getNextEdgeVersion() != null && !tbVersion.equals(currentEdgeVersion)) {
String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion(); String edgeVersion = edgeUpgradeInfo.getNextEdgeVersion();
String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md")); String linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md"));
if (edgeUpgradeInfo.isRequiresUpdateDb()) { if (edgeUpgradeInfo.isRequiresUpdateDb()) {

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

@ -17,6 +17,8 @@ package org.thingsboard.server.service.edge.instructions;
import org.thingsboard.server.common.data.EdgeUpgradeInfo; import org.thingsboard.server.common.data.EdgeUpgradeInfo;
import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeInstructions;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Map; import java.util.Map;
@ -27,4 +29,6 @@ public interface EdgeUpgradeInstructionsService {
void updateInstructionMap(Map<String, EdgeUpgradeInfo> upgradeVersions); void updateInstructionMap(Map<String, EdgeUpgradeInfo> upgradeVersions);
void setAppVersion(String version); void setAppVersion(String version);
boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception;
} }

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

@ -822,7 +822,7 @@ public final class EdgeGrpcSession implements Closeable {
} }
private void processSaveEdgeVersionAsAttribute(String edgeVersion) { private void processSaveEdgeVersionAsAttribute(String edgeVersion) {
AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry("edgeVersion", edgeVersion), System.currentTimeMillis()); AttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(new StringDataEntry(DataConstants.EDGE_VERSION_ATTR_KEY, edgeVersion), System.currentTimeMillis());
ctx.getAttributesService().save(this.tenantId, this.edge.getId(), DataConstants.SERVER_SCOPE, attributeKvEntry); ctx.getAttributesService().save(this.tenantId, this.edge.getId(), DataConstants.SERVER_SCOPE, attributeKvEntry);
} }

16
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java

@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
@ -35,12 +36,12 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.partition.AbstractPartitionBasedService; import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -48,6 +49,8 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -55,16 +58,16 @@ import java.util.concurrent.TimeUnit;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@SuppressWarnings("UnstableApiUsage")
public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService { public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService {
private final NotificationCenter notificationCenter; private final NotificationCenter notificationCenter;
private final NotificationRequestService notificationRequestService; private final NotificationRequestService notificationRequestService;
private final SchedulerComponent scheduler;
private final NotificationExecutorService notificationExecutor; private final NotificationExecutorService notificationExecutor;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("notification-scheduler"));
private final Map<NotificationRequestId, ScheduledRequestMetadata> scheduledNotificationRequests = new ConcurrentHashMap<>(); private final Map<NotificationRequestId, ScheduledRequestMetadata> scheduledNotificationRequests = new ConcurrentHashMap<>();
@Override
@PostConstruct @PostConstruct
public void init() { public void init() {
super.init(); super.init();
@ -166,6 +169,13 @@ public class DefaultNotificationSchedulerService extends AbstractPartitionBasedS
return "notifications-scheduler"; return "notifications-scheduler";
} }
@Override
@PreDestroy
public void stop() {
super.stop();
scheduler.shutdownNow();
}
@Data @Data
private static class ScheduledRequestMetadata { private static class ScheduledRequestMetadata {
private final TenantId tenantId; private final TenantId tenantId;

3
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java

@ -43,6 +43,9 @@ public class NotificationTargetExportService extends BaseEntityExportService<Not
CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) usersFilter; CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) usersFilter;
customerUsersFilter.setCustomerId(getExternalIdOrElseInternal(ctx, new CustomerId(customerUsersFilter.getCustomerId())).getId()); customerUsersFilter.setCustomerId(getExternalIdOrElseInternal(ctx, new CustomerId(customerUsersFilter.getCustomerId())).getId());
break; break;
case USER_LIST:
// users list stays as is and is replaced with current user id on import (due to user entities not being supported by VC)
break;
} }
} }
} }

7
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java

@ -41,7 +41,7 @@ import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
import java.util.stream.Collectors; import java.util.List;
@Service @Service
@TbCoreComponent @TbCoreComponent
@ -66,10 +66,7 @@ public class NotificationTargetImportService extends BaseEntityImportService<Not
break; break;
case USER_LIST: case USER_LIST:
UserListFilter userListFilter = (UserListFilter) usersFilter; UserListFilter userListFilter = (UserListFilter) usersFilter;
userListFilter.setUsersIds(userListFilter.getUsersIds().stream() userListFilter.setUsersIds(List.of(ctx.getUser().getUuidId())); // user entities are not supported by VC; replacing with current user id
.map(UserId::new).map(idProvider::getInternalId)
.map(UUIDBased::getId).collect(Collectors.toList())
);
break; break;
case TENANT_ADMINISTRATORS: case TENANT_ADMINISTRATORS:
if (CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantsIds()) || if (CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantsIds()) ||

5
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -41,7 +41,6 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -81,8 +80,6 @@ public class DefaultUpdateService implements UpdateService {
private final RestTemplate restClient = new RestTemplate(); private final RestTemplate restClient = new RestTemplate();
private UpdateMessage updateMessage; private UpdateMessage updateMessage;
private EdgeUpgradeMessage edgeUpgradeMessage;
private String edgeInstallVersion;
private String platform; private String platform;
private String version; private String version;
@ -94,7 +91,6 @@ public class DefaultUpdateService implements UpdateService {
updateMessage = new UpdateMessage(false, version, "", "", updateMessage = new UpdateMessage(false, version, "", "",
"https://thingsboard.io/docs/reference/releases", "https://thingsboard.io/docs/reference/releases",
"https://thingsboard.io/docs/reference/releases"); "https://thingsboard.io/docs/reference/releases");
edgeUpgradeMessage = new EdgeUpgradeMessage(new HashMap<>());
if (updatesEnabled) { if (updatesEnabled) {
try { try {
platform = System.getProperty("platform", "unknown"); platform = System.getProperty("platform", "unknown");
@ -173,5 +169,4 @@ public class DefaultUpdateService implements UpdateService {
public UpdateMessage checkUpdates() { public UpdateMessage checkUpdates() {
return updateMessage; return updateMessage;
} }
} }

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

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
@ -722,6 +723,88 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"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())); "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()));
} }
@Test
public void testFetchPublishTelemetryCommandsForDeviceWhenHostSetToNullInSettings() throws Exception {
loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
ObjectNode http = JacksonUtil.newObjectNode();
http.put("enabled", true);
http.put("host", " ");
http.put("port", 8080);
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", true);
mqtt.set("host", NullNode.getInstance());
mqtt.set("port", NullNode.getInstance());
config.set("mqtt", mqtt);
ObjectNode mqtts = JacksonUtil.newObjectNode();
mqtts.put("enabled", true);
mqtts.put("host", "");
mqtts.set("port", NullNode.getInstance());
config.set("mqtts", mqtts);
ObjectNode coap = JacksonUtil.newObjectNode();
coap.put("enabled", true);
coap.set("host", NullNode.getInstance());
coap.put("port", "");
config.set("coap", coap);
ObjectNode coaps = JacksonUtil.newObjectNode();
coaps.put("enabled", true);
coaps.set("host", NullNode.getInstance());
coaps.set("port", NullNode.getInstance());
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://localhost: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://localhost/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 localhost -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_CERT_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_CERT_PEM + " -h localhost -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = mqttCommands.get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h host.docker.internal -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/mosquitto-clients /bin/sh -c \"curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/mqtts/certificate/download && mosquitto_pub -d -q 1 --cafile " + CA_ROOT_CERT_PEM + " -h host.docker.internal -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"", credentials.getCredentialsId()));
JsonNode coapCommands = commands.get(COAP);
assertThat(coapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(coapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerCoapCommands = coapCommands.get(DOCKER);
assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/coap-clients coap-client -v 6 -m POST coap://host.docker.internal/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --add-host=host.docker.internal:host-gateway thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://host.docker.internal/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
}
private void setConnectivityHost(String host) throws Exception { private void setConnectivityHost(String host) throws Exception {
ObjectNode config = JacksonUtil.newObjectNode(); ObjectNode config = JacksonUtil.newObjectNode();

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

@ -44,11 +44,11 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EdgeUpgradeInfo;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.EdgeUpgradeInfo;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
@ -1200,4 +1200,40 @@ public class EdgeControllerTest extends AbstractControllerTest {
Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.1EDGE")); Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.1EDGE"));
Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.2EDGE")); Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.2EDGE"));
} }
@Test
public void testIsEdgeUpgradeAvailable() throws Exception {
Edge edge = constructEdge("Edge Upgrade Available", "default");
Edge savedEdge = doPost("/api/edge", edge, Edge.class);
// Test 3.5.0 Edge - upgrade not available
String body = "{\"edgeVersion\": \"V_3_5_0\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk());
edgeUpgradeInstructionsService.setAppVersion("3.6.0");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2.7");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
// Test 3.6.0 Edge - upgrade available
body = "{\"edgeVersion\": \"V_3_6_0\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk());
edgeUpgradeInstructionsService.setAppVersion("3.6.0");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.1.5");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
// Test 3.6.1 Edge - upgrade available
body = "{\"edgeVersion\": \"V_3_6_1\"}";
doPostAsync("/api/plugins/telemetry/EDGE/" + savedEdge.getId().getId() + "/attributes/SERVER_SCOPE", body, String.class, status().isOk());
edgeUpgradeInstructionsService.setAppVersion("3.6.1");
Assert.assertFalse(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
edgeUpgradeInstructionsService.setAppVersion("3.6.2.6");
Assert.assertTrue(edgeUpgradeInstructionsService.isUpgradeAvailable(savedEdge.getTenantId(), savedEdge.getId()));
}
} }

23
application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.controller.ImageController.ImageSpecs;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.resource.TbResourceRepository; import org.thingsboard.server.dao.sql.resource.TbResourceRepository;
@ -240,6 +241,28 @@ public class ImageControllerTest extends AbstractControllerTest {
doGet("/api/images/public/" + publicKey).andExpect(status().isNotFound()); doGet("/api/images/public/" + publicKey).andExpect(status().isNotFound());
} }
@Test
public void testGetImageUploadSpecs() throws Exception {
ImageSpecs specs = doGet("/api/image/specs", ImageSpecs.class);
assertThat(specs.getMaximumSize()).isZero();
loginSysAdmin();
updateDefaultTenantProfileConfig(tenantProfileConfig -> {
tenantProfileConfig.setMaxResourceSize(100);
});
loginTenantAdmin();
specs = doGet("/api/image/specs", ImageSpecs.class);
assertThat(specs.getMaximumSize()).isEqualTo(100);
loginSysAdmin();
updateDefaultTenantProfileConfig(tenantProfileConfig -> {
tenantProfileConfig.setMaxResourceSize(0);
});
loginTenantAdmin();
specs = doGet("/api/image/specs", ImageSpecs.class);
assertThat(specs.getMaximumSize()).isEqualTo(0);
}
private TbResourceInfo updateImagePublicStatus(String filename, boolean isPublic) throws Exception { private TbResourceInfo updateImagePublicStatus(String filename, boolean isPublic) throws Exception {
return doPut("/api/images/tenant/" + filename + "/public/" + isPublic, "", TbResourceInfo.class); return doPut("/api/images/tenant/" + filename + "/public/" + isPublic, "", TbResourceInfo.class);
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -124,6 +124,7 @@ public class DataConstants {
public static final String PASSWORD = "password"; public static final String PASSWORD = "password";
public static final String EDGE_MSG_SOURCE = "edge"; public static final String EDGE_MSG_SOURCE = "edge";
public static final String MSG_SOURCE_KEY = "source"; public static final String MSG_SOURCE_KEY = "source";
public static final String EDGE_VERSION_ATTR_KEY = "edgeVersion";
public static final String LAST_CONNECTED_GATEWAY = "lastConnectedGateway"; public static final String LAST_CONNECTED_GATEWAY = "lastConnectedGateway";

11
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -62,6 +62,7 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.getHost; import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.getHost;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.getPort;
@Service("DeviceConnectivityDaoService") @Service("DeviceConnectivityDaoService")
@Slf4j @Slf4j
@ -265,14 +266,14 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(MQTT); DeviceConnectivityInfo properties = getConnectivity(MQTT);
String mqttHost = getHost(baseUrl, properties, MQTT); String mqttHost = getHost(baseUrl, properties, MQTT);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); String mqttPort = getPort(properties);
return DeviceConnectivityUtil.getMqttPublishCommand(MQTT, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); return DeviceConnectivityUtil.getMqttPublishCommand(MQTT, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
} }
private List<String> getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { private List<String> getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(MQTTS); DeviceConnectivityInfo properties = getConnectivity(MQTTS);
String mqttHost = getHost(baseUrl, properties, MQTTS); String mqttHost = getHost(baseUrl, properties, MQTTS);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); String mqttPort = getPort(properties);
String pubCommand = DeviceConnectivityUtil.getMqttPublishCommand(MQTTS, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); String pubCommand = DeviceConnectivityUtil.getMqttPublishCommand(MQTTS, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
ArrayList<String> commands = new ArrayList<>(); ArrayList<String> commands = new ArrayList<>();
@ -287,7 +288,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(protocol); DeviceConnectivityInfo properties = getConnectivity(protocol);
String mqttHost = getHost(baseUrl, properties, protocol); String mqttHost = getHost(baseUrl, properties, protocol);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); String mqttPort = getPort(properties);
return DeviceConnectivityUtil.getDockerMqttPublishCommand(protocol, baseUrl, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); return DeviceConnectivityUtil.getDockerMqttPublishCommand(protocol, baseUrl, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials);
} }
@ -327,14 +328,14 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(protocol); DeviceConnectivityInfo properties = getConnectivity(protocol);
String hostName = getHost(baseUrl, properties, protocol); String hostName = getHost(baseUrl, properties, protocol);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); String port = StringUtils.isBlank(properties.getPort()) ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getCoapPublishCommand(protocol, hostName, port, deviceCredentials); return DeviceConnectivityUtil.getCoapPublishCommand(protocol, hostName, port, deviceCredentials);
} }
private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(protocol); DeviceConnectivityInfo properties = getConnectivity(protocol);
String host = getHost(baseUrl, properties, protocol); String host = getHost(baseUrl, properties, protocol);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); String port = StringUtils.isBlank(properties.getPort()) ? "" : ":" + properties.getPort();
return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials); return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials);
} }

4
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -138,8 +138,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
@Override @Override
@SneakyThrows @SneakyThrows
public TbResourceInfo saveImage(TbResource image) { public TbResourceInfo saveImage(TbResource image) {
if (image.getId() == null && StringUtils.isEmpty(image.getResourceKey())) { if (image.getId() == null) {
image.setResourceKey(getUniqueKey(image.getTenantId(), image.getFileName())); image.setResourceKey(getUniqueKey(image.getTenantId(), StringUtils.defaultIfEmpty(image.getResourceKey(), image.getFileName())));
} }
resourceValidator.validate(image, TbResourceInfo::getTenantId); resourceValidator.validate(image, TbResourceInfo::getTenantId);

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

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.util;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.device.DeviceConnectivityInfo; import org.thingsboard.server.dao.device.DeviceConnectivityInfo;
@ -63,7 +64,7 @@ public class DeviceConnectivityUtil {
if (MQTTS.equals(protocol)) { if (MQTTS.equals(protocol)) {
command.append(" --cafile ").append(CA_ROOT_CERT_PEM); command.append(" --cafile ").append(CA_ROOT_CERT_PEM);
} }
command.append(" -h ").append(host).append(port == null ? "" : " -p " + port); command.append(" -h ").append(host).append(StringUtils.isBlank(port) ? "" : " -p " + port);
command.append(" -t ").append(deviceTelemetryTopic); command.append(" -t ").append(deviceTelemetryTopic);
switch (deviceCredentials.getCredentialsType()) { switch (deviceCredentials.getCredentialsType()) {
@ -219,7 +220,7 @@ public class DeviceConnectivityUtil {
} }
public static String getHost(String baseUrl, DeviceConnectivityInfo properties, String protocol) throws URISyntaxException { public static String getHost(String baseUrl, DeviceConnectivityInfo properties, String protocol) throws URISyntaxException {
String initialHost = properties.getHost().isEmpty() ? baseUrl : properties.getHost(); String initialHost = StringUtils.isBlank(properties.getHost()) ? baseUrl : properties.getHost();
InetAddress inetAddress; InetAddress inetAddress;
String host = null; String host = null;
if (VALID_URL_PATTERN.matcher(initialHost).matches()) { if (VALID_URL_PATTERN.matcher(initialHost).matches()) {
@ -243,6 +244,10 @@ public class DeviceConnectivityUtil {
return host; return host;
} }
public static String getPort(DeviceConnectivityInfo properties) {
return StringUtils.isBlank(properties.getPort()) ? "" : properties.getPort();
}
public static boolean isLocalhost(String host) { public static boolean isLocalhost(String host) {
try { try {
InetAddress inetAddress = InetAddress.getByName(host); InetAddress inetAddress = InetAddress.getByName(host);

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

4
ui-ngx/src/app/core/http/edge.service.ts

@ -121,4 +121,8 @@ export class EdgeService {
public getEdgeUpgradeInstructions(edgeVersion: string, method: string = 'ubuntu', config?: RequestConfig): Observable<EdgeInstructions> { public getEdgeUpgradeInstructions(edgeVersion: string, method: string = 'ubuntu', config?: RequestConfig): Observable<EdgeInstructions> {
return this.http.get<EdgeInstructions>(`/api/edge/instructions/upgrade/${edgeVersion}/${method}`, defaultHttpOptionsFromConfig(config)); return this.http.get<EdgeInstructions>(`/api/edge/instructions/upgrade/${edgeVersion}/${method}`, defaultHttpOptionsFromConfig(config));
} }
public isEdgeUpgradeAvailable(edgeId: string, config?: RequestConfig): Observable<boolean> {
return this.http.get<boolean>(`/api/edge/${edgeId}/upgrade/available`, defaultHttpOptionsFromConfig(config));
}
} }

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -34,7 +34,7 @@
<mat-label translate>dashboard.state-controller</mat-label> <mat-label translate>dashboard.state-controller</mat-label>
<mat-select required formControlName="stateControllerId"> <mat-select required formControlName="stateControllerId">
<mat-option *ngFor="let stateControllerId of stateControllerIds" [value]="stateControllerId"> <mat-option *ngFor="let stateControllerId of stateControllerIds" [value]="stateControllerId">
{{stateControllerId}} {{ getStatesControllerTranslation(stateControllerId) }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

25
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts

@ -19,14 +19,12 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DashboardSettings, GridSettings, StateControllerId } from '@app/shared/models/dashboard.models'; import { DashboardSettings, GridSettings, StateControllerId } from '@app/shared/models/dashboard.models';
import { isDefined, isUndefined } from '@core/utils'; import { isDefined, isUndefined } from '@core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { StatesControllerService } from './states/states-controller.service'; import { StatesControllerService } from './states/states-controller.service';
export interface DashboardSettingsDialogData { export interface DashboardSettingsDialogData {
@ -46,21 +44,23 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
settings: DashboardSettings; settings: DashboardSettings;
gridSettings: GridSettings; gridSettings: GridSettings;
settingsFormGroup: UntypedFormGroup; settingsFormGroup: FormGroup;
gridSettingsFormGroup: UntypedFormGroup; gridSettingsFormGroup: FormGroup;
stateControllerIds: string[]; stateControllerIds: string[];
submitted = false; submitted = false;
private stateControllerTranslationMap = new Map<string, string>([
['default', 'dashboard.state-controller-default'],
]);
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData, @Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>, public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: UntypedFormBuilder, private fb: FormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService, private translate: TranslateService,
private statesControllerService: StatesControllerService) { private statesControllerService: StatesControllerService) {
super(store, router, dialogRef); super(store, router, dialogRef);
@ -182,7 +182,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
ngOnInit(): void { ngOnInit(): void {
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted); const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
@ -204,4 +204,11 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
this.dialogRef.close({settings, gridSettings}); this.dialogRef.close({settings, gridSettings});
} }
getStatesControllerTranslation(stateControllerId: string): string {
if (this.stateControllerTranslationMap.has(stateControllerId)) {
return this.translate.instant(this.stateControllerTranslationMap.get(stateControllerId));
}
return stateControllerId;
}
} }

2
ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts

@ -112,7 +112,7 @@ export class GeneralSettingsComponent extends PageComponent implements HasConfir
saveDeviceConnectivitySettings(): void { saveDeviceConnectivitySettings(): void {
this.deviceConnectivitySettings.jsonValue = { this.deviceConnectivitySettings.jsonValue = {
...this.deviceConnectivitySettings.jsonValue, ...this.deviceConnectivitySettings.jsonValue,
...this.deviceConnectivitySettingsForm.value ...this.deviceConnectivitySettingsForm.getRawValue()
}; };
this.adminService.saveAdminSettings<DeviceConnectivitySettings>(this.deviceConnectivitySettings) this.adminService.saveAdminSettings<DeviceConnectivitySettings>(this.deviceConnectivitySettings)
.subscribe(deviceConnectivitySettings => this.processDeviceConnectivitySettings(deviceConnectivitySettings)); .subscribe(deviceConnectivitySettings => this.processDeviceConnectivitySettings(deviceConnectivitySettings));

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

@ -149,7 +149,7 @@
<div class="tb-install-instruction-text"> <div class="tb-install-instruction-text">
<ng-container *ngTemplateOutlet="seeDocumentation; context: <ng-container *ngTemplateOutlet="seeDocumentation; context:
{doc: {text: 'device.connectivity.install-mqtt-windows', {doc: {text: 'device.connectivity.install-mqtt-windows',
href: 'https://thingsboard.io/docs/getting-started-guides/helloworld/?connectdevice=mqtt-windows#step-2-connect-device'}}"> href: 'https://thingsboard.io/docs/reference/mqtt-api/?connectdevice=mqtt-windows#mqtt-connect'}}">
</ng-container> </ng-container>
</div> </div>
</div> </div>

34
ui-ngx/src/app/modules/home/pages/edge/edge.component.ts

@ -20,15 +20,13 @@ import { AppState } from '@core/core.state';
import { EntityComponent } from '@home/components/entity/entity.component'; import { EntityComponent } from '@home/components/entity/entity.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { EdgeInfo, edgeVersionAttributeKey } from '@shared/models/edge.models'; import { EdgeInfo } from '@shared/models/edge.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { generateSecret, guid } from '@core/utils'; import { generateSecret, guid } from '@core/utils';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { environment as env } from '@env/environment'; import {EdgeService} from "@core/http/edge.service";
import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
@Component({ @Component({
selector: 'tb-edge', selector: 'tb-edge',
@ -44,7 +42,7 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected translate: TranslateService, protected translate: TranslateService,
private attributeService: AttributeService, private edgeService: EdgeService,
@Inject('entity') protected entityValue: EdgeInfo, @Inject('entity') protected entityValue: EdgeInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>, @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>,
public fb: UntypedFormBuilder, public fb: UntypedFormBuilder,
@ -100,7 +98,10 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
} }
}); });
this.generateRoutingKeyAndSecret(entity, this.entityForm); this.generateRoutingKeyAndSecret(entity, this.entityForm);
this.checkEdgeVersion(); this.edgeService.isEdgeUpgradeAvailable(this.entity.id.id)
.subscribe(isUpgradeAvailable => {
this.upgradeAvailable = isUpgradeAvailable;
});
} }
updateFormState() { updateFormState() {
@ -139,25 +140,4 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
form.get('secret').patchValue(generateSecret(20), {emitEvent: false}); form.get('secret').patchValue(generateSecret(20), {emitEvent: false});
} }
} }
checkEdgeVersion() {
this.attributeService.getEntityAttributes(this.entity.id, AttributeScope.SERVER_SCOPE, [edgeVersionAttributeKey])
.subscribe(attributes => {
if (attributes?.length) {
const edgeVersion = attributes[0].value;
const tbVersion = 'V_' + env.tbVersion.replaceAll('.', '_');
this.upgradeAvailable = this.versionUpgradeSupported(edgeVersion) && (edgeVersion !== tbVersion);
} else {
this.upgradeAvailable = false;
}
}
);
}
private versionUpgradeSupported(edgeVersion: string): boolean {
const edgeVersionArray = edgeVersion.split('_');
const major = parseInt(edgeVersionArray[1]);
const minor = parseInt(edgeVersionArray[2]);
return major >= 3 && minor >= 6;
}
} }

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

@ -310,10 +310,9 @@
<ng-template #loadingImages> <ng-template #loadingImages>
<div fxLayout="column" <div fxLayout="column"
fxLayoutAlign="center center" class="tb-absolute-fill"> fxLayoutAlign="center center" class="tb-absolute-fill">
<span class="mat-headline-5" style="padding-bottom: 20px;"> <span>
{{ 'common.loading' | translate }} {{ 'common.loading' | translate }}
</span> </span>
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div> </div>
</ng-template> </ng-template>
<ng-template #noImages> <ng-template #noImages>

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

@ -1186,6 +1186,7 @@
"hide-details": "Hide details", "hide-details": "Hide details",
"select-state": "Select target state", "select-state": "Select target state",
"state-controller": "State controller", "state-controller": "State controller",
"state-controller-default": "static (deprecated)",
"search": "Search dashboards", "search": "Search dashboards",
"selected-dashboards": "{ count, plural, =1 {1 dashboard} other {# dashboards} } selected", "selected-dashboards": "{ count, plural, =1 {1 dashboard} other {# dashboards} } selected",
"home-dashboard": "Home dashboard", "home-dashboard": "Home dashboard",
@ -4056,15 +4057,15 @@
"maximum-rule-chains": "Rule chains maximum number", "maximum-rule-chains": "Rule chains maximum number",
"maximum-rule-chains-required": "Rule chains maximum number is required.", "maximum-rule-chains-required": "Rule chains maximum number is required.",
"maximum-rule-chains-range": "Rule chains maximum number can't be negative", "maximum-rule-chains-range": "Rule chains maximum number can't be negative",
"maximum-resources-sum-data-size": "Resource files sum size", "maximum-resources-sum-data-size": "Maximum total size of resources files (bytes)",
"maximum-resources-sum-data-size-required": "Resource files sum size is required.", "maximum-resources-sum-data-size-required": "Maximum total size of resources files is required.",
"maximum-resources-sum-data-size-range": "Resource files sum size can't be negative", "maximum-resources-sum-data-size-range": "Maximum total size of resources files can't be negative",
"maximum-resource-size": "Resource maximum size", "maximum-resource-size": "Maximum resource file size (bytes)",
"maximum-resource-size-required": "Resource maximum size is required", "maximum-resource-size-required": "Maximum resource file size is required",
"maximum-resource-size-range": "Resource maximum size can't be negative", "maximum-resource-size-range": "Maximum resource file size can't be negative",
"maximum-ota-packages-sum-data-size": "OTA package files sum size", "maximum-ota-packages-sum-data-size": "Maximum total size of OTA package files (bytes)",
"maximum-ota-package-sum-data-size-required": "OTA package files sum size is required.", "maximum-ota-package-sum-data-size-required": "Maximum total size of OTA package files is required.",
"maximum-ota-package-sum-data-size-range": "OTA package files sum size can't be negative", "maximum-ota-package-sum-data-size-range": "Maximum total size of OTA package files can't be negative",
"rest-requests-for-tenant": "REST requests for tenant", "rest-requests-for-tenant": "REST requests for tenant",
"transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages",
"transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points",

29
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -3093,6 +3093,35 @@
"backup-code-auth-description": "Por favor, introduce el código de backup.", "backup-code-auth-description": "Por favor, introduce el código de backup.",
"backup-code-auth-placeholder": "Código de Backup" "backup-code-auth-placeholder": "Código de Backup"
}, },
"signup": {
"firstname": "Nombre",
"lastname": "Apellido",
"email": "Correo electrónico",
"signup": "Registrarse",
"create-password": "Crear una contraseña",
"repeat-password": "Repite tu contraseña",
"have-account": "¿Ya tienes una cuenta?",
"signin": "Iniciar sesión",
"no-captcha-message": "Debes confirmar que no eres un robot",
"password-length-message": "Tu contraseña debe tener al menos 6 caracteres",
"email-verification": "Verificación de correo electrónico",
"email-verification-message": "Se envió un correo electrónico con detalles de verificación a la dirección de correo electrónico especificada. Por favor, sigue las instrucciones proporcionadas en el correo electrónico para completar tu proceso de registro. Nota: si no has visto el correo electrónico durante un tiempo, por favor revisa tu carpeta de 'spam' o intenta reenviar el correo electrónico haciendo clic en el botón 'Reenviar'.",
"account-activation-title": "Activación de cuenta",
"account-activated": "¡Cuenta activada con éxito!",
"account-activated-text": "¡Felicidades! Tu cuenta ha sido activada.",
"resend": "Reenviar",
"inactive-user-exists-title": "Ya existe un usuario inactivo",
"inactive-user-exists-text": "Ya hay un usuario registrado con dirección de correo electrónico no verificada. Haz clic en el botón 'Reenviar' si deseas reenviar el correo electrónico de verificación.",
"activating-account": "Activando cuenta...",
"activating-account-text": "Tu cuenta se está activando actualmente. Por favor espera...",
"accept-privacy-policy": "Aceptar la Política de Privacidad",
"accept": "Aceptar",
"privacy-policy": "Política de Privacidad",
"accept-privacy-policy-message": "Debes aceptar nuestra Política de Privacidad",
"recaptcha-title": "reCAPTCHA",
"terms-of-use": "Términos de Uso",
"accept-terms-of-use-message": "Debes aceptar nuestros Términos de Uso"
},
"markdown": { "markdown": {
"edit": "Editar", "edit": "Editar",
"preview": "Previsualizar", "preview": "Previsualizar",

208
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -6,13 +6,20 @@
"access-forbidden": "Доступ заборонено", "access-forbidden": "Доступ заборонено",
"access-forbidden-text": "Недостатньо прав для доступу! <br/> Спробуйте увійти як інший користувач, якщо ви все ще хочете отримати доступ до цього ресурсу.", "access-forbidden-text": "Недостатньо прав для доступу! <br/> Спробуйте увійти як інший користувач, якщо ви все ще хочете отримати доступ до цього ресурсу.",
"refresh-token-expired": "Дані про сесію застарілі", "refresh-token-expired": "Дані про сесію застарілі",
"refresh-token-failed": "Не вдається відновити сеанс" "refresh-token-failed": "Не вдається відновити сеанс",
"permission-denied": "У дозволі відмовлено",
"permission-denied-text": "Ви не маєте дозволу на виконання цієї операції!"
},
"account": {
"account": "Обліковий запис",
"notification-settings": "Налаштування сповіщень"
}, },
"action": { "action": {
"activate": "Активувати", "activate": "Активувати",
"suspend": "Призупинити", "suspend": "Призупинити",
"save": "Зберегти", "save": "Зберегти",
"saveAs": "Зберегти як", "saveAs": "Зберегти як",
"move": "Перемістити",
"cancel": "Скасувати", "cancel": "Скасувати",
"ok": "OK", "ok": "OK",
"delete": "Видалити", "delete": "Видалити",
@ -21,6 +28,7 @@
"no": "Ні", "no": "Ні",
"update": "Оновити", "update": "Оновити",
"remove": "Видалити", "remove": "Видалити",
"select": "Вибрати",
"search": "Пошук", "search": "Пошук",
"clear-search": "Очистити пошук", "clear-search": "Очистити пошук",
"assign": "Надати", "assign": "Надати",
@ -32,6 +40,7 @@
"edit-mode": "Режим редагування", "edit-mode": "Режим редагування",
"enter-edit-mode": "Ввійти в режим редагування", "enter-edit-mode": "Ввійти в режим редагування",
"decline-changes": "Відхилити зміни", "decline-changes": "Відхилити зміни",
"decline": "Відхилити",
"close": "Закрити", "close": "Закрити",
"back": "Назад", "back": "Назад",
"run": "Запустити", "run": "Запустити",
@ -51,9 +60,27 @@
"share-via": "Поділитися через {{provider}}", "share-via": "Поділитися через {{provider}}",
"continue": "Продовжити", "continue": "Продовжити",
"discard-changes": "Скасувати зміни", "discard-changes": "Скасувати зміни",
"move": "Перемістити", "download": "Завантажити",
"select": "Вибрати", "next": "Наступний",
"done": "Завершено" "next-with-label": "Наступний: {{label}}",
"read-more": "Прочитати більше",
"hide": "Приховати",
"done": "Завершено",
"print": "Друкувати",
"restore": "Відновити",
"confirm": "Підтвердити",
"more": "Більше",
"less": "Менше",
"skip": "Пропустити",
"send": "Надіслати",
"reset": "Скидання",
"show-more": "Показати більше",
"dont-show-again": "Більше не показувати",
"see-documentation": "Див. документацію",
"clear": "Очистити",
"upload": "Надіслати",
"delete-anyway": "Все одно видалити",
"delete-selected": "Видалити вибране"
}, },
"aggregation": { "aggregation": {
"aggregation": "Агрегація", "aggregation": "Агрегація",
@ -68,6 +95,7 @@
"none": "Відсутня" "none": "Відсутня"
}, },
"admin": { "admin": {
"settings": "Налаштування",
"general": "Загальне", "general": "Загальне",
"general-settings": "Загальні налаштування", "general-settings": "Загальні налаштування",
"outgoing-mail": "Поштовий сервер", "outgoing-mail": "Поштовий сервер",
@ -126,15 +154,15 @@
"minimum-max-failed-login-attempts-range": "Максимальна кількість невдалих спроб входу не може бути негативною", "minimum-max-failed-login-attempts-range": "Максимальна кількість невдалих спроб входу не може бути негативною",
"user-lockout-notification-email": "У разі блокування облікового запису користувача, надішліть сповіщення на електронну пошту", "user-lockout-notification-email": "У разі блокування облікового запису користувача, надішліть сповіщення на електронну пошту",
"smpp-provider": { "smpp-provider": {
"smpp-version": "SMPP верія", "smpp-version": "SMPP верcія",
"smpp-host": "SMPP хост", "smpp-host": "SMPP хост",
"smpp-host-required": "Хост SMPP обов'язковий.", "smpp-host-required": "Хост SMPP обов'язковий.",
"smpp-port": "SMPP порт", "smpp-port": "SMPP порт",
"smpp-port-required": "Порт SMPP обов'язковий.", "smpp-port-required": "Порт SMPP обов'язковий.",
"system-id": "Id системи", "system-id": "Id системи",
"system-id-required": "Id системи обязателен.", "system-id-required": "ID системи обов'язковий.",
"password": "Пароль", "password": "Пароль",
"password-required": "Пароль обязателен.", "password-required": "Пароль обов'язковий.",
"type-settings": "Налаштування типів", "type-settings": "Налаштування типів",
"source-settings": "Налаштування джерела", "source-settings": "Налаштування джерела",
"destination-settings": "Налаштування призначення", "destination-settings": "Налаштування призначення",
@ -144,12 +172,13 @@
"service-type": "Тип обслуговування", "service-type": "Тип обслуговування",
"source-address": "Адреса джерела", "source-address": "Адреса джерела",
"source-ton": "Тип номера джерела", "source-ton": "Тип номера джерела",
"source-npi": "Идентификация плана нумерации джерела", "source-npi": "Ідентифікація плану нумерації джерела",
"destination-ton": "Тип номера призначення", "destination-ton": "Тип номера призначення",
"destination-npi": "Ідентифікація плану нумерації призначення", "destination-npi": "Ідентифікація плану нумерації призначення",
"address-range": "Діапазон адрес", "address-range": "Діапазон адрес",
"coding-scheme": "Схема кодування" "coding-scheme": "Схема кодування"
} },
"resources": "Ресурси"
}, },
"alarm": { "alarm": {
"alarm": "Сигнал тривоги", "alarm": "Сигнал тривоги",
@ -249,8 +278,8 @@
"filter-type": "Тип фільтра", "filter-type": "Тип фільтра",
"filter-type-required": "Необхідно вказати тип фільтра.", "filter-type-required": "Необхідно вказати тип фільтра.",
"entity-filter-no-entity-matched": "Не знайдено жодних сутностей, які відповідають вказаному фільтру.", "entity-filter-no-entity-matched": "Не знайдено жодних сутностей, які відповідають вказаному фільтру.",
"no-entity-filter-specified": "Фільтр обїектів не вказано", "no-entity-filter-specified": "Фільтр сутностей не вказано",
"root-state-entity": "Використовувати сутінсть стану як кореневу", "root-state-entity": "Використовувати сутність стану як кореневу",
"last-level-relation": "Використовувати лише відношення останнього рівня", "last-level-relation": "Використовувати лише відношення останнього рівня",
"group-state-entity": "Використовувати групу сутностей стану як кореневу", "group-state-entity": "Використовувати групу сутностей стану як кореневу",
"root-entity": "Коренева сутність", "root-entity": "Коренева сутність",
@ -323,7 +352,7 @@
"copyId": "Копіювати Id активу", "copyId": "Копіювати Id активу",
"idCopiedMessage": "Id активу був скопійований у буфер обміну", "idCopiedMessage": "Id активу був скопійований у буфер обміну",
"select-asset": "Виберіть актив", "select-asset": "Виберіть актив",
"no-assets-matching": "Не знайдено жодних активів, що відповідають'{{entity}}'.", "no-assets-matching": "Не знайдено жодних активів, що відповідають '{{entity}}'.",
"asset-required": "Необхідно задати актив", "asset-required": "Необхідно задати актив",
"name-starts-with": "Назва активу починається з", "name-starts-with": "Назва активу починається з",
"selected-assets": "{ count, plural, =1 {1 актив} other {# активи} } selected", "selected-assets": "{ count, plural, =1 {1 актив} other {# активи} } selected",
@ -367,6 +396,9 @@
"selected-attributes": "{ count, plural, =1 {1 attribute} other {# attributes} } selected ...вибрані вибрати", "selected-attributes": "{ count, plural, =1 {1 attribute} other {# attributes} } selected ...вибрані вибрати",
"selected-telemetry": "{ count, plural, =1 {1 telemetry unit} other {# telemetry units} } selected" "selected-telemetry": "{ count, plural, =1 {1 telemetry unit} other {# telemetry units} } selected"
}, },
"api-usage": {
"api-usage": "Використання API"
},
"audit-log": { "audit-log": {
"audit": "Операція", "audit": "Операція",
"audit-logs": "Журнал операцій", "audit-logs": "Журнал операцій",
@ -476,14 +508,14 @@
"test": "Тест", "test": "Тест",
"metadata": "Метадані", "metadata": "Метадані",
"metadata-required": "Записи метаданих не можуть бути порожніми.", "metadata-required": "Записи метаданих не можуть бути порожніми.",
"integration-metadata": "Метедані інтеграції", "integration-metadata": "Метадані інтеграції",
"integration-metadata-required": "Параметри метаданих інтеграції не можуть бути порожніми.", "integration-metadata-required": "Параметри метаданих інтеграції не можуть бути порожніми.",
"output": "Вихідні дані", "output": "Вихідні дані",
"import": "Імпорт перетворювача даних", "import": "Імпорт перетворювача даних",
"export": "Експорт перетворювача даних", "export": "Експорт перетворювача даних",
"export-failed-error": "Неможливо експортувати перетворювач даних: {{помилка}}", "export-failed-error": "Неможливо експортувати перетворювач даних: {{помилка}}",
"create-new-converter": "Створити новий перетворювач даних", "create-new-converter": "Створити новий перетворювач даних",
"converter-file": "Файл перетворювача даних(конвектер файл)", "converter-file": "Файл перетворювача даних",
"invalid-converter-file-error": "Неможливо імпортувати перетворювач даних: недійсна структура даних перетворювача.", "invalid-converter-file-error": "Неможливо імпортувати перетворювач даних: недійсна структура даних перетворювача.",
"type": "Тип", "type": "Тип",
"type-required": "Необхідно задати тип.", "type-required": "Необхідно задати тип.",
@ -620,7 +652,7 @@
"unassign-dashboard-text": "Після підтвердження, клієнт буде позбавлений панелі приладів. Панель приладів і пов'язані з нею дані будуть недоступні клієнтові.", "unassign-dashboard-text": "Після підтвердження, клієнт буде позбавлений панелі приладів. Панель приладів і пов'язані з нею дані будуть недоступні клієнтові.",
"unassign-dashboard": "Позбавити панелі приладів", "unassign-dashboard": "Позбавити панелі приладів",
"unassign-dashboards-title": "Ви впевнені, що хочете позбавити { count, plural, =1 {1 панелі приладів} other {# панелей приладів} }?", "unassign-dashboards-title": "Ви впевнені, що хочете позбавити { count, plural, =1 {1 панелі приладів} other {# панелей приладів} }?",
"unassign-dashboards-text": "Після підтвердження, клієтн буде позбавлений усіх вибраних панелей приладів і даних, які з ними пов'язані.", "unassign-dashboards-text": "Після підтвердження, клієнт буде позбавлений усіх вибраних панелей приладів і даних, які з ними пов'язані.",
"public-dashboard-title": "Панель приладів тепер публічна", "public-dashboard-title": "Панель приладів тепер публічна",
"public-dashboard-text": "Ваша панель приладів <b>{{dashboardTitle}}</b> тепер публічна і доступна іншим <a href='{{publicLink}}' target='_blank'>link</a>:", "public-dashboard-text": "Ваша панель приладів <b>{{dashboardTitle}}</b> тепер публічна і доступна іншим <a href='{{publicLink}}' target='_blank'>link</a>:",
"public-dashboard-notice": "<b>Note:</b> Не забудьте зробити спільні пристрої загальнодоступними, щоб отримати доступ до їхніх даних.", "public-dashboard-notice": "<b>Note:</b> Не забудьте зробити спільні пристрої загальнодоступними, щоб отримати доступ до їхніх даних.",
@ -645,7 +677,7 @@
"settings": "Налаштування", "settings": "Налаштування",
"columns-count": "Кількість стовпців", "columns-count": "Кількість стовпців",
"columns-count-required": "Необхідно вказати кількість стовпців.", "columns-count-required": "Необхідно вказати кількість стовпців.",
"min-columns-count-message": "Дозволений мінімум -10 стовпців.", "min-columns-count-message": "Дозволений мінімум - 10 стовпців.",
"max-columns-count-message": "Дозволений максимум - 1000 стовпців.", "max-columns-count-message": "Дозволений максимум - 1000 стовпців.",
"widgets-margins": "Відступ між віджетами", "widgets-margins": "Відступ між віджетами",
"horizontal-margin": "Горизонтальний відступ", "horizontal-margin": "Горизонтальний відступ",
@ -686,8 +718,8 @@
"widget-file": "Файл віджета", "widget-file": "Файл віджета",
"invalid-widget-file-error": "Неможливо імпортувати віджет: неправильна структура даних віджета.", "invalid-widget-file-error": "Неможливо імпортувати віджет: неправильна структура даних віджета.",
"widget-import-missing-aliases-title": "Налаштувати псевдоніми, що використовуються імпортованим віджетом", "widget-import-missing-aliases-title": "Налаштувати псевдоніми, що використовуються імпортованим віджетом",
"open-toolbar": "Відкрити панель інструменів ", "open-toolbar": "Відкрити панель інструментів",
"close-toolbar": "Закрити панель інструменів", "close-toolbar": "Закрити панель інструментів",
"configuration-error": "Помилка конфігурації", "configuration-error": "Помилка конфігурації",
"alias-resolution-error-title": "Помилка конфігурації псевдонімів панелі візуалізації", "alias-resolution-error-title": "Помилка конфігурації псевдонімів панелі візуалізації",
"invalid-aliases-config": "Неможливо знайти пристрої, які відповідають певному фільтру псевдонімів. <br/> Зверніться до свого адміністратора, щоб вирішити цю проблему.", "invalid-aliases-config": "Неможливо знайти пристрої, які відповідають певному фільтру псевдонімів. <br/> Зверніться до свого адміністратора, щоб вирішити цю проблему.",
@ -717,7 +749,7 @@
"show-details": "Показати деталі", "show-details": "Показати деталі",
"hide-details": "Приховати деталі", "hide-details": "Приховати деталі",
"select-state": "Виберіть цільовий стан", "select-state": "Виберіть цільовий стан",
"state-controller": "Контроллер стану" "state-controller": "Контролер стану"
}, },
"datakey": { "datakey": {
"settings": "Налаштування", "settings": "Налаштування",
@ -732,11 +764,11 @@
"timeseries": "Телеметрія", "timeseries": "Телеметрія",
"attributes": "Атрибути", "attributes": "Атрибути",
"entity-field": "Поле сутності", "entity-field": "Поле сутності",
"alarm": "Поля сигнала тривоги", "alarm": "Поля сигналу тривоги",
"timeseries-required": "Необхідно вказати Телеметрія.", "timeseries-required": "Необхідно вказати Телеметрія.",
"timeseries-or-attributes-required": "Необхідно вказати телеметрію/атрибути.", "timeseries-or-attributes-required": "Необхідно вказати телеметрію/атрибути.",
"maximum-timeseries-or-attributes": "Максимальні { count, plural, =1 {1 телеметрія/атрибут дозволені.} other {# телеметрія/атрибути дозволені} }", "maximum-timeseries-or-attributes": "Максимальні { count, plural, =1 {1 телеметрія/атрибут дозволені.} other {# телеметрія/атрибути дозволені} }",
"alarm-fields-required": "Необхідно вказати поля сигнала тривоги.", "alarm-fields-required": "Необхідно вказати поля сигналу тривоги.",
"function-types": "Типи функцій", "function-types": "Типи функцій",
"function-types-required": "Необхідно вказати типи функцій.", "function-types-required": "Необхідно вказати типи функцій.",
"maximum-function-types": "Maximum { count, plural, =1 {1 function type is allowed.} other {# function types are allowed} }", "maximum-function-types": "Maximum { count, plural, =1 {1 function type is allowed.} other {# function types are allowed} }",
@ -771,7 +803,7 @@
"no-keys-found": "Ключі не знайдено.", "no-keys-found": "Ключі не знайдено.",
"create-new-alias": "Створити новий!", "create-new-alias": "Створити новий!",
"create-new-key": "Створити новий!", "create-new-key": "Створити новий!",
"duplicate-alias-error": "Псевдонім з таким іменем '{{alias}}' вже існює.<br>Псевдоніми пристроїв повинні бути унікальними на панелі візуалізації.", "duplicate-alias-error": "Псевдонім з таким іменем '{{alias}}' вже існує.<br>Псевдоніми пристроїв повинні бути унікальними на панелі візуалізації.",
"configure-alias": "Налаштувати псевдонім '{{alias}}'", "configure-alias": "Налаштувати псевдонім '{{alias}}'",
"no-devices-matching": "Не знайдено жодних пристроїв, які відповідають '{{entity}}'.", "no-devices-matching": "Не знайдено жодних пристроїв, які відповідають '{{entity}}'.",
"alias": "Псевдонім", "alias": "Псевдонім",
@ -861,6 +893,14 @@
"list-of-groups": "{ count, plural, =1 {Одна група пристроїв} other {Список # груп пристроїв} }", "list-of-groups": "{ count, plural, =1 {Одна група пристроїв} other {Список # груп пристроїв} }",
"group-name-starts-with": "Групи пристроїв, імена яких починаються з '{{prefix}}'" "group-name-starts-with": "Групи пристроїв, імена яких починаються з '{{prefix}}'"
}, },
"asset-profile": {
"asset-profile": "Профіль активу",
"asset-profiles": "Профілі активів"
},
"device-profile": {
"device-profile": "Профіль пристрою",
"device-profiles": "Профілі пристроїв"
},
"dialog": { "dialog": {
"close": "Закрити діалогове вікно" "close": "Закрити діалогове вікно"
}, },
@ -868,6 +908,11 @@
"column": "Колонка", "column": "Колонка",
"row": "Рядок" "row": "Рядок"
}, },
"edge": {
"management": "Керування Edge",
"instances": "Інстанси",
"rulechain-templates": "Шаблони ланцюжків правил"
},
"error": { "error": {
"unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.", "unable-to-connect": "Неможливо підключитися до сервера! Перевірте підключення до Інтернету.",
"unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}", "unhandled-error-code": "Неопрацьований помилковий код: {{errorCode}}",
@ -879,7 +924,7 @@
"aliases": "Псевдоніми сутності", "aliases": "Псевдоніми сутності",
"entity-alias": "Псевдонім сутності", "entity-alias": "Псевдонім сутності",
"unable-delete-entity-alias-title": "Неможливо видалити псевдонім сутності", "unable-delete-entity-alias-title": "Неможливо видалити псевдонім сутності",
"unable-delete-entity-alias-text": "Псевдонім сутності'{{entityAlias}}' неможливо видалити, так як це використовується наступним віджетом(s):<br/>{{widgetsList}}", "unable-delete-entity-alias-text": "Псевдонім сутності '{{entityAlias}}' неможливо видалити, оскільки він використовується наступним віджетом(s):<br/>{{widgetsList}}",
"duplicate-alias-error": "Знайдено повторюваний псевдонім '{{alias}}'.<br> Псевдоніми сутностей повинні бути унікальними на панелі візуалізації.", "duplicate-alias-error": "Знайдено повторюваний псевдонім '{{alias}}'.<br> Псевдоніми сутностей повинні бути унікальними на панелі візуалізації.",
"missing-entity-filter-error": "Відсутній фільтр для псевдоніма '{{alias}}'.", "missing-entity-filter-error": "Відсутній фільтр для псевдоніма '{{alias}}'.",
"configure-alias": "Налаштувати '{{alias}}' псевдонім", "configure-alias": "Налаштувати '{{alias}}' псевдонім",
@ -1118,9 +1163,9 @@
"view-entity-views": "Переглянути представлення сутностей", "view-entity-views": "Переглянути представлення сутностей",
"entity-view-alias": "Псевдонім представлення сутності", "entity-view-alias": "Псевдонім представлення сутності",
"aliases": "Псевдоніми представлення сутності", "aliases": "Псевдоніми представлення сутності",
"no-alias-matching": "Псевдонім'{{alias}}' не знайдено.", "no-alias-matching": "Псевдонім '{{alias}}' не знайдено.",
"no-aliases-found": "Псевдоніми не знайдено.", "no-aliases-found": "Псевдоніми не знайдено.",
"no-key-matching": "'Ключ {{key}}' не знайдено.", "no-key-matching": "Ключ '{{key}}' не знайдено.",
"no-keys-found": "Ключі не знайдено.", "no-keys-found": "Ключі не знайдено.",
"create-new-alias": "Створити новий!", "create-new-alias": "Створити новий!",
"create-new-key": "Створити новий!", "create-new-key": "Створити новий!",
@ -1135,7 +1180,7 @@
"entity-view-list": "Список представленнь сутності", "entity-view-list": "Список представленнь сутності",
"use-entity-view-name-filter": "Використати фільтр", "use-entity-view-name-filter": "Використати фільтр",
"entity-view-list-empty": "Не вибрано жодного представлення сутності.", "entity-view-list-empty": "Не вибрано жодного представлення сутності.",
"entity-view-name-filter-required": "Необхідно вказвти фільтр назв представлення сутності.", "entity-view-name-filter-required": "Необхідно вказати фільтр назв представлення сутності.",
"entity-view-name-filter-no-entity-view-matched": "Представлення сутностей, назви яких починаються з '{{entityView}}' не знайдено.", "entity-view-name-filter-no-entity-view-matched": "Представлення сутностей, назви яких починаються з '{{entityView}}' не знайдено.",
"add": "Додати представлення сутності", "add": "Додати представлення сутності",
"assign-to-customer": "Призначити клієнту", "assign-to-customer": "Призначити клієнту",
@ -1398,8 +1443,11 @@
"import-extension": "Імпортувати розширення", "import-extension": "Імпортувати розширення",
"export-extension": "Експортувати розширення", "export-extension": "Експортувати розширення",
"file": "Файл розширень", "file": "Файл розширень",
"invalid-file-error": "Не правильний формат файла" "invalid-file-error": "Неправильний формат файлу"
}, },
"feature": {
"advanced-features": "Додаткові можливості"
},
"fullscreen": { "fullscreen": {
"expand": "Відкрити у повноекранному режимі", "expand": "Відкрити у повноекранному режимі",
"exit": "Вийти з повноекранного режиму", "exit": "Вийти з повноекранного режиму",
@ -1410,7 +1458,7 @@
"function": "Функція" "function": "Функція"
}, },
"grid": { "grid": {
"delete-item-title": "Ви впенені, що хочете видалити цей елемент?", "delete-item-title": "Ви впевнені, що хочете видалити цей елемент?",
"delete-item-text": "Будьте обережні, після підтвердження, цей елемент і всі пов'язані з ним дані, стануть недоступними.", "delete-item-text": "Будьте обережні, після підтвердження, цей елемент і всі пов'язані з ним дані, стануть недоступними.",
"delete-items-title": "Ви впенені, що хочете видалити { count, plural, =1 {1 елемент} other {# елементи} }?", "delete-items-title": "Ви впенені, що хочете видалити { count, plural, =1 {1 елемент} other {# елементи} }?",
"delete-items-action-title": "Видалити{ count, plural, =1 {1 елемент} other {# елементи} }", "delete-items-action-title": "Видалити{ count, plural, =1 {1 елемент} other {# елементи} }",
@ -1695,6 +1743,12 @@
"login-with": "Увійти через {{name}}", "login-with": "Увійти через {{name}}",
"or": "або" "or": "або"
}, },
"notification": {
"notification-center": "Центр сповіщень"
},
"ota-update": {
"ota-updates": "Оновлення OTA"
},
"position": { "position": {
"top": "Угорі", "top": "Угорі",
"bottom": "Знизу", "bottom": "Знизу",
@ -1710,6 +1764,12 @@
"tokenCopiedMessage": "JWT токен скопійовано в буфер обміну", "tokenCopiedMessage": "JWT токен скопійовано в буфер обміну",
"tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку." "tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку."
}, },
"profiles": {
"profiles": "Профілі"
},
"security": {
"security": "Безпека"
},
"relation": { "relation": {
"relations": "Відношення", "relations": "Відношення",
"direction": "Напрямок", "direction": "Напрямок",
@ -1752,41 +1812,44 @@
"additional-info": "Додаткова інформація (JSON)", "additional-info": "Додаткова інформація (JSON)",
"invalid-additional-info": "Не вдалося розібрати JSON з додатковою інформацією ." "invalid-additional-info": "Не вдалося розібрати JSON з додатковою інформацією ."
}, },
"resource": {
"resources-library": "Бібліотека ресурсів"
},
"rulechain": { "rulechain": {
"rulechain": "Ланцюг правил", "rulechain": "Ланцюжок правил",
"rulechains": "Ланцюги правил", "rulechains": "Ланцюжки правил",
"root": "Основний", "root": "Основний",
"delete": "Видалити ланцюг правил", "delete": "Видалити ланцюжок правил",
"name": "Ім'я", "name": "Ім'я",
"name-required": "Необхідно вказати ім'я.", "name-required": "Необхідно вказати ім'я.",
"description": "Опис", "description": "Опис",
"add": "Додати ланцюг правил", "add": "Додати ланцюжок правил",
"set-root": "Зробити ланцюг правил основним", "set-root": "Зробити ланцюжок правил основним",
"set-root-rulechain-title": "Ви впевнені, що хочете зробити ланцюг правил '{{ruleChainName}}' основним?", "set-root-rulechain-title": "Ви впевнені, що хочете зробити ланцюжок правил '{{ruleChainName}}' основним?",
"set-root-rulechain-text": "Після підтвердження ланцюг правил стане основним і буде обробляти всі вхідні транспортні повідомлення.", "set-root-rulechain-text": "Після підтвердження ланцюжок правил стане основним і буде обробляти всі вхідні транспортні повідомлення.",
"delete-rulechain-title": "Ви впевнені, що хочете видалити ланцюг правил '{{ruleChainName}}'?", "delete-rulechain-title": "Ви впевнені, що хочете видалити ланцюжок правил '{{ruleChainName}}'?",
"delete-rulechain-text": "Будьте обережні, після підтвердження ланцюг правил і всі пов'язані з ним дані стануть недоступними.", "delete-rulechain-text": "Будьте обережні, після підтвердження ланцюжок правил і всі пов'язані з ним дані стануть недоступними.",
"delete-rulechains-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 ланцюг правил} other {# ланцюги правил} }?", "delete-rulechains-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 ланцюг правил} other {# ланцюги правил} }?",
"delete-rulechains-action-title": "Видалити{ count, plural, =1 {1 ланцюг правил} other {# ланцюги правил} }", "delete-rulechains-action-title": "Видалити{ count, plural, =1 {1 ланцюг правил} other {# ланцюги правил} }",
"delete-rulechains-text": "Будьте обережні, після підтвердження, вибрані ланцюги правил і всі пов'язані з ними дані стануть недоступними.", "delete-rulechains-text": "Будьте обережні, після підтвердження, вибрані ланцюжки правил і всі пов'язані з ними дані стануть недоступними.",
"add-rulechain-text": "Додати новий ланцюг правил", "add-rulechain-text": "Додати новий ланцюжок правил",
"no-rulechains-text": "Ланцюг правил не знайдено", "no-rulechains-text": "Ланцюжок правил не знайдено",
"rulechain-details": "Деталі ланцюга правил", "rulechain-details": "Деталі ланцюжка правил",
"details": "Деталі", "details": "Деталі",
"events": "Події", "events": "Події",
"system": "Система", "system": "Система",
"import": "Імпортувати ланцюг правил", "import": "Імпортувати ланцюжок правил",
"export": "Експортувати ланцюг правил", "export": "Експортувати ланцюжок правил",
"export-failed-error": "Не вдалося експортувати ланцюг правил: {{error}}", "export-failed-error": "Не вдалося експортувати ланцюжок правил: {{error}}",
"create-new-rulechain": "Створити новий ланцюг правил", "create-new-rulechain": "Створити новий ланцюжок правил",
"rulechain-file": "Файл ланцюга правил", "rulechain-file": "Файл ланцюжка правил",
"invalid-rulechain-file-error": "Неможливо імпортувати ланцюг правил: недійсна структуру даних ланцюга правил.", "invalid-rulechain-file-error": "Неможливо імпортувати ланцюжок правил: недійсна структуру даних ланцюжка правил.",
"copyId": "Копіювати Id ланцюга правил", "copyId": "Копіювати Id ланцюжка правил",
"idCopiedMessage": "Id ланцюга правил скопійовано в буфер обміну", "idCopiedMessage": "Id ланцюжка правил скопійовано в буфер обміну",
"select-rulechain": "Вибрати ланцюг правил", "select-rulechain": "Вибрати ланцюжок правил",
"no-rulechains-matching": "Не знайдено жодних ланцюгів правил, які відповідають '{{entity}}'.", "no-rulechains-matching": "Не знайдено жодних ланцюжків правил, які відповідають '{{entity}}'.",
"rulechain-required": "Необхідно вказати ланцюг правил", "rulechain-required": "Необхідно вказати ланцюжок правил",
"management": "Управління ланцюгами правил", "management": "Управління ланцюжками правил",
"debug-mode": "Режим налагодження" "debug-mode": "Режим налагодження"
}, },
"rulenode": { "rulenode": {
@ -1820,7 +1883,7 @@
"link-labels": "Мітки посилання", "link-labels": "Мітки посилання",
"link-labels-required": "Необхідно вказати мітки посилання.", "link-labels-required": "Необхідно вказати мітки посилання.",
"no-link-labels-found": "Не знайдено жодних міток посилання", "no-link-labels-found": "Не знайдено жодних міток посилання",
"no-link-label-matching": "Мітка'{{label}}' не знайдена.", "no-link-label-matching": "Мітку '{{label}}' не знайдено.",
"create-new-link-label": "Створити нову!", "create-new-link-label": "Створити нову!",
"type-filter": "Фільтр", "type-filter": "Фільтр",
"type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами", "type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами",
@ -1834,15 +1897,15 @@
"type-analytics-details": "Виконує аналіз потокових або збережених даних", "type-analytics-details": "Виконує аналіз потокових або збережених даних",
"type-external": "Зовнішній", "type-external": "Зовнішній",
"type-external-details": "Взаємодіє з зовнішньою системою", "type-external-details": "Взаємодіє з зовнішньою системою",
"type-rule-chain": "Ланцюг правил", "type-rule-chain": "Ланцюжок правил",
"type-rule-chain-details": "Перенаправити вхідне повідомлення на вказаний ланцюг правил", "type-rule-chain-details": "Перенаправити вхідне повідомлення на вказаний ланцюжок правил",
"type-input": "Вхід", "type-input": "Вхід",
"type-input-details": "Логічний вхід ланцюга правил, перенаправляє вхідні повідомлення на наступний пов'язаний вузол правил", "type-input-details": "Логічний вхід ланцюжка правил, перенаправляє вхідні повідомлення на наступний пов'язаний вузол правил",
"type-unknown": "Невідомий", "type-unknown": "Невідомий",
"type-unknown-details": "Невизначений вузол правил", "type-unknown-details": "Невизначений вузол правил",
"directive-is-not-loaded": "Вказана директива конфігурації '{{directiveName}}' недоступна.", "directive-is-not-loaded": "Вказана директива конфігурації '{{directiveName}}' недоступна.",
"ui-resources-load-error": "Не вдалося завантажити UI ресурси.", "ui-resources-load-error": "Не вдалося завантажити UI ресурси.",
"invalid-target-rulechain": "Не вдається визначити цільовий ланцюг правил!", "invalid-target-rulechain": "Не вдається визначити цільовий ланцюжок правил!",
"test-script-function": "Протестувати скрипт", "test-script-function": "Протестувати скрипт",
"message": "Повідомлення", "message": "Повідомлення",
"message-type": "Тип повідомлення", "message-type": "Тип повідомлення",
@ -1860,7 +1923,7 @@
"scheduler-event": "Подія планувальника", "scheduler-event": "Подія планувальника",
"select-scheduler-event": "Вибрати подію", "select-scheduler-event": "Вибрати подію",
"no-scheduler-events-matching": "Не знайдено жодних подій, які відповідають '{{entity}}'.", "no-scheduler-events-matching": "Не знайдено жодних подій, які відповідають '{{entity}}'.",
"scheduler-event-required": "Необхвдно вказати заплановану подію", "scheduler-event-required": "Необхідно вказати заплановану подію",
"management": "Управління планувальником", "management": "Управління планувальником",
"scheduler-events": "Планування подій", "scheduler-events": "Планування подій",
"add-scheduler-event": "Додати подію", "add-scheduler-event": "Додати подію",
@ -1902,7 +1965,7 @@
"repeat-on-monday": "Повторити в понеділок", "repeat-on-monday": "Повторити в понеділок",
"repeat-on-tuesday": "Повторити у вівторок", "repeat-on-tuesday": "Повторити у вівторок",
"repeat-on-wednesday": "Повторити в середу", "repeat-on-wednesday": "Повторити в середу",
"repeat-on-thursday": "Повторити в червер", "repeat-on-thursday": "Повторити в четвер",
"repeat-on-friday": "Повторити в п'ятницю", "repeat-on-friday": "Повторити в п'ятницю",
"repeat-on-saturday": "Повторити в суботу", "repeat-on-saturday": "Повторити в суботу",
"event-type": "Тип події", "event-type": "Тип події",
@ -1915,7 +1978,7 @@
"week": "Тиждень", "week": "Тиждень",
"day": "День", "day": "День",
"agenda-week": "Порядок тижня", "agenda-week": "Порядок тижня",
"agenda-day": "Порялок дня", "agenda-day": "Порядок дня",
"list-year": "Список року", "list-year": "Список року",
"list-month": "Список місяця", "list-month": "Список місяця",
"list-week": "Список тижня", "list-week": "Список тижня",
@ -1994,7 +2057,7 @@
"download-blob-entity": "Завантажити файл", "download-blob-entity": "Завантажити файл",
"delete-blob-entity": "Видалити файл", "delete-blob-entity": "Видалити файл",
"delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?", "delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?",
"delete-blob-entity-text": "Будьте обережні, після підствердження, дані файлу стануть недоступними." "delete-blob-entity-text": "Будьте обережні, після підтвердження, дані з файлу стануть недоступними для відновлення."
}, },
"timezone": { "timezone": {
"timezone": "Часовий пояс", "timezone": "Часовий пояс",
@ -2018,7 +2081,7 @@
"add-tenant-text": "Додати нового власника", "add-tenant-text": "Додати нового власника",
"no-tenants-text": "Не знайдено жодного власника", "no-tenants-text": "Не знайдено жодного власника",
"tenant-details": "Подробиці про власника", "tenant-details": "Подробиці про власника",
"delete-tenant-title": "Ви впевнені, що хочете видалити власника'{{tenantTitle}}'?", "delete-tenant-title": "Ви впевнені, що хочете видалити власника '{{tenantTitle}}'?",
"delete-tenant-text": "Будьте обережні, після підтвердження власник і всі пов'язані з ним дані стануть недоступними.", "delete-tenant-text": "Будьте обережні, після підтвердження власник і всі пов'язані з ним дані стануть недоступними.",
"delete-tenants-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 власник} other {# власники} }?", "delete-tenants-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 власник} other {# власники} }?",
"delete-tenants-action-title": "Видалити { count, plural, =1 {1 власник} other {# власники} }", "delete-tenants-action-title": "Видалити { count, plural, =1 {1 власник} other {# власники} }",
@ -2076,7 +2139,7 @@
"add-user-text": "Додати нового користувача", "add-user-text": "Додати нового користувача",
"no-users-text": "Не знайдено жодного користувача", "no-users-text": "Не знайдено жодного користувача",
"user-details": "Подробиці про користувача", "user-details": "Подробиці про користувача",
"delete-user-title": "Ви впевнені, що хочете видалити користувача'{{userEmail}}'?", "delete-user-title": "Ви впевнені, що хочете видалити користувача '{{userEmail}}'?",
"delete-user-text": "Будьте обережні, після підтвердження, користувач і всі пов'язані з ним дані стануть недоступними.", "delete-user-text": "Будьте обережні, після підтвердження, користувач і всі пов'язані з ним дані стануть недоступними.",
"delete-users-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 користувача} other {# користувачів} }?", "delete-users-title": "Ви впевнені, що хочете видалити { count, plural, =1 {1 користувача} other {# користувачів} }?",
"delete-users-action-title": "Видалити { count, plural, =1 {1 користувача} other {# користувачів} }", "delete-users-action-title": "Видалити { count, plural, =1 {1 користувача} other {# користувачів} }",
@ -2128,6 +2191,9 @@
"true": "Правдиве", "true": "Правдиве",
"long": "Довге" "long": "Довге"
}, },
"version-control": {
"version-control": "Керування версіями"
},
"widget": { "widget": {
"widget-library": "Бібліотека віджетів", "widget-library": "Бібліотека віджетів",
"widget-bundle": "Пакет віджетів", "widget-bundle": "Пакет віджетів",
@ -2145,7 +2211,7 @@
"no-data-found": "Даних не знайдено", "no-data-found": "Даних не знайдено",
"latest": "Останні значення", "latest": "Останні значення",
"rpc": "Керуючий віджет", "rpc": "Керуючий віджет",
"alarm": "Віджет сигнала тривоги", "alarm": "Віджет сигналу тривоги",
"static": "Статичний віджет", "static": "Статичний віджет",
"select-widget-type": "Вибрати тип віджета", "select-widget-type": "Вибрати тип віджета",
"missing-widget-title-error": "Необхідно вказати назву віджета!", "missing-widget-title-error": "Необхідно вказати назву віджета!",
@ -2252,7 +2318,7 @@
"remove-datasource": "Видалити джерело даних", "remove-datasource": "Видалити джерело даних",
"add-datasource": "Додати джерело даних", "add-datasource": "Додати джерело даних",
"target-device": "Цільовий пристрій", "target-device": "Цільовий пристрій",
"alarm-source": "Джерело сигнала тривоги", "alarm-source": "Джерело сигналу тривоги",
"actions": "Дії", "actions": "Дії",
"action": "Дія", "action": "Дія",
"add-action": "Додати дію", "add-action": "Додати дію",
@ -2367,7 +2433,7 @@
"no-coordinate-specified": "Ключ для широти/довготи не вказаний", "no-coordinate-specified": "Ключ для широти/довготи не вказаний",
"no-support-geolocation": "Ваш браузер не підтримує геолокацію", "no-support-geolocation": "Ваш браузер не підтримує геолокацію",
"no-image": "Немає зображення", "no-image": "Немає зображення",
"no-support-web-camera": "Нет поддерживаемой веб-камеры", "no-support-web-camera": "Немає підтримуваної веб-камери",
"no-timeseries-selected": "Параметр телеметрії не вибрано", "no-timeseries-selected": "Параметр телеметрії не вибрано",
"secret-key": "Секретний ключ", "secret-key": "Секретний ключ",
"secret-key-required": "Необхідно вказати секретний ключ", "secret-key-required": "Необхідно вказати секретний ключ",
@ -2412,10 +2478,10 @@
"rpc-status": { "rpc-status": {
"QUEUED": "В ЧЕРЗІ", "QUEUED": "В ЧЕРЗІ",
"SENT": "ВІДПРАВЛЕНО", "SENT": "ВІДПРАВЛЕНО",
"DELIVERED": "ДОСТАВЛЕННО", "DELIVERED": "ДОСТАВЛЕНО",
"SUCCESSFUL": "УСПІШНО", "SUCCESSFUL": "УСПІШНО",
"TIMEOUT": "ЧАС МИНУВ", "TIMEOUT": "ЧАС МИНУВ",
"EXPIRED": "ПРОСРОЧЕНО", "EXPIRED": "ПРОТЕРМІНОВАНО",
"FAILED": "НЕ ВДАЛО" "FAILED": "НЕ ВДАЛО"
}, },
"rpc-search-status-all": "ВСІ", "rpc-search-status-all": "ВСІ",
@ -2426,7 +2492,7 @@
} }
}, },
"white-labeling": { "white-labeling": {
"white-labeling": "Білий маркування", "white-labeling": "Біле маркування",
"login-white-labeling": "Login White Labeling", "login-white-labeling": "Login White Labeling",
"preview": "Попередній перегляд", "preview": "Попередній перегляд",
"app-title": "Назва програми", "app-title": "Назва програми",
@ -2441,7 +2507,7 @@
"logo-size-error": "Зображення логотипу занадто велике. Максимально дозволений розмір зображення логотипу{{kbSize}} KBytes.", "logo-size-error": "Зображення логотипу занадто велике. Максимально дозволений розмір зображення логотипу{{kbSize}} KBytes.",
"logo-type-error": "Недійсний формат файлу логотипу. Приймаються тільки зображення.", "logo-type-error": "Недійсний формат файлу логотипу. Приймаються тільки зображення.",
"drop-logo-image": "Зніміть зображення логотипу або клацніть, щоб вибрати файл для завантаження.", "drop-logo-image": "Зніміть зображення логотипу або клацніть, щоб вибрати файл для завантаження.",
"no-logo-image": "Не вибрано жожного логотипу", "no-logo-image": "Не вибрано жодного логотипу",
"logo-height": "Висота логотипу, px", "logo-height": "Висота логотипу, px",
"primary-palette": "Основна палітра", "primary-palette": "Основна палітра",
"accent-palette": "Палітра акцент", "accent-palette": "Палітра акцент",

Loading…
Cancel
Save