Browse Source

Merge branch 'master' into feature/input-widget-locations

pull/2074/head
Vladyslav 7 years ago
committed by GitHub
parent
commit
c5a3c445ba
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 33
      tools/pom.xml
  3. 93
      tools/src/main/java/org/thingsboard/client/tools/migrator/MigratorTool.java
  4. 179
      tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaLatestMigrator.java
  5. 222
      tools/src/main/java/org/thingsboard/client/tools/migrator/PostgresToCassandraTelemetryMigrator.java
  6. 87
      tools/src/main/java/org/thingsboard/client/tools/migrator/README.md
  7. 84
      tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java
  8. 20
      ui/src/app/locale/locale.constant-en_US.json
  9. 95
      ui/src/app/locale/locale.constant-ru_RU.json
  10. 291
      ui/src/app/locale/locale.constant-uk_UA.json

12
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -347,14 +347,14 @@
"descriptor": { "descriptor": {
"type": "static", "type": "static",
"sizeX": 7.5, "sizeX": 7.5,
"sizeY": 4, "sizeY": 4.5,
"resources": [], "resources": [],
"templateHtml": "<form name=\"claimDeviceForm\" class=\"claim-form\" ng-submit=\"claim()\">\n <fieldset ng-disabled=\"$root.loading || loading\">\n <md-input-container class=\"md-block\">\n <label>Device name</label>\n <input ng-model=\"deviceObj.deviceName\" name=\"deviceName\" required>\n <div ng-messages=\"claimDeviceForm.deviceName.$error\">\n <div ng-message=\"required\">Device name is required.</div>\n </div>\n </md-input-container>\n <md-input-container ng-if=\"deviceSecretField\" class=\"md-block\">\n <label>Device secret</label>\n <input name=\"deviceSecret\" ng-model=\"deviceObj.deviceSecret\" required>\n <div ng-messages=\"claimDeviceForm.deviceSecret.$error\">\n <div ng-message=\"required\">Device secret is required.</div>\n </div>\n </md-input-container>\n </fieldset>\n <md-button type=\"submit\" ng-disabled=\"loading || claimDeviceForm.$invalid || !claimDeviceForm.$dirty\" class=\"md-raised md-primary\">Claim</md-button>\n</form>", "templateHtml": "<form name=\"claimDeviceForm\" class=\"claim-form\" ng-submit=\"claim()\">\n <fieldset ng-disabled=\"$root.loading || loading\">\n <md-input-container ng-class=\"{'show-label': showLabel}\" class=\"md-block\">\n <label>{{deviceLabel}}</label>\n <input ng-model=\"deviceObj.deviceName\" \n name=\"deviceName\" \n required\n >\n <div ng-messages=\"claimDeviceForm.deviceName.$error\">\n <div ng-message=\"required\">{{requiredErrorDevice}}</div>\n </div>\n </md-input-container>\n <md-input-container ng-if=\"secretKeyField\" class=\"md-block\" ng-class=\"{'show-label': showLabel}\">\n <label>{{secretKeyLabel}}</label>\n <input name=\"deviceSecret\" ng-model=\"deviceObj.deviceSecret\" required>\n <div ng-messages=\"claimDeviceForm.deviceSecret.$error\">\n <div ng-message=\"required\">{{requiredErrorSecretKey}}</div>\n </div>\n </md-input-container>\n </fieldset>\n <div class=\"md-block\" layout=\"row\" layout-align=\"end center\">\n <md-button type=\"submit\" ng-disabled=\"loading || claimDeviceForm.$invalid || !claimDeviceForm.$dirty\" class=\"md-raised md-primary\">{{labelClaimButon}}</md-button>\n </div>\n</form>",
"templateCss": ".claim-form {\n margin: 16px;\n}", "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}",
"controllerScript": "let $scope;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let $q = $injector.get('$q');\n let $http = $injector.get('$http');\n let toast = $scope.$injector.get('toast');\n $scope.deviceSecretField = self.ctx.settings.deviceSecret;\n $scope.deviceObj = {};\n \n $scope.claim = () => {\n $scope.loading = true;\n claimDevice($scope.deviceObj.deviceName, $scope.deviceObj.deviceSecret).then(\n (data) => {\n resetForm();\n $scope.claimDeviceForm.$setPristine();\n $scope.claimDeviceForm.$setUntouched();\n $scope.loading = false;\n if (data.response == \"SUCCESS\") {\n toast.showSuccess('Device was successfully claimed!', 2000, angular.element('.claim-form'), 'bottom left');\n }\n },\n () => {\n $scope.loading = false;\n }\n );\n }\n \n function claimDevice(deviceName, deviceSecret, config) {\n let deferred = $q.defer();\n let url = \"/api/customer/device/\" + deviceName + \"/claim\";\n let obj = deviceSecret ? { secretKey: deviceSecret } : {};\n $http.post(url, obj, config).then(\n (payload) => {\n deferred.resolve(payload.data); \n },\n () => {\n deferred.reject();\n }\n );\n return deferred.promise;\n }\n \n function resetForm() {\n $scope.deviceObj = {};\n }\n}\n\n", "controllerScript": "let $scope;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let $q = $injector.get('$q');\n let $http = $injector.get('$http');\n let toast = $scope.$injector.get('toast');\n let utils = $scope.$injector.get('utils');\n let $translate = $scope.$injector.get('$translate');\n let $rootScope = $scope.$injector.get('$rootScope');\n let settings = self.ctx.settings || {};\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n $scope.deviceObj = {};\n \n const config = {\n ignoreErrors: true \n };\n \n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claim = () => {\n $scope.loading = true;\n claimDevice($scope.deviceObj.deviceName, $scope.deviceObj.deviceSecret, config).then(\n (data) => {\n successClaim();\n },\n (error) => {\n $scope.loading = false;\n if(error.status == 404) {\n toast.showError(deviceNotFound, angular.element('.claim-form'),'bottom left');\n } else if(error.status == 400) {\n toast.showError(failedClaimDevice, angular.element('.claim-form'),'bottom left');\n }\n }\n );\n }\n \n function claimDevice(deviceName, deviceSecret, config) {\n let deferred = $q.defer();\n let url = \"/api/customer/device/\" + deviceName + \"/claim\";\n let obj = deviceSecret ? { secretKey: deviceSecret } : {};\n $http.post(url, obj, config).then(\n (payload) => {\n deferred.resolve(payload.data); \n },\n (error) => {\n deferred.reject(error);\n }\n );\n return deferred.promise;\n }\n \n function updateAliasData() {\n var aliasIds = [];\n for (var id in self.ctx.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n var tasks = [];\n aliasIds.forEach(function(aliasId) {\n self.ctx.aliasController.setAliasUnresolved(aliasId);\n tasks.push(self.ctx.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(function() {\n $rootScope.$broadcast('widgetForceReInit');\n });\n }\n \n function successClaim() {\n resetForm();\n $scope.claimDeviceForm.$setPristine();\n $scope.claimDeviceForm.$setUntouched();\n $scope.loading = false;\n toast.showSuccess(successfulClaim, 2000);\n updateAliasData();\n }\n \n function resetForm() {\n $scope.deviceObj = {};\n }\n}\n\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [],\n \"properties\": {\n \"deviceSecret\": {\n \"title\": \"Add 'Device secret' field\",\n \"type\": \"boolean\",\n \"default\": false\n }\n }\n },\n \"form\": [\n \"deviceSecret\"\n ]\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\n }]\n}",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
} }
} }
] ]

33
tools/pom.xml

@ -51,6 +51,39 @@
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>cassandra-all</artifactId>
<version>3.11.4</version>
</dependency>
<dependency>
<groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-core</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
</dependencies> </dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>org.thingsboard.client.tools.migrator.MigratorTool</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project> </project>

93
tools/src/main/java/org/thingsboard/client/tools/migrator/MigratorTool.java

@ -0,0 +1,93 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.client.tools.migrator;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import java.io.File;
public class MigratorTool {
public static void main(String[] args) {
CommandLine cmd = parseArgs(args);
try {
File latestSource = new File(cmd.getOptionValue("latestTelemetryFrom"));
File latestSaveDir = new File(cmd.getOptionValue("latestTelemetryOut"));
File tsSource = new File(cmd.getOptionValue("telemetryFrom"));
File tsSaveDir = new File(cmd.getOptionValue("telemetryOut"));
File partitionsSaveDir = new File(cmd.getOptionValue("partitionsOut"));
boolean castEnable = Boolean.parseBoolean(cmd.getOptionValue("castEnable"));
PgCaLatestMigrator.migrateLatest(latestSource, latestSaveDir, castEnable);
PostgresToCassandraTelemetryMigrator.migrateTs(tsSource, tsSaveDir, partitionsSaveDir, castEnable);
} catch (Throwable th) {
th.printStackTrace();
throw new IllegalStateException("failed", th);
}
}
private static CommandLine parseArgs(String[] args) {
Options options = new Options();
Option latestTsOpt = new Option("latestFrom", "latestTelemetryFrom", true, "latest telemetry source file path");
latestTsOpt.setRequired(true);
options.addOption(latestTsOpt);
Option latestTsOutOpt = new Option("latestOut", "latestTelemetryOut", true, "latest telemetry save dir");
latestTsOutOpt.setRequired(true);
options.addOption(latestTsOutOpt);
Option tsOpt = new Option("tsFrom", "telemetryFrom", true, "telemetry source file path");
tsOpt.setRequired(true);
options.addOption(tsOpt);
Option tsOutOpt = new Option("tsOut", "telemetryOut", true, "sstable save dir");
tsOutOpt.setRequired(true);
options.addOption(tsOutOpt);
Option partitionOutOpt = new Option("partitionsOut", "partitionsOut", true, "partitions save dir");
partitionOutOpt.setRequired(true);
options.addOption(partitionOutOpt);
Option castOpt = new Option("castEnable", "castEnable", true, "cast String to Double if possible");
castOpt.setRequired(true);
options.addOption(castOpt);
HelpFormatter formatter = new HelpFormatter();
CommandLineParser parser = new BasicParser();
try {
return parser.parse(options, args);
} catch (ParseException e) {
System.out.println(e.getMessage());
formatter.printHelp("utility-name", options);
System.exit(1);
}
return null;
}
}

179
tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaLatestMigrator.java

@ -0,0 +1,179 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.client.tools.migrator;
import com.google.common.collect.Lists;
import org.apache.cassandra.io.sstable.CQLSSTableWriter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class PgCaLatestMigrator {
private static final long LOG_BATCH = 1000000;
private static final long rowPerFile = 1000000;
private static long linesProcessed = 0;
private static long linesMigrated = 0;
private static long castErrors = 0;
private static long castedOk = 0;
private static long currentWriterCount = 1;
private static CQLSSTableWriter currentTsWriter = null;
public static void migrateLatest(File sourceFile, File outDir, boolean castStringsIfPossible) throws IOException {
long startTs = System.currentTimeMillis();
long stepLineTs = System.currentTimeMillis();
long stepOkLineTs = System.currentTimeMillis();
LineIterator iterator = FileUtils.lineIterator(sourceFile);
currentTsWriter = WriterBuilder.getTsWriter(outDir);
boolean isBlockStarted = false;
boolean isBlockFinished = false;
String line;
while (iterator.hasNext()) {
if (linesProcessed++ % LOG_BATCH == 0) {
System.out.println(new Date() + " linesProcessed = " + linesProcessed + " in " + (System.currentTimeMillis() - stepLineTs) + " castOk " + castedOk + " castErr " + castErrors);
stepLineTs = System.currentTimeMillis();
}
line = iterator.nextLine();
if (isBlockFinished) {
break;
}
if (!isBlockStarted) {
if (isBlockStarted(line)) {
System.out.println();
System.out.println();
System.out.println(line);
System.out.println();
System.out.println();
isBlockStarted = true;
}
continue;
}
if (isBlockFinished(line)) {
isBlockFinished = true;
} else {
try {
List<String> raw = Arrays.stream(line.trim().split("\t"))
.map(String::trim)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
List<Object> values = toValues(raw);
if (currentWriterCount == 0) {
System.out.println(new Date() + " close writer " + new Date());
currentTsWriter.close();
currentTsWriter = WriterBuilder.getLatestWriter(outDir);
}
if (castStringsIfPossible) {
currentTsWriter.addRow(castToNumericIfPossible(values));
} else {
currentTsWriter.addRow(values);
}
currentWriterCount++;
if (currentWriterCount >= rowPerFile) {
currentWriterCount = 0;
}
if (linesMigrated++ % LOG_BATCH == 0) {
System.out.println(new Date() + " migrated = " + linesMigrated + " in " + (System.currentTimeMillis() - stepOkLineTs));
stepOkLineTs = System.currentTimeMillis();
}
} catch (Exception ex) {
System.out.println(ex.getMessage() + " -> " + line);
}
}
}
long endTs = System.currentTimeMillis();
System.out.println();
System.out.println(new Date() + " Migrated rows " + linesMigrated + " in " + (endTs - startTs));
currentTsWriter.close();
System.out.println();
System.out.println("Finished migrate Latest Telemetry");
}
private static List<Object> castToNumericIfPossible(List<Object> values) {
try {
if (values.get(6) != null && NumberUtils.isNumber(values.get(6).toString())) {
Double casted = NumberUtils.createDouble(values.get(6).toString());
List<Object> numeric = Lists.newArrayList();
numeric.addAll(values);
numeric.set(6, null);
numeric.set(8, casted);
castedOk++;
return numeric;
}
} catch (Throwable th) {
castErrors++;
}
return values;
}
private static List<Object> toValues(List<String> raw) {
//expected Table structure:
// COPY public.ts_kv_latest (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) FROM stdin;
List<Object> result = new ArrayList<>();
result.add(raw.get(0));
result.add(fromString(raw.get(1)));
result.add(raw.get(2));
long ts = Long.parseLong(raw.get(3));
result.add(ts);
result.add(raw.get(4).equals("\\N") ? null : raw.get(4).equals("t") ? Boolean.TRUE : Boolean.FALSE);
result.add(raw.get(5).equals("\\N") ? null : raw.get(5));
result.add(raw.get(6).equals("\\N") ? null : Long.parseLong(raw.get(6)));
result.add(raw.get(7).equals("\\N") ? null : Double.parseDouble(raw.get(7)));
return result;
}
public static UUID fromString(String src) {
return UUID.fromString(src.substring(7, 15) + "-" + src.substring(3, 7) + "-1"
+ src.substring(0, 3) + "-" + src.substring(15, 19) + "-" + src.substring(19));
}
private static boolean isBlockStarted(String line) {
return line.startsWith("COPY public.ts_kv_latest");
}
private static boolean isBlockFinished(String line) {
return StringUtils.isBlank(line) || line.equals("\\.");
}
}

222
tools/src/main/java/org/thingsboard/client/tools/migrator/PostgresToCassandraTelemetryMigrator.java

@ -0,0 +1,222 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.client.tools.migrator;
import com.google.common.collect.Lists;
import org.apache.cassandra.io.sstable.CQLSSTableWriter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class PostgresToCassandraTelemetryMigrator {
private static final long LOG_BATCH = 1000000;
private static final long rowPerFile = 1000000;
private static long linesProcessed = 0;
private static long linesMigrated = 0;
private static long castErrors = 0;
private static long castedOk = 0;
private static long currentWriterCount = 1;
private static CQLSSTableWriter currentTsWriter = null;
private static CQLSSTableWriter currentPartitionWriter = null;
private static Set<String> partitions = new HashSet<>();
public static void migrateTs(File sourceFile, File outTsDir, File outPartitionDir, boolean castStringsIfPossible) throws IOException {
long startTs = System.currentTimeMillis();
long stepLineTs = System.currentTimeMillis();
long stepOkLineTs = System.currentTimeMillis();
LineIterator iterator = FileUtils.lineIterator(sourceFile);
currentTsWriter = WriterBuilder.getTsWriter(outTsDir);
currentPartitionWriter = WriterBuilder.getPartitionWriter(outPartitionDir);
boolean isBlockStarted = false;
boolean isBlockFinished = false;
String line;
while (iterator.hasNext()) {
if (linesProcessed++ % LOG_BATCH == 0) {
System.out.println(new Date() + " linesProcessed = " + linesProcessed + " in " + (System.currentTimeMillis() - stepLineTs) + " castOk " + castedOk + " castErr " + castErrors);
stepLineTs = System.currentTimeMillis();
}
line = iterator.nextLine();
if (isBlockFinished) {
break;
}
if (!isBlockStarted) {
if (isBlockStarted(line)) {
System.out.println();
System.out.println();
System.out.println(line);
System.out.println();
System.out.println();
isBlockStarted = true;
}
continue;
}
if (isBlockFinished(line)) {
isBlockFinished = true;
} else {
try {
List<String> raw = Arrays.stream(line.trim().split("\t"))
.map(String::trim)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
List<Object> values = toValues(raw);
if (currentWriterCount == 0) {
System.out.println(new Date() + " close writer " + new Date());
currentTsWriter.close();
currentTsWriter = WriterBuilder.getTsWriter(outTsDir);
}
if (castStringsIfPossible) {
currentTsWriter.addRow(castToNumericIfPossible(values));
} else {
currentTsWriter.addRow(values);
}
processPartitions(values);
currentWriterCount++;
if (currentWriterCount >= rowPerFile) {
currentWriterCount = 0;
}
if (linesMigrated++ % LOG_BATCH == 0) {
System.out.println(new Date() + " migrated = " + linesMigrated + " in " + (System.currentTimeMillis() - stepOkLineTs) + " partitions = " + partitions.size());
stepOkLineTs = System.currentTimeMillis();
}
} catch (Exception ex) {
System.out.println(ex.getMessage() + " -> " + line);
}
}
}
long endTs = System.currentTimeMillis();
System.out.println();
System.out.println(new Date() + " Migrated rows " + linesMigrated + " in " + (endTs - startTs));
System.out.println("Partitions collected " + partitions.size());
startTs = System.currentTimeMillis();
for (String partition : partitions) {
String[] split = partition.split("\\|");
List<Object> values = Lists.newArrayList();
values.add(split[0]);
values.add(UUID.fromString(split[1]));
values.add(split[2]);
values.add(Long.parseLong(split[3]));
currentPartitionWriter.addRow(values);
}
currentPartitionWriter.close();
endTs = System.currentTimeMillis();
System.out.println();
System.out.println();
System.out.println(new Date() + " Migrated partitions " + partitions.size() + " in " + (endTs - startTs));
currentTsWriter.close();
System.out.println();
System.out.println("Finished migrate Telemetry");
}
private static List<Object> castToNumericIfPossible(List<Object> values) {
try {
if (values.get(6) != null && NumberUtils.isNumber(values.get(6).toString())) {
Double casted = NumberUtils.createDouble(values.get(6).toString());
List<Object> numeric = Lists.newArrayList();
numeric.addAll(values);
numeric.set(6, null);
numeric.set(8, casted);
castedOk++;
return numeric;
}
} catch (Throwable th) {
castErrors++;
}
return values;
}
private static void processPartitions(List<Object> values) {
String key = values.get(0) + "|" + values.get(1) + "|" + values.get(2) + "|" + values.get(3);
partitions.add(key);
}
private static List<Object> toValues(List<String> raw) {
//expected Table structure:
// COPY public.ts_kv (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) FROM stdin;
List<Object> result = new ArrayList<>();
result.add(raw.get(0));
result.add(fromString(raw.get(1)));
result.add(raw.get(2));
long ts = Long.parseLong(raw.get(3));
long partition = toPartitionTs(ts);
result.add(partition);
result.add(ts);
result.add(raw.get(4).equals("\\N") ? null : raw.get(4).equals("t") ? Boolean.TRUE : Boolean.FALSE);
result.add(raw.get(5).equals("\\N") ? null : raw.get(5));
result.add(raw.get(6).equals("\\N") ? null : Long.parseLong(raw.get(6)));
result.add(raw.get(7).equals("\\N") ? null : Double.parseDouble(raw.get(7)));
return result;
}
public static UUID fromString(String src) {
return UUID.fromString(src.substring(7, 15) + "-" + src.substring(3, 7) + "-1"
+ src.substring(0, 3) + "-" + src.substring(15, 19) + "-" + src.substring(19));
}
private static long toPartitionTs(long ts) {
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC);
return time.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1).toInstant(ZoneOffset.UTC).toEpochMilli();
// return TsPartitionDate.MONTHS.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli();
}
private static boolean isBlockStarted(String line) {
return line.startsWith("COPY public.ts_kv");
}
private static boolean isBlockFinished(String line) {
return StringUtils.isBlank(line) || line.equals("\\.");
}
}

87
tools/src/main/java/org/thingsboard/client/tools/migrator/README.md

@ -0,0 +1,87 @@
# Description:
This tool used for migrating ThingsBoard into hybrid mode from Postgres.
Performance of this tool depends on disk type and instance type (mostly on CPU resources).
But in general here are few benchmarks:
1. Creating Dump of the postgres ts_kv table -> 100GB = 90 minutes
2. If postgres table has size 100GB then dump file will be about 30GB
3. Generation SSTables from dump -> 100GB = 3 hours
4. 100GB Dump file will be converted into SSTable with size about 18GB
# Tool build Instruction:
Switch to `tools` module in Command Line and execute
mvn clean compile assembly:single
It will generate single jar file with all required dependencies inside `target dir` -> `tools-2.4.1-SNAPSHOT-jar-with-dependencies.jar`.
# Prepare requred files and run Tool:
#### Dump data from the source Postgres Database
*Do not use compression if possible because Tool can only work with uncompressed file
1. Dump table `ts_kv` table:
`pg_dump -h localhost -U postgres -d thingsboard -t ts_kv > ts_kv.dmp`
2. Dump table `ts_kv_latest` table:
`pg_dump -h localhost -U postgres -d thingsboard -t ts_kv_latest > ts_kv_latest.dmp`
3. [Optional] move table dumps to the instance where cassandra will be hosted
#### Prepare directory structure for SSTables
Tool use 3 different directories for saving SSTables - `ts_kv_cf`, `ts_kv_latest_cf`, `ts_kv_partitions_cf`
Create 3 empty directories. For example:
/home/ubunut/migration/ts
/home/ubunut/migration/ts_latest
/home/ubunut/migration/ts_partition
#### Run tool
*Note: if you run this tool on remote instance - don't forget to execute this command in `screen` to avoid unexpected termination
```
java -jar ./tools-2.4.1-SNAPSHOT-jar-with-dependencies.jar
-latestFrom ./source/ts_kv_latest.dmp
-latestOut /home/ubunut/migration/ts_latest
-tsFrom ./source/ts_kv.dmp
-tsOut /home/ubunut/migration/ts
-partitionsOut /home/ubunut/migration/ts_partition
-castEnable false
```
Tool execution time depends on DB size, CPU resources and Disk throughput
## Adding SSTables into Cassandra
* Note that this this part works only for single node Cassandra Cluster. If you have more nodes - it is better to use `sstableloader` tool.
1. [Optional] install Cassandra on the instance
2. [Optional] Using `cqlsh` create `thingsboard` keyspace and requred tables from this file `schema-ts.cql`
3. Stop Cassandra
4. Copy generated SSTable files into cassandra data dir:
```
sudo find /home/ubunut/migration/ts -name '*.*' -exec mv {} /var/lib/cassandra/data/thingsboard/ts_kv_cf-0e9aaf00ee5511e9a5fa7d6f489ffd13/ \;
sudo find /home/ubunut/migration/ts_latest -name '*.*' -exec mv {} /var/lib/cassandra/data/thingsboard/ts_kv_latest_cf-161449d0ee5511e9a5fa7d6f489ffd13/ \;
sudo find /home/ubunut/migration/ts_partition -name '*.*' -exec mv {} /var/lib/cassandra/data/thingsboard/ts_kv_partitions_cf-12e8fa80ee5511e9a5fa7d6f489ffd13/ \;
```
5. Start Cassandra service and trigger compaction
```
trigger compactions: nodetool compact thingsboard
check compaction status: nodetool compactionstats
```
## Switch Thignsboard into Hybrid Mode
Modify Thingsboard properites file `thingsboard.yml`
- DATABASE_TS_TYPE = cassandra
- TS_KV_PARTITIONING = MONTHS
# Final steps
Start Thingsboard and verify migration

84
tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java

@ -0,0 +1,84 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.client.tools.migrator;
import org.apache.cassandra.io.sstable.CQLSSTableWriter;
import java.io.File;
public class WriterBuilder {
private static final String tsSchema = "CREATE TABLE thingsboard.ts_kv_cf (\n" +
" entity_type text, // (DEVICE, CUSTOMER, TENANT)\n" +
" entity_id timeuuid,\n" +
" key text,\n" +
" partition bigint,\n" +
" ts bigint,\n" +
" bool_v boolean,\n" +
" str_v text,\n" +
" long_v bigint,\n" +
" dbl_v double,\n" +
" PRIMARY KEY (( entity_type, entity_id, key, partition ), ts)\n" +
");";
private static final String latestSchema = "CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf (\n" +
" entity_type text, // (DEVICE, CUSTOMER, TENANT)\n" +
" entity_id timeuuid,\n" +
" key text,\n" +
" ts bigint,\n" +
" bool_v boolean,\n" +
" str_v text,\n" +
" long_v bigint,\n" +
" dbl_v double,\n" +
" PRIMARY KEY (( entity_type, entity_id ), key)\n" +
") WITH compaction = { 'class' : 'LeveledCompactionStrategy' };";
private static final String partitionSchema = "CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf (\n" +
" entity_type text, // (DEVICE, CUSTOMER, TENANT)\n" +
" entity_id timeuuid,\n" +
" key text,\n" +
" partition bigint,\n" +
" PRIMARY KEY (( entity_type, entity_id, key ), partition)\n" +
") WITH CLUSTERING ORDER BY ( partition ASC )\n" +
" AND compaction = { 'class' : 'LeveledCompactionStrategy' };";
public static CQLSSTableWriter getTsWriter(File dir) {
return CQLSSTableWriter.builder()
.inDirectory(dir)
.forTable(tsSchema)
.using("INSERT INTO thingsboard.ts_kv_cf (entity_type, entity_id, key, partition, ts, bool_v, str_v, long_v, dbl_v) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
.build();
}
public static CQLSSTableWriter getLatestWriter(File dir) {
return CQLSSTableWriter.builder()
.inDirectory(dir)
.forTable(latestSchema)
.using("INSERT INTO thingsboard.ts_kv_latest_cf (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
.build();
}
public static CQLSSTableWriter getPartitionWriter(File dir) {
return CQLSSTableWriter.builder()
.inDirectory(dir)
.forTable(partitionSchema)
.using("INSERT INTO thingsboard.ts_kv_partitions_cf (entity_type, entity_id, key, partition) " +
"VALUES (?, ?, ?, ?)")
.build();
}
}

20
ui/src/app/locale/locale.constant-en_US.json

@ -822,7 +822,7 @@
"no-keys-found": "No keys found.", "no-keys-found": "No keys found.",
"create-new-alias": "Create a new one!", "create-new-alias": "Create a new one!",
"create-new-key": "Create a new one!", "create-new-key": "Create a new one!",
"duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity View aliases must be unique whithin the dashboard.", "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity View aliases must be unique within the dashboard.",
"configure-alias": "Configure '{{alias}}' alias", "configure-alias": "Configure '{{alias}}' alias",
"no-entity-views-matching": "No entity views matching '{{entity}}' were found.", "no-entity-views-matching": "No entity views matching '{{entity}}' were found.",
"alias": "Alias", "alias": "Alias",
@ -845,21 +845,21 @@
"add-entity-view-text": "Add new entity view", "add-entity-view-text": "Add new entity view",
"delete": "Delete entity view", "delete": "Delete entity view",
"assign-entity-views": "Assign entity views", "assign-entity-views": "Assign entity views",
"assign-entity-views-text": "Assign { count, plural, 1 {1 entityView} other {# entityViews} } to customer", "assign-entity-views-text": "Assign { count, plural, 1 {1 entity view} other {# entity views} } to customer",
"delete-entity-views": "Delete entity views", "delete-entity-views": "Delete entity views",
"unassign-from-customer": "Unassign from customer", "unassign-from-customer": "Unassign from customer",
"unassign-entity-views": "Unassign entity views", "unassign-entity-views": "Unassign entity views",
"unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entityView} other {# entityViews} } from customer", "unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entity view} other {# entity views} } from customer",
"assign-new-entity-view": "Assign new entity view", "assign-new-entity-view": "Assign new entity view",
"delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?", "delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?",
"delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.", "delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.",
"delete-entity-views-title": "Are you sure you want to entity view { count, plural, 1 {1 entityView} other {# entityViews} }?", "delete-entity-views-title": "Are you sure you want to delete { count, plural, 1 {1 entity view} other {# entity views} }?",
"delete-entity-views-action-title": "Delete { count, plural, 1 {1 entityView} other {# entityViews} }", "delete-entity-views-action-title": "Delete { count, plural, 1 {1 entity view} other {# entity views} }",
"delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.", "delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.",
"unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", "unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?",
"unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.", "unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.",
"unassign-entity-view": "Unassign entity view", "unassign-entity-view": "Unassign entity view",
"unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entityView} other {# entityViews} }?", "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entity view} other {# entity views} }?",
"unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.", "unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.",
"entity-view-type": "Entity View type", "entity-view-type": "Entity View type",
"entity-view-type-required": "Entity View type is required.", "entity-view-type-required": "Entity View type is required.",
@ -1700,7 +1700,13 @@
"input-widgets": { "input-widgets": {
"attribute-not-allowed": "Attribute parameter cannot be used in this widget", "attribute-not-allowed": "Attribute parameter cannot be used in this widget",
"blocked-location": "Geolocation is blocked in your browser", "blocked-location": "Geolocation is blocked in your browser",
"claim-device": "Claim device",
"claim-failed": "Failed to claim the device!",
"claim-not-found": "Device not found!",
"claim-successful": "Device was successfully claimed!",
"date": "Date", "date": "Date",
"device-name": "Device name",
"device-name-required": "Device name is required",
"discard-changes": "Discard changes", "discard-changes": "Discard changes",
"entity-attribute-required": "Entity attribute is required", "entity-attribute-required": "Entity attribute is required",
"entity-coordinate-required": "Both fields, latitude and longitude, are required", "entity-coordinate-required": "Both fields, latitude and longitude, are required",
@ -1717,6 +1723,8 @@
"no-support-geolocation": "Your browser doesn't support geolocation", "no-support-geolocation": "Your browser doesn't support geolocation",
"no-support-web-camera": "No supported web camera", "no-support-web-camera": "No supported web camera",
"no-timeseries-selected": "No timeseries selected", "no-timeseries-selected": "No timeseries selected",
"secret-key": "Secret key",
"secret-key-required": "Secret key is required",
"switch-attribute-value": "Switch entity attribute value", "switch-attribute-value": "Switch entity attribute value",
"switch-camera": "Switch camera", "switch-camera": "Switch camera",
"switch-timeseries-value": "Switch entity timeseries value", "switch-timeseries-value": "Switch entity timeseries value",

95
ui/src/app/locale/locale.constant-ru_RU.json

@ -48,7 +48,9 @@
"paste-reference": "Вставить ссылку", "paste-reference": "Вставить ссылку",
"import": "Импортировать", "import": "Импортировать",
"export": "Экспортировать", "export": "Экспортировать",
"share-via": "Поделиться в {{provider}}" "share-via": "Поделиться в {{provider}}",
"continue": "Продолжить",
"discard-changes": "Отменить изменения"
}, },
"aggregation": { "aggregation": {
"aggregation": "Агрегация", "aggregation": "Агрегация",
@ -150,8 +152,8 @@
"filter-type-single-entity": "Отдельный объект", "filter-type-single-entity": "Отдельный объект",
"filter-type-entity-list": "Список объектов", "filter-type-entity-list": "Список объектов",
"filter-type-entity-name": "Название объекта", "filter-type-entity-name": "Название объекта",
"filter-type-state-entity": "Объект, полученный из дашборда", "filter-type-state-entity": "Объект из состояния дашборда",
"filter-type-state-entity-description": "Объект, полученный из параметров дашборда", "filter-type-state-entity-description": "Объект, полученный из параметров состояния дашборда",
"filter-type-asset-type": "Тип актива", "filter-type-asset-type": "Тип актива",
"filter-type-asset-type-description": "Активы типа '{{assetType}}'", "filter-type-asset-type-description": "Активы типа '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Активы типа '{{assetType}}' и названием, начинающимся с '{{prefix}}'", "filter-type-asset-type-and-name-description": "Активы типа '{{assetType}}' и названием, начинающимся с '{{prefix}}'",
@ -246,7 +248,9 @@
"select-asset": "Выбрать активы", "select-asset": "Выбрать активы",
"no-assets-matching": "Активы, соответствующие '{{entity}}', не найдены.", "no-assets-matching": "Активы, соответствующие '{{entity}}', не найдены.",
"asset-required": "Актив обязателен", "asset-required": "Актив обязателен",
"name-starts-with": "Название актива, начинающееся с" "name-starts-with": "Название актива, начинающееся с",
"import": "Импортировать активы",
"asset-file": "Файл с активами"
}, },
"attribute": { "attribute": {
"attributes": "Атрибуты", "attributes": "Атрибуты",
@ -665,7 +669,9 @@
"is-gateway": "Гейтвей", "is-gateway": "Гейтвей",
"public": "Общедоступный", "public": "Общедоступный",
"device-public": "Устройство общедоступно", "device-public": "Устройство общедоступно",
"select-device": "Выбрать устройство" "select-device": "Выбрать устройство",
"import": "Импортировать устройства",
"device-file": "Файл с устройствами"
}, },
"dialog": { "dialog": {
"close": "Закрыть диалог" "close": "Закрыть диалог"
@ -771,6 +777,7 @@
"search": "Поиск объектов", "search": "Поиск объектов",
"selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }", "selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }",
"entity-name": "Название объекта", "entity-name": "Название объекта",
"entity-label": "Метка объекта",
"details": "Подробности об объекте", "details": "Подробности об объекте",
"no-entities-prompt": "Объекты не найдены", "no-entities-prompt": "Объекты не найдены",
"no-data": "Нет данных для отображения", "no-data": "Нет данных для отображения",
@ -793,7 +800,7 @@
"duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.<br>В рамках одного дашборда псевдонимы представлений объектов должны быть уникальными.", "duplicate-alias-error": "Найден дубликат псевдонима '{{alias}}'.<br>В рамках одного дашборда псевдонимы представлений объектов должны быть уникальными.",
"configure-alias": "Настроить псевдоним '{{alias}}'", "configure-alias": "Настроить псевдоним '{{alias}}'",
"no-entity-views-matching": "Объекты, соответствующие '{{entity}}', не найдены.", "no-entity-views-matching": "Объекты, соответствующие '{{entity}}', не найдены.",
"alias": "Псевдонимы", "alias": "Псевдоним",
"alias-required": "Псевдоним представления объекта обязателен.", "alias-required": "Псевдоним представления объекта обязателен.",
"remove-alias": "Убрать псевдоним представления объекта", "remove-alias": "Убрать псевдоним представления объекта",
"add-alias": "Добавить псевдоним представления объекта", "add-alias": "Добавить псевдоним представления объекта",
@ -843,11 +850,12 @@
"events": "События", "events": "События",
"details": "Подробности", "details": "Подробности",
"copyId": "Копировать ИД представление объекта", "copyId": "Копировать ИД представление объекта",
"assignedToCustomer": "Назначенные клиенту", "assignedToCustomer": "Назначено клиенту",
"unable-entity-view-device-alias-title": "Не удалось удалить псевдоним представления объекта", "unable-entity-view-device-alias-title": "Не удалось удалить псевдоним представления объекта",
"unable-entity-view-device-alias-text": "Не удалось удалить псевдоним устройства '{{entityViewAlias}}', т.к. он используется следующими виджетами:<br/>{{widgetsList}}", "unable-entity-view-device-alias-text": "Не удалось удалить псевдоним устройства '{{entityViewAlias}}', т.к. он используется следующими виджетами:<br/>{{widgetsList}}",
"select-entity-view": "Выбрать представление объекта", "select-entity-view": "Выбрать представление объекта",
"make-public": "Открыть общий доступ к представлению объекта", "make-public": "Открыть общий доступ к представлению объекта",
"make-private": "Закрыть общий доступ к представлению объекта",
"start-date": "Дата начала", "start-date": "Дата начала",
"start-ts": "Время начала", "start-ts": "Время начала",
"end-date": "Дата окончания", "end-date": "Дата окончания",
@ -865,7 +873,11 @@
"attributes-propagation": "Пробросить атрибуты", "attributes-propagation": "Пробросить атрибуты",
"attributes-propagation-hint": "Представление объекта автоматически копирует указанные атрибуты с Целевого Объекта каждый раз, когда вы сохраняете или обновляете это представление. В целях производительности атрибуты целевого объекта не пробрасываются в представление объекта на каждом их изменении. Вы можете включить автоматический проброс, настроив в вашей цепочке правило \"copy to view\" и соединив его с сообщениями типа \"Post attributes\" и \"Attributes Updated\".", "attributes-propagation-hint": "Представление объекта автоматически копирует указанные атрибуты с Целевого Объекта каждый раз, когда вы сохраняете или обновляете это представление. В целях производительности атрибуты целевого объекта не пробрасываются в представление объекта на каждом их изменении. Вы можете включить автоматический проброс, настроив в вашей цепочке правило \"copy to view\" и соединив его с сообщениями типа \"Post attributes\" и \"Attributes Updated\".",
"timeseries-data": "Данные телеметрии", "timeseries-data": "Данные телеметрии",
"timeseries-data-hint": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения." "timeseries-data-hint": "Настроить ключи данных телеметрии целевого объекта, которые будут доступны представлению объекта. Эти данные только для чтения.",
"make-public-entity-view-title": "Вы уверенны, что хотите открыть общий доступ к представленю объекта '{{entityViewName}}'?",
"make-public-entity-view-text": "После подтверждения представление объекта и все связанные с ним данные станут публичными и доступными для других пользователей.",
"make-private-entity-view-title": "Вы уверенны, что хотите закрыть общий доступ к представлению объекта '{{entityViewName}}'?",
"make-private-entity-view-text": "После подтверждения представление объекта и все звязанные с ним данные станут приватными и не будут доступны для других пользователей."
}, },
"event": { "event": {
"event-type": "Тип события", "event-type": "Тип события",
@ -1012,6 +1024,7 @@
"modbus-add-server": "Добавить сервер/ведомое устройство", "modbus-add-server": "Добавить сервер/ведомое устройство",
"modbus-add-server-prompt": "Пожалуйста, добавить сервер/ведомое устройство", "modbus-add-server-prompt": "Пожалуйста, добавить сервер/ведомое устройство",
"modbus-transport": "Транспорт", "modbus-transport": "Транспорт",
"modbus-tcp-reconnect": "Переподключатсься автоматически",
"modbus-port-name": "Название последовательного порта", "modbus-port-name": "Название последовательного порта",
"modbus-encoding": "Кодирование символов", "modbus-encoding": "Кодирование символов",
"modbus-parity": "Паритет", "modbus-parity": "Паритет",
@ -1086,7 +1099,40 @@
}, },
"import": { "import": {
"no-file": "Файл не выбран", "no-file": "Файл не выбран",
"drop-file": "Перетащите JSON файл или кликните для выбора файла." "drop-file": "Перетащите JSON файл или кликните для выбора файла.",
"drop-file-csv": "Перетащите CSV файл или кликните для выбора файла.",
"column-value": "Значение",
"column-title": "Название",
"column-example": "Пример значений данных",
"column-key": "Ключ атрибута/телеметрии",
"csv-delimiter": "Разделитель в CSV файле",
"csv-first-line-header": "Первая строка содержит названия колонок",
"csv-update-data": "Обновить атрибут/телеметрию",
"import-csv-number-columns-error": "Файл должен содержать как минимум две колонки",
"import-csv-invalid-format-error": "Неверный формат данных. Строка: '{{line}}'",
"column-type": {
"name": "Название",
"type": "Тип",
"column-type": "Тип колонки",
"client-attribute": "Клиентский атрибут",
"shared-attribute": "Общий атрибут",
"server-attribute": "Серверный атрибут",
"timeseries": "Телеметрия",
"entity-field": "Entity field",
"access-token": "Токен"
},
"stepper-text": {
"select-file": "Выберите файл",
"configuration": "Конфигурация импорта",
"column-type": "Выберите тип колонок",
"creat-entities": "Создание новых объектов",
"done": "Завершено"
},
"message": {
"create-entities": "{{count}} новый(х) объект(ов) было успешно создано.",
"update-entities": "{{count}} объект(ов) успешно обновлено.",
"error-entities": "Возникла ошибка при создании {{count}} объекта(ов)."
}
}, },
"item": { "item": {
"selected": "Выбранные" "selected": "Выбранные"
@ -1347,7 +1393,8 @@
"edit": "Изменить временное окно", "edit": "Изменить временное окно",
"date-range": "Диапазон дат", "date-range": "Диапазон дат",
"last": "Последние", "last": "Последние",
"time-period": "Период времени" "time-period": "Период времени",
"hide": "Скрыть"
}, },
"user": { "user": {
"user": "Пользователь", "user": "Пользователь",
@ -1419,12 +1466,12 @@
"edit": "Редактировать виджет", "edit": "Редактировать виджет",
"remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?", "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?",
"remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.", "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.",
"timeseries": "Выборка по времени", "timeseries": "Телеметрия",
"search-data": "Search data", "search-data": "Поиск данных",
"no-data-found": "No data found", "no-data-found": "Данные не найдено",
"latest-values": "Последние значения", "latest-values": "Последние значения",
"rpc": "Управляющий виджет", "rpc": "Управляющий виджет",
"alarm": "Alarm widget", "alarm": "Виджет оповещений",
"static": "Статический виджет", "static": "Статический виджет",
"select-widget-type": "Выберите тип виджета", "select-widget-type": "Выберите тип виджета",
"missing-widget-title-error": "Укажите название виджета!", "missing-widget-title-error": "Укажите название виджета!",
@ -1521,6 +1568,7 @@
"decimals": "Количество цифр после запятой", "decimals": "Количество цифр после запятой",
"timewindow": "Временное окно", "timewindow": "Временное окно",
"use-dashboard-timewindow": "Использовать временное окно дашборда", "use-dashboard-timewindow": "Использовать временное окно дашборда",
"display-timewindow": "Показывать временное окно",
"display-legend": "Показать легенду", "display-legend": "Показать легенду",
"datasources": "Источники данных", "datasources": "Источники данных",
"maximum-datasources": "Максимальной количество источников данных равно {{count}}", "maximum-datasources": "Максимальной количество источников данных равно {{count}}",
@ -1545,7 +1593,10 @@
"edit-action": "Редактировать действие", "edit-action": "Редактировать действие",
"delete-action": "Удалить действие", "delete-action": "Удалить действие",
"delete-action-title": "Удалить действие виджета", "delete-action-title": "Удалить действие виджета",
"delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?" "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?",
"display-icon": "Показывать иконку в названии",
"icon-color": "Цвет иконки",
"icon-size": "Размер иконки"
}, },
"widget-type": { "widget-type": {
"import": "Импортировать тип виджета", "import": "Импортировать тип виджета",
@ -1616,7 +1667,13 @@
"input-widgets": { "input-widgets": {
"attribute-not-allowed": "Атрибут не может быть выбран в этом виджете", "attribute-not-allowed": "Атрибут не может быть выбран в этом виджете",
"blocked-location": "Геолокация заблокирована в вашем браузере", "blocked-location": "Геолокация заблокирована в вашем браузере",
"claim-device": "Подтвердить устройство",
"claim-failed": "Не удалось подтвердить устройство!",
"claim-not-found": "Устройство не найдено!",
"claim-successful": "Устройство успешно подтверждено!",
"discard-changes": "Отменить изменения", "discard-changes": "Отменить изменения",
"device-name": "Название устройства",
"device-name-required": "Необходимо указать название устройства",
"entity-attribute-required": "Значение атрибута обязателено", "entity-attribute-required": "Значение атрибута обязателено",
"entity-coordinate-required": "Необходимо указать широту и долготу", "entity-coordinate-required": "Необходимо указать широту и долготу",
"entity-timeseries-required": "Значение телеметрии обязательно", "entity-timeseries-required": "Значение телеметрии обязательно",
@ -1629,6 +1686,8 @@
"no-coordinate-specified": "Ключ для широты/долготы не указан", "no-coordinate-specified": "Ключ для широты/долготы не указан",
"no-support-geolocation": "Ваш браузер не поддерживает геолокацию", "no-support-geolocation": "Ваш браузер не поддерживает геолокацию",
"no-timeseries-selected": "Параметр телеметрии не выбран", "no-timeseries-selected": "Параметр телеметрии не выбран",
"secret-key": "Секретный ключ",
"secret-key-required": "Необходимо указать секретный ключ",
"switch-attribute-value": "Изменить значение атрибута", "switch-attribute-value": "Изменить значение атрибута",
"switch-timeseries-value": "Изменить значение телеметрии", "switch-timeseries-value": "Изменить значение телеметрии",
"timeseries-not-allowed": "Телеметрия не может быть выбрана в этом виджете", "timeseries-not-allowed": "Телеметрия не может быть выбрана в этом виджете",
@ -1647,11 +1706,11 @@
}, },
"custom": { "custom": {
"widget-action": { "widget-action": {
"action-cell-button": "Кнопка действия ячейки", "action-cell-button": "Кнопка действия в ячейке таблицы",
"row-click": "Действий при щелчке на строку", "row-click": "Действий при щелчке на строку",
"marker-click": "Действия при щелчке на указателе", "marker-click": "Действия при щелчке на маркер",
"polygon-click": "Действия при щелчке на полигон", "polygon-click": "Действия при щелчке на полигон",
"tooltip-tag-action": "Действие при подсказке" "tooltip-tag-action": "Действие при нажатии на ссылку в подсказке"
} }
}, },
"language": { "language": {

291
ui/src/app/locale/locale.constant-uk_UA.json

@ -49,6 +49,8 @@
"import": "Імпортувати", "import": "Імпортувати",
"export": "Експортувати", "export": "Експортувати",
"share-via": "Поділитися через {{provider}}", "share-via": "Поділитися через {{provider}}",
"continue": "Продовжити",
"discard-changes": "Скасувати зміни",
"move": "Перемістити", "move": "Перемістити",
"select": "Вибрати" "select": "Вибрати"
}, },
@ -193,20 +195,20 @@
"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", "no-entity-filter-specified": "Фільтр обїектів не вказано",
"root-state-entity": "Use dashboard state entity as root", "root-state-entity": "Використовувати сутінсть стану як кореневу",
"group-state-entity": "Use dashboard state entity as entity group", "group-state-entity": "Використовувати групу сутностей стану як кореневу",
"root-entity": "Root entity", "root-entity": "Коренева сутність",
"state-entity-parameter-name": "State entity parameter name", "state-entity-parameter-name": "Параметр сутності стану",
"default-state-entity": "Default state entity", "default-state-entity": "Сутність стану за замовчуванням",
"default-state-entity-group": "Default state entity group", "default-state-entity-group": "Група сутностей стану за замовчуванням",
"default-entity-parameter-name": "By default", "default-entity-parameter-name": "За замовчуванням",
"max-relation-level": "Max relation level", "max-relation-level": "Максимальна глибина відносин",
"unlimited-level": "Unlimited level", "unlimited-level": "Необмежена глибина",
"state-entity": "Dashboard state entity", "state-entity": "Сутність стану панелі пристроїв",
"entities-of-group-state-entity": "Entities from dashboard state entity group", "entities-of-group-state-entity": "Сутності із групи сутностей стану",
"all-entities": "All entities", "all-entities": "Всі сутності",
"any-relation": "any" "any-relation": "не вказано"
}, },
"asset": { "asset": {
"asset": "Актив", "asset": "Актив",
@ -276,7 +278,9 @@
"remove-assets-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# актив} } з групи '{entityGroup}'?", "remove-assets-from-group": "Ви впевнені, що хочете видалити { count, plural, 1 {1 актив} other {# актив} } з групи '{entityGroup}'?",
"group": "Група активів", "group": "Група активів",
"list-of-groups": "{ count, plural, 1 {Одна група активів} other {Список # груп активів} }", "list-of-groups": "{ count, plural, 1 {Одна група активів} other {Список # груп активів} }",
"group-name-starts-with": "Групи активів, чиї імена починаються з '{{prefix}}'" "group-name-starts-with": "Групи активів, чиї імена починаються з '{{prefix}}'",
"import": "Імпортувати активи",
"asset-file": "Файл з активами"
}, },
"attribute": { "attribute": {
"attributes": "Атрибути", "attributes": "Атрибути",
@ -400,8 +404,8 @@
"encoder": "Кодер", "encoder": "Кодер",
"test-decoder-fuction": "Тестування функції декодера", "test-decoder-fuction": "Тестування функції декодера",
"test-encoder-fuction": "Тестування функції кодера", "test-encoder-fuction": "Тестування функції кодера",
"decoder-input-params": "Параметри введення декодера", "decoder-input-params": "Вхідні параметри декодера",
"encoder-input-params": "Параметри введення кодера", "encoder-input-params": "Вхідні параметри кодера",
"payload": "Вхідне повідомлення", "payload": "Вхідне повідомлення",
"payload-content-type": "Тип контенту вхідного повідомлення", "payload-content-type": "Тип контенту вхідного повідомлення",
"payload-content": "Зміст вхідного повідомлення", "payload-content": "Зміст вхідного повідомлення",
@ -411,8 +415,8 @@
"test": "Тест", "test": "Тест",
"metadata": "Метадані", "metadata": "Метадані",
"metadata-required": "Записи метаданих не можуть бути порожніми.", "metadata-required": "Записи метаданих не можуть бути порожніми.",
"integration-metadata": "Інтеграція метаданих", "integration-metadata": "Метедані інтеграції",
"integration-metadata-required": "Записи інтеграції метаданих не можуть бути порожніми.", "integration-metadata-required": "Параметри метаданих інтеграції не можуть бути порожніми.",
"output": "Вихідні дані", "output": "Вихідні дані",
"import": "Імпорт перетворювача даних", "import": "Імпорт перетворювача даних",
"export": "Експорт перетворювача даних", "export": "Експорт перетворювача даних",
@ -482,7 +486,7 @@
"group": "Група клієнтів", "group": "Група клієнтів",
"list-of-groups": "{ count, plural, 1 {Одна група клієнтів} other {Список # груп клієнтів} }", "list-of-groups": "{ count, plural, 1 {Одна група клієнтів} other {Список # груп клієнтів} }",
"group-name-starts-with": "Групи клієнтів, імена яких починаються з '{{prefix}}'", "group-name-starts-with": "Групи клієнтів, імена яких починаються з '{{prefix}}'",
"select-default-customer": "Виберати клієнта за замовчуванням", "select-default-customer": "Вибрати клієнта за замовчуванням",
"default-customer": "Клієнт за замовчуванням", "default-customer": "Клієнт за замовчуванням",
"default-customer-required": "Необхідно вказати клієнта за замовчуванням для налагодження панелі візуалізації на рівні замовника", "default-customer-required": "Необхідно вказати клієнта за замовчуванням для налагодження панелі візуалізації на рівні замовника",
"allow-white-labeling": "Дозволити брендування" "allow-white-labeling": "Дозволити брендування"
@ -650,7 +654,7 @@
"show-details": "Показати деталі", "show-details": "Показати деталі",
"hide-details": "Приховати деталі", "hide-details": "Приховати деталі",
"select-state": "Виберіть цільовий стан", "select-state": "Виберіть цільовий стан",
"state-controller": "Контроль стану" "state-controller": "Контроллер стану"
}, },
"datakey": { "datakey": {
"settings": "Налаштування", "settings": "Налаштування",
@ -783,6 +787,8 @@
"public": "Публічно", "public": "Публічно",
"device-public": "Пристрій є публічним", "device-public": "Пристрій є публічним",
"select-device": "Виберіть пристрій", "select-device": "Виберіть пристрій",
"import": "Імпортувати пристрої",
"device-file": "Файл з пристроями",
"selected-devices": "{ count, plural, 1 {1 пристрій} other {# пристрої} } вибрано", "selected-devices": "{ count, plural, 1 {1 пристрій} other {# пристрої} } вибрано",
"search": "Шукати пристрої", "search": "Шукати пристрої",
"select-group-to-add": "Виберіть цільову групу, щоб додати вибраний пристрій", "select-group-to-add": "Виберіть цільову групу, щоб додати вибраний пристрій",
@ -884,9 +890,9 @@
"type-alarms": "Сигнали тривоги", "type-alarms": "Сигнали тривоги",
"list-of-alarms": "{ count, plural, 1 {Один сигнал тривоги} other {Список # сигналів тривоги} }", "list-of-alarms": "{ count, plural, 1 {Один сигнал тривоги} other {Список # сигналів тривоги} }",
"alarm-name-starts-with": "Сигнали тривоги, імена яких починаються '{{prefix}}'", "alarm-name-starts-with": "Сигнали тривоги, імена яких починаються '{{prefix}}'",
"type-rulechain": "Правило ланцюжка", "type-rulechain": "Ланцюжок правил",
"type-rulechains": "Правило ланцюжків", "type-rulechains": "Ланцюжки правил",
"list-of-rulechains": "{ count, plural, 1 {Одне правило ланцюжка} other {Список # правил ланцюжків} }", "list-of-rulechains": "{ count, plural, 1 {Один ланцюжок правил} other {Список # ланцюжків правил} }",
"rulechain-name-starts-with": "Правило ланцюжків, імена яких починаються '{{prefix}}'", "rulechain-name-starts-with": "Правило ланцюжків, імена яких починаються '{{prefix}}'",
"type-scheduler-event": "Scheduler event", "type-scheduler-event": "Scheduler event",
"type-scheduler-events": "Scheduler events", "type-scheduler-events": "Scheduler events",
@ -904,6 +910,7 @@
"search": "Пошук сутностей", "search": "Пошук сутностей",
"selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано", "selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано",
"entity-name": "Ім'я сутності", "entity-name": "Ім'я сутності",
"entity-label": "Мітка сутності",
"details": "Подробиці сутності", "details": "Подробиці сутності",
"no-entities-prompt": "Сутності не знайдено", "no-entities-prompt": "Сутності не знайдено",
"no-data": "Немає даних для відображення", "no-data": "Немає даних для відображення",
@ -966,7 +973,7 @@
}, },
"column-type-required": "Необхідно вказати тип стовпця.", "column-type-required": "Необхідно вказати тип стовпця.",
"entity-field": { "entity-field": {
"created-time": "Створений час", "created-time": "Час створення",
"name": "Ім'я", "name": "Ім'я",
"type": "Тип", "type": "Тип",
"assigned_customer": "Призначений клієнт", "assigned_customer": "Призначений клієнт",
@ -986,7 +993,7 @@
"sort-order": { "sort-order": {
"asc": "У порядку зростання", "asc": "У порядку зростання",
"desc": "У порядку зменшення", "desc": "У порядку зменшення",
"none": "Не має" "none": "Немає"
}, },
"details-mode": { "details-mode": {
"on-row-click": "Клацніть на рядок", "on-row-click": "Клацніть на рядок",
@ -995,7 +1002,7 @@
}, },
"add-to-group": "Додати до групи", "add-to-group": "Додати до групи",
"move-to-group": "Перемістити до групи", "move-to-group": "Перемістити до групи",
"select-entity-group": "Виберати групу сутностей", "select-entity-group": "Виберіть групу сутностей",
"no-entity-groups-matching": "Не знайдено жодних груп сутностей, що відповідають '{{entityGroup}}'.", "no-entity-groups-matching": "Не знайдено жодних груп сутностей, що відповідають '{{entityGroup}}'.",
"target-entity-group-required": "Необхідно вказати цільову групу сутності.", "target-entity-group-required": "Необхідно вказати цільову групу сутності.",
"remove-from-group": "Видалити з групи", "remove-from-group": "Видалити з групи",
@ -1013,7 +1020,7 @@
"enable-assets-management": "Увімкнути керування активами", "enable-assets-management": "Увімкнути керування активами",
"enable-devices-management": "Увімкнути керування пристроями", "enable-devices-management": "Увімкнути керування пристроями",
"enable-dashboards-management": "Увімкнути керування панелями візуалізації", "enable-dashboards-management": "Увімкнути керування панелями візуалізації",
"open-details-on": "Відкрити деталі сутності", "open-details-on": "Відкрити деталі сутності по",
"select-existing": "Виберіть існуючу групу сутностей", "select-existing": "Виберіть існуючу групу сутностей",
"create-new": "Створити нову групу сутностей", "create-new": "Створити нову групу сутностей",
"new-entity-group-name": "Нове ім'я групи сутностей", "new-entity-group-name": "Нове ім'я групи сутностей",
@ -1023,77 +1030,78 @@
"entity-group-name-filter-required": "Необхідно задати назву групи сутностей." "entity-group-name-filter-required": "Необхідно задати назву групи сутностей."
}, },
"entity-view": { "entity-view": {
"entity-view": "Перегляд сутності", "entity-view": "Представлення сутності",
"entity-view-required": "Необхідно вказати перегляд сутності.", "entity-view-required": "Необхідно вказати представлення сутності.",
"entity-views": "Перегляди сутностей", "entity-views": "Представлення сутностей",
"management": "Керування переглядом сутностей", "management": "Керування представленням сутностей",
"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": "Створити новий!",
"duplicate-alias-error": "Псевдонім з такою назвою вже існує '{{alias}}'.<br>Псевдоніми перегляду повинні бути унікальними на панелі візуалізації.", "duplicate-alias-error": "Псевдонім з такою назвою вже існує '{{alias}}'.<br>Псевдоніми представлення повинні бути унікальними на панелі візуалізації.",
"configure-alias": "Налаштувати псевдонім '{{alias}}'", "configure-alias": "Налаштувати псевдонім '{{alias}}'",
"no-entity-views-matching": "Сутності, які відповідають '{{entity}}' не знайдені.", "no-entity-views-matching": "Сутності, які відповідають '{{entity}}' не знайдені.",
"alias": "Псевдонім", "alias": "Псевдонім",
"alias-required": "Необхідно вказати псевдонім перегляду сутності.", "alias-required": "Необхідно вказати псевдонім представлення сутності.",
"remove-alias": "Видалити псевдонім перегляду сутності", "remove-alias": "Видалити псевдонім представлення сутності",
"add-alias": "Додати псевдонім перегляду сутності", "add-alias": "Додати псевдонім представлення сутності",
"name-starts-with": "Ім'я перегляду сутності починається з", "name-starts-with": "Ім'я представлення сутності починається з",
"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": "Призначити клієнту",
"assign-entity-view-to-customer": "Призначити перегляд(и) сутності(ей) клієнту", "assign-entity-view-to-customer": "Призначити представлення сутності(ей) клієнту",
"assign-entity-view-to-customer-text": "Будь ласка, виберіть перегляд сутності для призначення клієнту", "assign-entity-view-to-customer-text": "Будь ласка, виберіть представлення сутності для призначення клієнту",
"no-entity-views-text": "Перегляду сутності не знайдено", "no-entity-views-text": "Представлення сутності не знайдено",
"assign-to-customer-text": "Будь ласка, виберіть клієнта, для призначиення перегляду(ів) сутності(ей)", "assign-to-customer-text": "Будь ласка, виберіть клієнта, для призначиення представлення(ь) сутності(ей)",
"entity-view-details": "Деталі перегляду сутності", "entity-view-details": "Деталі представлення сутності",
"add-entity-view-text": "Додати новий перегляд сутносі", "add-entity-view-text": "Додати нове представлення сутності",
"delete": "Видалити перегляд сутності", "delete": "Видалити представлення сутності",
"assign-entity-views": "Призначити перегляд сутності", "assign-entity-views": "Призначити представлення сутності",
"assign-entity-views-text": "Призначити { count, plural, 1 {1 перегляд сутності} other {# перегляди сутностей } } клієнту", "assign-entity-views-text": "Призначити { count, plural, 1 {1 представлення сутності} other {# представлення сутностей } } клієнту",
"delete-entity-views": "Видалити перегляди сутностей", "delete-entity-views": "Видалити представлення сутностей",
"unassign-from-customer": "Позбавити клієнта", "unassign-from-customer": "Відкликати у клієнта",
"unassign-entity-views": "Позбавити переглядів сутностей", "unassign-entity-views": "Відкликати представлення сутностей",
"unassign-entity-views-action-title": "Позбавити { count, plural, 1 {1 перегляду сутності} other {# переглядів сутностей} } клієнта", "unassign-entity-views-action-title": "Відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} } у клієнта",
"assign-new-entity-view": "Призначити новий перегляд сутності", "assign-new-entity-view": "Призначити нове представлення сутності",
"delete-entity-view-title": "Ви впевнені, що хочете видалити перегляд сутності'{{entityViewName}}'?", "delete-entity-view-title": "Ви впевнені, що хочете видалити представлення сутності'{{entityViewName}}'?",
"delete-entity-view-text": "Будьте обережні, після підтвердження, перегляд сутності та всі пов'язані з нею дані стануть недоступними.", "delete-entity-view-text": "Будьте обережні, після підтвердження, представлення сутності та всі пов'язані з ним дані стануть недоступними.",
"delete-entity-views-title": "Ви впевнені, що хочете видалити перегляд сутності { count, plural, 1 {1 перегляд сутності } other {# перегляди сутностей } }?", "delete-entity-views-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }?",
"delete-entity-views-action-title": "Видалити { count, plural, 1 {1 перегляд сутності } other {# перегляди сутностей } }", "delete-entity-views-action-title": "Видалити { count, plural, 1 {1 представлення сутності } other {# представлення сутностей } }",
"delete-entity-views-text": "Будьте обережні, після підтвердження, всі виділені перегляди сутностей та дні, пов'язані з ними стануть недоступними.", "delete-entity-views-text": "Будьте обережні, після підтвердження, всі виділені представлення сутностей та дні, пов'язані з ними стануть недоступними.",
"unassign-entity-view-title": "Ви впевнені, що хочете позбавити перегляду сутності '{{entityViewName}}'?", "unassign-entity-view-title": "Ви впевнені, що хочете відкликати представлення сутності '{{entityViewName}}'?",
"unassign-entity-view-text": "Після підтвердження клієнт буде позбавлений перегляду сутності. Дані перегляду сутності не будуть доступні клієнту.", "unassign-entity-view-text": "Після підтвердження представлення сутності буде відкликане у клієнта. Дані представлення сутності не будуть доступні клієнту.",
"unassign-entity-view": "Позбавити перегляду сутності", "unassign-entity-view": "Відкликати представлення сутності",
"unassign-entity-views-title": "Ви впевнені, що хочете позбавити { count, plural, 1 {1 перегляду сутності} other {# переглядів сутностей} }?", "unassign-entity-views-title": "Ви впевнені, що хочете відкликати { count, plural, 1 {1 представлення сутності} other {# представлень сутностей} }?",
"unassign-entity-views-text": "Після підтвердження, клієнта буде позбавлено всіх виділених переглядів сутності. Дані переглядів сутностей не будуть доступні клієнту .", "unassign-entity-views-text": "Після підтвердження, клієнта буде позбавлено всіх виділених представлень сутностей. Дані представлень сутностей не будуть доступні клієнту.",
"entity-view-type": "Тип перегляду сутності", "entity-view-type": "Тип представлення сутності",
"entity-view-type-required": "Необхідно вказати тип перегляду сутності.", "entity-view-type-required": "Необхідно вказати тип представлення сутності.",
"select-entity-view-type": "Виберіть тип перегляду сутності", "select-entity-view-type": "Виберіть тип представлення сутності",
"enter-entity-view-type": "Введіть тип перегляду сутності", "enter-entity-view-type": "Введіть тип представлення сутності",
"any-entity-view": "Будь-який перегляд сутності", "any-entity-view": "Будь-яке представлення сутності",
"no-entity-view-types-matching": "Не знайдено жодних типів перегляду сутності, що відповідають '{{entitySubtype}}'.", "no-entity-view-types-matching": "Не знайдено жодних типів представлення сутності, що відповідають '{{entitySubtype}}'.",
"entity-view-type-list-empty": "Не вибрано тип перегляду сутності.", "entity-view-type-list-empty": "Не вибрано тип представлення сутності.",
"entity-view-types": "Типи перегляду сутності", "entity-view-types": "Типи представлення сутності",
"name": "Ім'я", "name": "Ім'я",
"name-required": "Необхідно вказати ім'я.", "name-required": "Необхідно вказати ім'я.",
"description": "Опис", "description": "Опис",
"events": "Події", "events": "Події",
"details": "Деталі", "details": "Деталі",
"copyId": "Скопіювати Id перегляду сутності", "copyId": "Скопіювати Id представлення сутності",
"assignedToCustomer": "Призначений клієнту", "assignedToCustomer": "Призначений клієнту",
"unable-entity-view-device-alias-title": "Неможливо видалити псевдонім перегляду сутності", "unable-entity-view-device-alias-title": "Неможливо видалити псевдонім представлення сутності",
"unable-entity-view-device-alias-text": "Не вдалося видалити псевдонім пристрою'{{entityViewAlias}}', так як він використовується наступним(и) віджетом(ами):<br/>{{widgetsList}}", "unable-entity-view-device-alias-text": "Не вдалося видалити псевдонім пристрою'{{entityViewAlias}}', так як він використовується наступним(и) віджетом(ами):<br/>{{widgetsList}}",
"select-entity-view": "Виберати перегляд сутності", "select-entity-view": "Вибрати представлення сутності",
"make-public": "Зробити перегляд сутності публічним", "make-public": "Зробити представлення сутності публічним",
"make-private": "Зробити представлення сутності приватним",
"start-date": "Дата початку", "start-date": "Дата початку",
"start-ts": "Час початку", "start-ts": "Час початку",
"end-date": "Дата закінчення", "end-date": "Дата закінчення",
@ -1109,9 +1117,13 @@
"timeseries-placeholder": "Телеметрія", "timeseries-placeholder": "Телеметрія",
"target-entity": "Цільова сутність", "target-entity": "Цільова сутність",
"attributes-propagation": "Поширення атрибутів", "attributes-propagation": "Поширення атрибутів",
"attributes-propagation-hint": "Перегляд сутностей автоматично копіюватиме вказані атрибути з цільової сутності кожного разу, коли ви зберігаєте або оновлюєте ці перегляди. В цілях продуктивності, атрибути цільової сутності не поширюються на представлення сутності на кожній зміні їх атрибутів. Можна ввімкнути автоматичне поширення, налаштувавши у вашому ланцюжку правило \"copy to view\" і пов'язуючи його з повідомленнями типу \"Post attributes\" і \"Attributes Updated\"..", "attributes-propagation-hint": "Представлення сутності автоматично копіюватиме вказані атрибути з цільової сутності кожного разу, коли ви зберігаєте або оновлюєте його. В цілях продуктивності, атрибути цільової сутності не поширюються на представлення сутності при кожній зміні її атрибутів. Можна ввімкнути автоматичне поширення, налаштувавши правило \"copy to view\" у вашому ланцюжку правил і пов'язуючи його з повідомленнями типу \"Post attributes\" і \"Attributes Updated\"..",
"timeseries-data": "Дані телеметрії", "timeseries-data": "Дані телеметрії",
"timeseries-data-hint": "Налаштуйте ключі даних телеметрії цільової сутності, які будуть доступні перегляду сутності. Ці дані доступні лише для читання." "timeseries-data-hint": "Налаштуйте ключі даних телеметрії цільової сутності, які будуть доступні представленню сутності. Ці дані доступні лише для читання.",
"make-public-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' публічним?",
"make-public-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть публічними і будуть доступні для інших користувачів.",
"make-private-entity-view-title": "Ви впевнені, що бажаєте зробити представлення сутності '{{entityViewName}}' приватним?",
"make-private-entity-view-text": "Після підтвердження представлення сутності і всі пов'язані з ним дані стануть приватними і не будуть доступні для інших користувачів."
}, },
"event": { "event": {
"events": "Події", "events": "Події",
@ -1264,6 +1276,7 @@
"modbus-add-server": "Додати сервер/ведений пристрій", "modbus-add-server": "Додати сервер/ведений пристрій",
"modbus-add-server-prompt": "Будь ласка, додайте сервер/ведений пристрій", "modbus-add-server-prompt": "Будь ласка, додайте сервер/ведений пристрій",
"modbus-transport": "Транспорт", "modbus-transport": "Транспорт",
"modbus-tcp-reconnect": "Перепідключатися автоматично",
"modbus-port-name": "Ім'я послідовного порту", "modbus-port-name": "Ім'я послідовного порту",
"modbus-encoding": "Кодування", "modbus-encoding": "Кодування",
"modbus-parity": "Паритет", "modbus-parity": "Паритет",
@ -1321,7 +1334,8 @@
"add-item-text": "Додати новий елемент", "add-item-text": "Додати новий елемент",
"no-items-text": "Не знайдено жодного елемента", "no-items-text": "Не знайдено жодного елемента",
"item-details": "Деталі елемента", "item-details": "Деталі елемента",
"delete-item": "Видалити елементи", "delete-item": "Видалити елемент",
"delete-items": "Видалити елементи",
"scroll-to-top": "Перейти угору" "scroll-to-top": "Перейти угору"
}, },
"help": { "help": {
@ -1338,7 +1352,39 @@
"import": { "import": {
"no-file": "Не вибрано жодного файлу", "no-file": "Не вибрано жодного файлу",
"drop-file": "Перетягніть JSON файл, або клацніть, щоб вибрати файл для завантаження.", "drop-file": "Перетягніть JSON файл, або клацніть, щоб вибрати файл для завантаження.",
"drop-csv-file": "Перетягніть CSV файл, або клацніть, щоб вибрати файл для завантаження." "drop-csv-file": "Перетягніть CSV файл, або клацніть, щоб вибрати файл для завантаження.",
"column-value": "Значення",
"column-title": "Назва",
"column-example": "Приклад значень даних",
"column-key": "Ключ атрибута/телеметрії",
"csv-delimiter": "Розділювач в CSV файлі",
"csv-first-line-header": "Перший рядок містить назви колонок",
"csv-update-data": "Оновити атрибути/телеметрію",
"import-csv-number-columns-error": "Файл має містити як мінімум дві колонки",
"import-csv-invalid-format-error": "Невірний формат даних. Рядок: '{{line}}'",
"column-type": {
"name": "Назва",
"type": "Тип",
"column-type": "Тип колонки",
"client-attribute": "Атрибут клієнта",
"shared-attribute": "Спільний атрибут",
"server-attribute": "Атрибут сервера",
"timeseries": "Телеметрія",
"entity-field": "Entity field",
"access-token": "Токен"
},
"stepper-text": {
"select-file": "Виберіть файл",
"configuration": "Конфігурація імпорту",
"column-type": "Виберіть тип колонок",
"creat-entities": "Створення нових сутностей",
"done": "Завершено"
},
"message": {
"create-entities": "{{count}} нову(их) сутність(ей) успішно створено.",
"update-entities": "{{count}} сутність(ей) успішно оновлено.",
"error-entities": "Виникла помилка при створенні {{count}} сутності(ей)."
}
}, },
"integration": { "integration": {
"integration": "Інтеграція", "integration": "Інтеграція",
@ -1681,7 +1727,7 @@
"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": "Filter", "type-filter": "Фільтр",
"type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами", "type-filter-details": "Фільтрувати вхідні повідомлення з заданими умовами",
"type-enrichment": "Насичення", "type-enrichment": "Насичення",
"type-enrichment-details": "Додати додаткову інформацію до метаданих повідомлень", "type-enrichment-details": "Додати додаткову інформацію до метаданих повідомлень",
@ -1690,7 +1736,7 @@
"type-action": "Дія", "type-action": "Дія",
"type-action-details": "Виконати задану дію", "type-action-details": "Виконати задану дію",
"type-analytics": "Аналітика", "type-analytics": "Аналітика",
"type-analytics-details": "Виконати аналіз потокових або збережених даних", "type-analytics-details": "Виконує аналіз потокових або збережених даних",
"type-external": "Зовнішній", "type-external": "Зовнішній",
"type-external-details": "Взаємодіє з зовнішньою системою", "type-external-details": "Взаємодіє з зовнішньою системою",
"type-rule-chain": "Ланцюг правил", "type-rule-chain": "Ланцюг правил",
@ -1711,12 +1757,13 @@
"metadata-required": "Записи метаданих не можуть бути порожніми.", "metadata-required": "Записи метаданих не можуть бути порожніми.",
"output": "Вихід", "output": "Вихід",
"test": "Тест", "test": "Тест",
"help": "Допомога" "help": "Допомога",
"reset-debug-mode": "Вимкнути режим налогодження у всіх правилах"
}, },
"scheduler": { "scheduler": {
"scheduler": "Планувальник", "scheduler": "Планувальник",
"scheduler-event": "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": "Управління планувальником",
@ -1726,7 +1773,7 @@
"created-time": "Час створення", "created-time": "Час створення",
"name": "Ім'я", "name": "Ім'я",
"type": "Тип", "type": "Тип",
"assigned_customer": "Призначений клієнт", "created_customer": "Створено клієнтом",
"edit-scheduler-event": "Редагувати подію", "edit-scheduler-event": "Редагувати подію",
"delete-scheduler-event": "Видалити подію", "delete-scheduler-event": "Видалити подію",
"no-scheduler-events": "Не знайдено жодних запланованих подій", "no-scheduler-events": "Не знайдено жодних запланованих подій",
@ -1748,8 +1795,9 @@
"daily": "Щодня", "daily": "Щодня",
"weekly": "Щотижня", "weekly": "Щотижня",
"repeats-required": "Потрібно вказати повторення.", "repeats-required": "Потрібно вказати повторення.",
"repeat-on": "Повторити на", "repeat-on": "Повторювати по",
"ends-on": "Завершити на", "repeat-every": "Повторювати кожний(у)",
"ends-on": "Завершення",
"sunday-label": "Нд", "sunday-label": "Нд",
"monday-label": "Пн", "monday-label": "Пн",
"tuesday-label": "Вт", "tuesday-label": "Вт",
@ -1784,7 +1832,7 @@
"navigate-next": "Перейти далі", "navigate-next": "Перейти далі",
"starting-from": "Починаючи з", "starting-from": "Починаючи з",
"until": "до", "until": "до",
"on": "на", "on": "в",
"sunday": "Неділя", "sunday": "Неділя",
"monday": "Понеділок", "monday": "Понеділок",
"tuesday": "Вівторок", "tuesday": "Вівторок",
@ -1792,17 +1840,22 @@
"thursday": "Четвер", "thursday": "Четвер",
"friday": "П'ятниця", "friday": "П'ятниця",
"saturday": "Субота", "saturday": "Субота",
"originator": "Засновник", "originator": "Ініціатор",
"single-entity": "Самостійна сутність", "single-entity": "Єдина сутність",
"group-of-entities": "Група сутностей", "group-of-entities": "Група сутностей",
"single-device": "Один пристрій", "single-device": "Один пристрій",
"group-of-devices": "Група пристроїв", "group-of-devices": "Група пристроїв",
"message-body": "Тіло повідомлення", "message-body": "Текст повідомлення",
"target": "Ціль", "target": "Ціль",
"rpc-method": "Метод", "rpc-method": "Метод",
"rpc-method-required": "Необхідно вказати метод", "rpc-method-required": "Необхідно вказати метод",
"rpc-params": "Парами. Парні.", "rpc-params": "Параметри",
"select-dashboard-state": "Виберіть стан панелі візуалізації" "select-dashboard-state": "Виберіть стан панелі візуалізації",
"hours": "Години",
"minutes": "Хвилини",
"seconds": "Секунди",
"time-interval-required": "Необхідно вказати часовий інтервал",
"time-unit-required": "Необхідно вказати одиниці часу"
}, },
"report": { "report": {
"report-config": "Конфігурація звіту", "report-config": "Конфігурація звіту",
@ -1828,8 +1881,8 @@
"bcc": "Bcc", "bcc": "Bcc",
"subject": "Тема", "subject": "Тема",
"subject-required": "Необхідно вказати тему.", "subject-required": "Необхідно вказати тему.",
"body": "Тіло", "body": "Текст",
"body-required": "Необідно вказати тіло." "body-required": "Лист не може бути пустим."
}, },
"blob-entity": { "blob-entity": {
"blob-entity": "Blob сутності", "blob-entity": "Blob сутності",
@ -1841,10 +1894,10 @@
"clear-search": "Очистити пошук", "clear-search": "Очистити пошук",
"no-blob-entities-prompt": "Файлів не знайдено", "no-blob-entities-prompt": "Файлів не знайдено",
"report": "Звіт", "report": "Звіт",
"created-time": "Створено час", "created-time": "Час створення",
"name": "Ім'я", "name": "Ім'я",
"type": "Тип", "type": "Тип",
"assigned_customer": "Призначений клієнт", "created_customer": "Створено клієнтом",
"download-blob-entity": "Завантажити файл", "download-blob-entity": "Завантажити файл",
"delete-blob-entity": "Видалити файл", "delete-blob-entity": "Видалити файл",
"delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?", "delete-blob-entity-title": "Ви впевнені, що хочете видалити файл '{{blobEntityName}}'?",
@ -1852,7 +1905,7 @@
}, },
"timezone": { "timezone": {
"timezone": "Часовий пояс", "timezone": "Часовий пояс",
"select-timezone": "Виберати часовий пояс ", "select-timezone": "Вибрати часовий пояс ",
"no-timezones-matching": "Не знайдено жодних часових поясів, які відповідають '{{timezone}}'.", "no-timezones-matching": "Не знайдено жодних часових поясів, які відповідають '{{timezone}}'.",
"timezone-required": "Необхідно вказати часовий пояс." "timezone-required": "Необхідно вказати часовий пояс."
}, },
@ -1877,7 +1930,7 @@
"description": "Опис", "description": "Опис",
"details": "Деталі", "details": "Деталі",
"events": "Події", "events": "Події",
"copyId": "Крпіювати Id власника", "copyId": "Копіювати Id власника",
"idCopiedMessage": "Id власника скопійовано в буфер обміну", "idCopiedMessage": "Id власника скопійовано в буфер обміну",
"select-tenant": "Вибрати власника", "select-tenant": "Вибрати власника",
"no-tenants-matching": "Не знайдено жодних власників, які відповідають '{{entity}}'.", "no-tenants-matching": "Не знайдено жодних власників, які відповідають '{{entity}}'.",
@ -1908,7 +1961,8 @@
"edit": "Редагувати вікно часу", "edit": "Редагувати вікно часу",
"date-range": "Проміжок часу", "date-range": "Проміжок часу",
"last": "Останнє", "last": "Останнє",
"time-period": "Період часу" "time-period": "Період часу",
"hide": "Приховати"
}, },
"user": { "user": {
"user": "Користувач", "user": "Користувач",
@ -2088,6 +2142,7 @@
"decimals": "Кількість цифр після коми", "decimals": "Кількість цифр після коми",
"timewindow": "Вікно часу", "timewindow": "Вікно часу",
"use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації",
"display-timewindow": "Показувати вікно часу",
"display-legend": "Показати легенду", "display-legend": "Показати легенду",
"datasources": "Джерела даних", "datasources": "Джерела даних",
"maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }",
@ -2112,7 +2167,10 @@
"edit-action": "Редагувати дію", "edit-action": "Редагувати дію",
"delete-action": "Видалити дію", "delete-action": "Видалити дію",
"delete-action-title": "Видалити дію віджета", "delete-action-title": "Видалити дію віджета",
"delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?" "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?",
"display-icon": "Показувати іконку у назві",
"icon-color": "Колір іконки",
"icon-size": "Розмір іконки"
}, },
"widget-type": { "widget-type": {
"import": "Імпортувати тип віджета", "import": "Імпортувати тип віджета",
@ -2183,7 +2241,13 @@
"input-widgets": { "input-widgets": {
"attribute-not-allowed": "Атрибут не може бути вибраний в цьому віджеті", "attribute-not-allowed": "Атрибут не може бути вибраний в цьому віджеті",
"blocked-location": "Геолокація заблокована у вашому браузері", "blocked-location": "Геолокація заблокована у вашому браузері",
"claim-device": "Підтвердити пристрій",
"claim-failed": "Не вдалося підтвердити пристрій!",
"claim-not-found": "Пристрій не знайдено!",
"claim-successful": "Пристрій успішно підтверджено!",
"discard-changes": "Скасувати зміни", "discard-changes": "Скасувати зміни",
"device-name": "Назва пристрою",
"device-name-required": "Необхідно вказати назву пристрою",
"entity-attribute-required": "Значення атрибута обов'язкове", "entity-attribute-required": "Значення атрибута обов'язкове",
"entity-coordinate-required": "Необхідно вказати широту та довготу", "entity-coordinate-required": "Необхідно вказати широту та довготу",
"entity-timeseries-required": "Значення телеметрії обов'язкове", "entity-timeseries-required": "Значення телеметрії обов'язкове",
@ -2196,6 +2260,8 @@
"no-coordinate-specified": "Ключ для широти/довготи не вказаний", "no-coordinate-specified": "Ключ для широти/довготи не вказаний",
"no-support-geolocation": "Ваш браузер не підтримує геолокацію", "no-support-geolocation": "Ваш браузер не підтримує геолокацію",
"no-timeseries-selected": "Параметр телеметрії не вибрано", "no-timeseries-selected": "Параметр телеметрії не вибрано",
"secret-key": "Секретний ключ",
"secret-key-required": "Необхідно вказати секретний ключ",
"switch-attribute-value": "Змінити значення атрибута", "switch-attribute-value": "Змінити значення атрибута",
"switch-timeseries-value": "Змінити значення телеметрії", "switch-timeseries-value": "Змінити значення телеметрії",
"timeseries-not-allowed": "Телеметрія не може бути вибрана в цьому віджеті", "timeseries-not-allowed": "Телеметрія не може бути вибрана в цьому віджеті",
@ -2239,17 +2305,18 @@
"domain-name": "Доменне ім'я" "domain-name": "Доменне ім'я"
}, },
"icon": { "icon": {
"icon": "веб-іконка", "icon": "Іконка",
"select-icon": "Виберіть веб-іконку", "select-icon": "Виберіть Іконку",
"material-icons": "Матеріал веб-іконки", "material-icons": "Іконки в стилі Material",
"show-all": "Показати всі веб-іконки" "show-all": "Показати всі іконки"
}, },
"custom": { "custom": {
"widget-action": { "widget-action": {
"action-cell-button": "Кнопка дії клітинки", "action-cell-button": "Кнопка дії в комірці таблиці",
"row-click": "Клацніть на рядок", "row-click": "Клацніть на рядок",
"marker-click": "Клацніть на маркер", "marker-click": "Клацніть на маркер",
"tooltip-tag-action": "Дії при підказці" "polygon-click": "Дія при натисканні на полігон",
"tooltip-tag-action": "Дія при натисканні на посилання в підказці"
} }
}, },
"language": { "language": {

Loading…
Cancel
Save