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);
}
}
@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;
import io.swagger.annotations.ApiParam;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.PageLink;
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.dao.resource.ImageCacheKey;
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);
}
@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 {
ImageCacheKey cacheKey = ImageCacheKey.forImage(getTenantId(type), key, preview);
return downloadIfChanged(cacheKey, etag, () -> checkImageInfo(type, key, Operation.READ));
@ -343,4 +359,14 @@ public class ImageController extends BaseController {
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.boot.autoconfigure.condition.ConditionalOnProperty;
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.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.service.install.InstallScripts;
@ -32,6 +37,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Service
@Slf4j
@ -47,6 +53,7 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc
private static final String UPGRADE_DIR = "upgrade";
private final InstallScripts installScripts;
private final AttributesService attributesService;
@Value("${app.version:unknown}")
@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) {
EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion);
if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || tbVersion.equals(currentEdgeVersion)) {
return new EdgeInstructions("Edge upgrade instruction for " + currentEdgeVersion + "EDGE is not available.");
}
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 dockerUpgradeInstructions = readFile(resolveFile("docker", "instructions.md"));
if (edgeUpgradeInfo.isRequiresUpdateDb()) {
@ -109,7 +144,7 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc
String upgrade_preparing = readFile(resolveFile("upgrade_preparing.md"));
upgrade_preparing = upgrade_preparing.replace("${OS}", os.equals("centos") ? "RHEL/CentOS 7/8" : "Ubuntu");
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 linuxUpgradeInstructions = readFile(resolveFile(os, "instructions.md"));
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.edge.EdgeInstructions;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Map;
@ -27,4 +29,6 @@ public interface EdgeUpgradeInstructionsService {
void updateInstructionMap(Map<String, EdgeUpgradeInfo> upgradeVersions);
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) {
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);
}

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 org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.id.EntityId;
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.TopicPartitionInfo;
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.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -48,6 +49,8 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -55,16 +58,16 @@ import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings("UnstableApiUsage")
public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService {
private final NotificationCenter notificationCenter;
private final NotificationRequestService notificationRequestService;
private final SchedulerComponent scheduler;
private final NotificationExecutorService notificationExecutor;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("notification-scheduler"));
private final Map<NotificationRequestId, ScheduledRequestMetadata> scheduledNotificationRequests = new ConcurrentHashMap<>();
@Override
@PostConstruct
public void init() {
super.init();
@ -166,6 +169,13 @@ public class DefaultNotificationSchedulerService extends AbstractPartitionBasedS
return "notifications-scheduler";
}
@Override
@PreDestroy
public void stop() {
super.stop();
scheduler.shutdownNow();
}
@Data
private static class ScheduledRequestMetadata {
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.setCustomerId(getExternalIdOrElseInternal(ctx, new CustomerId(customerUsersFilter.getCustomerId())).getId());
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.service.sync.vc.data.EntitiesImportCtx;
import java.util.stream.Collectors;
import java.util.List;
@Service
@TbCoreComponent
@ -66,10 +66,7 @@ public class NotificationTargetImportService extends BaseEntityImportService<Not
break;
case USER_LIST:
UserListFilter userListFilter = (UserListFilter) usersFilter;
userListFilter.setUsersIds(userListFilter.getUsersIds().stream()
.map(UserId::new).map(idProvider::getInternalId)
.map(UUIDBased::getId).collect(Collectors.toList())
);
userListFilter.setUsersIds(List.of(ctx.getUser().getUuidId())); // user entities are not supported by VC; replacing with current user id
break;
case TENANT_ADMINISTRATORS:
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.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -81,8 +80,6 @@ public class DefaultUpdateService implements UpdateService {
private final RestTemplate restClient = new RestTemplate();
private UpdateMessage updateMessage;
private EdgeUpgradeMessage edgeUpgradeMessage;
private String edgeInstallVersion;
private String platform;
private String version;
@ -94,7 +91,6 @@ public class DefaultUpdateService implements UpdateService {
updateMessage = new UpdateMessage(false, version, "", "",
"https://thingsboard.io/docs/reference/releases",
"https://thingsboard.io/docs/reference/releases");
edgeUpgradeMessage = new EdgeUpgradeMessage(new HashMap<>());
if (updatesEnabled) {
try {
platform = System.getProperty("platform", "unknown");
@ -173,5 +169,4 @@ public class DefaultUpdateService implements UpdateService {
public UpdateMessage checkUpdates() {
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.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.After;
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()));
}
@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 {
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.Device;
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.StringUtils;
import org.thingsboard.server.common.data.Tenant;
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.asset.Asset;
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.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.page.PageData;
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.sql.resource.TbResourceRepository;
@ -240,6 +241,28 @@ public class ImageControllerTest extends AbstractControllerTest {
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 {
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 EDGE_MSG_SOURCE = "edge";
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";

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.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.getHost;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.getPort;
@Service("DeviceConnectivityDaoService")
@Slf4j
@ -265,14 +266,14 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(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);
}
private List<String> getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(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);
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 {
DeviceConnectivityInfo properties = getConnectivity(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);
}
@ -327,14 +328,14 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(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);
}
private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = getConnectivity(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);
}

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

@ -138,8 +138,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
@Override
@SneakyThrows
public TbResourceInfo saveImage(TbResource image) {
if (image.getId() == null && StringUtils.isEmpty(image.getResourceKey())) {
image.setResourceKey(getUniqueKey(image.getTenantId(), image.getFileName()));
if (image.getId() == null) {
image.setResourceKey(getUniqueKey(image.getTenantId(), StringUtils.defaultIfEmpty(image.getResourceKey(), image.getFileName())));
}
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.Resource;
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.security.DeviceCredentials;
import org.thingsboard.server.dao.device.DeviceConnectivityInfo;
@ -63,7 +64,7 @@ public class DeviceConnectivityUtil {
if (MQTTS.equals(protocol)) {
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);
switch (deviceCredentials.getCredentialsType()) {
@ -219,7 +220,7 @@ public class DeviceConnectivityUtil {
}
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;
String host = null;
if (VALID_URL_PATTERN.matcher(initialHost).matches()) {
@ -243,6 +244,10 @@ public class DeviceConnectivityUtil {
return host;
}
public static String getPort(DeviceConnectivityInfo properties) {
return StringUtils.isBlank(properties.getPort()) ? "" : properties.getPort();
}
public static boolean isLocalhost(String host) {
try {
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> {
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-select required formControlName="stateControllerId">
<mat-option *ngFor="let stateControllerId of stateControllerIds" [value]="stateControllerId">
{{stateControllerId}}
{{ getStatesControllerTranslation(stateControllerId) }}
</mat-option>
</mat-select>
</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 { Store } from '@ngrx/store';
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 { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DashboardSettings, GridSettings, StateControllerId } from '@app/shared/models/dashboard.models';
import { isDefined, isUndefined } from '@core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { StatesControllerService } from './states/states-controller.service';
export interface DashboardSettingsDialogData {
@ -46,21 +44,23 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
settings: DashboardSettings;
gridSettings: GridSettings;
settingsFormGroup: UntypedFormGroup;
gridSettingsFormGroup: UntypedFormGroup;
settingsFormGroup: FormGroup;
gridSettingsFormGroup: FormGroup;
stateControllerIds: string[];
submitted = false;
private stateControllerTranslationMap = new Map<string, string>([
['default', 'dashboard.state-controller-default'],
]);
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private fb: FormBuilder,
private translate: TranslateService,
private statesControllerService: StatesControllerService) {
super(store, router, dialogRef);
@ -182,7 +182,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
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 customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
@ -204,4 +204,11 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
}
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 {
this.deviceConnectivitySettings.jsonValue = {
...this.deviceConnectivitySettings.jsonValue,
...this.deviceConnectivitySettingsForm.value
...this.deviceConnectivitySettingsForm.getRawValue()
};
this.adminService.saveAdminSettings<DeviceConnectivitySettings>(this.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">
<ng-container *ngTemplateOutlet="seeDocumentation; context:
{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>
</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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
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 { NULL_UUID } from '@shared/models/id/has-uuid';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { generateSecret, guid } from '@core/utils';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { environment as env } from '@env/environment';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import {EdgeService} from "@core/http/edge.service";
@Component({
selector: 'tb-edge',
@ -44,7 +42,7 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
private attributeService: AttributeService,
private edgeService: EdgeService,
@Inject('entity') protected entityValue: EdgeInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>,
public fb: UntypedFormBuilder,
@ -100,7 +98,10 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
}
});
this.generateRoutingKeyAndSecret(entity, this.entityForm);
this.checkEdgeVersion();
this.edgeService.isEdgeUpgradeAvailable(this.entity.id.id)
.subscribe(isUpgradeAvailable => {
this.upgradeAvailable = isUpgradeAvailable;
});
}
updateFormState() {
@ -139,25 +140,4 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
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>
<div fxLayout="column"
fxLayoutAlign="center center" class="tb-absolute-fill">
<span class="mat-headline-5" style="padding-bottom: 20px;">
<span>
{{ 'common.loading' | translate }}
</span>
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div>
</ng-template>
<ng-template #noImages>

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

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

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

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

Loading…
Cancel
Save