From 8e16a1562a62c87f0e321c3729c7cdd7d434c2a1 Mon Sep 17 00:00:00 2001 From: devaskim Date: Sat, 17 Feb 2024 18:58:11 +0500 Subject: [PATCH 001/121] Implemented cell click action for table widgets. --- .../widget_types/asset_admin_table.json | 2 +- .../widget_types/device_admin_table.json | 2 +- .../system/widget_types/entities_table.json | 2 +- .../entities-table-widget.component.html | 38 ++++++++++++++----- .../entity/entities-table-widget.component.ts | 30 +++++++++++++++ .../assets/locale/locale.constant-en_US.json | 2 + 6 files changed, 63 insertions(+), 13 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/asset_admin_table.json b/application/src/main/data/json/system/widget_types/asset_admin_table.json index 0a2eb2c37a..6e1dc45660 100644 --- a/application/src/main/data/json/system/widget_types/asset_admin_table.json +++ b/application/src/main/data/json/system/widget_types/asset_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n },\n 'cellDoubleClick': {\n name: 'widget-action.cell-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/device_admin_table.json b/application/src/main/data/json/system/widget_types/device_admin_table.json index af460a286c..61aecb377d 100644 --- a/application/src/main/data/json/system/widget_types/device_admin_table.json +++ b/application/src/main/data/json/system/widget_types/device_admin_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n },\n 'cellDoubleClick': {\n name: 'widget-action.cell-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index 1ebf8d4fde..5b8ccc1d50 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: false\n },\n 'cellDoubleClick': {\n name: 'widget-action.cell-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-entities-table-widget-settings", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html index 22e79fd526..46ea65141c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html @@ -42,10 +42,19 @@ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> {{ column.title }} - - + + + + + + + + - + + + + + + ): boolean { + let configured = false; + actionSourceIds.forEach(id => configured = configured || this.ctx.actionsApi.getActionDescriptors(id).length > 0 ); + return configured; + } + ngOnDestroy(): void { if (this.widgetResize$) { this.widgetResize$.disconnect(); @@ -310,6 +317,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni cssParser.cssPreviewNamespace = namespace; cssParser.createStyleElement(namespace, cssString); $(this.elementRef.nativeElement).addClass(namespace); + + this.useCellClickAction = this.isActionsConfigured(['cellClick', 'cellDoubleClick']) && + !this.isActionsConfigured(['rowClick', 'rowDoubleClick']); } private updateDatasources() { @@ -687,6 +697,26 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } } + public onCellClick($event: Event, entity: EntityData, key: EntityColumn, isDouble?: boolean) { + if ($event) { + $event.stopPropagation(); + } + this.entityDatasource.toggleCurrentEntity(entity); + const actionSourceId = isDouble ? 'cellDoubleClick' : 'cellClick'; + const descriptors = this.ctx.actionsApi.getActionDescriptors(actionSourceId); + if (descriptors.length) { + let entityId; + let entityName; + let entityLabel; + if (entity) { + entityId = entity.id; + entityName = entity.entityName; + entityLabel = entity.entityLabel; + } + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {entity, key}, entityLabel); + } + } + public onRowClick($event: Event, entity: EntityData, isDouble?: boolean) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 288db8c9d2..aca909ad88 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6911,6 +6911,7 @@ "widget-action": { "action-cell-button": "Action cell button", "row-click": "On row click", + "cell-click": "On cell click", "polygon-click": "On polygon click", "marker-click": "On marker click", "circle-click": "On circle click", @@ -6919,6 +6920,7 @@ "element-click": "On HTML element click", "pie-slice-click": "On slice click", "row-double-click": "On row double click", + "cell-double-click": "On cell double click", "card-click": "On card click", "click": "On click" } From 8636ed3b0d68af3160385c1ff302c4ebe281a824 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 19 Mar 2024 12:57:37 +0200 Subject: [PATCH 002/121] TB Core consumer - parallel processing; refactoring --- .../queue/DefaultTbCoreConsumerService.java | 432 ++++++++---------- .../DefaultTbRuleEngineConsumerService.java | 39 +- .../consumer/BasicQueueConsumerManager.java | 109 +++++ .../queue/consumer/QueueConsumerManager.java | 321 +++++++++++++ .../processing/AbstractConsumerService.java | 128 +++--- .../TbQueueConsumerManagerTask.java | 8 +- .../queue/ruleengine/TbQueueConsumerTask.java | 7 +- .../TbRuleEngineConsumerContext.java | 25 - .../TbRuleEngineQueueConsumerManager.java | 290 ++---------- .../TbRuleEngineQueueConsumerManagerTest.java | 60 ++- .../server/common/data/queue/Queue.java | 2 +- .../server/common/data/queue/QueueConfig.java | 24 + .../provider/KafkaMonolithQueueFactory.java | 4 +- .../provider/KafkaTbCoreQueueFactory.java | 7 +- .../common/util/ThingsBoardThreadFactory.java | 5 + 15 files changed, 809 insertions(+), 652 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java create mode 100644 application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 836c35aa85..c0ccaea9d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -18,12 +18,12 @@ package org.thingsboard.server.service.queue; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; +import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; @@ -75,9 +76,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceAct import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; -import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; @@ -86,6 +87,8 @@ import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; +import org.thingsboard.server.service.queue.consumer.BasicQueueConsumerManager; +import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; @@ -108,7 +111,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -121,7 +123,7 @@ import java.util.stream.Collectors; public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbCoreConsumerService { @Value("${queue.core.poll-interval}") - private long pollDuration; + private long pollInterval; @Value("${queue.core.pack-processing-timeout}") private long packProcessingTimeout; @Value("${queue.core.stats.enabled:false}") @@ -132,7 +134,6 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> mainConsumer; private final DeviceStateService stateService; private final TbApiUsageStateService statsService; private final TbLocalSubscriptionService localSubscriptionService; @@ -143,14 +144,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer; - private final TbQueueConsumer> firmwareStatesConsumer; + private final TbCoreQueueFactory queueFactory; private final TbImageService imageService; + private final TbCoreConsumerStats stats; + + private QueueConsumerManager, CoreQueueConfig> mainConsumer; + private BasicQueueConsumerManager> usageStatsConsumer; + private BasicQueueConsumerManager> firmwareStatesConsumer; - protected volatile ExecutorService consumersExecutor; - protected volatile ExecutorService usageStatsExecutor; - private volatile ExecutorService firmwareStatesExecutor; private volatile ListeningExecutorService deviceActivityEventsExecutor; public DefaultTbCoreConsumerService(TbCoreQueueFactory tbCoreQueueFactory, @@ -175,10 +176,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder() + .queueKey(new QueueKey(ServiceType.TB_CORE)) + .config(CoreQueueConfig.of(true, (int) pollInterval)) + .msgPackProcessor(this::processMsgs) + .consumerCreator(config -> queueFactory.createToCoreMsgConsumer()) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); + this.usageStatsConsumer = BasicQueueConsumerManager.>builder() + .key("usage-stats") + .name("TB Usage Stats") + .pollInterval(pollInterval) + .msgPackProcessor(this::processUsageStatsMsg) + .consumerCreator(queueFactory::createToUsageStatsServiceMsgConsumer) + .consumerExecutor(consumersExecutor) + .build(); + this.firmwareStatesConsumer = BasicQueueConsumerManager.>builder() + .key("firmware") + .name("TB Ota Package States") + .pollInterval(pollInterval) + .msgPackProcessor(this::processFirmwareMsgs) + .consumerCreator(queueFactory::createToOtaPackageStateServiceMsgConsumer) + .consumerExecutor(consumersExecutor) + .build(); } @PreDestroy public void destroy() { super.destroy(); - if (consumersExecutor != null) { - consumersExecutor.shutdownNow(); - } - if (usageStatsExecutor != null) { - usageStatsExecutor.shutdownNow(); - } - if (firmwareStatesExecutor != null) { - firmwareStatesExecutor.shutdownNow(); - } if (deviceActivityEventsExecutor != null) { deviceActivityEventsExecutor.shutdownNow(); } } - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void onApplicationEvent(ApplicationReadyEvent event) { - super.onApplicationEvent(event); - launchUsageStatsConsumer(); - launchOtaPackageUpdateNotificationConsumer(); + @Override + protected void startConsumers() { + super.startConsumers(); + firmwareStatesConsumer.subscribe(); + firmwareStatesConsumer.launch(); + usageStatsConsumer.launch(); } @Override protected void onTbApplicationEvent(PartitionChangeEvent event) { log.info("Subscribing to partitions: {}", event.getPartitions()); - this.mainConsumer.subscribe(event.getPartitions()); - this.usageStatsConsumer.subscribe( - event - .getPartitions() - .stream() - .map(tpi -> tpi.newByTopic(usageStatsConsumer.getTopic())) - .collect(Collectors.toSet())); - this.firmwareStatesConsumer.subscribe(); - } - - @Override - protected void launchMainConsumers() { - consumersExecutor.submit(() -> { - while (!stopped) { + mainConsumer.update(event.getPartitions()); + usageStatsConsumer.subscribe(event.getPartitions() + .stream() + .map(tpi -> tpi.newByTopic(usageStatsConsumer.getConsumer().getTopic())) + .collect(Collectors.toSet())); + } + + private void processMsgs(List> msgs, TbQueueConsumer> consumer, CoreQueueConfig config) throws Exception { + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + ConcurrentMap> pendingMap = orderedMsgList.stream().collect( + Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + PendingMsgHolder pendingMsgHolder = new PendingMsgHolder(); + Future packSubmitFuture = consumersExecutor.submit(() -> { + orderedMsgList.forEach((element) -> { + UUID id = element.getUuid(); + TbProtoQueueMsg msg = element.getMsg(); + log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); + TbCallback callback = new TbPackCallback<>(id, ctx); try { - List> msgs = mainConsumer.poll(pollDuration); - if (msgs.isEmpty()) { - continue; - } - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); - ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); - CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( - processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); - PendingMsgHolder pendingMsgHolder = new PendingMsgHolder(); - Future packSubmitFuture = consumersExecutor.submit(() -> { - orderedMsgList.forEach((element) -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); - log.trace("[{}] Creating main callback for message: {}", id, msg.getValue()); - TbCallback callback = new TbPackCallback<>(id, ctx); - try { - ToCoreMsg toCoreMsg = msg.getValue(); - pendingMsgHolder.setToCoreMsg(toCoreMsg); - if (toCoreMsg.hasToSubscriptionMgrMsg()) { - log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg()); - forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback); - } else if (toCoreMsg.hasToDeviceActorMsg()) { - log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg()); - forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback); - } else if (toCoreMsg.hasDeviceStateServiceMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg()); - forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback); - } else if (toCoreMsg.hasEdgeNotificationMsg()) { - log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg()); - forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback); - } else if (toCoreMsg.hasDeviceConnectMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg()); - forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback); - } else if (toCoreMsg.hasDeviceActivityMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg()); - forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback); - } else if (toCoreMsg.hasDeviceDisconnectMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg()); - forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback); - } else if (toCoreMsg.hasDeviceInactivityMsg()) { - log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg()); - forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback); - } else if (toCoreMsg.hasToDeviceActorNotification()) { - TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification()); - if (actorMsg != null) { - if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) { - tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg); - } else { - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); - actorContext.tell(actorMsg); - } - } - callback.onSuccess(); - } else if (!toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) { - // will be removed in 3.6.1 in favour of hasToDeviceActorNotification() - Optional actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray()); - if (actorMsg.isPresent()) { - TbActorMsg tbActorMsg = actorMsg.get(); - if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) { - tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg); - } else { - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); - actorContext.tell(actorMsg.get()); - } - } - callback.onSuccess(); - } else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) { - TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg(); - log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg()); - forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback); - } else if (toCoreMsg.hasErrorEventMsg()) { - forwardToEventService(toCoreMsg.getErrorEventMsg(), callback); - } else if (toCoreMsg.hasLifecycleEventMsg()) { - forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback); - } - } catch (Throwable e) { - log.warn("[{}] Failed to process message: {}", id, msg, e); - callback.onFailure(e); + ToCoreMsg toCoreMsg = msg.getValue(); + pendingMsgHolder.setToCoreMsg(toCoreMsg); + if (toCoreMsg.hasToSubscriptionMgrMsg()) { + log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg()); + forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback); + } else if (toCoreMsg.hasToDeviceActorMsg()) { + log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg()); + forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback); + } else if (toCoreMsg.hasDeviceStateServiceMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceStateServiceMsg()); + forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback); + } else if (toCoreMsg.hasEdgeNotificationMsg()) { + log.trace("[{}] Forwarding message to edge service {}", id, toCoreMsg.getEdgeNotificationMsg()); + forwardToEdgeNotificationService(toCoreMsg.getEdgeNotificationMsg(), callback); + } else if (toCoreMsg.hasDeviceConnectMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceConnectMsg()); + forwardToStateService(toCoreMsg.getDeviceConnectMsg(), callback); + } else if (toCoreMsg.hasDeviceActivityMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceActivityMsg()); + forwardToStateService(toCoreMsg.getDeviceActivityMsg(), callback); + } else if (toCoreMsg.hasDeviceDisconnectMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceDisconnectMsg()); + forwardToStateService(toCoreMsg.getDeviceDisconnectMsg(), callback); + } else if (toCoreMsg.hasDeviceInactivityMsg()) { + log.trace("[{}] Forwarding message to device state service {}", id, toCoreMsg.getDeviceInactivityMsg()); + forwardToStateService(toCoreMsg.getDeviceInactivityMsg(), callback); + } else if (toCoreMsg.hasToDeviceActorNotification()) { + TbActorMsg actorMsg = ProtoUtils.fromProto(toCoreMsg.getToDeviceActorNotification()); + if (actorMsg != null) { + if (actorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) { + tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) actorMsg); + } else { + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg); + actorContext.tell(actorMsg); } - }); - }); - if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { - if (!packSubmitFuture.isDone()) { - packSubmitFuture.cancel(true); - ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg(); - log.info("Timeout to process message: {}", lastSubmitMsg); } - ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); - ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); - } - mainConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain messages from queue.", e); - try { - Thread.sleep(pollDuration); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new requests", e2); + callback.onSuccess(); + } else if (!toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) { + // will be removed in 3.6.1 in favour of hasToDeviceActorNotification() + Optional actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray()); + if (actorMsg.isPresent()) { + TbActorMsg tbActorMsg = actorMsg.get(); + if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) { + tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg); + } else { + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get()); + actorContext.tell(actorMsg.get()); + } } + callback.onSuccess(); + } else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) { + TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg(); + log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg()); + forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback); + } else if (toCoreMsg.hasErrorEventMsg()) { + forwardToEventService(toCoreMsg.getErrorEventMsg(), callback); + } else if (toCoreMsg.hasLifecycleEventMsg()) { + forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback); } + } catch (Throwable e) { + log.warn("[{}] Failed to process message: {}", id, msg, e); + callback.onFailure(e); } - } - log.info("TB Core Consumer stopped."); + }); }); + if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { + if (!packSubmitFuture.isDone()) { + packSubmitFuture.cancel(true); + ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg(); + log.info("Timeout to process message: {}", lastSubmitMsg); + } + ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); + } + consumer.commit(); } private static class PendingMsgHolder { @@ -365,7 +355,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> createNotificationsConsumer() { + return queueFactory.createToCoreNotificationsMsgConsumer(); + } + @Override protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToCoreNotificationMsg toCoreNotification = msg.getValue(); @@ -430,92 +430,53 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService { - while (!stopped) { - try { - List> msgs = usageStatsConsumer.poll(getNotificationPollDuration()); - if (msgs.isEmpty()) { - continue; - } - ConcurrentMap> pendingMap = msgs.stream().collect( - Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); - CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( - processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); - pendingMap.forEach((id, msg) -> { - log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue()); - TbCallback callback = new TbPackCallback<>(id, ctx); - try { - handleUsageStats(msg, callback); - } catch (Throwable e) { - log.warn("[{}] Failed to process usage stats: {}", id, msg, e); - callback.onFailure(e); - } - }); - if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { - ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue())); - ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue())); - } - usageStatsConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain usage stats from queue.", e); - try { - Thread.sleep(getNotificationPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new usage stats", e2); - } - } - } + private void processUsageStatsMsg(List> msgs, TbQueueConsumer> consumer) throws Exception { + ConcurrentMap> pendingMap = msgs.stream().collect( + Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + pendingMap.forEach((id, msg) -> { + log.trace("[{}] Creating usage stats callback for message: {}", id, msg.getValue()); + TbCallback callback = new TbPackCallback<>(id, ctx); + try { + handleUsageStats(msg, callback); + } catch (Throwable e) { + log.warn("[{}] Failed to process usage stats: {}", id, msg, e); + callback.onFailure(e); } - log.info("TB Usage Stats Consumer stopped."); }); + if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { + ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process usage stats: {}", id, msg.getValue())); + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process usage stats: {}", id, msg.getValue())); + } + consumer.commit(); + } - private void launchOtaPackageUpdateNotificationConsumer() { + private void processFirmwareMsgs(List> msgs, TbQueueConsumer> consumer) { long maxProcessingTimeoutPerRecord = firmwarePackInterval / firmwarePackSize; - firmwareStatesExecutor.submit(() -> { - while (!stopped) { - try { - List> msgs = firmwareStatesConsumer.poll(getNotificationPollDuration()); - if (msgs.isEmpty()) { - continue; - } - long timeToSleep = maxProcessingTimeoutPerRecord; - for (TbProtoQueueMsg msg : msgs) { - try { - long startTime = System.currentTimeMillis(); - boolean isSuccessUpdate = handleOtaPackageUpdates(msg); - long endTime = System.currentTimeMillis(); - long spentTime = endTime - startTime; - timeToSleep = timeToSleep - spentTime; - if (isSuccessUpdate) { - if (timeToSleep > 0) { - log.debug("Spent time per record is: [{}]!", spentTime); - Thread.sleep(timeToSleep); - timeToSleep = 0; - } - timeToSleep += maxProcessingTimeoutPerRecord; - } - } catch (Throwable e) { - log.warn("Failed to process firmware update msg: {}", msg, e); - } - } - firmwareStatesConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain usage stats from queue.", e); - try { - Thread.sleep(getNotificationPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new firmware updates", e2); - } + long timeToSleep = maxProcessingTimeoutPerRecord; + for (TbProtoQueueMsg msg : msgs) { + try { + long startTime = System.currentTimeMillis(); + boolean isSuccessUpdate = handleOtaPackageUpdates(msg); + long endTime = System.currentTimeMillis(); + long spentTime = endTime - startTime; + timeToSleep = timeToSleep - spentTime; + if (isSuccessUpdate) { + if (timeToSleep > 0) { + log.debug("Spent time per record is: [{}]!", spentTime); + Thread.sleep(timeToSleep); + timeToSleep = 0; } + timeToSleep += maxProcessingTimeoutPerRecord; } + } catch (Throwable e) { + log.warn("Failed to process firmware update msg: {}", msg, e); } - log.info("TB Ota Package States Consumer stopped."); - }); + } + consumer.commit(); } private void handleUsageStats(TbProtoQueueMsg msg, TbCallback callback) { @@ -803,15 +764,16 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService consumers = new ConcurrentHashMap<>(); public DefaultTbRuleEngineConsumerService(TbRuleEngineConsumerContext ctx, - TbRuleEngineQueueFactory tbRuleEngineQueueFactory, ActorSystemContext actorContext, DataDecodingEncodingService encodingService, TbRuleEngineDeviceRpcService tbDeviceRpcService, @@ -89,8 +86,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< PartitionService partitionService, ApplicationEventPublisher eventPublisher, JwtSettingsService jwtSettingsService) { - super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, - eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), jwtSettingsService); + super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.ctx = ctx; this.tbDeviceRpcService = tbDeviceRpcService; this.queueService = queueService; @@ -98,7 +94,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< @PostConstruct public void init() { - super.init("tb-rule-engine-notifications-consumer"); + super.init("tb-rule-engine"); List queues = queueService.findAllQueues(); for (Queue configuration : queues) { if (partitionService.isManagedByCurrentService(configuration.getTenantId())) { @@ -130,20 +126,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< }); } - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) - public void onApplicationEvent(ApplicationReadyEvent event) { - super.onApplicationEvent(event); - ctx.setReady(true); - } - - @Override - protected void launchMainConsumers() {} - @Override protected void stopConsumers() { + super.stopConsumers(); consumers.values().forEach(TbRuleEngineQueueConsumerManager::stop); consumers.values().forEach(TbRuleEngineQueueConsumerManager::awaitStop); - ctx.stop(); } @Override @@ -161,6 +148,16 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< return ctx.getPackProcessingTimeout(); } + @Override + protected int getMgmtThreadPoolSize() { + return ctx.getMgmtThreadPoolSize(); + } + + @Override + protected TbQueueConsumer> createNotificationsConsumer() { + return ctx.getQueueFactory().createToRuleEngineNotificationsMsgConsumer(); + } + @Override protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception { ToRuleEngineNotificationMsg nfMsg = msg.getValue(); @@ -237,7 +234,13 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } private TbRuleEngineQueueConsumerManager createConsumer(QueueKey queueKey, Queue queue) { - var consumer = new TbRuleEngineQueueConsumerManager(ctx, queueKey); + var consumer = TbRuleEngineQueueConsumerManager.create() + .ctx(ctx) + .queueKey(queueKey) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); consumers.put(queueKey, consumer); consumer.init(queue); return consumer; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java new file mode 100644 index 0000000000..1d3773e3a1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue.consumer; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +@Slf4j +public class BasicQueueConsumerManager { + + private final String key; + private final String name; + private final long pollInterval; + private final MsgPackProcessor msgPackProcessor; + private final ExecutorService consumerExecutor; + + @Getter + private final TbQueueConsumer consumer; + private volatile boolean stopped; + + @Builder + public BasicQueueConsumerManager(String key, String name, + long pollInterval, + MsgPackProcessor msgPackProcessor, + Supplier> consumerCreator, + ExecutorService consumerExecutor) { + this.key = key; + this.name = name; + this.pollInterval = pollInterval; + this.msgPackProcessor = msgPackProcessor; + this.consumerExecutor = consumerExecutor; + this.consumer = consumerCreator.get(); + } + + public void subscribe() { + consumer.subscribe(); + } + + public void subscribe(Set partitions) { + consumer.subscribe(partitions); + } + + public void launch() { + log.info("[{}] Launching consumer", name); + consumerExecutor.submit(() -> { + ThingsBoardThreadFactory.addThreadNamePrefix(key); + try { + consumerLoop(consumer); + } catch (Throwable e) { + log.error("Failure in consumer loop", e); + } + }); + } + + private void consumerLoop(TbQueueConsumer consumer) { + while (!stopped && !consumer.isStopped()) { + try { + List msgs = consumer.poll(pollInterval); + if (msgs.isEmpty()) { + continue; + } + msgPackProcessor.process(msgs, consumer); + } catch (Exception e) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", e); + try { + Thread.sleep(pollInterval); + } catch (InterruptedException interruptedException) { + log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); + } + } + } + } + log.info("{} Consumer stopped", name); + } + + public void stop() { + log.debug("[{}] Stopping consumer", name); + stopped = true; + consumer.unsubscribe(); + } + + public interface MsgPackProcessor { + void process(List msgs, TbQueueConsumer consumer) throws Exception; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java new file mode 100644 index 0000000000..04538289ea --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java @@ -0,0 +1,321 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.queue.consumer; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.queue.QueueConfig; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.service.queue.ruleengine.QueueEvent; +import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerManagerTask; +import org.thingsboard.server.service.queue.ruleengine.TbQueueConsumerTask; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Slf4j +public class QueueConsumerManager { + + protected final QueueKey queueKey; + @Getter + protected C config; + protected final MsgPackProcessor msgPackProcessor; + protected final Function> consumerCreator; + protected final ExecutorService consumerExecutor; + protected final ScheduledExecutorService scheduler; + protected final ExecutorService taskExecutor; + + private final java.util.Queue tasks = new ConcurrentLinkedQueue<>(); + private final ReentrantLock lock = new ReentrantLock(); + + @Getter + private volatile Set partitions; + protected volatile ConsumerWrapper consumerWrapper; + protected volatile boolean stopped; + + @Builder + public QueueConsumerManager(QueueKey queueKey, + C config, + MsgPackProcessor msgPackProcessor, + Function> consumerCreator, + ExecutorService consumerExecutor, + ScheduledExecutorService scheduler, + ExecutorService taskExecutor) { + this.queueKey = queueKey; + this.config = config; + this.msgPackProcessor = msgPackProcessor; + this.consumerCreator = consumerCreator; + this.consumerExecutor = consumerExecutor; + this.scheduler = scheduler; + this.taskExecutor = taskExecutor; + if (config != null) { + init(config); + } + } + + public void init(C config) { + this.config = config; + if (config.isConsumerPerPartition()) { + this.consumerWrapper = new ConsumerPerPartitionWrapper(); + } else { + this.consumerWrapper = new SingleConsumerWrapper(); + } + log.debug("[{}] Initialized consumer for queue: {}", queueKey, config); + } + + public void update(C config) { + addTask(TbQueueConsumerManagerTask.configUpdate(config)); + } + + public void update(Set partitions) { + addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); + } + + protected void addTask(TbQueueConsumerManagerTask todo) { + if (stopped) { + return; + } + tasks.add(todo); + log.trace("[{}] Added task: {}", queueKey, todo); + tryProcessTasks(); + } + + private void tryProcessTasks() { + taskExecutor.submit(() -> { + if (lock.tryLock()) { + try { + C newConfig = null; + Set newPartitions = null; + while (!stopped) { + TbQueueConsumerManagerTask task = tasks.poll(); + if (task == null) { + break; + } + log.trace("[{}] Processing task: {}", queueKey, task); + + if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { + newPartitions = task.getPartitions(); + } else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { + newConfig = (C) task.getConfig(); + } else { + processTask(task); + } + } + if (stopped) { + return; + } + if (newConfig != null) { + doUpdate(newConfig); + } + if (newPartitions != null) { + doUpdate(newPartitions); + } + } catch (Exception e) { + log.error("[{}] Failed to process tasks", queueKey, e); + } finally { + lock.unlock(); + } + } else { + log.trace("[{}] Failed to acquire lock", queueKey); + scheduler.schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); + } + }); + } + + protected void processTask(TbQueueConsumerManagerTask task) { + } + + private void doUpdate(C newConfig) { + log.info("[{}] Processing queue update: {}", queueKey, newConfig); + var oldConfig = this.config; + this.config = newConfig; + if (log.isTraceEnabled()) { + log.trace("[{}] Old queue configuration: {}", queueKey, oldConfig); + log.trace("[{}] New queue configuration: {}", queueKey, newConfig); + } + + if (oldConfig == null) { + init(config); + } else if (newConfig.isConsumerPerPartition() != oldConfig.isConsumerPerPartition()) { + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); + + init(config); + if (partitions != null) { + doUpdate(partitions); // even if partitions number was changed, there can be no partition change event + } + } else { + // do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent, + // and changes to pollInterval/packProcessingTimeout/submitStrategy/processingStrategy will be picked up by consumer on the fly, + // and queue topic and name are immutable + } + } + + private void doUpdate(Set partitions) { + this.partitions = partitions; + consumerWrapper.updatePartitions(partitions); + } + + private void launchConsumer(TbQueueConsumerTask consumerTask) { + log.info("[{}] Launching consumer", consumerTask.getKey()); + Future consumerLoop = consumerExecutor.submit(() -> { + ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); + try { + consumerLoop(consumerTask.getConsumer()); + } catch (Throwable e) { + log.error("Failure in consumer loop", e); + } + }); + consumerTask.setTask(consumerLoop); + } + + private void consumerLoop(TbQueueConsumer consumer) { + while (!stopped && !consumer.isStopped()) { + try { + List msgs = consumer.poll(config.getPollInterval()); + if (msgs.isEmpty()) { + continue; + } + processMsgs(msgs, consumer, config); + } catch (Exception e) { + if (!consumer.isStopped()) { + log.warn("Failed to process messages from queue", e); + try { + Thread.sleep(config.getPollInterval()); + } catch (InterruptedException e2) { + log.trace("Failed to wait until the server has capacity to handle new requests", e2); + } + } + } + } + if (consumer.isStopped()) { + consumer.unsubscribe(); + } + log.info("{} Consumer stopped", queueKey); + } + + protected void processMsgs(List msgs, TbQueueConsumer consumer, C config) throws Exception { + msgPackProcessor.process(msgs, consumer, config); + } + + public void stop() { + log.debug("[{}] Stopping consumers", queueKey); + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); + stopped = true; + } + + public void awaitStop() { + consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); + log.debug("[{}] Unsubscribed and stopped consumers", queueKey); + } + + private static String partitionsToString(Collection partitions) { + return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); + } + + public interface MsgPackProcessor { + void process(List msgs, TbQueueConsumer consumer, C config) throws Exception; + } + + public interface ConsumerWrapper { + + void updatePartitions(Set partitions); + + Collection> getConsumers(); + + } + + class ConsumerPerPartitionWrapper implements ConsumerWrapper { + private final Map> consumers = new HashMap<>(); + + @Override + public void updatePartitions(Set partitions) { + Set addedPartitions = new HashSet<>(partitions); + addedPartitions.removeAll(consumers.keySet()); + + Set removedPartitions = new HashSet<>(consumers.keySet()); + removedPartitions.removeAll(partitions); + log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); + + removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); + removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); + + addedPartitions.forEach((tpi) -> { + String key = queueKey + "-" + tpi.getPartition().orElse(-999999); + TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config)); + consumers.put(tpi, consumer); + consumer.subscribe(Set.of(tpi)); + launchConsumer(consumer); + }); + } + + @Override + public Collection> getConsumers() { + return consumers.values(); + } + } + + class SingleConsumerWrapper implements ConsumerWrapper { + private TbQueueConsumerTask consumer; + + @Override + public void updatePartitions(Set partitions) { + log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); + if (partitions.isEmpty()) { + if (consumer != null && consumer.isRunning()) { + consumer.initiateStop(); + consumer.awaitCompletion(); + } + consumer = null; + return; + } + + if (consumer == null) { + consumer = new TbQueueConsumerTask<>(queueKey, consumerCreator.apply(config)); + } + consumer.subscribe(partitions); + if (!consumer.isRunning()) { + launchConsumer(consumer); + } + } + + @Override + public Collection> getConsumers() { + if (consumer == null) { + return Collections.emptyList(); + } + return List.of(consumer); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 26e23549e3..e2a6a3f117 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue.processing; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationEventPublisher; +import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.EntityType; @@ -45,6 +46,7 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbPackCallback; import org.thingsboard.server.service.queue.TbPackProcessingContext; +import org.thingsboard.server.service.queue.consumer.BasicQueueConsumerManager; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import javax.annotation.PreDestroy; @@ -55,15 +57,13 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j public abstract class AbstractConsumerService extends TbApplicationEventListener { - protected volatile ExecutorService notificationsConsumerExecutor; - protected volatile boolean stopped = false; - protected volatile boolean isReady = false; protected final ActorSystemContext actorContext; protected final DataDecodingEncodingService encodingService; protected final TbTenantProfileCache tenantProfileCache; @@ -72,16 +72,19 @@ public abstract class AbstractConsumerService> nfConsumer; protected final JwtSettingsService jwtSettingsService; + protected BasicQueueConsumerManager> nfConsumer; + + protected ExecutorService consumersExecutor; + protected ExecutorService mgmtExecutor; + protected ScheduledExecutorService scheduler; public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache, TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService, PartitionService partitionService, ApplicationEventPublisher eventPublisher, - TbQueueConsumer> nfConsumer, JwtSettingsService jwtSettingsService) { + JwtSettingsService jwtSettingsService) { this.actorContext = actorContext; this.encodingService = encodingService; this.tenantProfileCache = tenantProfileCache; @@ -90,21 +93,32 @@ public abstract class AbstractConsumerService>builder() + .key("notifications") + .name("TB Notifications") + .pollInterval(getNotificationPollDuration()) + .msgPackProcessor(this::processNotifications) + .consumerCreator(this::createNotificationsConsumer) + .consumerExecutor(consumersExecutor) + .build(); } @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void onApplicationEvent(ApplicationReadyEvent event) { - log.info("Subscribing to notifications: {}", nfConsumer.getTopic()); - this.nfConsumer.subscribe(); - this.isReady = true; - launchNotificationsConsumer(); - launchMainConsumers(); + startConsumers(); + } + + protected void startConsumers() { + nfConsumer.subscribe(); + nfConsumer.launch(); } @Override @@ -114,58 +128,42 @@ public abstract class AbstractConsumerService { - while (!stopped) { - try { - List> msgs = nfConsumer.poll(getNotificationPollDuration()); - if (msgs.isEmpty()) { - continue; - } - List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); - ConcurrentMap> pendingMap = orderedMsgList.stream().collect( - Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); - CountDownLatch processingTimeoutLatch = new CountDownLatch(1); - TbPackProcessingContext> ctx = new TbPackProcessingContext<>( - processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); - orderedMsgList.forEach(element -> { - UUID id = element.getUuid(); - TbProtoQueueMsg msg = element.getMsg(); - log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); - TbCallback callback = new TbPackCallback<>(id, ctx); - try { - handleNotification(id, msg, callback); - } catch (Throwable e) { - log.warn("[{}] Failed to process notification: {}", id, msg, e); - callback.onFailure(e); - } - }); - if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { - ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue())); - ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue())); - } - nfConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain notifications from queue.", e); - try { - Thread.sleep(getNotificationPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new notifications", e2); - } - } - } + protected abstract int getMgmtThreadPoolSize(); + + protected abstract TbQueueConsumer> createNotificationsConsumer(); + + protected void processNotifications(List> msgs, TbQueueConsumer> consumer) throws Exception { + List> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList()); + ConcurrentMap> pendingMap = orderedMsgList.stream().collect( + Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg)); + CountDownLatch processingTimeoutLatch = new CountDownLatch(1); + TbPackProcessingContext> ctx = new TbPackProcessingContext<>( + processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); + orderedMsgList.forEach(element -> { + UUID id = element.getUuid(); + TbProtoQueueMsg msg = element.getMsg(); + log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); + TbCallback callback = new TbPackCallback<>(id, ctx); + try { + handleNotification(id, msg, callback); + } catch (Throwable e) { + log.warn("[{}] Failed to process notification: {}", id, msg, e); + callback.onFailure(e); } - log.info("TB Notifications Consumer stopped."); }); + if (!processingTimeoutLatch.await(getNotificationPackProcessingTimeout(), TimeUnit.MILLISECONDS)) { + ctx.getAckMap().forEach((id, msg) -> log.warn("[{}] Timeout to process notification: {}", id, msg.getValue())); + ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process notification: {}", id, msg.getValue())); + } + consumer.commit(); } protected final void handleComponentLifecycleMsg(UUID id, ComponentLifecycleMsg componentLifecycleMsg) { @@ -219,13 +217,15 @@ public abstract class AbstractConsumerService partitions; private boolean drainQueue; @@ -37,8 +37,8 @@ public class TbQueueConsumerManagerTask { return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue); } - public static TbQueueConsumerManagerTask configUpdate(Queue queue) { - return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, queue, null, false); + public static TbQueueConsumerManagerTask configUpdate(QueueConfig config) { + return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false); } public static TbQueueConsumerManagerTask partitionChange(Set partitions) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java index 0e25efd014..84539d4d7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbQueueConsumerTask.java @@ -20,9 +20,8 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.TbQueueMsg; import java.util.Set; import java.util.concurrent.Future; @@ -30,12 +29,12 @@ import java.util.concurrent.TimeUnit; @RequiredArgsConstructor @Slf4j -public class TbQueueConsumerTask { +public class TbQueueConsumerTask { @Getter private final Object key; @Getter - private final TbQueueConsumer> consumer; + private final TbQueueConsumer consumer; @Setter private Future task; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java index cf876c6947..08d2931246 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineConsumerContext.java @@ -19,8 +19,6 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.queue.TbQueueAdmin; @@ -33,11 +31,6 @@ import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStr import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; import org.thingsboard.server.service.stats.RuleEngineStatisticsService; -import javax.annotation.PostConstruct; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - @Component @TbRuleEngineComponent @Slf4j @@ -68,22 +61,4 @@ public class TbRuleEngineConsumerContext { private final TbQueueProducerProvider producerProvider; private final TbQueueAdmin queueAdmin; - private ExecutorService consumersExecutor; - private ExecutorService mgmtExecutor; - private ScheduledExecutorService scheduler; - - private volatile boolean isReady = false; - - @PostConstruct - void init() { - this.consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer")); - this.mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(mgmtThreadPoolSize, "tb-rule-engine-mgmt"); - this.scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler")); - } - - public void stop() { - scheduler.shutdownNow(); - consumersExecutor.shutdownNow(); - mgmtExecutor.shutdownNow(); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index 2da3dbc6dc..b93313b104 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -16,9 +16,8 @@ package org.thingsboard.server.service.queue.ruleengine; import com.google.protobuf.ProtocolStringList; -import lombok.Getter; +import lombok.Builder; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -38,173 +37,54 @@ import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; +import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Future; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @Slf4j -public class TbRuleEngineQueueConsumerManager { +public class TbRuleEngineQueueConsumerManager extends QueueConsumerManager, Queue> { public static final String SUCCESSFUL_STATUS = "successful"; public static final String FAILED_STATUS = "failed"; private final TbRuleEngineConsumerContext ctx; - private final QueueKey queueKey; private final TbRuleEngineConsumerStats stats; - private final ReentrantLock lock = new ReentrantLock(); //NonfairSync - @Getter - private volatile Queue queue; - @Getter - private volatile Set partitions; - private volatile ConsumerWrapper consumerWrapper; - - private volatile boolean stopped; - - private final java.util.Queue tasks = new ConcurrentLinkedQueue<>(); - - public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx, QueueKey queueKey) { + @Builder(builderMethodName = "create") // not to conflict with super.builder() + public TbRuleEngineQueueConsumerManager(TbRuleEngineConsumerContext ctx, + QueueKey queueKey, + ExecutorService consumerExecutor, + ScheduledExecutorService scheduler, + ExecutorService taskExecutor) { + super(queueKey, null, null, ctx.getQueueFactory()::createToRuleEngineMsgConsumer, consumerExecutor, scheduler, taskExecutor); this.ctx = ctx; - this.queueKey = queueKey; this.stats = new TbRuleEngineConsumerStats(queueKey, ctx.getStatsFactory()); } - public void init(Queue queue) { - this.queue = queue; - if (queue.isConsumerPerPartition()) { - this.consumerWrapper = new ConsumerPerPartitionWrapper(); - } else { - this.consumerWrapper = new SingleConsumerWrapper(); - } - log.debug("[{}] Initialized consumer for queue: {}", queueKey, queue); - } - - public void update(Queue queue) { - addTask(TbQueueConsumerManagerTask.configUpdate(queue)); - } - - public void update(Set partitions) { - addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); - } - public void delete(boolean drainQueue) { addTask(TbQueueConsumerManagerTask.delete(drainQueue)); } - private void addTask(TbQueueConsumerManagerTask todo) { - if (stopped) { - return; - } - tasks.add(todo); - log.trace("[{}] Added task: {}", queueKey, todo); - tryProcessTasks(); - } - - private void tryProcessTasks() { - if (!ctx.isReady()) { - log.debug("[{}] TbRuleEngineConsumerContext is not ready yet, will process tasks later", queueKey); - ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); - return; - } - ctx.getMgmtExecutor().submit(() -> { - if (lock.tryLock()) { - try { - Queue newConfiguration = null; - Set newPartitions = null; - while (!stopped) { - TbQueueConsumerManagerTask task = tasks.poll(); - if (task == null) { - break; - } - log.trace("[{}] Processing task: {}", queueKey, task); - - if (task.getEvent() == QueueEvent.PARTITION_CHANGE) { - newPartitions = task.getPartitions(); - } else if (task.getEvent() == QueueEvent.CONFIG_UPDATE) { - newConfiguration = task.getQueue(); - } else if (task.getEvent() == QueueEvent.DELETE) { - doDelete(task.isDrainQueue()); - return; - } - } - if (stopped) { - return; - } - if (newConfiguration != null) { - doUpdate(newConfiguration); - } - if (newPartitions != null) { - doUpdate(newPartitions); - } - } catch (Exception e) { - log.error("[{}] Failed to process tasks", queueKey, e); - } finally { - lock.unlock(); - } - } else { - log.trace("[{}] Failed to acquire lock", queueKey); - ctx.getScheduler().schedule(this::tryProcessTasks, 1, TimeUnit.SECONDS); - } - }); - } - - private void doUpdate(Queue newQueue) { - log.info("[{}] Processing queue update: {}", queueKey, newQueue); - var oldQueue = this.queue; - this.queue = newQueue; - if (log.isTraceEnabled()) { - log.trace("[{}] Old queue configuration: {}", queueKey, oldQueue); - log.trace("[{}] New queue configuration: {}", queueKey, newQueue); - } - - if (oldQueue == null) { - init(queue); - } else if (newQueue.isConsumerPerPartition() != oldQueue.isConsumerPerPartition()) { - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); - - init(queue); - if (partitions != null) { - doUpdate(partitions); // even if partitions number was changed, there can be no partition change event - } - } else { - // do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent, - // and changes to pollInterval/packProcessingTimeout/submitStrategy/processingStrategy will be picked up by consumer on the fly, - // and queue topic and name are immutable + @Override + protected void processTask(TbQueueConsumerManagerTask task) { + if (task.getEvent() == QueueEvent.DELETE) { + doDelete(task.isDrainQueue()); } } - private void doUpdate(Set partitions) { - this.partitions = partitions; - consumerWrapper.updatePartitions(partitions); - } - - public void stop() { - log.debug("[{}] Stopping consumers", queueKey); - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::initiateStop); - stopped = true; - } - - public void awaitStop() { - consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); - log.debug("[{}] Unsubscribed and stopped consumers", queueKey); - } - private void doDelete(boolean drainQueue) { stopped = true; log.info("[{}] Handling queue deletion", queueKey); @@ -212,7 +92,7 @@ public class TbRuleEngineQueueConsumerManager { List>> queueConsumers = consumerWrapper.getConsumers().stream() .map(TbQueueConsumerTask::getConsumer).collect(Collectors.toList()); - ctx.getConsumersExecutor().submit(() -> { + consumerExecutor.submit(() -> { if (drainQueue) { drainQueue(queueConsumers); } @@ -235,47 +115,10 @@ public class TbRuleEngineQueueConsumerManager { }); } - private void launchConsumer(TbQueueConsumerTask consumerTask) { - log.info("[{}] Launching consumer", consumerTask.getKey()); - Future consumerLoop = ctx.getConsumersExecutor().submit(() -> { - ThingsBoardThreadFactory.updateCurrentThreadName(consumerTask.getKey().toString()); - try { - consumerLoop(consumerTask.getConsumer()); - } catch (Throwable e) { - log.error("Failure in consumer loop", e); - } - }); - consumerTask.setTask(consumerLoop); - } - - private void consumerLoop(TbQueueConsumer> consumer) { - while (!stopped && !consumer.isStopped()) { - try { - List> msgs = consumer.poll(queue.getPollInterval()); - if (msgs.isEmpty()) { - continue; - } - processMsgs(msgs, consumer, queue); - } catch (Exception e) { - if (!consumer.isStopped()) { - log.warn("Failed to process messages from queue", e); - try { - Thread.sleep(ctx.getPollDuration()); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new requests", e2); - } - } - } - } - if (consumer.isStopped()) { - consumer.unsubscribe(); - } - log.info("Rule Engine consumer stopped"); - } - - private void processMsgs(List> msgs, - TbQueueConsumer> consumer, - Queue queue) throws InterruptedException { + @Override + protected void processMsgs(List> msgs, + TbQueueConsumer> consumer, + Queue queue) throws Exception { TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue); TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue); submitStrategy.init(msgs); @@ -320,7 +163,7 @@ public class TbRuleEngineQueueConsumerManager { } private void submitMessage(TbMsgPackProcessingContext packCtx, UUID id, TbProtoQueueMsg msg) { - log.trace("[{}] Creating callback for topic {} message: {}", id, queue.getName(), msg.getValue()); + log.trace("[{}] Creating callback for topic {} message: {}", id, config.getName(), msg.getValue()); ToRuleEngineMsg toRuleEngineMsg = msg.getValue(); TenantId tenantId = TenantId.fromUUID(new UUID(toRuleEngineMsg.getTenantIdMSB(), toRuleEngineMsg.getTenantIdLSB())); TbMsgCallback callback = ctx.isPrometheusStatsEnabled() ? @@ -328,7 +171,7 @@ public class TbRuleEngineQueueConsumerManager { new TbMsgPackCallback(id, tenantId, packCtx); try { if (!toRuleEngineMsg.getTbMsg().isEmpty()) { - forwardToRuleEngineActor(queue.getName(), tenantId, toRuleEngineMsg, callback); + forwardToRuleEngineActor(config.getName(), tenantId, toRuleEngineMsg, callback); } else { callback.onSuccess(); } @@ -356,7 +199,7 @@ public class TbRuleEngineQueueConsumerManager { log.info("[{}] {} to process [{}] messages", queueKey, prefix, map.size()); for (Map.Entry> pending : map.entrySet()) { ToRuleEngineMsg tmp = pending.getValue().getValue(); - TbMsg tmpMsg = TbMsg.fromBytes(queue.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); + TbMsg tmpMsg = TbMsg.fromBytes(config.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey()); if (printAll) { log.trace("[{}][{}] {} to process message: {}, Last Rule Node: {}", queueKey, TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); @@ -379,7 +222,7 @@ public class TbRuleEngineQueueConsumerManager { int n = 0; while (System.currentTimeMillis() <= finishTs) { for (TbQueueConsumer> consumer : consumers) { - List> msgs = consumer.poll(queue.getPollInterval()); + List> msgs = consumer.poll(config.getPollInterval()); if (msgs.isEmpty()) { continue; } @@ -388,7 +231,7 @@ public class TbRuleEngineQueueConsumerManager { MsgProtos.TbMsgProto tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg().toByteArray()); EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB())); - TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, queue.getName(), TenantId.SYS_TENANT_ID, originator); + TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, config.getName(), TenantId.SYS_TENANT_ID, originator); ctx.getProducerProvider().getRuleEngineMsgProducer().send(tpi, msg, null); n++; } catch (Throwable e) { @@ -399,90 +242,11 @@ public class TbRuleEngineQueueConsumerManager { } } if (n > 0) { - log.info("Moved {} messages from {} to system {}", n, queueKey, queue.getName()); + log.info("Moved {} messages from {} to system {}", n, queueKey, config.getName()); } } catch (Exception e) { log.error("[{}] Failed to drain queue", queueKey, e); } } - private static String partitionsToString(Collection partitions) { - return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.joining(", ", "[", "]")); - } - - interface ConsumerWrapper { - - void updatePartitions(Set partitions); - - Collection getConsumers(); - - } - - class ConsumerPerPartitionWrapper implements ConsumerWrapper { - private final Map consumers = new HashMap<>(); - - @Override - public void updatePartitions(Set partitions) { - Set addedPartitions = new HashSet<>(partitions); - addedPartitions.removeAll(consumers.keySet()); - - Set removedPartitions = new HashSet<>(consumers.keySet()); - removedPartitions.removeAll(partitions); - log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); - - removedPartitions.forEach((tpi) -> { - consumers.get(tpi).initiateStop(); - }); - removedPartitions.forEach((tpi) -> { - consumers.remove(tpi).awaitCompletion(); - }); - - addedPartitions.forEach((tpi) -> { - String key = queueKey + "-" + tpi.getPartition().orElse(-999999); - TbQueueConsumerTask consumer = new TbQueueConsumerTask(key, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue)); - consumers.put(tpi, consumer); - consumer.subscribe(Set.of(tpi)); - launchConsumer(consumer); - }); - } - - @Override - public Collection getConsumers() { - return consumers.values(); - } - } - - class SingleConsumerWrapper implements ConsumerWrapper { - private TbQueueConsumerTask consumer; - - @Override - public void updatePartitions(Set partitions) { - log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); - if (partitions.isEmpty()) { - if (consumer != null && consumer.isRunning()) { - consumer.initiateStop(); - consumer.awaitCompletion(); - } - consumer = null; - return; - } - - if (consumer == null) { - consumer = new TbQueueConsumerTask(queueKey, ctx.getQueueFactory().createToRuleEngineMsgConsumer(queue)); - } - consumer.subscribe(partitions); - if (!consumer.isRunning()) { - launchConsumer(consumer); - } - } - - @Override - public Collection getConsumers() { - if (consumer == null) { - return Collections.emptyList(); - } - return List.of(consumer); - } - } - } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 6bd0cd9c0e..a00f45b8db 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -27,6 +27,8 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.QueueId; @@ -65,6 +67,9 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; @@ -86,7 +91,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -112,6 +116,9 @@ public class TbRuleEngineQueueConsumerManagerTest { @Mock private TbQueueAdmin queueAdmin; private TbRuleEngineConsumerContext ruleEngineConsumerContext; + private ExecutorService consumersExecutor; + private ScheduledExecutorService scheduler; + private ExecutorService mgmtExecutor; private TbRuleEngineQueueConsumerManager consumerManager; private Queue queue; @@ -141,10 +148,10 @@ public class TbRuleEngineQueueConsumerManagerTest { }).when(actorContext).tell(any()); ruleEngineMsgProducer = mock(TbQueueProducer.class); when(producerProvider.getRuleEngineMsgProducer()).thenReturn(ruleEngineMsgProducer); - ruleEngineConsumerContext.setMgmtThreadPoolSize(2); + consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer")); + mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(3, "tb-rule-engine-mgmt"); + scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-rule-engine-consumer-scheduler")); ruleEngineConsumerContext.setTopicDeletionDelayInSec(5); - ruleEngineConsumerContext.init(); - ruleEngineConsumerContext.setReady(false); queue = new Queue(); queue.setName("Test"); @@ -174,14 +181,23 @@ public class TbRuleEngineQueueConsumerManagerTest { }).when(queueFactory).createToRuleEngineMsgConsumer(any()); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); - consumerManager = new TbRuleEngineQueueConsumerManager(ruleEngineConsumerContext, queueKey); + consumerManager = TbRuleEngineQueueConsumerManager.create() + .ctx(ruleEngineConsumerContext) + .queueKey(queueKey) + .consumerExecutor(consumersExecutor) + .scheduler(scheduler) + .taskExecutor(mgmtExecutor) + .build(); } @After public void afterEach() { consumerManager.stop(); consumerManager.awaitStop(); - ruleEngineConsumerContext.stop(); + + consumersExecutor.shutdownNow(); + scheduler.shutdownNow(); + mgmtExecutor.shutdownNow(); if (generateQueueMsgs) { await().atMost(10, TimeUnit.SECONDS) @@ -199,14 +215,7 @@ public class TbRuleEngineQueueConsumerManagerTest { Set partitions = createTpis(2, 3, 4); consumerManager.update(partitions); - partitions = createTpis(3, 4, 5); - consumerManager.update(partitions); - partitions = createTpis(1, 2, 3); - consumerManager.update(partitions); - // simulated multiple partition change events before consumer is ready; only latest partitions should be processed - verifyNoInteractions(queueFactory); - ruleEngineConsumerContext.setReady(true); await().atMost(2, TimeUnit.SECONDS) .until(() -> consumers.size() == 3); for (TopicPartitionInfo partition : partitions) { @@ -222,14 +231,7 @@ public class TbRuleEngineQueueConsumerManagerTest { Set partitions = createTpis(2, 3, 4); consumerManager.update(partitions); - partitions = createTpis(3, 4, 5); - consumerManager.update(partitions); - partitions = createTpis(1, 2, 3); - consumerManager.update(partitions); - - verifyNoInteractions(queueFactory); - ruleEngineConsumerContext.setReady(true); await().atMost(2, TimeUnit.SECONDS) .until(() -> consumers.size() == 1); TestConsumer consumer = getConsumer(); @@ -240,7 +242,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testPartitionsUpdate_singleConsumer() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = Collections.emptySet(); consumerManager.update(partitions); @@ -273,7 +274,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testPartitionsUpdate_consumerPerPartition() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); consumerManager.update(Collections.emptySet()); verify(queueFactory, after(1000).never()).createToRuleEngineMsgConsumer(any()); @@ -316,7 +316,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_singleConsumer() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer = getConsumer(); @@ -342,7 +341,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_consumerPerPartition() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer1 = getConsumer(1); @@ -375,7 +373,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_fromSingleToConsumerPerPartition() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer = getConsumer(); @@ -395,7 +392,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testConfigUpdate_fromConsumerPerPartitionToSingle() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2, 3); consumerManager.update(partitions); TestConsumer consumer1 = getConsumer(1); @@ -419,7 +415,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testStop() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); consumerManager.update(createTpis(1)); TestConsumer consumer = getConsumer(1); verifySubscribedAndLaunched(consumer, 1); @@ -437,7 +432,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testDelete_consumerPerPartition() { queue.setConsumerPerPartition(true); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2); consumerManager.update(partitions); TestConsumer consumer1 = getConsumer(1); @@ -459,7 +453,7 @@ public class TbRuleEngineQueueConsumerManagerTest { int msgCount = totalConsumedMsgs.get(); await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) - .atMost(7, TimeUnit.SECONDS) + .atMost(10, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() .map(TopicPartitionInfo::getFullTopicName) @@ -481,7 +475,6 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testDelete_singleConsumer() { queue.setConsumerPerPartition(false); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Set partitions = createTpis(1, 2); consumerManager.update(partitions); TestConsumer consumer = getConsumer(); @@ -499,7 +492,7 @@ public class TbRuleEngineQueueConsumerManagerTest { int msgCount = totalConsumedMsgs.get(); await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) - .atMost(7, TimeUnit.SECONDS) + .atMost(10, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() .map(TopicPartitionInfo::getFullTopicName) @@ -520,10 +513,9 @@ public class TbRuleEngineQueueConsumerManagerTest { public void testManyDifferentUpdates() throws Exception { queue.setConsumerPerPartition(RandomUtils.nextBoolean()); consumerManager.init(queue); - ruleEngineConsumerContext.setReady(true); Supplier queueConfigUpdater = () -> { - Queue oldConfig = consumerManager.getQueue(); + Queue oldConfig = consumerManager.getConfig(); Queue newConfig = JacksonUtil.clone(oldConfig); newConfig.setConsumerPerPartition(RandomUtils.nextBoolean()); newConfig.setPollInterval(RandomUtils.nextInt(100, 501)); @@ -571,7 +563,7 @@ public class TbRuleEngineQueueConsumerManagerTest { Set expectedPartitions = latestPartitions; await().atMost(5, TimeUnit.SECONDS) .untilAsserted(() -> { - assertThat(consumerManager.getQueue()).isEqualTo(expectedConfig); + assertThat(consumerManager.getConfig()).isEqualTo(expectedConfig); assertThat(consumerManager.getPartitions()).isEqualTo(expectedPartitions); }); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java index e3c6836aab..1c08072e61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.util.Optional; @Data -public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId { +public class Queue extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, QueueConfig { private TenantId tenantId; @NoXss @Length(fieldName = "name") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java new file mode 100644 index 0000000000..383c613a38 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/QueueConfig.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.queue; + +public interface QueueConfig { + + boolean isConsumerPerPartition(); + + int getPollInterval(); + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 3459096e08..88470296ed 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -39,8 +39,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -212,7 +212,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic())); - consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.clientId("monolith-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("monolith-core-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 1852693304..21078d7f94 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -38,8 +38,8 @@ import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -55,6 +55,7 @@ import org.thingsboard.server.queue.settings.TbQueueVersionControlSettings; import javax.annotation.PreDestroy; import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicLong; @Component @ConditionalOnExpression("'${queue.type:null}'=='kafka' && '${service.type:null}'=='tb-core'") @@ -81,6 +82,8 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin fwUpdatesAdmin; private final TbQueueAdmin vcAdmin; + private final AtomicLong consumerCount = new AtomicLong(); + public KafkaTbCoreQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, @@ -169,7 +172,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName(coreSettings.getTopic())); - consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId()); + consumerBuilder.clientId("tb-core-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-node")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java index de097cfd6c..dc263b80e7 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java @@ -51,6 +51,11 @@ public class ThingsBoardThreadFactory implements ThreadFactory { Thread.currentThread().setName(name); } + public static void addThreadNamePrefix(String prefix) { + String name = Thread.currentThread().getName(); + name = prefix + name; + Thread.currentThread().setName(name); + } @Override public Thread newThread(Runnable r) { From 134db4249472f094523e35d2bb3efeb6f64be26f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 19 Mar 2024 13:49:46 +0200 Subject: [PATCH 003/121] Configurable consumer per partition for tb-core queue --- .../server/service/queue/DefaultTbCoreConsumerService.java | 5 ++++- .../service/queue/processing/AbstractConsumerService.java | 3 +-- application/src/main/resources/thingsboard.yml | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index c0ccaea9d0..474efc5ed7 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -126,6 +126,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder() .queueKey(new QueueKey(ServiceType.TB_CORE)) - .config(CoreQueueConfig.of(true, (int) pollInterval)) + .config(CoreQueueConfig.of(consumerPerPartitionEnabled, (int) pollInterval)) .msgPackProcessor(this::processMsgs) .consumerCreator(config -> queueFactory.createToCoreMsgConsumer()) .consumerExecutor(consumersExecutor) @@ -764,6 +766,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService Date: Wed, 20 Mar 2024 12:49:40 +0200 Subject: [PATCH 004/121] Minor refactoring for consumer managers --- .../service/queue/DefaultTbCoreConsumerService.java | 12 ++++++++---- .../queue/DefaultTbRuleEngineConsumerService.java | 5 +---- .../queue/consumer/BasicQueueConsumerManager.java | 2 +- .../service/queue/consumer/QueueConsumerManager.java | 8 +++++--- .../queue/processing/AbstractConsumerService.java | 2 +- application/src/main/resources/thingsboard.yml | 2 +- .../TbRuleEngineQueueConsumerManagerTest.java | 4 ++-- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index a2bed09151..e1bd18c7bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -127,8 +127,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder() .queueKey(new QueueKey(ServiceType.TB_CORE)) - .config(CoreQueueConfig.of(consumerPerPartitionEnabled, (int) pollInterval)) + .config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval)) .msgPackProcessor(this::processMsgs) .consumerCreator(config -> queueFactory.createToCoreMsgConsumer()) .consumerExecutor(consumersExecutor) @@ -325,7 +325,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); + if (log.isDebugEnabled()) { + ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); + } ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); } consumer.commit(); @@ -452,6 +454,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService { } catch (Throwable e) { log.error("Failure in consumer loop", e); } + log.info("[{}] Consumer stopped", name); }); } @@ -94,7 +95,6 @@ public class BasicQueueConsumerManager { } } } - log.info("{} Consumer stopped", name); } public void stop() { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java index 04538289ea..acd28f25ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java @@ -176,8 +176,9 @@ public class QueueConsumerManager { doUpdate(partitions); // even if partitions number was changed, there can be no partition change event } } else { + log.trace("[{}] Silently applied new config, because consumer-per-partition not changed", queueKey); // do nothing, because partitions change (if they changed) will be handled on PartitionChangeEvent, - // and changes to pollInterval/packProcessingTimeout/submitStrategy/processingStrategy will be picked up by consumer on the fly, + // and changes to other config values will be picked up by consumer on the fly, // and queue topic and name are immutable } } @@ -196,6 +197,7 @@ public class QueueConsumerManager { } catch (Throwable e) { log.error("Failure in consumer loop", e); } + log.info("[{}] Consumer stopped", consumerTask.getKey()); }); consumerTask.setTask(consumerLoop); } @@ -222,7 +224,6 @@ public class QueueConsumerManager { if (consumer.isStopped()) { consumer.unsubscribe(); } - log.info("{} Consumer stopped", queueKey); } protected void processMsgs(List msgs, TbQueueConsumer consumer, C config) throws Exception { @@ -236,6 +237,7 @@ public class QueueConsumerManager { } public void awaitStop() { + log.debug("[{}] Waiting for consumers to stop", queueKey); consumerWrapper.getConsumers().forEach(TbQueueConsumerTask::awaitCompletion); log.debug("[{}] Unsubscribed and stopped consumers", queueKey); } @@ -272,7 +274,7 @@ public class QueueConsumerManager { removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); addedPartitions.forEach((tpi) -> { - String key = queueKey + "-" + tpi.getPartition().orElse(-999999); + String key = queueKey + "-" + tpi.getPartition().orElse(-1); TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, consumerCreator.apply(config)); consumers.put(tpi, consumer); consumer.subscribe(Set.of(tpi)); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index fa43eaa78d..79942cf6df 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.queue.processing; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -48,7 +49,6 @@ import org.thingsboard.server.service.queue.TbPackProcessingContext; import org.thingsboard.server.service.queue.consumer.BasicQueueConsumerManager; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; -import jakarta.annotation.PreDestroy; import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d38c83a9ef..ca186ed0a5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1596,7 +1596,7 @@ queue: # Timeout for processing a message pack by Core microservices pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}" # Enable/disable a separate consumer per partition for Core queue - consumer-per-partition-enabled: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION_ENABLED:true}" + consumer-per-partition: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION:true}" ota: # Default topic name for OTA updates topic: "${TB_QUEUE_CORE_OTA_TOPIC:tb_ota_package}" diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index a00f45b8db..75df04333d 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -453,7 +453,7 @@ public class TbRuleEngineQueueConsumerManagerTest { int msgCount = totalConsumedMsgs.get(); await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) - .atMost(10, TimeUnit.SECONDS) + .atMost(20, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() .map(TopicPartitionInfo::getFullTopicName) @@ -492,7 +492,7 @@ public class TbRuleEngineQueueConsumerManagerTest { int msgCount = totalConsumedMsgs.get(); await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call) - .atMost(10, TimeUnit.SECONDS) + .atMost(20, TimeUnit.SECONDS) .untilAsserted(() -> { partitions.stream() .map(TopicPartitionInfo::getFullTopicName) From 47cfebe2a7a8646df5ef225a0f2034129031efb6 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 20 Mar 2024 13:25:30 +0200 Subject: [PATCH 005/121] Use QueueConsumerManager for other consumers --- .../queue/DefaultTbCoreConsumerService.java | 20 ++--- ...ger.java => MainQueueConsumerManager.java} | 15 ++-- .../processing/AbstractConsumerService.java | 8 +- .../TbRuleEngineQueueConsumerManager.java | 4 +- .../common/consumer/QueueConsumerManager.java | 14 ++- .../service/DefaultTransportService.java | 71 +++++++-------- .../common/util/ThingsBoardThreadFactory.java | 2 +- .../DefaultClusterVersionControlService.java | 87 ++++++++----------- 8 files changed, 96 insertions(+), 125 deletions(-) rename application/src/main/java/org/thingsboard/server/service/queue/consumer/{QueueConsumerManager.java => MainQueueConsumerManager.java} (95%) rename application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java => common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java (87%) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index e1bd18c7bf..fdbbde3222 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -90,8 +90,8 @@ import org.thingsboard.server.service.notification.NotificationSchedulerService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.service.queue.consumer.BasicQueueConsumerManager; -import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.resource.TbImageService; @@ -151,9 +151,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig> mainConsumer; - private BasicQueueConsumerManager> usageStatsConsumer; - private BasicQueueConsumerManager> firmwareStatesConsumer; + private MainQueueConsumerManager, CoreQueueConfig> mainConsumer; + private QueueConsumerManager> usageStatsConsumer; + private QueueConsumerManager> firmwareStatesConsumer; private volatile ListeningExecutorService deviceActivityEventsExecutor; @@ -199,7 +199,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService, CoreQueueConfig>builder() + this.mainConsumer = MainQueueConsumerManager., CoreQueueConfig>builder() .queueKey(new QueueKey(ServiceType.TB_CORE)) .config(CoreQueueConfig.of(consumerPerPartition, (int) pollInterval)) .msgPackProcessor(this::processMsgs) @@ -208,19 +208,19 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService>builder() + this.usageStatsConsumer = QueueConsumerManager.>builder() .key("usage-stats") .name("TB Usage Stats") - .pollInterval(pollInterval) .msgPackProcessor(this::processUsageStatsMsg) + .pollInterval(pollInterval) .consumerCreator(queueFactory::createToUsageStatsServiceMsgConsumer) .consumerExecutor(consumersExecutor) .build(); - this.firmwareStatesConsumer = BasicQueueConsumerManager.>builder() + this.firmwareStatesConsumer = QueueConsumerManager.>builder() .key("firmware") .name("TB Ota Package States") - .pollInterval(pollInterval) .msgPackProcessor(this::processFirmwareMsgs) + .pollInterval(pollInterval) .consumerCreator(queueFactory::createToOtaPackageStateServiceMsgConsumer) .consumerExecutor(consumersExecutor) .build(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java rename to application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java index acd28f25ba..6792422b57 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/consumer/MainQueueConsumerManager.java @@ -45,7 +45,7 @@ import java.util.function.Function; import java.util.stream.Collectors; @Slf4j -public class QueueConsumerManager { +public class MainQueueConsumerManager { protected final QueueKey queueKey; @Getter @@ -65,13 +65,12 @@ public class QueueConsumerManager { protected volatile boolean stopped; @Builder - public QueueConsumerManager(QueueKey queueKey, - C config, - MsgPackProcessor msgPackProcessor, - Function> consumerCreator, - ExecutorService consumerExecutor, - ScheduledExecutorService scheduler, - ExecutorService taskExecutor) { + public MainQueueConsumerManager(QueueKey queueKey, C config, + MsgPackProcessor msgPackProcessor, + Function> consumerCreator, + ExecutorService consumerExecutor, + ScheduledExecutorService scheduler, + ExecutorService taskExecutor) { this.queueKey = queueKey; this.config = config; this.msgPackProcessor = msgPackProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 79942cf6df..3c769c911a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -46,7 +46,7 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbPackCallback; import org.thingsboard.server.service.queue.TbPackProcessingContext; -import org.thingsboard.server.service.queue.consumer.BasicQueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import java.util.List; @@ -73,7 +73,7 @@ public abstract class AbstractConsumerService> nfConsumer; + protected QueueConsumerManager> nfConsumer; protected ExecutorService consumersExecutor; protected ExecutorService mgmtExecutor; @@ -84,11 +84,11 @@ public abstract class AbstractConsumerService>builder() + this.nfConsumer = QueueConsumerManager.>builder() .key("notifications") .name("TB Notifications") - .pollInterval(getNotificationPollDuration()) .msgPackProcessor(this::processNotifications) + .pollInterval(getNotificationPollDuration()) .consumerCreator(this::createNotificationsConsumer) .consumerExecutor(consumersExecutor) .build(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index b93313b104..c2823d3c00 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -37,7 +37,7 @@ import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; -import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; +import org.thingsboard.server.service.queue.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy; @@ -55,7 +55,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j -public class TbRuleEngineQueueConsumerManager extends QueueConsumerManager, Queue> { +public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager, Queue> { public static final String SUCCESSFUL_STATUS = "successful"; public static final String FAILED_STATUS = "failed"; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java similarity index 87% rename from application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java index 4b4866c6b4..bf103bef60 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/BasicQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.queue.consumer; +package org.thingsboard.server.queue.common.consumer; import lombok.Builder; import lombok.Getter; @@ -29,12 +29,12 @@ import java.util.concurrent.ExecutorService; import java.util.function.Supplier; @Slf4j -public class BasicQueueConsumerManager { +public class QueueConsumerManager { private final String key; private final String name; - private final long pollInterval; private final MsgPackProcessor msgPackProcessor; + private final long pollInterval; private final ExecutorService consumerExecutor; @Getter @@ -42,11 +42,9 @@ public class BasicQueueConsumerManager { private volatile boolean stopped; @Builder - public BasicQueueConsumerManager(String key, String name, - long pollInterval, - MsgPackProcessor msgPackProcessor, - Supplier> consumerCreator, - ExecutorService consumerExecutor) { + public QueueConsumerManager(String key, String name, MsgPackProcessor msgPackProcessor, + long pollInterval, Supplier> consumerCreator, + ExecutorService consumerExecutor) { this.key = key; this.name = name; this.pollInterval = pollInterval; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 8854ad3a7d..8f814ec052 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -20,6 +20,8 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonObject; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -96,6 +98,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.AsyncCallbackTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; @@ -105,14 +108,12 @@ import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbTransportComponent; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -179,19 +180,17 @@ public class DefaultTransportService extends TransportActivityManager implements protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate; protected TbQueueProducer> ruleEngineMsgProducer; protected TbQueueProducer> tbCoreMsgProducer; - protected TbQueueConsumer> transportNotificationsConsumer; + protected QueueConsumerManager> transportNotificationsConsumer; protected MessagesStats ruleEngineProducerStats; protected MessagesStats tbCoreProducerStats; protected MessagesStats transportApiStats; protected ExecutorService transportCallbackExecutor; - private ExecutorService mainConsumerExecutor; + private ExecutorService consumerExecutor; private final Map toServerRpcPendingMap = new ConcurrentHashMap<>(); - private volatile boolean stopped = false; - public DefaultTransportService(PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, TbTransportQueueFactory queueProvider, @@ -232,42 +231,34 @@ public class DefaultTransportService extends TransportActivityManager implements transportApiRequestTemplate.setMessagesStats(transportApiStats); ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer(); - transportNotificationsConsumer = queueProvider.createTransportNotificationsConsumer(); - TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); - transportNotificationsConsumer.subscribe(Collections.singleton(tpi)); transportApiRequestTemplate.init(); - mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); + consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("consumer")); + transportNotificationsConsumer = QueueConsumerManager.>builder() + .key("transport") + .name("TB Transport") + .msgPackProcessor(this::processNotificationMsgs) + .pollInterval(notificationsPollDuration) + .consumerCreator(queueProvider::createTransportNotificationsConsumer) + .consumerExecutor(consumerExecutor) + .build(); } @AfterStartUp(order = AfterStartUp.TRANSPORT_SERVICE) public void start() { - mainConsumerExecutor.execute(() -> { - while (!stopped) { - try { - List> records = transportNotificationsConsumer.poll(notificationsPollDuration); - if (records.size() == 0) { - continue; - } - records.forEach(record -> { - try { - processToTransportMsg(record.getValue()); - } catch (Throwable e) { - log.warn("Failed to process the notification.", e); - } - }); - transportNotificationsConsumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain messages from queue.", e); - try { - Thread.sleep(notificationsPollDuration); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new requests", e2); - } - } - } + TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); + transportNotificationsConsumer.subscribe(Set.of(tpi)); + transportNotificationsConsumer.launch(); + } + + private void processNotificationMsgs(List> msgs, TbQueueConsumer> consumer) { + msgs.forEach(msg -> { + try { + processToTransportMsg(msg.getValue()); + } catch (Throwable e) { + log.warn("Failed to process the notification.", e); } }); + consumer.commit(); } private void invalidateRateLimits() { @@ -276,16 +267,14 @@ public class DefaultTransportService extends TransportActivityManager implements @PreDestroy public void destroy() { - stopped = true; - if (transportNotificationsConsumer != null) { - transportNotificationsConsumer.unsubscribe(); + transportNotificationsConsumer.stop(); } if (transportCallbackExecutor != null) { transportCallbackExecutor.shutdownNow(); } - if (mainConsumerExecutor != null) { - mainConsumerExecutor.shutdownNow(); + if (consumerExecutor != null) { + consumerExecutor.shutdownNow(); } if (transportApiRequestTemplate != null) { transportApiRequestTemplate.stop(); diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java index dc263b80e7..4d0876bf8e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java @@ -53,7 +53,7 @@ public class ThingsBoardThreadFactory implements ThreadFactory { public static void addThreadNamePrefix(String prefix) { String name = Thread.currentThread().getName(); - name = prefix + name; + name = prefix + "-" + name; Thread.currentThread().setName(name); } diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index edf998dc8e..a5a7021d8d 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -21,14 +21,13 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.jgit.errors.LargeObjectException; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.core.annotation.Order; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.EntityType; @@ -68,16 +67,16 @@ import org.thingsboard.server.gen.transport.TransportProtos.VersionedEntityInfoP import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbVersionControlQueueFactory; +import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbVersionControlComponent; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -115,9 +114,8 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe private final Map pendingCommitMap = new HashMap<>(); private volatile ExecutorService consumerExecutor; - private volatile TbQueueConsumer> consumer; + private volatile QueueConsumerManager> consumer; private volatile TbQueueProducer> producer; - private volatile boolean stopped = false; @Value("${queue.vc.poll-interval:25}") private long pollDuration; @@ -134,20 +132,26 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe @PostConstruct public void init() { - consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("vc-consumer")); + consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("consumer")); var threadFactory = ThingsBoardThreadFactory.forName("vc-io-thread"); for (int i = 0; i < ioPoolSize; i++) { ioThreads.add(MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(threadFactory))); } producer = producerProvider.getTbCoreNotificationsMsgProducer(); - consumer = queueFactory.createToVersionControlMsgConsumer(); + consumer = QueueConsumerManager.>builder() + .key("vc") + .name("TB Version Control") + .msgPackProcessor(this::processMsgs) + .pollInterval(pollDuration) + .consumerCreator(queueFactory::createToVersionControlMsgConsumer) + .consumerExecutor(consumerExecutor) + .build(); } @PreDestroy public void stop() { - stopped = true; if (consumer != null) { - consumer.unsubscribe(); + consumer.stop(); } if (consumerExecutor != null) { consumerExecutor.shutdownNow(); @@ -179,48 +183,29 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe return ServiceType.TB_VC_EXECUTOR.equals(event.getServiceType()); } - @EventListener(ApplicationReadyEvent.class) - @Order(value = 2) - public void onApplicationEvent(ApplicationReadyEvent event) { - consumerExecutor.execute(() -> consumerLoop(consumer)); + @AfterStartUp(order = 2) + public void afterStartUp() { + consumer.launch(); } - void consumerLoop(TbQueueConsumer> consumer) { - while (!stopped && !consumer.isStopped()) { - List> futures = new ArrayList<>(); - try { - List> msgs = consumer.poll(pollDuration); - if (msgs.isEmpty()) { - continue; - } - for (TbProtoQueueMsg msgWrapper : msgs) { - ToVersionControlServiceMsg msg = msgWrapper.getValue(); - var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings())); - long startTs = System.currentTimeMillis(); - log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg); - int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize); - ListenableFuture future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg)); - logTaskExecution(ctx, future, startTs); - futures.add(future); - } - try { - Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - log.info("Timeout for processing the version control tasks.", e); - } - consumer.commit(); - } catch (Exception e) { - if (!stopped) { - log.warn("Failed to obtain version control requests from queue.", e); - try { - Thread.sleep(pollDuration); - } catch (InterruptedException e2) { - log.trace("Failed to wait until the server has capacity to handle new version control messages", e2); - } - } - } + void processMsgs(List> msgs, TbQueueConsumer> consumer) throws Exception { + List> futures = new ArrayList<>(); + for (TbProtoQueueMsg msgWrapper : msgs) { + ToVersionControlServiceMsg msg = msgWrapper.getValue(); + var ctx = new VersionControlRequestCtx(msg, msg.hasClearRepositoryRequest() ? null : ProtoUtils.fromProto(msg.getVcSettings())); + long startTs = System.currentTimeMillis(); + log.trace("[{}][{}] RECEIVED task: {}", ctx.getTenantId(), ctx.getRequestId(), msg); + int threadIdx = Math.abs(ctx.getTenantId().hashCode() % ioPoolSize); + ListenableFuture future = ioThreads.get(threadIdx).submit(() -> processMessage(ctx, msg)); + logTaskExecution(ctx, future, startTs); + futures.add(future); + } + try { + Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + log.info("Timeout for processing the version control tasks.", e); } - log.info("TB Version Control request consumer stopped."); + consumer.commit(); } private Void processMessage(VersionControlRequestCtx ctx, ToVersionControlServiceMsg msg) { @@ -273,7 +258,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe var ids = vcService.listEntitiesAtVersion(ctx.getTenantId(), request.getVersionId(), path) .stream().skip(request.getOffset()).limit(request.getLimit()).collect(Collectors.toList()); if (!ids.isEmpty()) { - for (int i = 0; i < ids.size(); i++){ + for (int i = 0; i < ids.size(); i++) { VersionedEntityInfo info = ids.get(i); var data = vcService.getFileContentAtCommit(ctx.getTenantId(), getRelativePath(info.getExternalId().getEntityType(), info.getExternalId().getId().toString()), request.getVersionId()); From 873aef9522ba14e58eb8f2f282347ede3230a442 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Wed, 27 Mar 2024 13:44:10 +0200 Subject: [PATCH 006/121] UI: added mqtt gateway connector configuration --- .../broker-security.component.html | 122 ++++ .../broker-security.component.scss | 20 + .../broker-security.component.ts | 167 ++++++ .../device-info-table.component.html | 87 +++ .../device-info-table.component.scss | 27 + .../device-info-table.component.ts | 169 ++++++ .../mapping-data-keys-panel.component.html | 152 +++++ .../mapping-data-keys-panel.component.scss | 32 ++ .../mapping-data-keys-panel.component.ts | 219 +++++++ .../mapping-table.component.html | 94 +++ .../mapping-table.component.scss | 134 +++++ .../mapping-table.component.ts | 342 +++++++++++ .../mqtt-basic-config.component.html | 175 ++++++ .../mqtt-basic-config.component.scss | 27 + .../mqtt-basic-config.component.ts | 150 +++++ .../add-connector-dialog.component.html | 102 ++++ .../add-connector-dialog.component.scss | 26 + .../dialog/add-connector-dialog.component.ts | 122 ++++ .../dialog/mapping-dialog.component.html | 539 ++++++++++++++++++ .../dialog/mapping-dialog.component.scss | 36 ++ .../dialog/mapping-dialog.component.ts | 351 ++++++++++++ .../gateway/gateway-connectors.component.html | 267 +++++++-- .../gateway/gateway-connectors.component.scss | 38 +- .../gateway/gateway-connectors.component.ts | 214 +++++-- .../lib/gateway/gateway-widget.models.ts | 158 +++++ .../widget/widget-components.module.ts | 21 + .../assets/locale/locale.constant-en_US.json | 141 +++++ .../connector-default-configs/mqtt.json | 218 +++++++ ui-ngx/src/form.scss | 12 + 29 files changed, 4083 insertions(+), 79 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts create mode 100644 ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html new file mode 100644 index 0000000000..30200f0fa5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html @@ -0,0 +1,122 @@ + +
+
+
gateway.security
+ + + {{ SecurityTypeTranslations.get(type) | translate }} + + +
+ + +
+
gateway.username
+
+ + + + warning + + +
+
+
+
gateway.password
+
+ + +
+ + + warning + +
+
+
+
+
+ +
{{ 'gateway.path-hint' | translate }}
+
+
gateway.CA-certificate-path
+
+ + + + warning + + +
+
+
+
gateway.private-key-path
+
+ + + + warning + + +
+
+
+
gateway.client-cert-path
+
+ + + + warning + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss new file mode 100644 index 0000000000..416f368279 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + display: block; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts new file mode 100644 index 0000000000..6cc2e1c128 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts @@ -0,0 +1,167 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, forwardRef, + NgZone, + OnDestroy, + OnInit, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Subject } from 'rxjs'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, ValidationErrors, Validator, Validators +} from '@angular/forms'; +import { + BrokerSecurityTypes, + BrokerSecurityTypeTranslations, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { startWith, takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'tb-broker-security', + templateUrl: './broker-security.component.html', + styleUrls: ['./broker-security.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BrokerSecurityComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => BrokerSecurityComponent), + multi: true + } + ] +}) +export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { + + BrokerSecurityTypes = BrokerSecurityTypes; + + securityTypes = Object.values(BrokerSecurityTypes); + + SecurityTypeTranslations = BrokerSecurityTypeTranslations; + + securityFormGroup: UntypedFormGroup; + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(protected store: Store, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private elementRef: ElementRef, + private fb: FormBuilder) { + super(store); + this.securityFormGroup = this.fb.group({ + type: [BrokerSecurityTypes.ANONYMOUS, []], + username: ['', [Validators.required]], + password: ['', [Validators.required]], + pathToCACert: ['', [Validators.required]], + pathToPrivateKey: ['', [Validators.required]], + pathToClientCert: ['', [Validators.required]] + }); + this.securityFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateView(value); + }); + this.securityFormGroup.get('type').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((type) => { + this.updateValidators(type); + }); + } + + ngOnInit() { + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit() { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + writeValue(deviceInfo: any) { + if (!deviceInfo.type) { + deviceInfo.type = BrokerSecurityTypes.ANONYMOUS; + } + this.securityFormGroup.reset(deviceInfo); + this.updateView(deviceInfo); + } + + validate(): ValidationErrors | null { + return this.securityFormGroup.valid ? null : { + securityForm: { valid: false } + }; + } + + updateView(value: any) { + this.propagateChange(value); + } + + private updateValidators(type) { + if (type) { + this.securityFormGroup.get('username').disable({emitEvent: false}); + this.securityFormGroup.get('password').disable({emitEvent: false}); + this.securityFormGroup.get('pathToCACert').disable({emitEvent: false}); + this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false}); + this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false}); + if (type === BrokerSecurityTypes.BASIC) { + this.securityFormGroup.get('username').enable({emitEvent: false}); + this.securityFormGroup.get('password').enable({emitEvent: false}); + } else if (type === BrokerSecurityTypes.CERTIFICATES) { + this.securityFormGroup.get('pathToCACert').enable({emitEvent: false}); + this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false}); + this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false}); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html new file mode 100644 index 0000000000..bd748788c1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html @@ -0,0 +1,87 @@ + +
+
+ gateway.device-info-settings +
+
+
+
gateway.device-info.parameter
+
gateway.device-info.source
+
+ gateway.device-info.expression +
+
+
+ +
+
gateway.device-info.device-name
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + +
+
+ + + + warning + + +
+
+ +
+
gateway.device-info.device-profile
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + +
+
+ + + + warning + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss new file mode 100644 index 0000000000..2853e6ada6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + display: block; + + .tb-form-row { + .tb-form-table-header { + min-height: 48px; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts new file mode 100644 index 0000000000..9ae150abe8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts @@ -0,0 +1,169 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + NgZone, + OnDestroy, + OnInit, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { + ControlValueAccessor, + FormBuilder, NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, ValidationErrors, Validator, Validators +} from '@angular/forms'; +import { + SourceTypes, + SourceTypeTranslationsMap +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-device-info-table', + templateUrl: './device-info-table.component.html', + styleUrls: ['./device-info-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceInfoTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceInfoTableComponent), + multi: true + } + ] +}) +export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { + + sourceTypes = Object.values(SourceTypes); + + SourceTypeTranslationsMap = SourceTypeTranslationsMap; + + @coerceBoolean() + @Input() + useSource = true; + + @coerceBoolean() + @Input() + required = false; + + deviceInfoTypeValue: any; + + get deviceInfoType(): any { + return this.deviceInfoTypeValue; + } + + @Input() + set deviceInfoType(value: any) { + if (this.deviceInfoTypeValue !== value) { + this.deviceInfoTypeValue = value; + } + } + + mappingFormGroup: UntypedFormGroup; + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(protected store: Store, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private elementRef: ElementRef, + private fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.mappingFormGroup = this.fb.group({ + deviceNameExpression: ['', this.required ? [Validators.required] : []] + }); + this.mappingFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateView(value); + }); + if (this.useSource) { + this.mappingFormGroup.addControl('deviceNameExpressionSource', + this.fb.control(SourceTypes.MSG, [])); + } + if (this.deviceInfoTypeValue === 'full') { + if (this.useSource) { + this.mappingFormGroup.addControl('deviceProfileExpressionSource', + this.fb.control(SourceTypes.MSG, [])); + } + this.mappingFormGroup.addControl('deviceProfileExpression', + this.fb.control('', this.required ? [Validators.required] : [])); + } + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit() { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + writeValue(deviceInfo: any) { + this.mappingFormGroup.patchValue(deviceInfo, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.mappingFormGroup.valid ? null : { + mappingForm: { valid: false } + }; + } + + updateView(value: any) { + this.propagateChange(value); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html new file mode 100644 index 0000000000..bef11bfc5b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html @@ -0,0 +1,152 @@ + +
+
+
{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}
+ +
+ + + + +
+
+ + + + + + + {{ keyControl.get('key').value + '-' + valueTitle(keyControl.get('value').value) }} + + + + + + + + + + + + + + +
+
gateway.key
+
+ + + + warning + + + + + +
+
+
+
gateway.value
+
+ + + +
+ + + + {{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}} + +
+
+ + + + + {{ valueTypes.get(valueTypeEnum[valueType]).name | translate }} + + + + {{ 'gateway.raw' | translate }} + +
+
+ + + + warning + + + + + + +
+
+
+
+
+
+ +
+ +
+
+ +
+
+ + {{ noKeysText }} + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss new file mode 100644 index 0000000000..2a86058fee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .tb-mapping-keys-panel { + min-width: 700px; + max-width: 700px; + + .key-panel { + max-height: 500px; + overflow: auto; + } + + tb-value-input { + width: 100%; + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts new file mode 100644 index 0000000000..b6a4a6a6b8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts @@ -0,0 +1,219 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + EventEmitter, + Input, + OnInit, + Output +} from '@angular/core'; +import { + AbstractControl, + UntypedFormArray, + UntypedFormBuilder, + Validators +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; +import { dataKeyRowValidator, dataKeyValid } from '@home/components/widget/config/basic/common/data-key-row.component'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { UtilsService } from '@core/services/utils.service'; +import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { WidgetService } from '@core/http/widget.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { ColorSettings } from '@shared/models/widget-settings.models'; +import { TranslateService } from '@ngx-translate/core'; +import { isDefinedAndNotNull } from '@core/utils'; +import { ValueType, valueTypesMap } from '@shared/models/constants'; +import { MappingKeysType } from '@home/components/widget/lib/gateway/gateway-widget.models'; + +@Component({ + selector: 'tb-mapping-data-keys-panel', + templateUrl: './mapping-data-keys-panel.component.html', + styleUrls: ['./mapping-data-keys-panel.component.scss'], + providers: [] +}) +export class MappingDataKeysPanelComponent extends PageComponent implements OnInit { + + @Input() + panelTitle: string; + + @Input() + addKeyTitle: string; + + @Input() + deleteKeyTitle: string; + + @Input() + noKeysText: string; + + @Input() + keys: string; + + @Input() + keysType: string; + + @Input() + @coerceBoolean() + rawData = false; + + @Input() + popover: TbPopoverComponent; + + @Output() + keysDataApplied = new EventEmitter>(); + + valueTypeKeys = Object.keys(ValueType); + + MappingKeysType = MappingKeysType; + + valueTypeEnum = ValueType; + + valueTypes = valueTypesMap; + + dataKeyType: DataKeyType; + + keysListFormArray: UntypedFormArray; + + errorText = ''; + + // get dragEnabled(): boolean { + // return this.keysFormArray().controls.length > 1; + // } + + constructor(private fb: UntypedFormBuilder, + protected store: Store) { + super(store); + } + + ngOnInit() { + this.keysListFormArray = this.prepareKeysFormArray(this.keys) + // this.keysListFormArray.valueChanges.subscribe( + // () => { + // let keys: DataKey[] = this.keysListFormArray.get('keys').value; + // if (keys) { + // keys = keys.filter(k => dataKeyValid(k)); + // } + // } + // ); + } + + // keyDrop(event: CdkDragDrop) { + // const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; + // const key = keysArray.at(event.previousIndex); + // keysArray.removeAt(event.previousIndex); + // keysArray.insert(event.currentIndex, key); + // } + + // keysFormArray(): UntypedFormArray { + // return this.keysListFormArray; + // } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + this.keysListFormArray.removeAt(index); + } + + addKey(): void { + const dataKeyFormGroup = this.fb.group({ + key: ['', Validators.required], + value: ['', Validators.required] + }); + if (this.keysType !== MappingKeysType.CUSTOM) { + dataKeyFormGroup.addControl('type', this.fb.control(this.rawData ? 'raw' : ValueType.STRING)); + // if (this.rawData) { + // dataKeyFormGroup.get('type').disable(); + // } + } + // const keyControl = this.fb.control(dataKey, [this.dataKeyValidator]); + this.keysListFormArray.push(dataKeyFormGroup); + } + + deleteKey($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + } + this.keysListFormArray.removeAt(index); + } + + cancel() { + this.popover?.hide(); + } + + applyKeysData() { + let keys = this.keysListFormArray.value; + if (this.keysType === MappingKeysType.CUSTOM) { + keys = {}; + for (let key of this.keysListFormArray.value) { + keys[key.key] = key.value; + } + } + this.keysDataApplied.emit(keys); + } + + private prepareKeysFormArray(keys: any): UntypedFormArray { + const keysControlGroups: Array = []; + if (keys) { + if (this.keysType === MappingKeysType.CUSTOM) { + keys = Object.keys(keys).map(key => { + return {key, value: keys[key], type: ''}; + }); + } + keys.forEach((keyData) => { + const { key, value, type } = keyData; + const dataKeyFormGroup = this.fb.group({ + key: [key, Validators.required], + value: [value, Validators.required], + type: [type, []] + }); + keysControlGroups.push(dataKeyFormGroup); + }); + console.log(keysControlGroups, 'keysControlGroups'); + } + return this.fb.array(keysControlGroups); + } + + valueTitle(value: any): any { + if (isDefinedAndNotNull(value)) { + if (typeof value === 'object') { + return JSON.stringify(value); + } + return value; + } + return ''; + } + + // private dataKeyValidator(control: AbstractControl): ValidationErrors | null { + // const dataKey: any = control.value; + // if (!(!!dataKey && !!dataKey.type && !!dataKey.name)) { + // return { + // required: true + // }; + // } + // return null; + // } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html new file mode 100644 index 0000000000..6e7afd1a45 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html @@ -0,0 +1,94 @@ + +
+
+ +
+
+ {{mappingTypeTranslationsMap.get(mappingType) | translate}} +
+ + + +
+
+ +
+ + +   + + + +
+
+
+ + + {{ column.title | translate }} + {{ mapping[column.def] }} + + + + +
+ + +
+
+
+ + +
+
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss new file mode 100644 index 0000000000..f95c8504b5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss @@ -0,0 +1,134 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../scss/constants'; + +:host { + width: 100%; + height: 100%; + display: block; + .tb-entity-table { + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; + overflow: hidden; + + &.tb-outlined-border { + box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%); + border: solid 1px #e0e0e0; + border-radius: 4px; + } + + .mat-toolbar-tools{ + min-height: auto; + } + + .title-container{ + overflow: hidden; + } + + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + .mat-mdc-table { + table-layout: fixed; + min-width: 450px; + } + + .tb-add-new { + display: flex; + z-index: 1; + pointer-events: none; + + button.connector { + height: auto; + padding-right: 12px; + font-size: 20px; + border-style: dashed; + border-width: 2px; + border-radius: 8px; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + color: rgba(0, 0, 0, 0.38); + } + } + } + + .tb-entity-table-info{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .button-widget-action{ + margin-left: auto; + overflow: hidden; + text-overflow: ellipsis; + } + + .ellipsis { + overflow: hidden; + text-overflow: ellipsis; + } + + .attribute-copy { + visibility: hidden; + transition: visibility 0.1s; + } + + .attribute:hover { + .attribute-copy { + visibility: visible; + } + } + } + } + + @media #{$mat-xs} { + .mat-toolbar { + height: auto; + min-height: 100px; + + .tb-entity-table-title{ + padding-bottom: 5px; + width: 100%; + } + } + } +} + +:host ::ng-deep { + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } + mat-cell.tb-value-cell { + cursor: pointer; + .mat-icon { + height: 24px; + width: 24px; + font-size: 24px; + color: #757575 + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts new file mode 100644 index 0000000000..9e1f9801ee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts @@ -0,0 +1,342 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + NgZone, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { PageLink } from '@shared/models/page/page-link'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, take, takeUntil, tap } from 'rxjs/operators'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { ControlValueAccessor, FormArray, FormBuilder, NG_VALUE_ACCESSOR, UntypedFormArray } from '@angular/forms'; +import { + ConvertorTypeTranslationsMap, + MappingTypes, + MappingTypeTranslationsMap, + RequestTypes +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; +import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; + +@Component({ + selector: 'tb-mapping-table', + templateUrl: './mapping-table.component.html', + styleUrls: ['./mapping-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MappingTableComponent), + multi: true + } + ] +}) +export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { + + mappingTypeTranslationsMap = MappingTypeTranslationsMap; + displayedColumns = []; + mappingColumns = []; + textSearchMode = false; + dataSource: MappingDatasource; + hidePageSize = false; + + activeValue = false; + dirtyValue = false; + + viewsInited = false; + + mappingTypeValue: MappingTypes; + + get mappingType(): MappingTypes { + return this.mappingTypeValue; + } + + @Input() + set mappingType(value: MappingTypes) { + if (this.mappingTypeValue !== value) { + this.mappingTypeValue = value; + } + } + + @ViewChild('searchInput') searchInputField: ElementRef; + + // @ViewChild(MatPaginator) paginator: MatPaginator; + // @ViewChild(MatSort) sort: MatSort; + + mappingFormGroup: UntypedFormArray; + textSearch = this.fb.control('', {nonNullable: true}); + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(protected store: Store, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private elementRef: ElementRef, + private fb: FormBuilder) { + super(store); + this.mappingFormGroup = this.fb.array([]); + this.dirtyValue = !this.activeValue; + // const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; + this.dataSource = new MappingDatasource(); + } + + ngOnInit() { + if (this.mappingType === MappingTypes.DATA) { + this.mappingColumns.push( + {def: 'topicFilter', title: 'gateway.topic-filter'}, + {def: 'QoS', title: 'gateway.mqtt-qos'}, + {def: 'converter', title: 'gateway.converter'} + ) + } else { + this.mappingColumns.push( + {def: 'type', title: 'gateway.type'}, + {def: 'details', title: 'gateway.details'} + ); + } + this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions'); + this.mappingFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.updateTableData(value); + this.updateView(value); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit() { + this.textSearch.valueChanges.pipe( + debounceTime(150), + distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()), + takeUntil(this.destroy$) + ).subscribe((text) => { + const searchText = text.trim(); + this.updateTableData(this.mappingFormGroup.value, searchText.trim()) + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + writeValue(config: any) { + if (isUndefinedOrNull(config)) { + config = this.mappingType === MappingTypes.REQUESTS ? {} : []; + } + let mappingConfigs = config; + if (this.mappingType === MappingTypes.REQUESTS) { + mappingConfigs = []; + + Object.keys(config).forEach((configKey) => { + for (let mapping of config[configKey]) { + mappingConfigs.push({ + requestType: configKey, + requestValue: mapping + }); + } + }); + } + this.mappingFormGroup.clear({emitEvent: false}); + for (let mapping of mappingConfigs) { + this.mappingFormGroup.push(this.fb.group(mapping), {emitEvent: false}); + } + this.updateTableData(mappingConfigs); + } + + updateView(mappingConfigs: Array) { + let config; + if (this.mappingType === MappingTypes.REQUESTS) { + config = {}; + for (let mappingConfig of mappingConfigs) { + if (config[mappingConfig.requestType]) { + config[mappingConfig.requestType].push(mappingConfig.requestValue); + } else { + config[mappingConfig.requestType] = [mappingConfig.requestValue]; + } + } + } else { + config = mappingConfigs; + } + + this.propagateChange(config); + } + + enterFilterMode() { + this.textSearchMode = true; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.textSearch.reset(); + } + + manageMapping($event: Event, index?: number) { + if ($event) { + $event.stopPropagation(); + } + const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {}; + this.dialog.open(MappingDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + mappingType: this.mappingType, + value, + buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply' + } + }).afterClosed().subscribe( + (res) => { + if (res) { + if (isDefinedAndNotNull(index)) { + this.mappingFormGroup.at(index).patchValue(res); + } else { + this.mappingFormGroup.push(this.fb.group(res)); + } + } + } + ); + } + + updateTableData(value, textSearch?: string): void { + let tableValue = value; + if (this.mappingType === MappingTypes.DATA) { + tableValue = tableValue.map((value) => { + return { + topicFilter: value.topicFilter, + QoS: value.subscriptionQos, + converter: this.translate.instant(ConvertorTypeTranslationsMap.get(value.converter.type)) + }; + }); + } else { + tableValue = tableValue.map((value) => { + let details; + if (value.requestType === RequestTypes.ATTRIBUTE_UPDATE) { + details = value.requestValue.attributeFilter; + } else if (value.requestType === RequestTypes.SERVER_SIDE_RPC) { + details = value.requestValue.methodFilter; + } else { + details = value.requestValue.topicFilter; + } + return { + type: value.requestType, + details + }; + }); + } + if (textSearch) { + tableValue = tableValue.filter(value => + Object.values(value).some(val => + val.toString().toLowerCase().includes(textSearch.toLowerCase()) + ) + ); + } + this.dataSource.loadMappings(tableValue); + } + + deleteMapping($event: Event, index: number) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('gateway.delete-mapping-title'), + '', + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.mappingFormGroup.removeAt(index); + } + }); + } + +} + +export class MappingDatasource implements DataSource { + + private mappingSubject = new BehaviorSubject([]); + + private allMappings: Observable>; + + constructor() {} + + connect(collectionViewer: CollectionViewer): Observable> { + return this.mappingSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.mappingSubject.complete(); + } + + loadMappings(mappings: any, pageLink?: PageLink, reload: boolean = false): void { + if (reload) { + this.allMappings = null; + } + this.mappingSubject.next(mappings); + } + + isEmpty(): Observable { + return this.mappingSubject.pipe( + map((mappings) => !mappings.length) + ); + } + + total(): Observable { + return this.mappingSubject.pipe( + map((mappings) => mappings.length) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.html new file mode 100644 index 0000000000..be64ea7e98 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.html @@ -0,0 +1,175 @@ + + + + +
+
+
gateway.broker.name
+
+ + + + warning + + +
+
+
+
gateway.host
+
+ + + + warning + + +
+
+
+
gateway.port
+
+ + + + warning + + +
+
+
+
gateway.mqtt-version
+
+ + + {{ version.name }} + + +
+
+
+
gateway.client-id
+
+ + + + + + + + + + + + + + + +
+
+ + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ gateway.max-number-of-workers +
+
+ + + + warning + + +
+
+
+
+ gateway.max-messages-queue-for-worker +
+
+ + + + warning + + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss new file mode 100644 index 0000000000..4d6211765a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + //width: 100%; + //height: 100%; + //display: block; + // + //.tb-form-row { + // .tb-form-table-header { + // min-height: 48px; + // } + //} + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts new file mode 100644 index 0000000000..e1b9b48833 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts @@ -0,0 +1,150 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + NgZone, + OnDestroy, + OnInit, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { Overlay } from '@angular/cdk/overlay'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { + ControlValueAccessor, + FormBuilder, FormGroup, NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, ValidationErrors, Validator, Validators +} from '@angular/forms'; +import { + MappingTypes, + MqttVersions, + SourceTypes, + SourceTypeTranslationsMap +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { generateSecret } from '@core/utils'; + +@Component({ + selector: 'tb-mqtt-basic-config', + templateUrl: './mqtt-basic-config.component.html', + styleUrls: ['./mqtt-basic-config.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttBasicConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MqttBasicConfigComponent), + multi: true + } + ] +}) +export class MqttBasicConfigComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { + + mqttConfigFormGroup: UntypedFormGroup; + + mqttVersions = MqttVersions; + + mappingTypes = MappingTypes; + + private destroy$ = new Subject(); + private propagateChange = (v: any) => {}; + + constructor(protected store: Store, + public translate: TranslateService, + public dialog: MatDialog, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private dialogService: DialogService, + private entityService: EntityService, + private utils: UtilsService, + private zone: NgZone, + private cd: ChangeDetectorRef, + private elementRef: ElementRef, + private fb: FormBuilder) { + super(store); + console.log('mqttconfig inited'); + } + + ngOnInit() { + this.mqttConfigFormGroup = this.fb.group({ + broker: this.fb.group({ + name: ['', [Validators.required]], + host: ['', [Validators.required]], + port: [null, [Validators.required]], + version: [5, []], + clientId: ['', []], + maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]], + maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]], + security: [{}, [Validators.required]] + }), + dataMapping: [[], Validators.required], + requestsMapping: [{}, []] + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngAfterViewInit() { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void {} + + writeValue(deviceInfo: any) { + this.mqttConfigFormGroup.patchValue(deviceInfo, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.mqttConfigFormGroup.valid ? null : { + mqttConfigForm: { valid: false } + }; + } + + updateView(value: any) { + this.propagateChange(value); + } + + generate(formControlName: string) { + this.mqttConfigFormGroup.get(formControlName).patchValue(generateSecret(5)); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html new file mode 100644 index 0000000000..b90c2a758f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html @@ -0,0 +1,102 @@ + +
+ +

{{ "gateway.add-connector" | translate}}

+ +
+ +
+ + +
+
+
+
+
gateway.type
+
+ + + + {{ type.value }} + + + +
+
+
+
gateway.name
+
+ + + + warning + + +
+
+
+
gateway.remote-logging-level
+
+ + + {{ logLevel }} + + +
+
+
+ + + {{ 'gateway.fill-connector-defaults' | translate }} + + +
+
+ + + {{ 'gateway.send-change-data' | translate }} + + +
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss new file mode 100644 index 0000000000..813b4ec9ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + form { + min-width: 400px; + width: 500px; + } +} + +:host ::ng-deep .mat-padding{ + padding: 0; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts new file mode 100644 index 0000000000..0f0b2bae0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + GatewayConnectorDefaultTypesTranslates, + GatewayLogLevel, getDefaultConfig +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { forkJoin, Observable, of, Subject } from 'rxjs'; +import { catchError, map, takeUntil } from 'rxjs/operators'; +import { camelCase } from '@core/utils'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { ResourcesService } from '@core/services/resources.service'; + +@Component({ + selector: 'tb-add-connector-dialog', + templateUrl: './add-connector-dialog.component.html', + styleUrls: ['./add-connector-dialog.component.scss'], + providers: [], +}) +export class AddConnectorDialogComponent extends DialogComponent> implements OnInit, OnDestroy { + + connectorForm: UntypedFormGroup; + + gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslates; + gatewayLogLevel = Object.values(GatewayLogLevel); + + submitted = false; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private resourcesService: ResourcesService) { + super(store, router, dialogRef); + this.connectorForm = this.fb.group({ + type: ['mqtt', []], + name: ['', [Validators.required, this.uniqNameRequired()]], + logLevel: [GatewayLogLevel.INFO, []], + useDefaults: [true, []], + sendDataOnlyOnChange: [false, []], + }); + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } + + helpLinkId(): string { + return ""; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + const value = this.connectorForm.getRawValue(); + if (value.useDefaults) { + getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => { + console.log(defaultConfig, 'defaultConfig'); + value.configurationJson = defaultConfig; + if (this.connectorForm.valid) { + this.dialogRef.close(value); + } + }); + } + if (this.connectorForm.valid) { + this.dialogRef.close(value); + } + } + + private uniqNameRequired(): ValidatorFn { + return (c: UntypedFormControl) => { + const newName = c.value.trim().toLowerCase(); + const found = this.data.dataSourceData.find((connectorAttr) => { + const connectorData = connectorAttr.value; + return connectorData.name.toLowerCase() === newName; + }); + if (found) { + if (c.hasError('required')) { + return c.getError('required'); + } + return { + duplicateName: { + valid: false + } + }; + } + return null; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html new file mode 100644 index 0000000000..0fc5156f80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html @@ -0,0 +1,539 @@ + +
+ +

{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}

+ +
+ +
+ + +
+
+
+
+ {{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }} +
+ + +
+
gateway.topic-filter
+
+ + + + warning + + +
+
+
+
gateway.mqtt-qos
+
+ + + + {{ QualityTranslationsMap.get(type) | translate }} + + + +
+
+ +
+
+
gateway.converter
+ + + {{ ConvertorTypeTranslationsMap.get(type) | translate }} + + +
+ + + + + + + + + +
+
+
gateway.attributes
+
+ + + {{ attribute.key }} + + + +
+
+
+
gateway.telemetry
+
+ + + {{ telemetry.key }} + + + + +
+
+
+ + +
+
+ +
gateway.extension
+
+ + + +
+
+
+
gateway.extension-configuration
+
{{ 'gateway.extension-configuration-hint' | translate }}
+
+
gateway.keys
+
+ + + {{ telemetry.key }} + + + + +
+
+
+
+ +
+
+
+
+ +
+ +
gateway.request-type
+
+ + + + {{ RequestTypesTranslationsMap.get(type) | translate }} + + + +
+
+ + +
+ +
gateway.topic-filter
+
+ + + + warning + + +
+
+ + + + + + + + + +
+
gateway.from-device-request-settings
+ + +
+
+ gateway.attribute-name-expression +
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + + + + + warning + + +
+
+
+
+
gateway.to-device-response-settings
+
+
+ gateway.response-topic-expression +
+
+ + + + warning + + +
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
+
+ +
+ gateway.response-value-expression +
+
+ + + + warning + + +
+
+
+
+ +
+ +
gateway.device-name-filter
+
+ + + + warning + + +
+
+
+ +
+ gateway.attribute-filter +
+
+ + + + warning + + +
+
+
+ +
gateway.response-topic-expression
+
+ + + + warning + + +
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
+
+ +
gateway.response-value-expression
+
+ + + + warning + + +
+
+
+ +
+ + + {{ 'gateway.with-response' | translate }} + + + {{ 'gateway.without-response' | translate }} + + +
+
+ +
+ gateway.device-name-filter +
+
+ + + + warning + + +
+
+
+ +
+ gateway.method-filter +
+
+ + + + warning + + +
+
+
+ +
+ gateway.request-topic-expression +
+
+ + + + warning + + +
+
+
+
+ gateway.value-expression +
+
+ + + + warning + + +
+
+ +
+
gateway.response-topic-expression
+
+ + + + warning + + +
+
+
+
gateway.response-topic-Qos
+ + + + {{ QualityTranslationsMap.get(type) | translate }} + + + +
+
+
gateway.response-timeout
+
+ + + + warning + + +
+
+
+
+
+
+
+
+ + + + + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss new file mode 100644 index 0000000000..d2fd342407 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + form { + min-width: 700px; + width: 900px; + } +} + +:host ::ng-deep { + .mat-padding { + padding: 0; + } + form { + .mat-mdc-chip-listbox { + .mdc-evolution-chip-set__chips { + justify-content: flex-end; + align-items: center; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts new file mode 100644 index 0000000000..8fe22ac45b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -0,0 +1,351 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnDestroy, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + ConvertorTypes, + ConvertorTypeTranslationsMap, + MappingHintTranslationsMap, + MappingKeysAddKeyTranslationsMap, + MappingKeysDeleteKeyTranslationsMap, + MappingKeysNoKeysTextTranslationsMap, + MappingKeysPanelTitleTranslationsMap, + MappingKeysType, + MappingTypes, + MappingTypeTranslationsMap, + QualityTypes, + QualityTypeTranslationsMap, + RequestTypes, + RequestTypesTranslationsMap, + ServerSideRPCType, + SourceTypes, + SourceTypeTranslationsMap +} from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { Subject } from 'rxjs'; +import { startWith, takeUntil } from 'rxjs/operators'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component'; + +@Component({ + selector: 'tb-mapping-dialog', + templateUrl: './mapping-dialog.component.html', + styleUrls: ['./mapping-dialog.component.scss'], + providers: [], +}) +export class MappingDialogComponent extends DialogComponent> implements OnInit, OnDestroy { + + mappingForm: UntypedFormGroup; + + MappingTypes = MappingTypes; + + qualityTypes = QualityTypes; + QualityTranslationsMap = QualityTypeTranslationsMap; + + convertorTypes = Object.values(ConvertorTypes); + ConvertorTypes = ConvertorTypes; + ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap; + + sourceTypes = Object.values(SourceTypes); + SourceTypeTranslationsMap = SourceTypeTranslationsMap; + + requestTypes = Object.values(RequestTypes); + RequestTypes = RequestTypes; + RequestTypesTranslationsMap = RequestTypesTranslationsMap; + + ServerSideRPCType = ServerSideRPCType; + + MappingKeysType = MappingKeysType; + + MappingHintTranslationsMap = MappingHintTranslationsMap; + + MappingTypeTranslationsMap = MappingTypeTranslationsMap; + + submitted = false; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) { + super(store, router, dialogRef); + + this.createMappingForm(); + } + + get converterAttributes(): Array { + if (this.converterType) { + return this.mappingForm.get('converter').get(this.converterType).value.attributes; + } + } + + get converterTelemetry(): Array { + if (this.converterType) { + return this.mappingForm.get('converter').get(this.converterType).value.timeseries; + } + } + + get converterType(): ConvertorTypes { + return this.mappingForm.get('converter').get('type').value; + } + + get customKeys(): any { + return this.mappingForm.get('converter').get('custom').value.extensionConfig; + } + + get requestMappingType(): any { + return this.mappingForm.get('requestType').value; + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } + + private createMappingForm(): void { + this.mappingForm = this.fb.group({}); + + // switch (this.data.connectorType) { + // case 'mqtt': + if (this.data.mappingType === MappingTypes.DATA) { + this.mappingForm.addControl('topicFilter', this.fb.control('', [Validators.required])); + this.mappingForm.addControl('subscriptionQos', this.fb.control(0)); + this.mappingForm.addControl('converter', this.fb.group({ + type: [ConvertorTypes.JSON, []], + json: this.fb.group({ + // deviceNameSource: ['', []], + // deviceProfileSource: ['', []], + // deviceNameExpression: ['', []], + // deviceProfileExpression: ['', []], + deviceInfo: [{}, []], + attributes: [[], []], + timeseries: [[], []] + }), + bytes: this.fb.group({ + // deviceNameExpression: ['', []], + // deviceProfileExpression: ['', []], + deviceInfo: [{}, []], + attributes: [[], []], + timeseries: [[], []] + }), + custom: this.fb.group({ + extension: ['', []], + extensionConfig: [{}, []] + }), + })); + // this.createConvertorGroup(); + this.mappingForm.patchValue(this.prepareFormValueData()); + this.mappingForm.get('converter.type').valueChanges.pipe( + startWith(this.mappingForm.get('converter.type').value), + takeUntil(this.destroy$) + ).subscribe((value) => { + const converterGroup = this.mappingForm.get('converter'); + converterGroup.get('json').disable({emitEvent: false}); + converterGroup.get('bytes').disable({emitEvent: false}); + converterGroup.get('custom').disable({emitEvent: false}); + converterGroup.get(value).enable({emitEvent: false}); + }) + } + + if (this.data.mappingType === MappingTypes.REQUESTS) { + this.mappingForm.addControl('requestType', this.fb.control(RequestTypes.CONNECT_REQUEST, [])); + this.mappingForm.addControl('requestValue', this.fb.group({ + connectRequests: this.fb.group({ + topicFilter: ['', [Validators.required]], + deviceInfo: [{}, []], + // deviceNameSource: ['', []], + // deviceProfileSource: ['', []], + // deviceNameExpression: ['', []], + // deviceProfileExpression: ['', []], + }), + disconnectRequests: this.fb.group({ + topicFilter: ['', [Validators.required]], + deviceInfo: [{}, []], + // deviceNameSource: ['', []], + // deviceNameExpression: ['', []] + }), + attributeRequests: this.fb.group({ + topicFilter: ['', [Validators.required]], + deviceInfo: [{}, [Validators.required]], + attributeNameExpressionSource: [SourceTypes.MSG], + attributeNameExpression: ['', [Validators.required]], + topicExpression: ['', [Validators.required]], + valueExpression: ['', [Validators.required]], + retain: [false, []] + }), + attributeUpdates: this.fb.group({ + deviceNameFilter: ['', [Validators.required]], + attributeFilter: ['', [Validators.required]], + topicExpression: ['', [Validators.required]], + valueExpression: ['', [Validators.required]], + retain: [true, []] + }), + serverSideRpc: this.fb.group({ + type: [ServerSideRPCType.TWO_WAY, []], + deviceNameFilter: ['', [Validators.required]], + methodFilter: ['', [Validators.required]], + requestTopicExpression: ['', [Validators.required]], + responseTopicExpression: ['', [Validators.required]], + valueExpression: ['', [Validators.required]], + responseTopicQoS: [0, []], + responseTimeout: [10000, [Validators.required]], + }) + })); + this.mappingForm.patchValue(this.prepareFormValueData()); + this.mappingForm.get('requestType').valueChanges.pipe( + startWith(this.mappingForm.get('requestType').value), + takeUntil(this.destroy$) + ).subscribe((value) => { + const requestValueGroup = this.mappingForm.get('requestValue'); + requestValueGroup.get('connectRequests').disable({emitEvent: false}); + requestValueGroup.get('disconnectRequests').disable({emitEvent: false}); + requestValueGroup.get('attributeRequests').disable({emitEvent: false}); + requestValueGroup.get('attributeUpdates').disable({emitEvent: false}); + requestValueGroup.get('serverSideRpc').disable({emitEvent: false}); + requestValueGroup.get(value).enable({emitEvent: false}); + }); + this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe( + startWith(this.mappingForm.get('requestValue.serverSideRpc.type').value), + takeUntil(this.destroy$) + ).subscribe((value) => { + const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc'); + if (value === ServerSideRPCType.ONE_WAY) { + requestValueGroup.get('responseTopicExpression').disable({emitEvent: false}); + requestValueGroup.get('responseTopicQoS').disable({emitEvent: false}); + requestValueGroup.get('responseTimeout').disable({emitEvent: false}); + } else { + requestValueGroup.get('responseTopicExpression').enable({emitEvent: false}); + requestValueGroup.get('responseTopicQoS').enable({emitEvent: false}); + requestValueGroup.get('responseTimeout').enable({emitEvent: false}); + } + }); + } + // } + } + + // private createConvertorGroup(): void { + // + // } + + helpLinkId(): string { + return ""; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + if (this.mappingForm.valid) { + this.dialogRef.close(this.prepareMappingData()); + // this.dialogRef.close(this.mappingForm.getRawValue()); + } + } + + manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const keysControl = this.mappingForm.get('converter').get(this.converterType).get(keysType); + const ctx: any = { + keys: keysControl.value, + keysType: keysType, + rawData: this.mappingForm.get('converter.type').value === ConvertorTypes.BYTES, + panelTitle: MappingKeysPanelTitleTranslationsMap.get(keysType), + addKeyTitle: MappingKeysAddKeyTranslationsMap.get(keysType), + deleteKeyTitle: MappingKeysDeleteKeyTranslationsMap.get(keysType), + noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType) + }; + const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', true, null, + ctx, + {}, + {}, {}, true); + dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover; + dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.subscribe((keysData) => { + dataKeysPanelPopover.hide(); + keysControl.patchValue(keysData); + }); + } + } + + private prepareMappingData(): any { + // try to optimise just by using getRawValue() + const formValue = this.mappingForm.value; + if (this.data.mappingType === MappingTypes.DATA) { + const { converter, topicFilter, subscriptionQos } = formValue; + return { + topicFilter, + subscriptionQos, + converter: { + type: converter.type, + ...converter[converter.type] + } + }; + } else { + return { + requestType: formValue.requestType, + requestValue: formValue.requestValue[formValue.requestType] + }; + } + } + + private prepareFormValueData(): any { + console.log(this.data, 'this.data'); + if (this.data.value && Object.keys(this.data.value).length) { + if (this.data.mappingType === MappingTypes.DATA) { + const {converter, topicFilter, subscriptionQos} = this.data.value; + return { + topicFilter, + subscriptionQos, + converter: { + type: converter.type, + [converter.type]: { ...converter } + } + }; + } else { + return { + requestType: this.data.value.requestType, + requestValue: { + [this.data.value.requestType]: this.data.value.requestValue + } + }; + } + } + return this.data.value; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index 2b9329dc21..f9fa84d52f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html @@ -22,13 +22,21 @@
+
+ +
@@ -137,49 +145,226 @@ + *matRowDef="let attribute; let i = index; columns: displayedColumns;" (click)="selectConnector(attribute)">
-
-
-
- - {{ 'gateway.connectors-table-name' | translate }} - - - - {{ 'gateway.connectors-table-type' | translate }} - - {{ type.value }} - - - - {{ 'gateway.connectors-table-key' | translate }} - - - - {{ 'gateway.connectors-table-class' | translate }} - - - - gateway.remote-logging-level - - {{ logLevel }} - - -
-
-
- - +
+
+
+ {{initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : ''}} + {{'gateway.configuration' | translate}} +
+ + + {{ 'gateway.basic' | translate }} + + + {{ 'gateway.advanced' | translate }} + + +
+
+ + +
+
+
gateway.name
+
+ + + + warning + + +
+
+
+
gateway.remote-logging-level
+
+ + + {{ logLevel }} + + +
+
+
+ + + {{ 'gateway.send-change-data' | translate }} + + +
+
+
+ + + +
+
+
gateway.broker.name
+
+ + + + warning + + +
+
+
+
gateway.host
+
+ + + + warning + + +
+
+
+
gateway.port
+
+ + + + warning + + +
+
+
+
gateway.mqtt-version
+
+ + + {{ version.name }} + + +
+
+
+
gateway.client-id
+
+ + + + +
+
+ + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ gateway.max-number-of-workers +
+
+ + + + warning + + +
+
+
+
+ gateway.max-messages-queue-for-worker +
+
+ + + + warning + + +
+
+
+
+
+
+
+ + + + + + +
-
gateway.device-info.device-name
@@ -55,7 +54,6 @@
-
gateway.device-info.device-profile
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts index 9ae150abe8..2f6b903646 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts @@ -70,8 +70,6 @@ import { coerceBoolean } from '@shared/decorators/coercion'; }) export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { - sourceTypes = Object.values(SourceTypes); - SourceTypeTranslationsMap = SourceTypeTranslationsMap; @coerceBoolean() @@ -82,6 +80,9 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa @Input() required = false; + @Input() + sourceTypes: Array = Object.values(SourceTypes); + deviceInfoTypeValue: any; get deviceInfoType(): any { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html index bef11bfc5b..f9abdd7434 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html @@ -18,32 +18,14 @@
{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}
- -
- - - - -
+
+
- - - + {{ keyControl.get('key').value + '-' + valueTitle(keyControl.get('value').value) }} - - - - - - - - - - - @@ -60,9 +42,6 @@ class="tb-error"> warning - - -
@@ -83,9 +62,9 @@ - + - {{ valueTypes.get(valueTypeEnum[valueType]).name | translate }} + {{ valueTypes.get(valueType).name | translate }} @@ -105,10 +84,6 @@ warning - - - -
@@ -123,7 +98,6 @@ delete
-
-
- -
-
gateway.extension
@@ -151,19 +147,16 @@ (click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)"> edit -
-
-
gateway.request-type
@@ -268,7 +261,6 @@
-
gateway.response-value-expression
@@ -291,7 +283,6 @@
-
gateway.device-name-filter
@@ -329,7 +320,6 @@
-
gateway.response-topic-expression
@@ -354,7 +344,6 @@
-
gateway.response-value-expression
@@ -404,7 +393,6 @@
-
gateway.method-filter
@@ -424,7 +412,6 @@
-
gateway.request-topic-expression
@@ -513,13 +500,6 @@ - - - - - - -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index 8fe22ac45b..bcf0d24365 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -67,6 +67,7 @@ export class MappingDialogComponent extends DialogComponent { + const converterGroup = this.mappingForm.get('converter'); + converterGroup.get('json').disable({emitEvent: false}); + converterGroup.get('bytes').disable({emitEvent: false}); + converterGroup.get('custom').disable({emitEvent: false}); + converterGroup.get(value).enable({emitEvent: false}); + }) + } - // switch (this.data.connectorType) { - // case 'mqtt': - if (this.data.mappingType === MappingTypes.DATA) { - this.mappingForm.addControl('topicFilter', this.fb.control('', [Validators.required])); - this.mappingForm.addControl('subscriptionQos', this.fb.control(0)); - this.mappingForm.addControl('converter', this.fb.group({ - type: [ConvertorTypes.JSON, []], - json: this.fb.group({ - // deviceNameSource: ['', []], - // deviceProfileSource: ['', []], - // deviceNameExpression: ['', []], - // deviceProfileExpression: ['', []], - deviceInfo: [{}, []], - attributes: [[], []], - timeseries: [[], []] - }), - bytes: this.fb.group({ - // deviceNameExpression: ['', []], - // deviceProfileExpression: ['', []], - deviceInfo: [{}, []], - attributes: [[], []], - timeseries: [[], []] - }), - custom: this.fb.group({ - extension: ['', []], - extensionConfig: [{}, []] - }), - })); - // this.createConvertorGroup(); - this.mappingForm.patchValue(this.prepareFormValueData()); - this.mappingForm.get('converter.type').valueChanges.pipe( - startWith(this.mappingForm.get('converter.type').value), - takeUntil(this.destroy$) - ).subscribe((value) => { - const converterGroup = this.mappingForm.get('converter'); - converterGroup.get('json').disable({emitEvent: false}); - converterGroup.get('bytes').disable({emitEvent: false}); - converterGroup.get('custom').disable({emitEvent: false}); - converterGroup.get(value).enable({emitEvent: false}); - }) - } - - if (this.data.mappingType === MappingTypes.REQUESTS) { - this.mappingForm.addControl('requestType', this.fb.control(RequestTypes.CONNECT_REQUEST, [])); - this.mappingForm.addControl('requestValue', this.fb.group({ - connectRequests: this.fb.group({ - topicFilter: ['', [Validators.required]], - deviceInfo: [{}, []], - // deviceNameSource: ['', []], - // deviceProfileSource: ['', []], - // deviceNameExpression: ['', []], - // deviceProfileExpression: ['', []], - }), - disconnectRequests: this.fb.group({ - topicFilter: ['', [Validators.required]], - deviceInfo: [{}, []], - // deviceNameSource: ['', []], - // deviceNameExpression: ['', []] - }), - attributeRequests: this.fb.group({ - topicFilter: ['', [Validators.required]], - deviceInfo: [{}, [Validators.required]], - attributeNameExpressionSource: [SourceTypes.MSG], - attributeNameExpression: ['', [Validators.required]], - topicExpression: ['', [Validators.required]], - valueExpression: ['', [Validators.required]], - retain: [false, []] - }), - attributeUpdates: this.fb.group({ - deviceNameFilter: ['', [Validators.required]], - attributeFilter: ['', [Validators.required]], - topicExpression: ['', [Validators.required]], - valueExpression: ['', [Validators.required]], - retain: [true, []] - }), - serverSideRpc: this.fb.group({ - type: [ServerSideRPCType.TWO_WAY, []], - deviceNameFilter: ['', [Validators.required]], - methodFilter: ['', [Validators.required]], - requestTopicExpression: ['', [Validators.required]], - responseTopicExpression: ['', [Validators.required]], - valueExpression: ['', [Validators.required]], - responseTopicQoS: [0, []], - responseTimeout: [10000, [Validators.required]], - }) - })); - this.mappingForm.patchValue(this.prepareFormValueData()); - this.mappingForm.get('requestType').valueChanges.pipe( - startWith(this.mappingForm.get('requestType').value), - takeUntil(this.destroy$) - ).subscribe((value) => { - const requestValueGroup = this.mappingForm.get('requestValue'); - requestValueGroup.get('connectRequests').disable({emitEvent: false}); - requestValueGroup.get('disconnectRequests').disable({emitEvent: false}); - requestValueGroup.get('attributeRequests').disable({emitEvent: false}); - requestValueGroup.get('attributeUpdates').disable({emitEvent: false}); - requestValueGroup.get('serverSideRpc').disable({emitEvent: false}); - requestValueGroup.get(value).enable({emitEvent: false}); - }); - this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe( - startWith(this.mappingForm.get('requestValue.serverSideRpc.type').value), - takeUntil(this.destroy$) - ).subscribe((value) => { - const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc'); - if (value === ServerSideRPCType.ONE_WAY) { - requestValueGroup.get('responseTopicExpression').disable({emitEvent: false}); - requestValueGroup.get('responseTopicQoS').disable({emitEvent: false}); - requestValueGroup.get('responseTimeout').disable({emitEvent: false}); - } else { - requestValueGroup.get('responseTopicExpression').enable({emitEvent: false}); - requestValueGroup.get('responseTopicQoS').enable({emitEvent: false}); - requestValueGroup.get('responseTimeout').enable({emitEvent: false}); - } - }); + if (this.data.mappingType === MappingTypes.REQUESTS) { + this.mappingForm.addControl('requestType', this.fb.control(RequestTypes.CONNECT_REQUEST, [])); + this.mappingForm.addControl('requestValue', this.fb.group({ + connectRequests: this.fb.group({ + topicFilter: ['', [Validators.required]], + deviceInfo: [{}, []] + }), + disconnectRequests: this.fb.group({ + topicFilter: ['', [Validators.required]], + deviceInfo: [{}, []] + }), + attributeRequests: this.fb.group({ + topicFilter: ['', [Validators.required]], + deviceInfo: [{}, [Validators.required]], + attributeNameExpressionSource: [SourceTypes.MSG], + attributeNameExpression: ['', [Validators.required]], + topicExpression: ['', [Validators.required]], + valueExpression: ['', [Validators.required]], + retain: [false, []] + }), + attributeUpdates: this.fb.group({ + deviceNameFilter: ['', [Validators.required]], + attributeFilter: ['', [Validators.required]], + topicExpression: ['', [Validators.required]], + valueExpression: ['', [Validators.required]], + retain: [true, []] + }), + serverSideRpc: this.fb.group({ + type: [ServerSideRPCType.TWO_WAY, []], + deviceNameFilter: ['', [Validators.required]], + methodFilter: ['', [Validators.required]], + requestTopicExpression: ['', [Validators.required]], + responseTopicExpression: ['', [Validators.required]], + valueExpression: ['', [Validators.required]], + responseTopicQoS: [0, []], + responseTimeout: [10000, [Validators.required]], + }) + })); + this.mappingForm.patchValue(this.prepareFormValueData()); + this.mappingForm.get('requestType').valueChanges.pipe( + startWith(this.mappingForm.get('requestType').value), + takeUntil(this.destroy$) + ).subscribe((value) => { + const requestValueGroup = this.mappingForm.get('requestValue'); + requestValueGroup.get('connectRequests').disable({emitEvent: false}); + requestValueGroup.get('disconnectRequests').disable({emitEvent: false}); + requestValueGroup.get('attributeRequests').disable({emitEvent: false}); + requestValueGroup.get('attributeUpdates').disable({emitEvent: false}); + requestValueGroup.get('serverSideRpc').disable({emitEvent: false}); + requestValueGroup.get(value).enable({emitEvent: false}); + }); + this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe( + startWith(this.mappingForm.get('requestValue.serverSideRpc.type').value), + takeUntil(this.destroy$) + ).subscribe((value) => { + const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc'); + if (value === ServerSideRPCType.ONE_WAY) { + requestValueGroup.get('responseTopicExpression').disable({emitEvent: false}); + requestValueGroup.get('responseTopicQoS').disable({emitEvent: false}); + requestValueGroup.get('responseTimeout').disable({emitEvent: false}); + } else { + requestValueGroup.get('responseTopicExpression').enable({emitEvent: false}); + requestValueGroup.get('responseTopicQoS').enable({emitEvent: false}); + requestValueGroup.get('responseTimeout').enable({emitEvent: false}); } - // } + }); + } } - // private createConvertorGroup(): void { - // - // } - helpLinkId(): string { return ""; } @@ -268,7 +248,6 @@ export class MappingDialogComponent extends DialogComponent div { flex-grow: 1; } - //tb-toggle-password { - // TODO move to form.scss + .mat-mdc-icon-button.mat-mdc-button-base { width: 36px; height: 40px; @@ -140,6 +139,5 @@ .mat-mdc-tab-group, .mat-mdc-tab-body-wrapper { height: 100%; } - //} } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 93e4212990..2a70d60b02 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -21,7 +21,7 @@ import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } f import { EntityId } from '@shared/models/id/entity-id'; import { AttributeService } from '@core/http/attribute.service'; import { TranslateService } from '@ngx-translate/core'; -import { forkJoin, Observable, Subscription } from 'rxjs'; +import { forkJoin, Observable, Subject, Subscription } from 'rxjs'; import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { PageComponent } from '@shared/components/page.component'; import { PageLink } from '@shared/models/page/page-link'; @@ -44,13 +44,13 @@ import { ConnectorConfigurationModes, GatewayConnector, GatewayConnectorDefaultTypesTranslates, - GatewayLogLevel, getDefaultConfig, + GatewayLogLevel, MappingTypes, MqttVersions } from './gateway-widget.models'; import { MatDialog } from '@angular/material/dialog'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; -import { ResourcesService } from '@core/services/resources.service'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'tb-gateway-connector', @@ -121,6 +121,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } }; + private destroy$ = new Subject(); private subscription: IWidgetSubscription; constructor(protected store: Store, @@ -149,9 +150,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie key: ['auto'], class: [''], configuration: [''], - // TODO dynamic structure building configurationJson: [{}, [Validators.required]], - basicConfig: this.fb.group({ // TODO TRANSITION TO CUSTOM COMPONENT IN THE END + basicConfig: this.fb.group({ }) }); this.connectorForm.disable(); @@ -159,7 +159,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie ngAfterViewInit() { - this.connectorForm.get('type').valueChanges.subscribe(type => { + this.connectorForm.get('type').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(type => { if (type && !this.initialConnector) { this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE, [`${type.toUpperCase()}_DEFAULT_CONFIG`], {ignoreErrors: true}).subscribe(defaultConfig=>{ @@ -174,7 +176,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } }); - this.connectorForm.get('configurationJson').valueChanges.subscribe((config) => { + this.connectorForm.get('configurationJson').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((config) => { const basicConfig = this.connectorForm.get('basicConfig'); const connectorName = this.connectorForm.get('name').value; const type = this.connectorForm.get('type').value; @@ -245,6 +249,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }; } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } + saveConnector(): void { const value = this.connectorForm.value; value.configuration = camelCase(value.name) + '.json'; @@ -313,7 +323,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.generateSubscription(); }); this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => { - // console.log(this.activeData, 'updateData (this.activeData)'); this.sharedAttributeData = data.data.filter(value => this.activeConnectors.includes(value.key)); this.combineData(); }); @@ -343,24 +352,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } private combineData() { - // console.log('combineData: ', this.activeData, this.inactiveData, this.sharedAttributeData); this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => index === self.findIndex((t) => t.key === item.key) ).map(attribute => { attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value; return attribute; }); - // console.log(this.dataSource.data, 'this.dataSource.data'); } - // addAttribute(): void { - // if (this.connectorForm.disabled) { - // this.connectorForm.enable(); - // } - // this.nameInput.nativeElement.focus(); - // this.clearOutConnectorForm(); - // } - private clearOutConnectorForm(): void { this.initialConnector = null; this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false}); @@ -439,8 +438,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie if ($event) { $event.stopPropagation(); } - //TODO discuss this change - // this.initialConnector = attribute.value; const title = `Delete connector ${attribute.key}?`; const content = `All connector data will be deleted.`; this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { @@ -546,10 +543,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie if (this.connectorForm.disabled) { this.connectorForm.enable(); } - // if (value.useDefaults) { - // value.configurationJson = getDefaultConfig(this.resourcesService, value.type); - // console.log(value.configurationJson, 'value.configurationJson'); - // } if (!value.configurationJson) { value.configurationJson = {}; } @@ -631,7 +624,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie if (this.basicConfigSub) { this.basicConfigSub.unsubscribe(); } - this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.subscribe((config) => { + this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((config) => { const configJson = this.connectorForm.get('configurationJson'); const connectorName = this.connectorForm.get('name').value; const type = this.connectorForm.get('type').value; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 3f53e1cb6e..a402d259ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -14,11 +14,9 @@ /// limitations under the License. /// -import { Input } from '@angular/core'; -import { Validators } from '@angular/forms'; import { ResourcesService } from '@core/services/resources.service'; import { Observable } from 'rxjs'; -import { Unit } from '@shared/models/unit.models'; +import { ValueTypeData } from '@shared/models/constants'; export enum StorageTypes { MEMORY = 'memory', @@ -99,8 +97,7 @@ export const GecurityTypesTranslationsMap = new Map( [ [SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'], [SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'], - [SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'], - // [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'], + [SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'] ] ); @@ -223,7 +220,6 @@ export enum SourceTypes { MSG = 'message', TOPIC = 'topic', CONST = 'constant' - // CUSTOM = 'json' } export const SourceTypeTranslationsMap = new Map( @@ -298,3 +294,44 @@ export enum ServerSideRPCType { export const getDefaultConfig = (resourcesService: ResourcesService, type: string): Observable => resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`); + +export enum MappingValueType { + STRING = 'string', + INTEGER = 'integer', + DOUBLE = 'double', + BOOLEAN = 'boolean' +} + +export const mappingValueTypesMap = new Map( + [ + [ + MappingValueType.STRING, + { + name: 'value.string', + icon: 'mdi:format-text' + } + ], + [ + MappingValueType.INTEGER, + { + name: 'value.integer', + icon: 'mdi:numeric' + } + ], + [ + MappingValueType.DOUBLE, + { + name: 'value.double', + icon: 'mdi:numeric' + } + ], + [ + MappingValueType.BOOLEAN, + { + name: 'value.boolean', + icon: 'mdi:checkbox-marked-outline' + } + ] + ] +); + diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 476a0c30de..01b2eb0bf2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -81,7 +81,6 @@ import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dial import { DeviceInfoTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table.component'; import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component'; import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/connectors-configuration/broker-security.component'; -import { MqttBasicConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component'; @NgModule({ declarations: @@ -113,7 +112,6 @@ import { MqttBasicConfigComponent } from '@home/components/widget/lib/gateway/co DeviceInfoTableComponent, MappingDataKeysPanelComponent, BrokerSecurityComponent, - MqttBasicConfigComponent, GatewayLogsComponent, GatewayStatisticsComponent, GatewayServiceRPCComponent, @@ -172,7 +170,6 @@ import { MqttBasicConfigComponent } from '@home/components/widget/lib/gateway/co DeviceInfoTableComponent, MappingDataKeysPanelComponent, BrokerSecurityComponent, - MqttBasicConfigComponent, GatewayLogsComponent, GatewayStatisticsComponent, GatewayServiceRPCComponent, diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/bacnet.json b/ui-ngx/src/assets/metadata/connector-default-configs/bacnet.json new file mode 100644 index 0000000000..a03bbdb975 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/bacnet.json @@ -0,0 +1,58 @@ +{ + "general": { + "objectName": "TB_gateway", + "address": "0.0.0.0:47808", + "objectIdentifier": 599, + "maxApduLengthAccepted": 1476, + "segmentationSupported": "segmentedBoth", + "vendorIdentifier": 15 + }, + "devices": [ + { + "deviceName": "BACnet Device ${objectName}", + "deviceType": "default", + "address": "192.168.2.110:47808", + "pollPeriod": 10000, + "attributes": [ + { + "key": "temperature", + "type": "string", + "objectId": "analogOutput:1", + "propertyId": "presentValue" + } + ], + "timeseries": [ + { + "key": "state", + "type": "bool", + "objectId": "binaryValue:1", + "propertyId": "presentValue" + } + ], + "attributeUpdates": [ + { + "key": "brightness", + "requestType": "writeProperty", + "objectId": "analogOutput:1", + "propertyId": "presentValue" + } + ], + "serverSideRpc": [ + { + "method": "set_state", + "requestType": "writeProperty", + "requestTimeout": 10000, + "objectId": "binaryOutput:1", + "propertyId": "presentValue" + }, + { + "method": "get_state", + "requestType": "readProperty", + "requestTimeout": 10000, + "objectId": "binaryOutput:1", + "propertyId": "presentValue" + } + ] + } + ] +} \ No newline at end of file diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/ble.json b/ui-ngx/src/assets/metadata/connector-default-configs/ble.json new file mode 100644 index 0000000000..609e742f2e --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/ble.json @@ -0,0 +1,73 @@ +{ + "passiveScanMode": true, + "showMap": false, + "scanner": { + "timeout": 10000, + "deviceName": "Device name" + }, + "devices": [ + { + "name": "Temperature and humidity sensor", + "MACAddress": "4C:65:A8:DF:85:C0", + "pollPeriod": 500000, + "showMap": false, + "timeout": 10000, + "connectRetry": 5, + "connectRetryInSeconds": 0, + "waitAfterConnectRetries": 10, + "telemetry": [ + { + "key": "temperature", + "method": "notify", + "characteristicUUID": "226CAA55-6476-4566-7562-66734470666D", + "valueExpression": "[2]" + }, + { + "key": "humidity", + "method": "notify", + "characteristicUUID": "226CAA55-6476-4566-7562-66734470666D", + "valueExpression": "[0]" + } + ], + "attributes": [ + { + "key": "name", + "method": "read", + "characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", + "valueExpression": "[0:2]cm [2:]A" + }, + { + "key": "values", + "method": "read", + "characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", + "valueExpression": "All values: [:]" + } + ], + "attributeUpdates": [ + { + "attributeOnThingsBoard": "sharedName", + "characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB" + } + ], + "serverSideRpc": [ + { + "methodRPC": "rpcMethod1", + "withResponse": true, + "characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", + "methodProcessing": "read" + }, + { + "methodRPC": "rpcMethod2", + "withResponse": true, + "characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB", + "methodProcessing": "write" + }, + { + "methodRPC": "rpcMethod3", + "withResponse": true, + "methodProcessing": "scan" + } + ] + } + ] +} diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/can.json b/ui-ngx/src/assets/metadata/connector-default-configs/can.json new file mode 100644 index 0000000000..ea9d162960 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/can.json @@ -0,0 +1,89 @@ +{ + "interface": "socketcan", + "channel": "vcan0", + "backend": { + "fd": true + }, + "reconnectPeriod": 5, + "devices": [ + { + "name": "Car", + "sendDataOnlyOnChange": false, + "enableUnknownRpc": true, + "strictEval": false, + "attributes": [ + { + "key": "isDriverDoorOpened", + "nodeId": 41, + "command": "2:2:big:8717", + "value": "4:1:int", + "expression": "bool(value & 0b00000100)", + "polling": { + "type": "once", + "dataInHex": "AB CD AB CD" + } + } + ], + "timeseries": [ + { + "key": "rpm", + "nodeId": 1918, + "isExtendedId": true, + "command": "2:2:big:48059", + "value": "4:2:big:int", + "expression": "value / 4", + "polling": { + "type": "always", + "period": 5, + "dataInHex": "aaaa bbbb aaaa bbbb" + } + }, + { + "key": "milliage", + "nodeId": 1918, + "isExtendedId": true, + "value": "4:2:little:int", + "expression": "value * 10", + "polling": { + "type": "always", + "period": 30, + "dataInHex": "aa bb cc dd ee ff aa bb" + } + } + ], + "attributeUpdates": [ + { + "attributeOnThingsBoard": "softwareVersion", + "nodeId": 64, + "isExtendedId": true, + "dataLength": 4, + "dataExpression": "value + 5", + "dataByteorder": "little" + } + ], + "serverSideRpc": [ + { + "method": "sendSameData", + "nodeId": 4, + "isExtendedId": true, + "isFd": true, + "bitrateSwitch": true, + "dataInHex": "aa bb cc dd ee ff aa bb aa bb cc d ee ff" + }, + { + "method": "setLightLevel", + "nodeId": 5, + "dataLength": 2, + "dataByteorder": "little", + "dataBefore": "00AA" + }, + { + "method": "setSpeed", + "nodeId": 16, + "dataAfter": "0102", + "dataExpression": "userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed" + } + ] + } + ] +} \ No newline at end of file diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/ftp.json b/ui-ngx/src/assets/metadata/connector-default-configs/ftp.json new file mode 100644 index 0000000000..3a193e43da --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/ftp.json @@ -0,0 +1,65 @@ +{ + "host": "0.0.0.0", + "port": 21, + "TLSSupport": false, + "security": { + "type": "basic", + "username": "admin", + "password": "admin" + }, + "paths": [ + { + "devicePatternName": "asd", + "devicePatternType": "Device", + "delimiter": ",", + "path": "fol/*_hello*.txt", + "readMode": "FULL", + "maxFileSize": 5, + "pollPeriod": 500, + "txtFileDataView": "SLICED", + "withSortingFiles": true, + "attributes": [ + { + "key": "temp", + "value": "[1:]" + }, + { + "key": "tmp", + "value": "[0:1]" + } + ], + "timeseries": [ + { + "type": "int", + "key": "[0:1]", + "value": "[0:1]" + }, + { + "type": "int", + "key": "temp", + "value": "[1:]" + } + ] + } + ], + "attributeUpdates": [ + { + "path": "fol/hello.json", + "deviceNameFilter": ".*", + "writingMode": "WRITE", + "valueExpression": "{'${attributeKey}':'${attributeValue}'}" + } + ], + "serverSideRpc": [ + { + "deviceNameFilter": ".*", + "methodFilter": "read", + "valueExpression": "${params}" + }, + { + "deviceNameFilter": ".*", + "methodFilter": "write", + "valueExpression": "${params}" + } + ] +} diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json new file mode 100644 index 0000000000..437a6abd95 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/modbus.json @@ -0,0 +1,248 @@ +{ + "master": { + "slaves": [ + { + "host": "127.0.0.1", + "port": 5021, + "type": "tcp", + "method": "socket", + "timeout": 35, + "byteOrder": "LITTLE", + "wordOrder": "LITTLE", + "retries": true, + "retryOnEmpty": true, + "retryOnInvalid": true, + "pollPeriod": 5000, + "unitId": 1, + "deviceName": "Temp Sensor", + "sendDataOnlyOnChange": true, + "connectAttemptTimeMs": 5000, + "connectAttemptCount": 5, + "waitAfterFailedAttemptsMs": 300000, + "attributes": [ + { + "tag": "string_read", + "type": "string", + "functionCode": 4, + "objectsCount": 4, + "address": 1 + }, + { + "tag": "bits_read", + "type": "bits", + "functionCode": 4, + "objectsCount": 1, + "address": 5 + }, + { + "tag": "8int_read", + "type": "8int", + "functionCode": 4, + "objectsCount": 1, + "address": 6 + }, + { + "tag": "16int_read", + "type": "16int", + "functionCode": 4, + "objectsCount": 1, + "address": 7 + }, + { + "tag": "32int_read_divider", + "type": "32int", + "functionCode": 4, + "objectsCount": 2, + "address": 8, + "divider": 10 + }, + { + "tag": "8int_read_multiplier", + "type": "8int", + "functionCode": 4, + "objectsCount": 1, + "address": 10, + "multiplier": 10 + }, + { + "tag": "32int_read", + "type": "32int", + "functionCode": 4, + "objectsCount": 2, + "address": 11 + }, + { + "tag": "64int_read", + "type": "64int", + "functionCode": 4, + "objectsCount": 4, + "address": 13 + } + ], + "timeseries": [ + { + "tag": "8uint_read", + "type": "8uint", + "functionCode": 4, + "objectsCount": 1, + "address": 17 + }, + { + "tag": "16uint_read", + "type": "16uint", + "functionCode": 4, + "objectsCount": 2, + "address": 18 + }, + { + "tag": "32uint_read", + "type": "32uint", + "functionCode": 4, + "objectsCount": 4, + "address": 20 + }, + { + "tag": "64uint_read", + "type": "64uint", + "functionCode": 4, + "objectsCount": 1, + "address": 24 + }, + { + "tag": "16float_read", + "type": "16float", + "functionCode": 4, + "objectsCount": 1, + "address": 25 + }, + { + "tag": "32float_read", + "type": "32float", + "functionCode": 4, + "objectsCount": 2, + "address": 26 + }, + { + "tag": "64float_read", + "type": "64float", + "functionCode": 4, + "objectsCount": 4, + "address": 28 + } + ], + "attributeUpdates": [ + { + "tag": "shared_attribute_write", + "type": "32int", + "functionCode": 6, + "objectsCount": 2, + "address": 29 + } + ], + "rpc": [ + { + "tag": "setValue", + "type": "bits", + "functionCode": 5, + "objectsCount": 1, + "address": 31 + }, + { + "tag": "getValue", + "type": "bits", + "functionCode": 1, + "objectsCount": 1, + "address": 31 + }, + { + "tag": "setCPUFanSpeed", + "type": "32int", + "functionCode": 16, + "objectsCount": 2, + "address": 33 + }, + { + "tag": "getCPULoad", + "type": "32int", + "functionCode": 4, + "objectsCount": 2, + "address": 35 + } + ] + } + ] + }, + "slave": { + "type": "tcp", + "host": "127.0.0.1", + "port": 5026, + "method": "socket", + "deviceName": "Modbus Slave Example", + "deviceType": "default", + "pollPeriod": 5000, + "sendDataToThingsBoard": false, + "byteOrder": "LITTLE", + "wordOrder": "LITTLE", + "unitId": 0, + "values": { + "holding_registers": [ + { + "attributes": [ + { + "address": 1, + "type": "string", + "tag": "sm", + "objectsCount": 1, + "value": "ON" + } + ], + "timeseries": [ + { + "address": 2, + "type": "int", + "tag": "smm", + "objectsCount": 1, + "value": "12334" + } + ], + "attributeUpdates": [ + { + "tag": "shared_attribute_write", + "type": "32int", + "functionCode": 6, + "objectsCount": 2, + "address": 29, + "value": 1243 + } + ], + "rpc": [ + { + "tag": "setValue", + "type": "bits", + "functionCode": 5, + "objectsCount": 1, + "address": 31, + "value": 22 + } + ] + } + ], + "coils_initializer": [ + { + "attributes": [ + { + "address": 5, + "type": "string", + "tag": "sm", + "objectsCount": 1, + "value": "12" + } + ], + "timeseries": [], + "attributeUpdates": [], + "rpc": [] + } + ] + } + } +} diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json index cb67ab690f..4d1a12b3f8 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json @@ -30,29 +30,29 @@ "timeout": 60000, "attributes": [ { - "type": "STRING", + "type": "string", "key": "model", "value": "${sensorModel}" }, { - "type": "STRING", + "type": "string", "key": "${sensorModel}", "value": "on" } ], "timeseries": [ { - "type": "DOUBLE", + "type": "string", "key": "temperature", "value": "${temp}" }, { - "type": "DOUBLE", + "type": "double", "key": "humidity", "value": "${hum}" }, { - "type": "STRING", + "type": "string", "key": "combine", "value": "${hum}:${temp}" } @@ -74,19 +74,19 @@ "timeout": 60000, "attributes": [ { - "type": "STRING", + "type": "string", "key": "model", "value": "${sensorModel}" } ], "timeseries": [ { - "type": "DOUBLE", + "type": "double", "key": "temperature", "value": "${temp}" }, { - "type": "DOUBLE", + "type": "string", "key": "humidity", "value": "${hum}" } @@ -99,7 +99,9 @@ "converter": { "type": "bytes", "deviceInfo": { + "deviceNameExpressionSource": "message", "deviceNameExpression": "[0:4]", + "deviceProfileExpressionSource": "constant", "deviceProfileExpression": "default" }, "sendDataOnlyOnChange": false, diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/ocpp.json b/ui-ngx/src/assets/metadata/connector-default-configs/ocpp.json new file mode 100644 index 0000000000..35d878836a --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/ocpp.json @@ -0,0 +1,66 @@ +{ + "centralSystem": { + "name": "Central System", + "host": "127.0.0.1", + "port": 9000, + "connection": { + "type": "insecure" + }, + "security": [ + { + "type": "token", + "tokens": [ + "Bearer ACCESS_TOKEN" + ] + }, + { + "type": "basic", + "credentials": [ + { + "username": "admin", + "password": "admin" + } + ] + } + ] + }, + "chargePoints": [ + { + "idRegexpPattern": "bidon/hello/CP_1", + "deviceNameExpression": "${Vendor} ${Model}", + "deviceTypeExpression": "default", + "attributes": [ + { + "messageTypeFilter": "MeterValues,", + "key": "temp1", + "value": "${meter_value[:].sampled_value[:].value}" + }, + { + "messageTypeFilter": "MeterValues,", + "key": "vendorId", + "value": "${connector_id}" + } + ], + "timeseries": [ + { + "messageTypeFilter": "DataTransfer,", + "key": "temp", + "value": "${data.temp}" + } + ], + "attributeUpdates": [ + { + "attributeOnThingsBoard": "shared", + "valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}" + } + ], + "serverSideRpc": [ + { + "methodRPC": "rpc1", + "withResponse": true, + "valueExpression": "${params}" + } + ] + } + ] +} \ No newline at end of file diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/odbc.json b/ui-ngx/src/assets/metadata/connector-default-configs/odbc.json new file mode 100644 index 0000000000..dc70cdf421 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/odbc.json @@ -0,0 +1,58 @@ +{ + "connection": { + "str": "Driver={PostgreSQL};Server=localhost;Port=5432;Database=thingsboard;Uid=postgres;Pwd=postgres;", + "attributes": { + "autocommit": true, + "timeout": 0 + }, + "encoding": "utf-8", + "decoding": { + "char": "utf-8", + "wchar": "utf-8", + "metadata": "utf-16le" + }, + "reconnect": true, + "reconnectPeriod": 60 + }, + "pyodbc": { + "pooling": false + }, + "polling": { + "query": "SELECT bool_v, str_v, dbl_v, long_v, entity_id, ts FROM ts_kv WHERE ts > ? ORDER BY ts ASC LIMIT 10", + "period": 10, + "iterator": { + "column": "ts", + "query": "SELECT MIN(ts) - 1 FROM ts_kv", + "persistent": false + } + }, + "mapping": { + "device": { + "type": "postgres", + "name": "'ODBC ' + entity_id" + }, + "sendDataOnlyOnChange": false, + "attributes": "*", + "timeseries": [ + { + "name": "value", + "value": "[i for i in [str_v, long_v, dbl_v,bool_v] if i is not None][0]" + } + ] + }, + "serverSideRpc": { + "enableUnknownRpc": false, + "overrideRpcConfig": true, + "methods": [ + "procedureOne", + { + "name": "procedureTwo", + "args": [ + "One", + 2, + 3.0 + ] + } + ] + } +} \ No newline at end of file diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json new file mode 100644 index 0000000000..91ee295648 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json @@ -0,0 +1,52 @@ +{ + "server": { + "name": "OPC-UA Default Server", + "url": "localhost:4840/freeopcua/server/", + "timeoutInMillis": 5000, + "scanPeriodInMillis": 5000, + "disableSubscriptions": false, + "subCheckPeriodInMillis": 100, + "showMap": false, + "security": "Basic128Rsa15", + "identity": { + "type": "anonymous" + }, + "mapping": [ + { + "deviceNodePattern": "Root\\.Objects\\.Device1", + "deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}", + "attributes": [ + { + "key": "temperature °C", + "path": "${ns=2;i=5}" + } + ], + "timeseries": [ + { + "key": "humidity", + "path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}" + }, + { + "key": "batteryLevel", + "path": "${Battery\\.batteryLevel}" + } + ], + "rpc_methods": [ + { + "method": "multiply", + "arguments": [ + 2, + 4 + ] + } + ], + "attributes_updates": [ + { + "attributeOnThingsBoard": "deviceName", + "attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json new file mode 100644 index 0000000000..91ee295648 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json @@ -0,0 +1,52 @@ +{ + "server": { + "name": "OPC-UA Default Server", + "url": "localhost:4840/freeopcua/server/", + "timeoutInMillis": 5000, + "scanPeriodInMillis": 5000, + "disableSubscriptions": false, + "subCheckPeriodInMillis": 100, + "showMap": false, + "security": "Basic128Rsa15", + "identity": { + "type": "anonymous" + }, + "mapping": [ + { + "deviceNodePattern": "Root\\.Objects\\.Device1", + "deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}", + "attributes": [ + { + "key": "temperature °C", + "path": "${ns=2;i=5}" + } + ], + "timeseries": [ + { + "key": "humidity", + "path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}" + }, + { + "key": "batteryLevel", + "path": "${Battery\\.batteryLevel}" + } + ], + "rpc_methods": [ + { + "method": "multiply", + "arguments": [ + 2, + 4 + ] + } + ], + "attributes_updates": [ + { + "attributeOnThingsBoard": "deviceName", + "attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/request.json b/ui-ngx/src/assets/metadata/connector-default-configs/request.json new file mode 100644 index 0000000000..c5b96ff4cf --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/request.json @@ -0,0 +1,169 @@ +{ + "host": "http://127.0.0.1:5000", + "SSLVerify": true, + "security": { + "type": "basic", + "username": "user", + "password": "password" + }, + "mapping": [ + { + "url": "getdata", + "httpMethod": "GET", + "httpHeaders": { + "ACCEPT": "application/json" + }, + "content": { + "name": "morpheus", + "job": "leader" + }, + "allowRedirects": true, + "timeout": 0.5, + "scanPeriod": 5, + "converter": { + "type": "json", + "deviceNameJsonExpression": "SD8500", + "deviceTypeJsonExpression": "SD", + "attributes": [ + { + "key": "serialNumber", + "type": "string", + "value": "${serial}" + } + ], + "telemetry": [ + { + "key": "Maintainer", + "type": "string", + "value": "${Developer}" + }, + { + "key": "combine", + "type": "string", + "value": "${Developer}:${hum}" + } + ] + } + }, + { + "url": "get_info", + "httpMethod": "GET", + "httpHeaders": { + "ACCEPT": "application/json" + }, + "allowRedirects": true, + "timeout": 0.5, + "scanPeriod": 100, + "converter": { + "type": "custom", + "deviceNameJsonExpression": "SD8500", + "deviceTypeJsonExpression": "SD", + "extension": "CustomRequestUplinkConverter", + "extension-config": [ + { + "key": "Totaliser", + "type": "float", + "fromByte": 0, + "toByte": 4, + "byteorder": "big", + "signed": true, + "multiplier": 1 + }, + { + "key": "Flow", + "type": "int", + "fromByte": 4, + "toByte": 6, + "byteorder": "big", + "signed": true, + "multiplier": 0.01 + }, + { + "key": "Temperature", + "type": "int", + "fromByte": 8, + "toByte": 10, + "byteorder": "big", + "signed": true, + "multiplier": 0.01 + }, + { + "key": "Pressure", + "type": "int", + "fromByte": 12, + "toByte": 14, + "byteorder": "big", + "signed": true, + "multiplier": 0.01 + }, + { + "key": "deviceStatus", + "type": "int", + "byteAddress": 15, + "fromBit": 4, + "toBit": 8, + "byteorder": "big", + "signed": false + }, + { + "key": "OUT2", + "type": "int", + "byteAddress": 15, + "fromBit": 1, + "toBit": 2, + "byteorder": "big" + }, + { + "key": "OUT1", + "type": "int", + "byteAddress": 15, + "fromBit": 0, + "toBit": 1, + "byteorder": "big" + } + ] + } + } + ], + "attributeUpdates": [ + { + "httpMethod": "POST", + "httpHeaders": { + "CONTENT-TYPE": "application/json" + }, + "timeout": 0.5, + "tries": 3, + "allowRedirects": true, + "deviceNameFilter": "SD.*", + "attributeFilter": "send_data", + "requestUrlExpression": "sensor/${deviceName}/${attributeKey}", + "requestValueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}" + } + ], + "serverSideRpc": [ + { + "deviceNameFilter": ".*", + "methodFilter": "echo", + "requestUrlExpression": "sensor/${deviceName}/request/${methodName}/${requestId}", + "responseTimeout": 1, + "httpMethod": "GET", + "requestValueExpression": "${params}", + "responseValueExpression": "${temp}", + "timeout": 0.5, + "tries": 3, + "httpHeaders": { + "Content-Type": "application/json" + } + }, + { + "deviceNameFilter": ".*", + "methodFilter": "no-reply", + "requestUrlExpression": "sensor/${deviceName}/request/${methodName}/${requestId}", + "httpMethod": "POST", + "requestValueExpression": "${params}", + "httpHeaders": { + "Content-Type": "application/json" + } + } + ] +} diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/rest.json b/ui-ngx/src/assets/metadata/connector-default-configs/rest.json new file mode 100644 index 0000000000..f927b8f8b2 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/rest.json @@ -0,0 +1,190 @@ +{ + "host": "127.0.0.1", + "port": "5000", + "SSL": false, + "security": { + "cert": "~/ssl/cert.pem", + "key": "~/ssl/key.pem" + }, + "mapping": [ + { + "endpoint": "/my_devices", + "HTTPMethods": [ + "POST" + ], + "security": { + "type": "anonymous" + }, + "converter": { + "type": "json", + "deviceNameExpression": "${sensorName}", + "deviceTypeExpression": "${sensorType}", + "attributes": [ + { + "type": "string", + "key": "model", + "value": "${sensorModel}" + }, + { + "type": "string", + "key": "certificateNumber", + "value": "${certificateNumber}" + } + ], + "timeseries": [ + { + "type": "double", + "key": "temperature", + "value": "${temp}" + }, + { + "type": "double", + "key": "humidity", + "value": "${hum}" + } + ] + } + }, + { + "endpoint": "/anon1", + "HTTPMethods": [ + "GET", + "POST" + ], + "security": { + "type": "anonymous" + }, + "converter": { + "type": "json", + "deviceNameExpression": "Device 2", + "deviceTypeExpression": "default", + "attributes": [ + { + "type": "string", + "key": "model", + "value": "Model2" + } + ], + "timeseries": [ + { + "type": "double", + "key": "temperature", + "value": "${temp}" + }, + { + "type": "double", + "key": "humidity", + "value": "${hum}" + } + ] + } + }, + { + "endpoint": "/anon2", + "HTTPMethods": [ + "POST" + ], + "security": { + "type": "anonymous" + }, + "converter": { + "type": "custom", + "deviceNameExpression": "SuperAnonDevice", + "deviceTypeExpression": "default", + "extension": "CustomRestUplinkConverter", + "extension-config": [ + { + "key": "Totaliser", + "datatype": "float", + "fromByte": 0, + "toByte": 4, + "byteorder": "big", + "signed": true, + "multiplier": 1 + } + ] + } + } + ], + "attributeRequests": [ + { + "endpoint": "/sharedAttributes", + "type": "shared", + "HTTPMethods": [ + "POST" + ], + "security": { + "type": "anonymous" + }, + "timeout": 10.0, + "deviceNameExpression": "${deviceName}", + "attributeNameExpression": "${attribute}${attribute1}" + } + ], + "attributeUpdates": [ + { + "HTTPMethod": "POST", + "SSLVerify": false, + "httpHeaders": { + "CONTENT-TYPE": "application/json" + }, + "security": { + "type": "anonymous" + }, + "timeout": 0.5, + "tries": 3, + "allowRedirects": true, + "deviceNameFilter": "SN.*", + "attributeFilter": ".*", + "requestUrlExpression": "http://127.0.0.1:5001/", + "valueExpression": "{\"deviceName\":\"${deviceName}\",\"${attributeKey}\":\"${attributeValue}\"}" + } + ], + "serverSideRpc": [ + { + "deviceNameFilter": ".*", + "methodFilter": "echo", + "requestUrlExpression": "http://127.0.0.1:5001/${deviceName}", + "responseTimeout": 1, + "HTTPMethod": "GET", + "valueExpression": "${params}", + "timeout": 10.0, + "tries": 3, + "httpHeaders": { + "Content-Type": "application/json" + }, + "security": { + "type": "anonymous" + } + }, + { + "deviceNameFilter": "SN.*", + "methodFilter": "post_attributes", + "requestUrlExpression": "http://127.0.0.1:5000/my_devices", + "responseTimeout": 1, + "HTTPMethod": "POST", + "valueExpression": "{\"sensorName\":\"${deviceName}\", \"sensorModel\":\"${params.sensorModel}\", \"certificateNumber\":\"${params.certificateNumber}\", \"temp\":\"${params.temp}\", \"hum\":\"${params.hum}\"}", + "timeout": 10.0, + "tries": 3, + "httpHeaders": { + "Content-Type": "application/json" + }, + "security": { + "type": "anonymous" + } + }, + { + "deviceNameFilter": ".*", + "methodFilter": "no-reply", + "requestUrlExpression": "sensor/${deviceName}/request/${methodName}/${requestId}", + "HTTPMethod": "POST", + "valueExpression": "${params}", + "httpHeaders": { + "Content-Type": "application/json" + }, + "security": { + "type": "anonymous" + } + } + ] +} \ No newline at end of file diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/snmp.json b/ui-ngx/src/assets/metadata/connector-default-configs/snmp.json new file mode 100644 index 0000000000..1d782dc665 --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/snmp.json @@ -0,0 +1,138 @@ +{ + "devices": [ + { + "deviceName": "SNMP router", + "deviceType": "snmp", + "ip": "snmp.live.gambitcommunications.com", + "port": 161, + "pollPeriod": 5000, + "community": "public", + "attributes": [ + { + "key": "ReceivedFromGet", + "method": "get", + "oid": "1.3.6.1.2.1.1.1.0", + "timeout": 6 + }, + { + "key": "ReceivedFromMultiGet", + "method": "multiget", + "oid": [ + "1.3.6.1.2.1.1.1.0", + "1.3.6.1.2.1.1.2.0" + ], + "timeout": 6 + }, + { + "key": "ReceivedFromGetNext", + "method": "getnext", + "oid": "1.3.6.1.2.1.1.1.0", + "timeout": 6 + }, + { + "key": "ReceivedFromMultiWalk", + "method": "multiwalk", + "oid": [ + "1.3.6.1.2.1.1.1.0", + "1.3.6.0.1.2.1" + ] + }, + { + "key": "ReceivedFromBulkWalk", + "method": "bulkwalk", + "oid": [ + "1.3.6.1.2.1.1.1.0", + "1.3.6.1.2.1.1.2.0" + ] + }, + { + "key": "ReceivedFromBulkGet", + "method": "bulkget", + "scalarOid": [ + "1.3.6.1.2.1.1.1.0", + "1.3.6.1.2.1.1.2.0" + ], + "repeatingOid": [ + "1.3.6.1.2.1.1.1.0", + "1.3.6.1.2.1.1.2.0" + ], + "maxListSize": 10 + } + ], + "telemetry": [ + { + "key": "ReceivedFromWalk", + "community": "private", + "method": "walk", + "oid": "1.3.6.1.2.1.1.1.0" + }, + { + "key": "ReceivedFromTable", + "method": "table", + "oid": "1.3.6.1.2.1.1" + } + ], + "attributeUpdateRequests": [ + { + "attributeFilter": "dataToSet", + "method": "set", + "oid": "1.3.6.1.2.1.1.1.0" + }, + { + "attributeFilter": "dataToMultiSet", + "method": "multiset", + "mappings": { + "1.2.3": "10", + "2.3.4": "${attribute}" + } + } + ], + "serverSideRpcRequests": [ + { + "requestFilter": "setData", + "method": "set", + "oid": "1.3.6.1.2.1.1.1.0" + }, + { + "requestFilter": "multiSetData", + "method": "multiset" + }, + { + "requestFilter": "getData", + "method": "get", + "oid": "1.3.6.1.2.1.1.1.0" + }, + { + "requestFilter": "runBulkWalk", + "method": "bulkwalk", + "oid": [ + "1.3.6.1.2.1.1.1.0", + "1.3.6.1.2.1.1.2.0" + ] + } + ] + }, + { + "deviceName": "SNMP router", + "deviceType": "snmp", + "ip": "127.0.0.1", + "pollPeriod": 5000, + "community": "public", + "converter": "CustomSNMPConverter", + "attributes": [ + { + "key": "ReceivedFromGetWithCustomConverter", + "method": "get", + "oid": "1.3.6.1.2.1.1.1.0" + } + ], + "telemetry": [ + { + "key": "ReceivedFromTableWithCustomConverter", + "method": "table", + "oid": "1.3.6.1.2.1.1.1.0" + } + ] + } + ] +} diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/socket.json b/ui-ngx/src/assets/metadata/connector-default-configs/socket.json new file mode 100644 index 0000000000..47fd59af4a --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/socket.json @@ -0,0 +1,59 @@ +{ + "type": "TCP", + "address": "127.0.0.1", + "port": 50000, + "bufferSize": 1024, + "devices": [ + { + "address": "127.0.0.1:50001", + "deviceName": "Device Example", + "deviceType": "default", + "encoding": "utf-8", + "telemetry": [ + { + "key": "temp", + "byteFrom": 0, + "byteTo": -1 + }, + { + "key": "hum", + "byteFrom": 0, + "byteTo": 2 + } + ], + "attributes": [ + { + "key": "name", + "byteFrom": 0, + "byteTo": -1 + }, + { + "key": "num", + "byteFrom": 2, + "byteTo": 4 + } + ], + "attributeRequests": [ + { + "type": "shared", + "requestExpression": "${[0:3]==atr}", + "attributeNameExpression": "[3:]" + } + ], + "attributeUpdates": [ + { + "encoding": "utf-16", + "attributeOnThingsBoard": "sharedName" + } + ], + "serverSideRpc": [ + { + "methodRPC": "rpcMethod1", + "withResponse": true, + "methodProcessing": "write", + "encoding": "utf-8" + } + ] + } + ] +} diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/xmpp.json b/ui-ngx/src/assets/metadata/connector-default-configs/xmpp.json new file mode 100644 index 0000000000..e66504363e --- /dev/null +++ b/ui-ngx/src/assets/metadata/connector-default-configs/xmpp.json @@ -0,0 +1,53 @@ +{ + "server": { + "jid": "gateway@localhost", + "password": "password", + "host": "localhost", + "port": 5222, + "use_ssl": false, + "disable_starttls": false, + "force_starttls": true, + "timeout": 10000, + "plugins": [ + "xep_0030", + "xep_0323", + "xep_0325" + ] + }, + "devices": [ + { + "jid": "device@localhost/TMP_1101", + "deviceNameExpression": "${serialNumber}", + "deviceTypeExpression": "default", + "attributes": [ + { + "key": "temperature", + "value": "${temp}" + } + ], + "timeseries": [ + { + "key": "humidity", + "value": "${hum}" + }, + { + "key": "combination", + "value": "${temp}:${hum}" + } + ], + "attributeUpdates": [ + { + "attributeOnThingsBoard": "shared", + "valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}" + } + ], + "serverSideRpc": [ + { + "methodRPC": "rpc1", + "withResponse": true, + "valueExpression": "${params}" + } + ] + } + ] +} From 89f13592f000aa269bbda3cc06cdef9c09d73691 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Thu, 28 Mar 2024 11:03:30 +0200 Subject: [PATCH 008/121] UI: added error state matcher for gateway connectors config --- .../gateway/gateway-connectors.component.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 2a70d60b02..4ad9fce364 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -17,7 +17,15 @@ import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, NgForm, + UntypedFormControl, + ValidatorFn, + Validators +} from '@angular/forms'; import { EntityId } from '@shared/models/id/entity-id'; import { AttributeService } from '@core/http/attribute.service'; import { TranslateService } from '@ngx-translate/core'; @@ -51,10 +59,18 @@ import { import { MatDialog } from '@angular/material/dialog'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; import { takeUntil } from 'rxjs/operators'; +import { ErrorStateMatcher } from '@angular/material/core'; + +export class ForceErrorStateMatcher implements ErrorStateMatcher { + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + return (control && control.invalid); + } +} @Component({ selector: 'tb-gateway-connector', templateUrl: './gateway-connectors.component.html', + providers: [{ provide: ErrorStateMatcher, useClass: ForceErrorStateMatcher }], styleUrls: ['./gateway-connectors.component.scss'] }) export class GatewayConnectorComponent extends PageComponent implements AfterViewInit { From a78e8fce5e7fc9ee9f4bb9a8697ce38a461f6708 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Thu, 28 Mar 2024 18:25:00 +0200 Subject: [PATCH 009/121] UI: imrovements for gateway mqtt connector --- .../mapping-data-keys-panel.component.html | 9 +- .../mapping-data-keys-panel.component.scss | 2 +- .../mapping-data-keys-panel.component.ts | 1 - .../mqtt-basic-config.component.html | 175 ------------------ .../mqtt-basic-config.component.scss | 27 --- .../mqtt-basic-config.component.ts | 150 --------------- .../dialog/mapping-dialog.component.html | 2 +- .../dialog/mapping-dialog.component.ts | 11 +- ui-ngx/src/form.scss | 3 + 9 files changed, 21 insertions(+), 359 deletions(-) delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html index f9abdd7434..594cd03874 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html @@ -18,8 +18,9 @@
{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}
-
-
+
+
@@ -106,7 +107,9 @@
- {{ noKeysText }} +
+ {{ noKeysText }} +
- - - - - - - - - - - - -
-
- - -
- - - -
- -
-
- -
- -
-
- -
- -
-
-
- gateway.max-number-of-workers -
-
- - - - warning - - -
-
-
-
- gateway.max-messages-queue-for-worker -
-
- - - - warning - - -
-
-
-
-
-
- diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss deleted file mode 100644 index 4d6211765a..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - //width: 100%; - //height: 100%; - //display: block; - // - //.tb-form-row { - // .tb-form-table-header { - // min-height: 48px; - // } - //} - -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts deleted file mode 100644 index e1b9b48833..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config.component.ts +++ /dev/null @@ -1,150 +0,0 @@ -/// -/// Copyright © 2016-2024 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - forwardRef, - Input, - NgZone, - OnDestroy, - OnInit, - ViewContainerRef -} from '@angular/core'; -import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { TranslateService } from '@ngx-translate/core'; -import { MatDialog } from '@angular/material/dialog'; -import { DialogService } from '@core/services/dialog.service'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { Overlay } from '@angular/cdk/overlay'; -import { UtilsService } from '@core/services/utils.service'; -import { EntityService } from '@core/http/entity.service'; -import { - ControlValueAccessor, - FormBuilder, FormGroup, NG_VALIDATORS, - NG_VALUE_ACCESSOR, - UntypedFormGroup, ValidationErrors, Validator, Validators -} from '@angular/forms'; -import { - MappingTypes, - MqttVersions, - SourceTypes, - SourceTypeTranslationsMap -} from '@home/components/widget/lib/gateway/gateway-widget.models'; -import { coerceBoolean } from '@shared/decorators/coercion'; -import { generateSecret } from '@core/utils'; - -@Component({ - selector: 'tb-mqtt-basic-config', - templateUrl: './mqtt-basic-config.component.html', - styleUrls: ['./mqtt-basic-config.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MqttBasicConfigComponent), - multi: true - }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => MqttBasicConfigComponent), - multi: true - } - ] -}) -export class MqttBasicConfigComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { - - mqttConfigFormGroup: UntypedFormGroup; - - mqttVersions = MqttVersions; - - mappingTypes = MappingTypes; - - private destroy$ = new Subject(); - private propagateChange = (v: any) => {}; - - constructor(protected store: Store, - public translate: TranslateService, - public dialog: MatDialog, - private overlay: Overlay, - private viewContainerRef: ViewContainerRef, - private dialogService: DialogService, - private entityService: EntityService, - private utils: UtilsService, - private zone: NgZone, - private cd: ChangeDetectorRef, - private elementRef: ElementRef, - private fb: FormBuilder) { - super(store); - console.log('mqttconfig inited'); - } - - ngOnInit() { - this.mqttConfigFormGroup = this.fb.group({ - broker: this.fb.group({ - name: ['', [Validators.required]], - host: ['', [Validators.required]], - port: [null, [Validators.required]], - version: [5, []], - clientId: ['', []], - maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]], - maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]], - security: [{}, [Validators.required]] - }), - dataMapping: [[], Validators.required], - requestsMapping: [{}, []] - }); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - ngAfterViewInit() { - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void {} - - writeValue(deviceInfo: any) { - this.mqttConfigFormGroup.patchValue(deviceInfo, {emitEvent: false}); - } - - validate(): ValidationErrors | null { - return this.mqttConfigFormGroup.valid ? null : { - mqttConfigForm: { valid: false } - }; - } - - updateView(value: any) { - this.propagateChange(value); - } - - generate(formControlName: string) { - this.mqttConfigFormGroup.get(formControlName).patchValue(generateSecret(5)); - } - -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html index e7639c1c2b..b1b48654eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html @@ -512,7 +512,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index bcf0d24365..566ab0016a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -82,6 +82,8 @@ export class MappingDialogComponent extends DialogComponent(); @@ -241,7 +243,9 @@ export class MappingDialogComponent extends DialogComponent { dataKeysPanelPopover.hide(); keysControl.patchValue(keysData); + keysControl.markAsDirty(); + }); + dataKeysPanelPopover.tbHideStart.subscribe(() => { + this.keysPopupClosed = true; }); } } diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index e72f38deaf..3d0eef9718 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -245,6 +245,9 @@ display: flex; flex: 1; gap: 8px; + &.no-flex { + flex: none; + } &.row { flex-direction: row; } From 9b7b31f97b223f250f678f50c4b1ea4da73bae63 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Tue, 2 Apr 2024 11:45:47 +0300 Subject: [PATCH 010/121] UI: refactoring gateway connectors --- .../broker-security.component.html | 42 +---------- .../broker-security.component.ts | 31 ++++---- .../device-info-table.component.ts | 8 +- .../mapping-data-keys-panel.component.ts | 7 +- .../mapping-table.component.html | 6 +- .../mapping-table.component.scss | 51 +------------ .../mapping-table.component.ts | 44 +++++------ .../add-connector-dialog.component.html | 4 +- .../dialog/add-connector-dialog.component.ts | 9 ++- .../dialog/mapping-dialog.component.html | 69 +++++++++-------- .../dialog/mapping-dialog.component.scss | 12 +++ .../dialog/mapping-dialog.component.ts | 60 ++++++++------- .../gateway/gateway-connectors.component.html | 39 ++++------ .../gateway/gateway-connectors.component.scss | 71 +++++++++++------- .../gateway/gateway-connectors.component.ts | 75 ++++++++++--------- ...gateway-service-rpc-connector.component.ts | 4 +- .../lib/gateway/gateway-widget.models.ts | 67 ++++++++++------- .../assets/locale/locale.constant-en_US.json | 1 + 18 files changed, 291 insertions(+), 309 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html index 30200f0fa5..319b10f0b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html @@ -20,12 +20,12 @@
gateway.security
- {{ SecurityTypeTranslations.get(type) | translate }} + {{ SecurityTypeTranslationsMap.get(type) | translate }}
- +
gateway.username
@@ -50,36 +50,18 @@
- - warning -
- +
{{ 'gateway.path-hint' | translate }}
gateway.CA-certificate-path
- - warning -
@@ -88,15 +70,6 @@
- - warning -
@@ -105,15 +78,6 @@
- - warning -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts index f0e397e1ac..179329a38d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts @@ -40,11 +40,14 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormGroup, ValidationErrors, Validator, Validators + UntypedFormGroup, + ValidationErrors, + Validator, + Validators } from '@angular/forms'; import { - BrokerSecurityTypes, - BrokerSecurityTypeTranslations, + BrokerSecurityType, + BrokerSecurityTypeTranslationsMap, } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { takeUntil } from 'rxjs/operators'; @@ -68,11 +71,11 @@ import { takeUntil } from 'rxjs/operators'; }) export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { - BrokerSecurityTypes = BrokerSecurityTypes; + BrokerSecurityType = BrokerSecurityType; - securityTypes = Object.values(BrokerSecurityTypes); + securityTypes = Object.values(BrokerSecurityType); - SecurityTypeTranslations = BrokerSecurityTypeTranslations; + SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap; securityFormGroup: UntypedFormGroup; @@ -93,12 +96,12 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal private fb: FormBuilder) { super(store); this.securityFormGroup = this.fb.group({ - type: [BrokerSecurityTypes.ANONYMOUS, []], + type: [BrokerSecurityType.ANONYMOUS, []], username: ['', [Validators.required]], - password: ['', [Validators.required]], - pathToCACert: ['', [Validators.required]], - pathToPrivateKey: ['', [Validators.required]], - pathToClientCert: ['', [Validators.required]] + password: ['', []], + pathToCACert: ['', []], + pathToPrivateKey: ['', []], + pathToClientCert: ['', []] }); this.securityFormGroup.valueChanges.pipe( takeUntil(this.destroy$) @@ -132,7 +135,7 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal writeValue(deviceInfo: any) { if (!deviceInfo.type) { - deviceInfo.type = BrokerSecurityTypes.ANONYMOUS; + deviceInfo.type = BrokerSecurityType.ANONYMOUS; } this.securityFormGroup.reset(deviceInfo); this.updateView(deviceInfo); @@ -155,10 +158,10 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal this.securityFormGroup.get('pathToCACert').disable({emitEvent: false}); this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false}); this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false}); - if (type === BrokerSecurityTypes.BASIC) { + if (type === BrokerSecurityType.BASIC) { this.securityFormGroup.get('username').enable({emitEvent: false}); this.securityFormGroup.get('password').enable({emitEvent: false}); - } else if (type === BrokerSecurityTypes.CERTIFICATES) { + } else if (type === BrokerSecurityType.CERTIFICATES) { this.securityFormGroup.get('pathToCACert').enable({emitEvent: false}); this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false}); this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts index 2f6b903646..e9cec11420 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts @@ -42,9 +42,13 @@ import { ControlValueAccessor, FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormGroup, ValidationErrors, Validator, Validators + UntypedFormGroup, + ValidationErrors, + Validator, + Validators } from '@angular/forms'; import { + DeviceInfoType, SourceTypes, SourceTypeTranslationsMap } from '@home/components/widget/lib/gateway/gateway-widget.models'; @@ -129,7 +133,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa this.mappingFormGroup.addControl('deviceNameExpressionSource', this.fb.control(SourceTypes.MSG, [])); } - if (this.deviceInfoTypeValue === 'full') { + if (this.deviceInfoType === DeviceInfoType.FULL) { if (this.useSource) { this.mappingFormGroup.addControl('deviceProfileExpressionSource', this.fb.control(SourceTypes.MSG, [])); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts index 341646e6f1..a9ee443f22 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts @@ -35,6 +35,7 @@ import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; import { isDefinedAndNotNull } from '@core/utils'; import { + MappingDataKey, MappingKeysType, MappingValueType, mappingValueTypesMap @@ -61,7 +62,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn noKeysText: string; @Input() - keys: string; + keys: Array | {[key: string]: any}; @Input() keysType: string; @@ -140,7 +141,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn this.keysDataApplied.emit(keys); } - private prepareKeysFormArray(keys: any): UntypedFormArray { + private prepareKeysFormArray(keys: Array | {[key: string]: any}): UntypedFormArray { const keysControlGroups: Array = []; if (keys) { if (this.keysType === MappingKeysType.CUSTOM) { @@ -161,7 +162,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn return this.fb.array(keysControlGroups); } - valueTitle(value: any): any { + valueTitle(value: any): string { if (isDefinedAndNotNull(value)) { if (typeof value === 'object') { return JSON.stringify(value); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html index 6e7afd1a45..2cc388a2aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html @@ -15,12 +15,12 @@ limitations under the License. --> -
-
+
+
- {{mappingTypeTranslationsMap.get(mappingType) | translate}} + {{mappingTypeTranslationsMap.get(mappingType) | translate}}
-
+
- - {{ column.title | translate }} - {{ mapping[column.def] }} + + + {{ column.title | translate }} + + + {{ mapping[column.def] }} + - - - -
+ + + + +
+
+ + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss index 51ef102eae..affc4e5982 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss @@ -52,6 +52,15 @@ .mat-mdc-table { table-layout: fixed; min-width: 450px; + + .table-value-column { + padding: 0 12px; + width: 23%; + + &.request-column { + width: 38%; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts index 9f940f3aec..a5c6addfe4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts @@ -69,6 +69,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { mappingTypeTranslationsMap = MappingTypeTranslationsMap; + mappingTypeEnum = MappingType; displayedColumns = []; mappingColumns = []; textSearchMode = false; @@ -124,7 +125,7 @@ export class MappingTableComponent extends PageComponent implements ControlValue this.mappingColumns.push( {def: 'topicFilter', title: 'gateway.topic-filter'}, {def: 'QoS', title: 'gateway.mqtt-qos'}, - {def: 'converter', title: 'gateway.converter'} + {def: 'converter', title: 'gateway.payload-type'} ) } else { this.mappingColumns.push( @@ -299,7 +300,7 @@ export class MappingTableComponent extends PageComponent implements ControlValue } -export class MappingDatasource implements DataSource { +export class MappingDatasource implements DataSource<{[key: string]: any}> { private mappingSubject = new BehaviorSubject>([]); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html index b4a04e1cbd..b4dd8515df 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ "gateway.add-connector" | translate}}

@@ -30,7 +30,7 @@
-
+
gateway.type
@@ -47,12 +47,14 @@
gateway.name
- + warning @@ -83,7 +85,7 @@
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss index 813b4ec9ff..56e4bc229c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss @@ -15,12 +15,8 @@ */ :host { - form { + .add-connector { min-width: 400px; width: 500px; } } - -:host ::ng-deep .mat-padding{ - padding: 0; -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index 41994b3bf0..b635618ec9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -25,7 +25,9 @@ import { Router } from '@angular/router'; import { ConnectorType, GatewayConnectorDefaultTypesTranslatesMap, - GatewayLogLevel, getDefaultConfig + GatewayLogLevel, + getDefaultConfig, + noLeadTrailSpacesRegex } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { Subject } from 'rxjs'; import { ResourcesService } from '@core/services/resources.service'; @@ -58,7 +60,7 @@ export class AddConnectorDialogComponent extends DialogComponent -
+

{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}

@@ -26,26 +26,15 @@ close
- - -
-
+
{{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }}
-
-
gateway.topic-filter
-
-
-
+
gateway.topic-filter
@@ -53,15 +42,24 @@ matTooltipPosition="above" matTooltipClass="tb-error-tooltip" [matTooltip]="('gateway.topic-required') | translate" - *ngIf="mappingForm.get('topicFilter').hasError('required') && mappingForm.get('topicFilter').touched" + *ngIf="mappingForm.get('topicFilter').hasError('required') && + mappingForm.get('topicFilter').touched;" class="tb-error"> warning +
+
-
gateway.mqtt-qos
+
+ gateway.mqtt-qos +
@@ -73,14 +71,18 @@
+
+
gateway.payload-type
+ + + {{ ConvertorTypeTranslationsMap.get(type) | translate }} + + +
-
-
gateway.converter
- - - {{ ConvertorTypeTranslationsMap.get(type) | translate }} - - +
gateway.data-conversion
+
+ {{ DataConversionTranslationsMap.get(converterType) | translate }}
@@ -97,9 +99,12 @@
gateway.attributes
- + - {{ attribute.key }} + {{ attribute }} + + +
-
gateway.telemetry
+
gateway.timeseries
- + - {{ telemetry.key }} + {{ telemetry }} + + +
@@ -228,18 +237,40 @@
gateway.from-device-request-settings
- - -
+
-
gateway.attribute-name-expression
-
-
+
gateway.device-info.device-name-expression
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + + + + + warning + +
+
+
+
+
+
+
gateway.attribute-name-expression
@@ -259,6 +290,12 @@ class="tb-error"> warning +
+
@@ -266,62 +303,58 @@
gateway.to-device-response-settings
-
-
gateway.response-topic-expression
-
-
-
+
gateway.response-value-expression
- + warning +
+
- - - {{ 'gateway.retain' | translate }} - - -
-
-
-
gateway.response-value-expression
-
-
-
+
gateway.response-topic-expression
- + warning +
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
@@ -362,62 +395,58 @@
-
-
gateway.response-topic-expression
-
-
-
+
gateway.response-value-expression
- + warning +
+
- - - {{ 'gateway.retain' | translate }} - - -
-
-
-
gateway.response-value-expression
-
-
-
+
gateway.response-topic-expression
- + warning +
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
@@ -469,15 +498,7 @@
-
-
gateway.request-topic-expression
-
-
-
+
gateway.request-topic-expression
@@ -490,19 +511,17 @@ class="tb-error"> warning +
+
-
-
gateway.value-expression
-
-
-
+
gateway.value-expression
@@ -515,20 +534,18 @@ class="tb-error"> warning +
+
-
-
gateway.response-topic-expression
-
-
-
+
gateway.response-topic-expression
@@ -541,11 +558,19 @@ class="tb-error"> warning +
+
-
gateway.response-topic-Qos
+
+ gateway.response-topic-Qos +
@@ -577,20 +602,19 @@ -
+
- + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss index d976d5df7b..9dbe29d77b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss @@ -15,17 +15,25 @@ */ :host { - form { + .key-mapping { min-width: 700px; width: 900px; + height: 770px; + display: flex; + flex-direction: column; + + .mat-toolbar { + min-height: 64px; + } + + tb-toggle-select { + padding: 4px 0; + } } } :host ::ng-deep { - .mat-padding { - padding: 0; - } - form { + .key-mapping { .mat-mdc-chip-listbox { .mdc-evolution-chip-set__chips { justify-content: flex-end; @@ -45,4 +53,14 @@ width: 0; } } + + .see-example { + width: 32px; + height: 32px; + margin: 4px; + } + + .mat-mdc-form-field-icon-suffix { + display: flex; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index 9cb558e20c..0457c3693a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -24,8 +24,12 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { ConvertorType, - ConvertorTypeTranslationsMap, DeviceInfoType, MappingDataKey, - MappingHintTranslationsMap, MappingInfo, + ConvertorTypeTranslationsMap, + DataConversionTranslationsMap, + DeviceInfoType, + MappingDataKey, + MappingHintTranslationsMap, + MappingInfo, MappingKeysAddKeyTranslationsMap, MappingKeysDeleteKeyTranslationsMap, MappingKeysNoKeysTextTranslationsMap, @@ -33,6 +37,7 @@ import { MappingKeysType, MappingType, MappingTypeTranslationsMap, + noLeadTrailSpacesRegex, QualityTypes, QualityTypeTranslationsMap, RequestType, @@ -84,6 +89,10 @@ export class MappingDialogComponent extends DialogComponent { + get converterAttributes(): Array { if (this.converterType) { - return this.mappingForm.get('converter').get(this.converterType).value.attributes; + return this.mappingForm.get('converter').get(this.converterType).value.attributes.map(value => value.key); } } get converterTelemetry(): Array { if (this.converterType) { - return this.mappingForm.get('converter').get(this.converterType).value.timeseries; + return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key); } } @@ -120,7 +129,7 @@ export class MappingDialogComponent extends DialogComponent + *matRowDef="let attribute; let i = index; columns: displayedColumns;" (click)="selectConnector($event, attribute)">
-
-
- {{initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : ''}} - {{'gateway.configuration' | translate}} +
+
+ {{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }} + {{ 'gateway.configuration' | translate }}
@@ -164,6 +164,11 @@
+ + gateway.select-connector +
@@ -184,14 +189,24 @@
-
-
gateway.remote-logging-level
-
- - - {{ logLevel }} - - +
+
gateway.logs-configuration
+
+ + + {{ 'gateway.enable-remote-logging' | translate }} + + +
+
+
gateway.remote-logging-level
+
+ + + {{ logLevel }} + + +
@@ -230,7 +245,7 @@
gateway.port
- + ([]); this.connectorForm = this.fb.group({ - mode: [ConnectorConfigurationModes.BASIC, []], - name: ['', [Validators.required, this.uniqNameRequired()]], - type: ['', [Validators.required]], - logLevel: ['', [Validators.required]], - sendDataOnlyOnChange: [false, []], - key: ['auto'], - class: [''], - configuration: [''], - configurationJson: [{}, [Validators.required]], - basicConfig: this.fb.group({ + mode: [ConnectorConfigurationModes.BASIC, []], + name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], + type: ['', [Validators.required]], + enableRemoteLogging: [false, []], + logLevel: ['', [Validators.required]], + sendDataOnlyOnChange: [false, []], + key: ['auto'], + class: [''], + configuration: [''], + configurationJson: [{}, [Validators.required]], + basicConfig: this.fb.group({ }) }); this.connectorForm.disable(); @@ -392,6 +394,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie name: '', type: ConnectorType.MQTT, sendDataOnlyOnChange: false, + enableRemoteLogging: false, logLevel: GatewayLogLevel.INFO, key: 'auto', class: '', @@ -402,34 +405,41 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.connectorForm.markAsPristine(); } - selectConnector(attribute: AttributeData): void { - const connector = attribute.value; - if (connector?.name !== this.initialConnector?.name) { - if (this.connectorForm.disabled) { - this.connectorForm.enable(); - } - if (!connector.configuration) { - connector.configuration = ''; - } - if (!connector.key) { - connector.key = 'auto'; - } - if (!connector.configurationJson) { - connector.configurationJson = {}; - } - connector.basicConfig = connector.configurationJson; + selectConnector($event: Event, attribute: AttributeData): void { + if ($event) { + $event.stopPropagation(); + } + this.confirmConnectorChange().subscribe((result) => { + if (result) { + const connector = attribute.value; + if (connector?.name !== this.initialConnector?.name) { + if (this.connectorForm.disabled) { + this.connectorForm.enable(); + } + if (!connector.configuration) { + connector.configuration = ''; + } + if (!connector.key) { + connector.key = 'auto'; + } + if (!connector.configurationJson) { + connector.configurationJson = {}; + } + connector.basicConfig = connector.configurationJson; - this.initialConnector = connector; + this.initialConnector = connector; - if (connector.type === ConnectorType.MQTT) { - this.addMQTTConfigControls(); - } else { - this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false}); - } + if (connector.type === ConnectorType.MQTT) { + this.addMQTTConfigControls(); + } else { + this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false}); + } - this.connectorForm.patchValue(connector, {emitEvent: false}); - this.connectorForm.markAsPristine(); - } + this.connectorForm.patchValue(connector, {emitEvent: false}); + this.connectorForm.markAsPristine(); + } + } + }); } isSameConnector(attribute: AttributeData): boolean { @@ -462,7 +472,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie if ($event) { $event.stopPropagation(); } - const title = `Delete connector ${attribute.key}?`; + const title = `Delete connector \"${attribute.key}\"?`; const content = `All connector data will be deleted.`; this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { if (result) { @@ -562,28 +572,31 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie dataSourceData: this.dataSource.data } }).afterClosed().subscribe((value) => { - if (value) { - this.initialConnector = null; - if (this.connectorForm.disabled) { - this.connectorForm.enable(); - } - if (!value.configurationJson) { - value.configurationJson = {}; - } - value.basicConfig = value.configurationJson; - if (value.type === ConnectorType.MQTT) { - this.addMQTTConfigControls(); - } else { - this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false}); + this.confirmConnectorChange().subscribe((changeConfirmed) => { + if (value && changeConfirmed) { + this.initialConnector = null; + if (this.connectorForm.disabled) { + this.connectorForm.enable(); + } + if (!value.configurationJson) { + value.configurationJson = {}; + } + value.basicConfig = value.configurationJson; + if (value.type === ConnectorType.MQTT) { + this.addMQTTConfigControls(); + } else { + this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false}); + } + this.connectorForm.patchValue(value, {emitEvent: false}); + this.generate('basicConfig.broker.clientId'); + this.saveConnector(); } - this.connectorForm.patchValue(value, {emitEvent: false}); - this.saveConnector() - } + }) }); } generate(formControlName: string): void { - this.connectorForm.get(formControlName).patchValue(generateSecret(5)); + this.connectorForm.get(formControlName).patchValue('tb_gw_' + generateSecret(5)); } private onDataUpdateError(e: any): void { @@ -625,10 +638,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const configControl = this.fb.group({}); const brokerGroup = this.fb.group({ name: ['', []], - host: ['', [Validators.required]], + host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], port: [null, [Validators.required]], version: [5, []], - clientId: ['', []], + clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]], maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]], security: [{}, [Validators.required]] @@ -663,4 +676,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } }); } + + private confirmConnectorChange(): Observable { + if (this.initialConnector && this.connectorForm.dirty) { + return this.dialogService.confirm( + this.translate.instant('gateway.change-connector-title'), + this.translate.instant('gateway.change-connector-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ); + } + return of(true); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts index e1919041f5..2ea5964b84 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts @@ -37,6 +37,7 @@ import { HTTPMethods, ModbusCodesTranslate, ModbusCommandTypes, + noLeadTrailSpacesRegex, RPCCommand, RPCTemplateConfig, SNMPMethods, @@ -53,8 +54,6 @@ import { import { jsonRequired } from '@shared/components/json-object-edit.component'; import { deepClone } from '@core/utils'; -export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?( [ [MappingKeysType.ATTRIBUTES, 'gateway.attributes'], - [MappingKeysType.TIMESERIES, 'gateway.telemetry'], + [MappingKeysType.TIMESERIES, 'gateway.timeseries'], [MappingKeysType.CUSTOM, 'gateway.keys'] ] ); @@ -445,7 +447,7 @@ export const MappingKeysPanelTitleTranslationsMap = new Map( [ [MappingKeysType.ATTRIBUTES, 'gateway.add-attribute'], - [MappingKeysType.TIMESERIES, 'gateway.add-telemetry'], + [MappingKeysType.TIMESERIES, 'gateway.add-timeseries'], [MappingKeysType.CUSTOM, 'gateway.add-key'] ] ); @@ -453,7 +455,7 @@ export const MappingKeysAddKeyTranslationsMap = new Map export const MappingKeysDeleteKeyTranslationsMap = new Map( [ [MappingKeysType.ATTRIBUTES, 'gateway.delete-attribute'], - [MappingKeysType.TIMESERIES, 'gateway.delete-telemetry'], + [MappingKeysType.TIMESERIES, 'gateway.delete-timeseries'], [MappingKeysType.CUSTOM, 'gateway.delete-key'] ] ); @@ -461,7 +463,7 @@ export const MappingKeysDeleteKeyTranslationsMap = new Map( [ [MappingKeysType.ATTRIBUTES, 'gateway.no-attributes'], - [MappingKeysType.TIMESERIES, 'gateway.no-telemetry'], + [MappingKeysType.TIMESERIES, 'gateway.no-timeseries'], [MappingKeysType.CUSTOM, 'gateway.no-keys'] ] ); @@ -515,3 +517,10 @@ export const mappingValueTypesMap = new Map( ] ); +export const DataConversionTranslationsMap = new Map( + [ + [ConvertorType.JSON, 'gateway.JSON-hint'], + [ConvertorType.BYTES, 'gateway.bytes-hint'], + [ConvertorType.CUSTOM, 'gateway.custom-hint'] + ] +); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 855c035ff1..6c9a733b47 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -92,6 +92,7 @@ import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dial import { DeviceInfoTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table.component'; import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component'; import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/connectors-configuration/broker-security.component'; +import { EllipsisChipListDirective } from '@home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive'; @NgModule({ declarations: @@ -132,6 +133,7 @@ import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/con GatewayConfigurationComponent, GatewayRemoteConfigurationDialogComponent, GatewayServiceRPCConnectorTemplateDialogComponent, + EllipsisChipListDirective, ValueCardWidgetComponent, AggregatedValueCardWidgetComponent, CountWidgetComponent, @@ -189,6 +191,7 @@ import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/con GatewayLogsComponent, GatewayServiceRPCConnectorComponent, GatewayServiceRPCConnectorTemplatesComponent, + EllipsisChipListDirective, GatewayStatisticsComponent, GatewayServiceRPCComponent, DeviceGatewayCommandComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4a3fa1f34f..36e198d49b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2708,12 +2708,12 @@ "add-entry": "Add configuration", "add-attribute": "Add attribute", "add-key": "Add key", - "add-telemetry": "Add telemetry", + "add-timeseries": "Add timeseries", "add-mapping": "Add mapping", "advanced": "Advanced", "attributes": "Attributes", "attribute-filter": "Attribute filter", - "attribute-filter-hint": "Filter for incoming attribute name, supports regular expression.", + "attribute-filter-hint": "Filter for incoming attribute name from ThingsBoard, supports regular expression.", "attribute-filter-required": "Attribute filter required.", "attribute-name-expression": "Attribute name expression", "attribute-name-expression-required": "Attribute name expression required.", @@ -2731,6 +2731,8 @@ }, "CA-certificate-path": "Path to CA certificate file", "path-to-CA-cert-required": "Path to CA certificate file is required.", + "change-connector-title": "Confirm connector change", + "change-connector-text": "Switching connectors will discard any unsaved changes. Continue?", "checking-device-activity": "Checking device activity", "command": "Docker commands", "command-copied-message": "Docker command has been copied to clipboard", @@ -2760,14 +2762,15 @@ "install-docker-compose": "Use the instructions to download, install and setup docker compose", "device-info-settings": "Device info settings", "device-info": { - "parameter": "Parameter", + "entity-field": "Entity field", "source": "Source", - "expression": "Expression", + "expression": "Value / Expression", "expression-hint": "Show help", - "device-name": "Device name", - "device-profile": "Device profile", + "name": "Name", + "profile-name": "Profile name", + "device-name-expression": "Device name expression", "device-name-expression-required": "Device name expression required.", - "device-profile-expression-required": "Device profil expression required." + "device-profile-expression-required": "Device profile expression required." }, "device-name-filter": "Device name filter", "device-name-filter-hint": "Regular expression for device name.", @@ -2776,8 +2779,11 @@ "delete-mapping-title": "Delete mapping ?", "download-configuration-file": "Download configuration file", "download-docker-compose": "Download docker-compose.yml for your gateway", + "enable-remote-logging": "Enable remote logging", + "ellipsis-chips-text": "+ {{count}} more", "launch-gateway": "Launch gateway", "launch-docker-compose": "Start the gateway using the following command in the terminal from folder with docker-compose.yml file", + "logs-configuration": "Logs configuration", "create-new-gateway": "Create a new gateway", "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", "created-time": "Created time", @@ -2786,19 +2792,26 @@ "configuration-delete-dialog-input": "Gateway name", "configuration-delete-dialog-input-required": "Gateway name is mandatory", "configuration-delete-dialog-confirm": "Turn Off", - "converter": "Converter", + "connector-duplicate-name": "Connector with such name already exists.", + "connector-side": "Connector side", + "payload-type": "Payload type", + "platform-side": "Platform side", "JSON": "JSON", + "JSON-hint": "Converter for this payload type processes MQTT messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the message. And regular expressions to get device details from topics.", "bytes": "Bytes", + "bytes-hint": "Converter for this payload type designed for binary MQTT payloads, this converter directly interprets binary data to retrieve device names and device profile names, along with attributes and time series, using specific byte positions for data extraction.", "custom": "Custom", + "custom-hint": "This option allows you to use a custom converter for specific data tasks. You need to add your custom converter to the extension folder and enter its class name in the UI settings. Any keys you provide will be sent as configuration to your custom converter.", "client-cert-path": "Path to client certificate file", "path-to-client-cert-required": "Path to client certificate file is required.", "client-id": "Client ID", + "data-conversion": "Data conversion", "data-mapping": "Data mapping", "data-mapping-hint": "Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and telemetry data keys.", "delete": "Delete configuration", "delete-attribute": "Delete attribute", "delete-key": "Delete key", - "delete-telemetry": "Delete telemetry", + "delete-timeseries": "Delete timeseries", "default": "Default", "download-tip": "Download configuration file", "drop-file": "Drop file here or", @@ -2808,8 +2821,8 @@ "extension-configuration-hint": "Configuration for convertor", "fill-connector-defaults": "Fill configuration with default values", "fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.", - "from-device-request-settings": "From device request settings", - "to-device-response-settings": "To device request settings", + "from-device-request-settings": "Input request parsing", + "to-device-response-settings": "Output request handling", "gateway": "Gateway", "gateway-exists": "Device with same name is already exists.", "gateway-name": "Gateway name", @@ -2890,7 +2903,7 @@ "no-data": "No configurations", "no-gateway-found": "No gateway found.", "no-gateway-matching": " '{{item}}' not found.", - "no-telemetry": "No telemetry", + "no-timeseries": "No timeseries", "no-keys": "No keys", "path-hint": "The path is local to the gateway file system", "path-logs": "Path to log files", @@ -3016,6 +3029,7 @@ "response-timeout": "Response timeout (ms)", "response-timeout-required": "Response timeout is required.", "response-topic-Qos": "Response topic QoS", + "response-topic-Qos-hint": "MQTT Quality of Service (QoS) is an agreement between the message sender and receiver that defines the level of delivery guarantee for a specific message.", "response-topic-expression": "Response topic expression", "response-topic-expression-hint": "Response topic expression hint", "response-topic-expression-required": "Response topic expression is required.", @@ -3040,6 +3054,7 @@ "tls-access-token": "TLS + Access Token", "tls-private-key": "TLS + Private Key" }, + "select-connector": "Select connector to display config", "send-change-data": "Send data only on change", "send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and telemetry in the converter output.", "server-port": "Server port", @@ -3110,7 +3125,7 @@ "workers-settings": "Workers settings", "thingsboard": "ThingsBoard", "general": "General", - "telemetry": "Telemetry", + "timeseries": "Timeseries", "key": "Key", "keys": "Keys", "key-required": "Key is required.", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json index 4d1a12b3f8..c60e1dd956 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json +++ b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json @@ -9,9 +9,7 @@ "maxNumberOfWorkers": 100, "sendDataOnlyOnChange": false, "security": { - "type": "basic", - "username": "user", - "password": "password" + "type": "anonymous" } }, "dataMapping": [ diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 3d0eef9718..9e96571b6f 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -505,6 +505,13 @@ gap: 12px; padding-bottom: 12px; + &.no-padding { + padding: 0; + } + &.no-gap { + gap: 0; + } + .tb-form-table-body { display: flex; flex-direction: column; From 214f9d178e7b28a917ec2346d7c5fdd41781fcf4 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Tue, 16 Apr 2024 15:47:47 +0300 Subject: [PATCH 014/121] UI: gateway dashboard improvements --- .../main/data/json/tenant/dashboards/gateways.json | 4 ++-- .../dialog/add-connector-dialog.component.ts | 2 +- .../gateway/dialog/mapping-dialog.component.scss | 13 +++++++++---- .../lib/gateway/dialog/mapping-dialog.component.ts | 7 +------ .../lib/gateway/gateway-connectors.component.html | 11 ++++++----- .../lib/gateway/gateway-connectors.component.ts | 11 ++++++++++- ui-ngx/src/assets/locale/locale.constant-en_US.json | 9 +++++---- 7 files changed, 34 insertions(+), 23 deletions(-) diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index 4ccee86d5e..8acf269f8a 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -5218,7 +5218,7 @@ "widgets": { "02a05113-d878-b086-328e-75bd6f02b2bb": { "sizeX": 24, - "sizeY": 11, + "sizeY": 12, "row": 0, "col": 0 } @@ -5230,7 +5230,7 @@ "backgroundSizeMode": "100%", "autoFillHeight": true, "backgroundImageUrl": null, - "mobileAutoFillHeight": false, + "mobileAutoFillHeight": true, "mobileRowHeight": 70, "outerMargin": true } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index b635618ec9..6294eebbc4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -77,7 +77,7 @@ export class AddConnectorDialogComponent extends DialogComponentgateway.name
- + gateway.port
- + warning diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index d006031f47..79776f93e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -178,6 +178,15 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.connectorForm.disable(); } + get portErrorTooltip(): string { + if (this.connectorForm.get('basicConfig.broker.port').hasError('required')) { + return 'gateway.port-required'; + } else if (this.connectorForm.get('basicConfig.broker.port').hasError('min')) { + return 'gateway.only-natural-numbers'; + } + return ''; + } + ngAfterViewInit() { this.connectorForm.get('type').valueChanges.pipe( takeUntil(this.destroy$) @@ -639,7 +648,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie const brokerGroup = this.fb.group({ name: ['', []], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - port: [null, [Validators.required]], + port: [null, [Validators.required, Validators.min(0)]], version: [5, []], clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]], diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 36e198d49b..d9f4105ee4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2797,9 +2797,9 @@ "payload-type": "Payload type", "platform-side": "Platform side", "JSON": "JSON", - "JSON-hint": "Converter for this payload type processes MQTT messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the message. And regular expressions to get device details from topics.", + "JSON-hint": "Converter for this payload type processes MQTT messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and timeseries from the message. And regular expressions to get device details from topics.", "bytes": "Bytes", - "bytes-hint": "Converter for this payload type designed for binary MQTT payloads, this converter directly interprets binary data to retrieve device names and device profile names, along with attributes and time series, using specific byte positions for data extraction.", + "bytes-hint": "Converter for this payload type designed for binary MQTT payloads, this converter directly interprets binary data to retrieve device names and device profile names, along with attributes and timeseries, using specific byte positions for data extraction.", "custom": "Custom", "custom-hint": "This option allows you to use a custom converter for specific data tasks. You need to add your custom converter to the extension folder and enter its class name in the UI settings. Any keys you provide will be sent as configuration to your custom converter.", "client-cert-path": "Path to client certificate file", @@ -2807,7 +2807,7 @@ "client-id": "Client ID", "data-conversion": "Data conversion", "data-mapping": "Data mapping", - "data-mapping-hint": "Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and telemetry data keys.", + "data-mapping-hint": "Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and timeseries data keys.", "delete": "Delete configuration", "delete-attribute": "Delete attribute", "delete-key": "Delete key", @@ -3044,6 +3044,7 @@ "with-response": "With response", "without-response": "Without response", "other": "Other", + "only-natural-numbers": "Only natural numbers allowed.", "save-tip": "Save configuration file", "security": "Security", "security-type": "Security type", @@ -3056,7 +3057,7 @@ }, "select-connector": "Select connector to display config", "send-change-data": "Send data only on change", - "send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and telemetry in the converter output.", + "send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and timeseries in the converter output.", "server-port": "Server port", "set": "Set", "statistics": { From d0ff4a2d869d2cb91015ce91e1e7b16ad4252c50 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 18 Apr 2024 11:25:05 +0300 Subject: [PATCH 015/121] mobile app QR code initial implementation --- .../main/data/upgrade/3.6.4/schema_update.sql | 12 +++ .../server/controller/AdminController.java | 31 ++++++ .../controller/MobileAppLinksController.java | 86 +++++++++++++++++ .../server/controller/QRCodeController.java | 94 +++++++++++++++++++ .../service/qr/QRSecretCaffeineCache.java | 33 +++++++ .../server/service/qr/QRSecretEvictEvent.java | 25 +++++ .../server/service/qr/QRSecretRedisCache.java | 36 +++++++ .../server/service/qr/QRService.java | 28 ++++++ .../server/service/qr/QRServiceImpl.java | 66 +++++++++++++ .../service/security/permission/Resource.java | 4 +- .../permission/SysAdminPermissions.java | 1 + .../permission/TenantAdminPermissions.java | 1 + .../system/DefaultSystemSecurityService.java | 4 +- .../src/main/resources/thingsboard.yml | 6 ++ .../controller/AdminControllerTest.java | 24 +++++ .../resources/application-test.properties | 1 + .../server/common/data/CacheConstants.java | 2 + .../common/data/mobile/MobileAppSettings.java | 63 +++++++++++++ .../common/data/security/model/JwtPair.java | 4 +- .../data/security/model/SecuritySettings.java | 2 + .../mobile/BaseMobileAppSettingsService.java | 92 ++++++++++++++++++ .../MobileAppSettingsCaffeineCache.java | 34 +++++++ .../dao/mobile/MobileAppSettingsDao.java | 29 ++++++ .../mobile/MobileAppSettingsEvictEvent.java | 24 +++++ .../mobile/MobileAppSettingsRedisCache.java | 36 +++++++ .../dao/mobile/MobileAppSettingsService.java | 27 ++++++ .../server/dao/model/ModelConstants.java | 9 ++ .../model/sql/MobileAppSettingsEntity.java | 92 ++++++++++++++++++ .../dao/sql/mobile/JpaMobileAppDao.java | 53 +++++++++++ .../mobile/MobileAppSettingsRepository.java | 34 +++++++ .../main/resources/sql/schema-entities.sql | 8 ++ 31 files changed, 957 insertions(+), 4 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java create mode 100644 application/src/main/java/org/thingsboard/server/controller/QRCodeController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java diff --git a/application/src/main/data/upgrade/3.6.4/schema_update.sql b/application/src/main/data/upgrade/3.6.4/schema_update.sql index d7d8887d13..99c503d8f9 100644 --- a/application/src/main/data/upgrade/3.6.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.4/schema_update.sql @@ -134,3 +134,15 @@ DELETE FROM asset WHERE type='TbServiceQueue'; DELETE FROM asset_profile WHERE name ='TbServiceQueue'; -- QUEUE STATS UPDATE END + +-- MOBILE APPS TABLE CREATE START + +CREATE TABLE IF NOT EXISTS mobile_app_settings ( + tenant_id UUID NOT NULL, + app_package VARCHAR(100) UNIQUE, + sha256_cert_fingerprints VARCHAR(10000), + app_id VARCHAR(100) UNIQUE, + settings VARCHAR(100000) +); + +-- MOBILE APPS TABLE CREATE END \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 818a2a15a6..2a6593f452 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -65,6 +65,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.SecuritySettings; @@ -75,6 +76,7 @@ import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo; import org.thingsboard.server.common.data.sync.vc.VcUtils; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; @@ -95,6 +97,7 @@ import java.util.Optional; import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @@ -119,6 +122,7 @@ public class AdminController extends BaseController { private final UpdateService updateService; private final SystemInfoService systemInfoService; private final AuditLogService auditLogService; + private final MobileAppSettingsService mobileAppSettingsService; @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @@ -482,4 +486,31 @@ public class AdminController extends BaseController { adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings); response.sendRedirect(prevUri); } + + @ApiOperation(value = "Create Or Update the Mobile application settings (saveMobileAppSettings)", + notes = "The payload contains platform qr code widget settings and associated android and iOS applications." + SYSTEM_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/mobileAppSettings", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public MobileAppSettings saveMobileAppSettings(@Parameter(description = "A JSON value representing the mobile apps configuration") + @RequestBody MobileAppSettings mobileAppSettings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); + mobileAppSettings.setTenantId(getTenantId()); + + return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings); + } + + @ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)", + notes = "The payload contains platform qr code widget settings and creds for associated android and iOS applications." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/mobileAppSettings", method = RequestMethod.GET) + @ResponseBody + public MobileAppSettings getMobileAppSettings() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); + accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); + + return mobileAppSettingsService.getMobileAppSettings(currentUser.getTenantId()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java new file mode 100644 index 0000000000..1265715126 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppLinksController.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@RequiredArgsConstructor +@RestController +@TbCoreComponent +public class MobileAppLinksController extends BaseController { + + public static final String ASSET_LINKS_PATTERN = "[{\n" + + " \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n" + + " \"target\": {\n" + + " \"namespace\": \"android_app\",\n" + + " \"package_name\": \"%s\",\n" + + " \"sha256_cert_fingerprints\":\n" + + " [\"%s\"]\n" + + " }\n" + + "}]"; + + public static final String APPLE_APP_SITE_ASSOCIATION_PATTERN = "{\n" + + " \"applinks\": {\n" + + " \"apps\": [],\n" + + " \"details\": [\n" + + " {\n" + + " \"appID\": \"%s\",\n" + + " \"paths\": [ \"*\" ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + + private final MobileAppSettingsService mobileAppSettingsService; + + + @ApiOperation(value = "Get associated android applications (getAssetLinks)") + @RequestMapping(value = "/.well-known/assetlinks.json", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getAssetLinks() { + MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + if (mobileAppSettings != null && mobileAppSettings.getAppPackage() != null && mobileAppSettings.getSha256CertFingerprints() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(ASSET_LINKS_PATTERN, mobileAppSettings.getAppPackage(), mobileAppSettings.getSha256CertFingerprints()))); + } else { + return ResponseEntity.notFound().build(); + } + } + + @ApiOperation(value = "Get associated ios applications (getAppleAppSiteAssociation)") + @RequestMapping(value = "/.well-known/apple-app-site-association", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getAppleAppSiteAssociation() { + MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); + if (mobileAppSettings != null && mobileAppSettings.getAppId() != null) { + return ResponseEntity.ok(JacksonUtil.toJsonNode(String.format(APPLE_APP_SITE_ASSOCIATION_PATTERN, mobileAppSettings.getAppId()))); + } else { + return ResponseEntity.notFound().build(); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java b/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java new file mode 100644 index 0000000000..aed35cb153 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/QRCodeController.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.qr.QRService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import java.net.URI; +import java.net.URISyntaxException; + +@RequiredArgsConstructor +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class QRCodeController extends BaseController { + + public static final String SECRET = "secret"; + public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-live secret key"; + public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io"; + public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s"; + private final QRService qrService; + private final SystemSecurityService systemSecurityService; + + private final MobileAppSettingsService mobileAppSettingsService; + + @ApiOperation(value = "Get the deep link to the associated mobile application (getQRCodeDeepLink)", + notes = "Fetch the url that takes user to associated mobile application ") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/qr/deepLink", method = RequestMethod.GET, produces = "text/plain") + @ResponseBody + public String getQRCodeDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { + SecurityUser currentUser = getCurrentUser(); + String secret = qrService.generateSecret(currentUser); + + String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + String platformDomain = new URI(baseUrl).getHost(); + JsonNode mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID).getSettings(); + String appDomain; + if (mobileAppSettings != null && mobileAppSettings.get("useDefault") != null + && !mobileAppSettings.get("useDefault").asBoolean()) { + appDomain = platformDomain; + } else { + appDomain = DEFAULT_APP_DOMAIN; + } + String deepLink = String.format(DEEP_LINK_PATTERN, appDomain, secret); + if (!appDomain.equals(platformDomain)) { + deepLink = deepLink + "&host=" + baseUrl; + } + return deepLink; + } + + @ApiOperation(value = "Get User Token (getUserToken)", + notes = "Returns the token of the User based on the provided secret key.") + @RequestMapping(value = "/noauth/qr/{secret}", method = RequestMethod.GET) + @ResponseBody + public JwtPair getUserToken(@Parameter(description = SECRET_PARAM_DESCRIPTION) + @PathVariable(SECRET) String secret) throws ThingsboardException { + checkParameter(SECRET, secret); + return qrService.getJwtPair(secret); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java new file mode 100644 index 0000000000..d74307926c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.qr; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.security.model.JwtPair; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("QRSecretCache") +public class QRSecretCaffeineCache extends CaffeineTbTransactionalCache { + + public QRSecretCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.QR_SECRET_KEY_CACHE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java new file mode 100644 index 0000000000..bf0c7b67fe --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretEvictEvent.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.qr; + +import lombok.Data; + +@Data +public class QRSecretEvictEvent { + + private final String secret; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java new file mode 100644 index 0000000000..8efe664187 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRSecretRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.qr; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.model.JwtPair; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("QRSecretCache") +public class QRSecretRedisCache extends RedisTbTransactionalCache { + + public QRSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.QR_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class)); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRService.java b/application/src/main/java/org/thingsboard/server/service/qr/QRService.java new file mode 100644 index 0000000000..9502d3440b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRService.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.qr; + +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.service.security.model.SecurityUser; + +public interface QRService { + + String generateSecret(SecurityUser securityUser); + + JwtPair getJwtPair(String secret) throws ThingsboardException; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java new file mode 100644 index 0000000000..36bf72c74e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/qr/QRServiceImpl.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.qr; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.cache.TbCacheValueWrapper; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.security.model.JwtPair; +import org.thingsboard.server.dao.entity.AbstractCachedService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import static org.thingsboard.server.service.security.system.DefaultSystemSecurityService.DEFAULT_QR_SECRET_KEY_LENGTH; + +@Service +@Slf4j +@RequiredArgsConstructor +public class QRServiceImpl extends AbstractCachedService implements QRService { + + private final JwtTokenFactory tokenFactory; + private final SystemSecurityService systemSecurityService; + + @Override + public String generateSecret(SecurityUser securityUser) { + log.trace("Executing generateSecret for user [{}]", securityUser.getId()); + Integer qrSecretKeyLength = systemSecurityService.getSecuritySettings().getQrSecretKeyLength(); + String secret = StringUtils.generateSafeToken(qrSecretKeyLength == null ? DEFAULT_QR_SECRET_KEY_LENGTH : qrSecretKeyLength); + cache.put(secret, tokenFactory.createTokenPair(securityUser)); + return secret; + } + + @Override + public JwtPair getJwtPair(String secret) throws ThingsboardException { + TbCacheValueWrapper jwtPair = cache.get(secret); + if (jwtPair != null) { + return jwtPair.get(); + } else { + throw new ThingsboardException("Jwt token not found or expired!", ThingsboardErrorCode.JWT_TOKEN_EXPIRED); + } + } + + @TransactionalEventListener(classes = QRSecretEvictEvent.class) + @Override + public void handleEvictEvent(QRSecretEvictEvent event) { + cache.evict(event.getSecret()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 5aa869f2a8..d43663fb97 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -46,8 +46,8 @@ public enum Resource { QUEUE(EntityType.QUEUE), VERSION_CONTROL, NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, - EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE); - + EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), + MOBILE_APP_SETTINGS; private final Set entityTypes; Resource() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index 67cc105c91..d906938e47 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -41,6 +41,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.TB_RESOURCE, systemEntityPermissionChecker); put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.NOTIFICATION, systemEntityPermissionChecker); + put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index bfe0af966f..f4117e1a36 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -50,6 +50,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.QUEUE, queuePermissionChecker); put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.NOTIFICATION, tenantEntityPermissionChecker); + put(Resource.MOBILE_APP_SETTINGS, PermissionChecker.allowAllPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index d3f945cda1..a804565f59 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -57,7 +57,6 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserServiceImpl; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; -import org.thingsboard.server.service.security.exception.UserPasswordNotValidException; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.utils.MiscUtils; import ua_parser.Client; @@ -75,6 +74,8 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING @Slf4j public class DefaultSystemSecurityService implements SystemSecurityService { + public static final int DEFAULT_QR_SECRET_KEY_LENGTH = 64; + @Autowired private AdminSettingsService adminSettingsService; @@ -109,6 +110,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { securitySettings.setPasswordPolicy(new UserPasswordPolicy()); securitySettings.getPasswordPolicy().setMinimumLength(6); securitySettings.getPasswordPolicy().setMaximumLength(72); + securitySettings.setQrSecretKeyLength(DEFAULT_QR_SECRET_KEY_LENGTH); } return securitySettings; } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 33146b1b82..2a0c8693ad 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -579,6 +579,12 @@ cache: alarmTypes: timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" # Alarm types cache TTL maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" # 0 means the cache is disabled + mobileAppSettings: + timeToLiveInMinutes: "${CACHE_SPECS_MOBILE_APP_SETTINGS_TTL:1440}" # Mobile application cache TTL + maxSize: "${CACHE_SPECS_MOBILE_APP_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + qrSecretKey: + timeToLiveInMinutes: "${CACHE_QR_SECRET_KEY_TTL:2}" # QR secret key cache TTL + maxSize: "${CACHE_QR_SECRET_KEY_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: diff --git a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java index 430ff2eb09..2d1ae9fdbb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -181,4 +182,27 @@ public class AdminControllerTest extends AbstractControllerTest { resetJwtSettingsToDefault(); } + @Test + public void testSaveMobileAppSettings() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/admin/mobileAppSettings", MobileAppSettings.class); + assertThat(mobileAppSettings.getSettings().get("qrSecretKeyRefreshRateInMin").asInt()).isEqualTo(1); + + JsonNode jsonValue = mobileAppSettings.getSettings(); + ((ObjectNode) jsonValue).put("useDefault", false); + + doPost("/api/admin/mobileAppSettings", mobileAppSettings) + .andExpect(status().isOk()); + + doGet("/api/admin/mobileAppSettings") + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.settings.useDefault", is(false))); + + // check refresh rate should be less than secret ttl (5 min) + ((ObjectNode) jsonValue).put("qrSecretKeyRefreshRateInMin", 6); + doPost("/api/admin/mobileAppSettings", mobileAppSettings) + .andExpect(status().isBadRequest()); + } + } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 92959e4552..2e5944e0dc 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -8,6 +8,7 @@ transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credent transport.lwm2m.security.trust-credentials.enabled=true transport.lwm2m.security.trust-credentials.type=KEYSTORE transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/lwm2mtruststorechain.jks +cache.specs.qrSecretKey.timeToLiveInMinutes=5 # Edge disabled to speed up the context init. Will be enabled by @TestPropertySource in respective tests edges.enabled=false diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 9fda1c6bb5..8eb8cd8678 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -46,4 +46,6 @@ public class CacheConstants { public static final String ENTITY_COUNT_CACHE = "entityCount"; public static final String RESOURCE_INFO_CACHE = "resourceInfo"; public static final String ALARM_TYPES_CACHE = "alarmTypes"; + public static final String MOBILE_APP_SETTINGS_CACHE = "mobileAppSettings"; + public static final String QR_SECRET_KEY_CACHE = "qrSecretKey"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java new file mode 100644 index 0000000000..039143f4a9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.mobile; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.Serializable; + +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.BaseDataWithAdditionalInfo.setJson; + +@Data +public class MobileAppSettings implements Serializable { + + private static final long serialVersionUID = 2628323657987010348L; + + private TenantId tenantId; + + @NoXss + @Length(fieldName = "appPackage") + private String appPackage; + + @NoXss + @Length(fieldName = "sha256CertFingerprints") + private String sha256CertFingerprints; + + @NoXss + @Length(fieldName = "appId") + private String appId; + + @NoXss + @Length(fieldName = "settings", max = 10000000) + private transient JsonNode settings; + + @JsonIgnore + private byte[] settingsBytes; + + public JsonNode getSettings() { + return getJson(() -> settings, () -> settingsBytes); + } + + public void setSettings(JsonNode settings) { + setJson(settings, json -> this.settings = json, bytes -> this.settingsBytes = bytes); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java index 459aa7b26f..ae484136ce 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java @@ -20,10 +20,12 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.security.Authority; +import java.io.Serializable; + @Schema(description = "JWT Pair") @Data @NoArgsConstructor -public class JwtPair { +public class JwtPair implements Serializable { @Schema(description = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") private String token; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java index 07262c7eb4..b6ee1a17a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java @@ -32,4 +32,6 @@ public class SecuritySettings implements Serializable { private Integer maxFailedLoginAttempts; @Schema(description = "Email to use for notifications about locked users." ) private String userLockoutNotificationEmail; + @Schema(description = "QR secret key length" ) + private Integer qrSecretKeyLength; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java new file mode 100644 index 0000000000..fa035cc32d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.entity.AbstractCachedService; +import org.thingsboard.server.dao.exception.DataValidationException; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BaseMobileAppSettingsService extends AbstractCachedService implements MobileAppSettingsService { + + @Value("${cache.specs.qrSecretKey.timeToLiveInMinutes:2}") + private int secretKeyTtl; + + private static final int MIN_TIME_TO_DOWNLOAD_MOBILE_APP_IN_MIN = 1; + private final MobileAppSettingsDao mobileAppSettingsDao; + + @Override + public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings) { + if (settings.getSettings() != null && settings.getSettings().get("qrSecretKeyRefreshRateInMin") != null + && (settings.getSettings().get("qrSecretKeyRefreshRateInMin").asInt() + MIN_TIME_TO_DOWNLOAD_MOBILE_APP_IN_MIN > secretKeyTtl)) { + throw new DataValidationException("Refresh rate should be less than server secret key ttl"); + } + MobileAppSettings mobileAppSettings = mobileAppSettingsDao.save(tenantId, settings); + publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); + return mobileAppSettings; + } + + @Override + public MobileAppSettings getMobileAppSettings(TenantId tenantId) { + log.trace("Executing getMobileAppSettings for tenant [{}] ", tenantId); + MobileAppSettings mobileAppSettings = cache.getAndPutInTransaction(tenantId, + () -> mobileAppSettingsDao.findByTenantId(tenantId), false); + return constructMobileAppSettings(mobileAppSettings); + } + + @TransactionalEventListener(classes = MobileAppSettingsEvictEvent.class) + @Override + public void handleEvictEvent(MobileAppSettingsEvictEvent event) { + cache.evict(event.getTenantId()); + } + + private MobileAppSettings constructMobileAppSettings(MobileAppSettings mobileAppSettings) { + if (mobileAppSettings == null) { + mobileAppSettings = new MobileAppSettings(); + + ObjectNode settings = JacksonUtil.newObjectNode(); + settings.put("useDefault", true); + settings.put("showOnHomePage", true); + settings.put("qrLabel", "Scan to connect or download mobile app"); + settings.put("qrSecretKeyRefreshRateInMin", 1); + + ObjectNode androidSettings = JacksonUtil.newObjectNode(); + androidSettings.put("badge", "original"); + androidSettings.put("badgePosition", "Right"); + settings.set("android", androidSettings); + + ObjectNode iOSSettings = JacksonUtil.newObjectNode(); + iOSSettings.put("badge", "original"); + iOSSettings.put("badgePosition", "Right"); + settings.set("iOS", iOSSettings); + + mobileAppSettings.setSettings(settings); + } + return mobileAppSettings; + } + + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java new file mode 100644 index 0000000000..9e15ae8572 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsCaffeineCache.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("MobileAppCache") +public class MobileAppSettingsCaffeineCache extends CaffeineTbTransactionalCache { + + public MobileAppSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.MOBILE_APP_SETTINGS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java new file mode 100644 index 0000000000..ea8b4c3b3c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + + +public interface MobileAppSettingsDao { + + MobileAppSettings save(TenantId tenantId, MobileAppSettings whiteLabeling); + + MobileAppSettings findByTenantId(TenantId tenantId); + + void removeByTenantId(TenantId tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java new file mode 100644 index 0000000000..459e61ef6d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsEvictEvent.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class MobileAppSettingsEvictEvent { + private final TenantId tenantId; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java new file mode 100644 index 0000000000..9dd149abcb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsRedisCache.java @@ -0,0 +1,36 @@ + /** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("MobileAppCache") +public class MobileAppSettingsRedisCache extends RedisTbTransactionalCache { + + public MobileAppSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.MOBILE_APP_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(MobileAppSettings.class)); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java new file mode 100644 index 0000000000..b170e4ab91 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.mobile; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; + +public interface MobileAppSettingsService { + + MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings); + + MobileAppSettings getMobileAppSettings(TenantId tenantId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index ae15c2d029..f7f061e37a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -659,6 +659,15 @@ public class ModelConstants { public static final String NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY = "notification_type"; public static final String NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY = "configuration"; + /** + * Mobile application settings constants. + */ + public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings"; + public static final String MOBILE_APP_SETTINGS_APP_PACKAGE_PROPERTY = "app_package"; + public static final String MOBILE_APP_SETTINGS_SHA256_CERT_FINGERPRINTS_PROPERTY = "sha256_cert_fingerprints"; + public static final String MOBILE_APP_APP_ID_PROPERTY = "app_id"; + public static final String MOBILE_APP_SETTINGS_PROPERTY = "settings"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java new file mode 100644 index 0000000000..faceffdca8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.ToData; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.io.Serializable; +import java.util.UUID; + +@Data +@NoArgsConstructor +@Entity +@Table(name = ModelConstants.MOBILE_APP_SETTINGS_TABLE_NAME) +public class MobileAppSettingsEntity implements ToData, Serializable { + + @Id + @Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid") + protected UUID tenantId; + + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_APP_PACKAGE_PROPERTY) + private String appPackage; + + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_SHA256_CERT_FINGERPRINTS_PROPERTY) + private String sha256CertFingerprints; + + @Column(name = ModelConstants.MOBILE_APP_APP_ID_PROPERTY) + private String appId; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_PROPERTY) + private JsonNode settings; + + public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { + this.tenantId = mobileAppSettings.getTenantId().getId(); + if (mobileAppSettings.getAppPackage() != null) { + this.appPackage = mobileAppSettings.getAppPackage(); + } + if (mobileAppSettings.getSha256CertFingerprints() != null) { + this.sha256CertFingerprints = mobileAppSettings.getSha256CertFingerprints(); + } + if (mobileAppSettings.getAppId() != null) { + this.appId = mobileAppSettings.getAppId(); + } + if (mobileAppSettings.getSettings() != null) { + this.settings = mobileAppSettings.getSettings(); + } + } + + @Override + public MobileAppSettings toData() { + MobileAppSettings mobileAppSettings = new MobileAppSettings(); + mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId)); + if (appPackage != null) { + mobileAppSettings.setAppPackage(appPackage); + } + if (sha256CertFingerprints != null) { + mobileAppSettings.setSha256CertFingerprints(sha256CertFingerprints); + } + if (appId != null) { + mobileAppSettings.setAppId(appId); + } + if (settings != null) { + mobileAppSettings.setSettings(settings); + } + return mobileAppSettings; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java new file mode 100644 index 0000000000..f445cb0829 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.mobile; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.mobile.MobileAppSettingsDao; +import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; +import org.thingsboard.server.dao.util.SqlDao; + + +@Component +@Slf4j +@SqlDao +public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService implements MobileAppSettingsDao { + + @Autowired + private MobileAppSettingsRepository mobileAppSettingsRepository; + + @Override + public MobileAppSettings save(TenantId tenantId, MobileAppSettings whiteLabeling) { + return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(whiteLabeling))); + } + + @Override + public MobileAppSettings findByTenantId(TenantId tenantId) { + return DaoUtil.getData(mobileAppSettingsRepository.findById(tenantId.getId())); + } + + @Override + public void removeByTenantId(TenantId tenantId) { + mobileAppSettingsRepository.deleteByTenantId(tenantId.getId()); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java new file mode 100644 index 0000000000..a2339e1354 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.mobile; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity; + +import java.util.UUID; + + +public interface MobileAppSettingsRepository extends JpaRepository { + + @Transactional + @Modifying + @Query("DELETE FROM MobileAppSettingsEntity r WHERE r.tenantId = :tenantId") + void deleteByTenantId(@Param("tenantId") UUID tenantId); +} diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 1199f45181..cc6b950be9 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -893,4 +893,12 @@ CREATE TABLE IF NOT EXISTS queue_stats ( queue_name varchar(255) NOT NULL, service_id varchar(255) NOT NULL, CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id) +); + +CREATE TABLE IF NOT EXISTS mobile_app_settings ( + tenant_id UUID NOT NULL, + app_package VARCHAR(100) UNIQUE, + sha256_cert_fingerprints VARCHAR(10000), + app_id VARCHAR(100) UNIQUE, + settings VARCHAR(100000) ); \ No newline at end of file From 355acfc30a25eafefdea95abc0da23ae5eba2db5 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Fri, 19 Apr 2024 12:56:11 +0300 Subject: [PATCH 016/121] UI: gateway dashboard bug-fixes and improvements --- .../data/json/tenant/dashboards/gateways.json | 9 +- .../broker-security.component.ts | 16 ++-- .../device-info-table.component.ts | 9 +- .../ellipsis-chip-list.directive.ts | 37 +++++---- .../mapping-data-keys-panel.component.html | 10 ++- .../add-connector-dialog.component.html | 6 +- .../dialog/add-connector-dialog.component.ts | 13 ++- .../dialog/mapping-dialog.component.html | 18 +++- .../dialog/mapping-dialog.component.scss | 1 + .../dialog/mapping-dialog.component.ts | 12 +-- .../gateway/gateway-connectors.component.html | 13 +-- .../gateway/gateway-connectors.component.ts | 83 ++++++++++--------- .../lib/gateway/gateway-widget.models.ts | 15 ++++ .../widget/lib/gateway/expressions_fn.md | 3 - .../assets/locale/locale.constant-en_US.json | 52 ++++++------ 15 files changed, 168 insertions(+), 129 deletions(-) diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index 8acf269f8a..3b0ccc3cd8 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -136,7 +136,7 @@ { "name": "active_connectors", "type": "attribute", - "label": "Active Connectors", + "label": "Enabled Connectors", "color": "#3f51b5", "settings": { "columnWidth": "20%", @@ -248,7 +248,7 @@ "type": "customPretty", "customHtml": "
\r\n \r\n

Add gateway

\r\n \r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n Name\r\n \r\n \r\n Gateway name is required.\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n", "customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n", - "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n setTimeout(function() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").addEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").addEventListener('click', goToConfigState);\r\n }, 500);\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").removeEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").removeEventListener('click', goToConfigState);\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", + "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", "customResources": [], "openInSeparateDialog": false, "openInPopover": false, @@ -567,7 +567,8 @@ "padding": "8px", "settings": { "useMarkdownTextFunction": true, - "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Active | Inactive)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", + "markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.", + "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return ``\n } else {\n return \"\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1]?data[1].count:0)} `\n + \" | \" + \n `${(data[2]?data[2][\"count 2\"]:0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} `\n + \" | \" + \n `${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} `\n , \"Connectors (Enabled | Disabled)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", "applyDefaultMarkdownStyle": false, "markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}" }, @@ -6741,4 +6742,4 @@ }, "externalId": null, "name": "Gateway" -} +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts index e67bb61207..655545e5fb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts @@ -15,14 +15,13 @@ /// import { - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, forwardRef, + ElementRef, + forwardRef, NgZone, OnDestroy, - OnInit, ViewContainerRef } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; @@ -47,7 +46,8 @@ import { } from '@angular/forms'; import { BrokerSecurityType, - BrokerSecurityTypeTranslationsMap, noLeadTrailSpacesRegex, + BrokerSecurityTypeTranslationsMap, + noLeadTrailSpacesRegex } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { takeUntil } from 'rxjs/operators'; @@ -69,7 +69,7 @@ import { takeUntil } from 'rxjs/operators'; } ] }) -export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { +export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy { BrokerSecurityType = BrokerSecurityType; @@ -115,18 +115,12 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal }); } - ngOnInit() { - } - ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); super.ngOnDestroy(); } - ngAfterViewInit() { - } - registerOnChange(fn: any): void { this.propagateChange = fn; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts index f292993682..8a40f25430 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts @@ -15,7 +15,6 @@ /// import { - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -40,7 +39,8 @@ import { UtilsService } from '@core/services/utils.service'; import { EntityService } from '@core/http/entity.service'; import { ControlValueAccessor, - FormBuilder, NG_VALIDATORS, + FormBuilder, + NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormGroup, ValidationErrors, @@ -73,7 +73,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; } ] }) -export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { +export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { SourceTypeTranslationsMap = SourceTypeTranslationsMap; @@ -156,9 +156,6 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa this.destroy$.complete(); } - ngAfterViewInit() { - } - registerOnChange(fn: any): void { this.propagateChange = fn; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts index 6d525aa4c0..ac9e976e50 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts @@ -15,26 +15,31 @@ /// import { - AfterViewInit, Directive, ElementRef, + Inject, Input, OnDestroy, Renderer2 } from '@angular/core'; import { isEqual } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; +import { WINDOW } from '@core/services/window.service'; +import { fromEvent, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector selector: '[tb-ellipsis-chip-list]' }) -export class EllipsisChipListDirective implements AfterViewInit, OnDestroy { +export class EllipsisChipListDirective implements OnDestroy { chipsValue: string[]; + private destroy$ = new Subject(); + @Input('tb-ellipsis-chip-list') - set chips(value: any[]) { + set chips(value: string[]) { if (!isEqual(this.chipsValue, value)) { this.chipsValue = value; setTimeout(() => { @@ -45,11 +50,15 @@ export class EllipsisChipListDirective implements AfterViewInit, OnDestroy { constructor(private el: ElementRef, private renderer: Renderer2, - private translate: TranslateService) {} - - ngAfterViewInit(): void { - this.adjustChips(); - window.addEventListener('resize', this.adjustChips.bind(this)); + private translate: TranslateService, + @Inject(WINDOW) private window: Window) { + this.renderer.setStyle(this.el.nativeElement, 'max-height', '48px'); + this.renderer.setStyle(this.el.nativeElement, 'overflow', 'auto'); + fromEvent(window, 'resize').pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + this.adjustChips(); + }); } private adjustChips(): void { @@ -58,17 +67,16 @@ export class EllipsisChipListDirective implements AfterViewInit, OnDestroy { const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)'); const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip'); this.renderer.setStyle(ellipsisChip,'display', 'inline-flex'); + ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text', + {count: (this.chipsValue.length)}); - const margin = parseFloat(window.getComputedStyle(ellipsisChip).marginLeft) | 0; + const margin = parseFloat(this.window.getComputedStyle(ellipsisChip).marginLeft) || 0; const availableWidth = chipListElement.offsetWidth - (ellipsisChip.offsetWidth + margin); let usedWidth = 0; let visibleChipsCount = 0; chipNodes.forEach((chip) => { this.renderer.setStyle(chip, 'display', 'inline-flex'); - }); - - chipNodes.forEach((chip) => { if ((usedWidth + (chip.offsetWidth + margin) <= availableWidth) && (visibleChipsCount < this.chipsValue.length)) { visibleChipsCount++; usedWidth += chip.offsetWidth + margin; @@ -85,7 +93,8 @@ export class EllipsisChipListDirective implements AfterViewInit, OnDestroy { } } - ngOnDestroy() { - window.removeEventListener('resize', this.adjustChips.bind(this)); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html index d7b2222e59..95274d54e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html @@ -35,7 +35,10 @@
gateway.platform-side
-
gateway.key
+
+ gateway.key +
@@ -89,7 +92,10 @@
-
gateway.value
+
+ gateway.value +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html index b4dd8515df..ec1fa9a87f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html @@ -26,9 +26,6 @@ close - - -
@@ -91,13 +88,12 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index 6294eebbc4..78a22cd267 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Component, Inject, OnDestroy } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -23,7 +23,9 @@ import { BaseData, HasId } from '@shared/models/base-data'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { + AddConnectorConfigData, ConnectorType, + CreatedConnectorConfigData, GatewayConnectorDefaultTypesTranslatesMap, GatewayLogLevel, getDefaultConfig, @@ -38,7 +40,7 @@ import { ResourcesService } from '@core/services/resources.service'; styleUrls: ['./add-connector-dialog.component.scss'], providers: [], }) -export class AddConnectorDialogComponent extends DialogComponent> implements OnInit, OnDestroy { +export class AddConnectorDialogComponent extends DialogComponent> implements OnDestroy { connectorForm: UntypedFormGroup; @@ -53,8 +55,8 @@ export class AddConnectorDialogComponent extends DialogComponent, protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: any, - public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData, + public dialogRef: MatDialogRef, private fb: FormBuilder, private resourcesService: ResourcesService) { super(store, router, dialogRef); @@ -67,9 +69,6 @@ export class AddConnectorDialogComponent extends DialogComponent
-
gateway.extension
+
+ gateway.extension +
@@ -237,6 +240,9 @@
gateway.from-device-request-settings
+
+ gateway.from-device-request-settings-hint +
gateway.device-info.device-name-expression
@@ -302,6 +308,9 @@
gateway.to-device-response-settings
+
+ gateway.to-device-response-settings-hint +
gateway.response-value-expression
@@ -359,7 +368,10 @@
-
gateway.device-name-filter
+
+ gateway.device-name-filter +
@@ -368,7 +380,7 @@ matTooltipClass="tb-error-tooltip" [matTooltip]="('gateway.device-name-filter-required') | translate" *ngIf="mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').hasError('required') && - mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').touched" + mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').touched" class="tb-error"> warning diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss index 253a81e0d8..8a35e7b17d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss @@ -34,6 +34,7 @@ .mat-mdc-dialog-content { max-height: 670px; + height: 670px; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index fc51d17bc0..1347116fd4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Inject, OnDestroy, OnInit, Renderer2, ViewContainerRef } from '@angular/core'; +import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -27,7 +27,6 @@ import { ConvertorTypeTranslationsMap, DataConversionTranslationsMap, DeviceInfoType, - MappingDataKey, MappingHintTranslationsMap, MappingInfo, MappingKeysAddKeyTranslationsMap, @@ -58,7 +57,7 @@ import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gatew styleUrls: ['./mapping-dialog.component.scss'], providers: [], }) -export class MappingDialogComponent extends DialogComponent> implements OnInit, OnDestroy { +export class MappingDialogComponent extends DialogComponent> implements OnDestroy { mappingForm: UntypedFormGroup; @@ -118,7 +117,7 @@ export class MappingDialogComponent extends DialogComponent { + get converterTelemetry(): Array { if (this.converterType) { return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key); } @@ -128,7 +127,7 @@ export class MappingDialogComponent extends DialogComponent { return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig); } @@ -136,9 +135,6 @@ export class MappingDialogComponent extends DialogComponent @@ -127,7 +128,7 @@ private_connectivity
+ + widget.no-data-found + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss index affc4e5982..707b5fa056 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss @@ -71,6 +71,10 @@ } } + .no-data-found { + height: calc(100% - 120px); + } + @media #{$mat-xs} { .mat-toolbar { height: auto; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts index a5c6addfe4..4ef9695c65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts @@ -215,6 +215,7 @@ export class MappingTableComponent extends PageComponent implements ControlValue } exitFilterMode() { + this.updateTableData(this.mappingFormGroup.value); this.textSearchMode = false; this.textSearch.reset(); } From 0003caf4fb79c7dd4fc350ce56af64ef64616c03 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 30 Apr 2024 18:46:38 +0300 Subject: [PATCH 033/121] added id, createdTime to mobile app setting entity, added MobileAppSettingsValidator --- .../main/data/upgrade/3.6.4/schema_update.sql | 4 +- .../MobileApplicationController.java | 27 +++++- .../MobileApplicationControllerTest.java | 85 +++++++++++++++++-- .../common/data/id/MobileAppSettingsId.java | 37 ++++++++ .../common/data/mobile/MobileAppSettings.java | 13 ++- .../mobile/BaseMobileAppSettingsService.java | 30 +++++-- .../dao/mobile/MobileAppSettingsDao.java | 5 +- .../server/dao/model/ModelConstants.java | 4 +- .../model/sql/MobileAppSettingsEntity.java | 44 ++++------ .../MobileAppSettingsDataValidator.java | 51 +++++++++++ ...pDao.java => JpaMobileAppSettingsDao.java} | 22 +++-- .../mobile/MobileAppSettingsRepository.java | 2 + .../main/resources/sql/schema-entities.sql | 4 +- 13 files changed, 268 insertions(+), 60 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java rename dao/src/main/java/org/thingsboard/server/dao/sql/mobile/{JpaMobileAppDao.java => JpaMobileAppSettingsDao.java} (73%) diff --git a/application/src/main/data/upgrade/3.6.4/schema_update.sql b/application/src/main/data/upgrade/3.6.4/schema_update.sql index 5f00b7c2cf..03e8ca0a29 100644 --- a/application/src/main/data/upgrade/3.6.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.4/schema_update.sql @@ -138,7 +138,9 @@ DELETE FROM asset_profile WHERE name ='TbServiceQueue'; -- MOBILE APP SETTINGS TABLE CREATE START CREATE TABLE IF NOT EXISTS mobile_app_settings ( - tenant_id UUID NOT NULL, + id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid UNIQUE NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index e8ac13b077..96b1bf4d35 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -20,17 +20,17 @@ import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.AndroidConfig; import org.thingsboard.server.common.data.mobile.IosConfig; @@ -81,10 +81,13 @@ public class MobileApplicationController extends BaseController { " }\n" + "}"; + public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app"; + public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695"; public static final String SECRET = "secret"; public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-live secret key"; public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io"; public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s"; + private final SystemSecurityService systemSecurityService; private final MobileAppSecretService mobileAppSecretService; private final MobileAppSettingsService mobileAppSettingsService; @@ -143,7 +146,7 @@ public class MobileApplicationController extends BaseController { @GetMapping(value = "/api/mobile/deepLink", produces = "text/plain") public String getMobileAppDeepLink(HttpServletRequest request) throws ThingsboardException, URISyntaxException { String secret = mobileAppSecretService.generateMobileAppSecret(getCurrentUser()); - String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, null, request); String platformDomain = new URI(baseUrl).getHost(); MobileAppSettings mobileAppSettings = mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); String appDomain; @@ -156,7 +159,7 @@ public class MobileApplicationController extends BaseController { if (!appDomain.equals(platformDomain)) { deepLink = deepLink + "&host=" + baseUrl; } - return deepLink; + return "\"" + deepLink + "\""; } @ApiOperation(value = "Get User Token (getUserTokenByMobileSecret)", @@ -167,4 +170,20 @@ public class MobileApplicationController extends BaseController { checkParameter(SECRET, secret); return mobileAppSecretService.getJwtPair(secret); } + + @GetMapping(value = "/api/noauth/qr") + public ResponseEntity getApplicationRedirect(@RequestHeader(value = "User-Agent") String userAgent) { + if (userAgent.contains("Android")) { + return ResponseEntity.status(HttpStatus.FOUND) + .header("Location", ANDROID_APPLICATION_STORE_LINK) + .build(); + } else if (userAgent.contains("iPhone") || userAgent.contains("iPad")) { + return ResponseEntity.status(HttpStatus.FOUND) + .header("Location", APPLE_APPLICATION_STORE_LINK) + .build(); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .build(); + } + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index b7dd9b5b3b..c4f1301457 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -31,6 +31,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j @@ -48,10 +49,10 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { public void setUp() throws Exception { loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); QRCodeConfig qrCodeConfig = new QRCodeConfig(); qrCodeConfig.setQrCodeLabel(TEST_LABEL); - MobileAppSettings mobileAppSettings = new MobileAppSettings(); mobileAppSettings.setUseDefaultApp(true); AndroidConfig androidConfig = AndroidConfig.builder() .appPackage(ANDROID_PACKAGE_NAME) @@ -67,7 +68,8 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { mobileAppSettings.setIosConfig(iosConfig); mobileAppSettings.setQrCodeConfig(qrCodeConfig); - doPost("/api/mobile/app/settings", mobileAppSettings); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); } @Test @@ -86,6 +88,79 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { assertThat(updatedMobileAppSettings.isUseDefaultApp()).isFalse(); } + @Test + public void testShouldNotSaveMobileAppSettingsWithoutRequiredConfig() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + + mobileAppSettings.setUseDefaultApp(false); + mobileAppSettings.setAndroidConfig(null); + mobileAppSettings.setIosConfig(null); + mobileAppSettings.setQrCodeConfig(null); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); + + mobileAppSettings.setAndroidConfig(AndroidConfig.builder().enabled(false).build()); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Android/ios settings are required to use custom application!"))); + + mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build()); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Qr code config is required!"))); + + mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build()); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + + @Test + public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + AndroidConfig androidConfig = AndroidConfig.builder() + .enabled(true) + .appPackage(null) + .sha256CertFingerprints(null) + .build(); + mobileAppSettings.setAndroidConfig(androidConfig); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + + androidConfig.setAppPackage("test_app_package"); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + + androidConfig.setSha256CertFingerprints("test_sha_256"); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + + @Test + public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + IosConfig iosConfig = IosConfig.builder() + .enabled(true) + .appId(null) + .build(); + mobileAppSettings.setIosConfig(iosConfig); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Application id is required for enabled ios settings!"))); + + iosConfig.setAppId("test_app_id"); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + @Test public void testGetApplicationAssociations() throws Exception { JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); @@ -101,7 +176,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { loginSysAdmin(); String deepLink = doGet("/api/mobile/deepLink", String.class); - Pattern expectedPattern = Pattern.compile("https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)"); + Pattern expectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)&host=([^&]+)\""); Matcher parsedDeepLink = expectedPattern.matcher(deepLink); assertThat(parsedDeepLink.matches()).isTrue(); String appHost = parsedDeepLink.group(1); @@ -138,7 +213,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { doPost("/api/mobile/app/settings", mobileAppSettings); String customAppDeepLink = doGet("/api/mobile/deepLink", String.class); - Pattern customAppExpectedPattern = Pattern.compile("https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)"); + Pattern customAppExpectedPattern = Pattern.compile("\"https://([^/]+)/api/noauth/qr\\?secret=([^&]+)&ttl=([^&]+)\""); Matcher customAppParsedDeepLink = customAppExpectedPattern.matcher(customAppDeepLink); assertThat(customAppParsedDeepLink.matches()).isTrue(); assertThat(customAppParsedDeepLink.group(1)).isEqualTo("localhost"); @@ -154,7 +229,5 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { Matcher customerCustomAppParsedDeepLink = customAppExpectedPattern.matcher(customerCustomAppDeepLink); assertThat(customerCustomAppParsedDeepLink.matches()).isTrue(); assertThat(customerCustomAppParsedDeepLink.group(1)).isEqualTo("localhost"); - - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java new file mode 100644 index 0000000000..d152701fc4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/MobileAppSettingsId.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.UUID; + +@Schema +public class MobileAppSettingsId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public MobileAppSettingsId(@JsonProperty("id") UUID id) { + super(id); + } + + public static MobileAppSettingsId fromString(String mobileAppSettingsId) { + return new MobileAppSettingsId(UUID.fromString(mobileAppSettingsId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java index 217db862f8..9a7b88f22c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/MobileAppSettings.java @@ -17,12 +17,13 @@ package org.thingsboard.server.common.data.mobile; import jakarta.validation.Valid; import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.MobileAppSettingsId; import org.thingsboard.server.common.data.id.TenantId; -import java.io.Serializable; - @Data -public class MobileAppSettings implements Serializable { +public class MobileAppSettings extends BaseData implements HasTenantId { private static final long serialVersionUID = 2628323657987010348L; @@ -35,4 +36,10 @@ public class MobileAppSettings implements Serializable { @Valid private QRCodeConfig qrCodeConfig; + public MobileAppSettings() { + } + public MobileAppSettings(MobileAppSettingsId id) { + super(id); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java index a72498b3b2..86de46fed1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.mobile; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.id.TenantId; @@ -26,24 +27,41 @@ import org.thingsboard.server.common.data.mobile.BadgeStyle; import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; -import org.thingsboard.server.dao.entity.AbstractCachedService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; import static org.thingsboard.server.dao.service.Validator.validateId; @Service @Slf4j @RequiredArgsConstructor -public class BaseMobileAppSettingsService extends AbstractCachedService implements MobileAppSettingsService { +public class BaseMobileAppSettingsService extends AbstractCachedEntityService implements MobileAppSettingsService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final String DEFAULT_QR_CODE_LABEL = "Scan to connect or download mobile app"; + private final MobileAppSettingsDao mobileAppSettingsDao; + private final DataValidator mobileAppSettingsDataValidator; @Override - public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings settings) { - MobileAppSettings mobileAppSettings = mobileAppSettingsDao.save(tenantId, settings); - publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); - return mobileAppSettings; + public MobileAppSettings saveMobileAppSettings(TenantId tenantId, MobileAppSettings mobileAppSettings) { + mobileAppSettingsDataValidator.validate(mobileAppSettings, s -> tenantId); + try { + MobileAppSettings savedMobileAppSettings = mobileAppSettingsDao.save(tenantId, mobileAppSettings); + publishEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); + return savedMobileAppSettings; + } catch (Exception exception) { + if (mobileAppSettings != null) { + handleEvictEvent(new MobileAppSettingsEvictEvent(tenantId)); + } + ConstraintViolationException e = extractConstraintViolationException(exception).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("mobile_app_settings_tenant_id_key")) { + throw new DataValidationException("Mobile application for specified tenant already exists!"); + } else { + throw exception; + } + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java index 54661bccb0..094ead1a5b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppSettingsDao.java @@ -17,11 +17,10 @@ package org.thingsboard.server.dao.mobile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.dao.Dao; -public interface MobileAppSettingsDao { - - MobileAppSettings save(TenantId tenantId, MobileAppSettings appSettings); +public interface MobileAppSettingsDao extends Dao { MobileAppSettings findByTenantId(TenantId tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 2b770cbc8e..623d899293 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -665,8 +665,8 @@ public class ModelConstants { public static final String MOBILE_APP_SETTINGS_TABLE_NAME = "mobile_app_settings"; public static final String MOBILE_APP_SETTINGS_USE_DEFAULT_APP_PROPERTY = "use_default_app"; public static final String MOBILE_APP_SETTINGS_ANDROID_CONFIG_PROPERTY = "android_config"; - public static final String MOBILE_APP_IOS_CONFIG_PROPERTY = "ios_config"; - public static final String MOBILE_APP_QR_CODE_CONFIG_PROPERTY = "qr_code_config"; + public static final String MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY = "ios_config"; + public static final String MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY = "qr_code_config"; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java index aa3f77a58d..0e6ad933dc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppSettingsEntity.java @@ -19,30 +19,27 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; -import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.MobileAppSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.AndroidConfig; import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.ToData; import org.thingsboard.server.dao.util.mapping.JsonConverter; -import java.io.Serializable; import java.util.UUID; @Data @NoArgsConstructor @Entity @Table(name = ModelConstants.MOBILE_APP_SETTINGS_TABLE_NAME) -public class MobileAppSettingsEntity implements ToData, Serializable { +public class MobileAppSettingsEntity extends BaseSqlEntity { - @Id @Column(name = ModelConstants.TENANT_ID_COLUMN, columnDefinition = "uuid") protected UUID tenantId; @@ -54,41 +51,34 @@ public class MobileAppSettingsEntity implements ToData, Seria private JsonNode androidConfig; @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.MOBILE_APP_IOS_CONFIG_PROPERTY) + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_IOS_CONFIG_PROPERTY) private JsonNode iosConfig; @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.MOBILE_APP_QR_CODE_CONFIG_PROPERTY) + @Column(name = ModelConstants.MOBILE_APP_SETTINGS_QR_CODE_CONFIG_PROPERTY) private JsonNode qrCodeConfig; public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { + if (mobileAppSettings.getId() != null) { + this.setId(mobileAppSettings.getId().getId()); + } + this.setCreatedTime(mobileAppSettings.getCreatedTime()); this.tenantId = mobileAppSettings.getTenantId().getId(); this.useDefaultApp = mobileAppSettings.isUseDefaultApp(); - if (mobileAppSettings.getAndroidConfig() != null) { - this.androidConfig = JacksonUtil.valueToTree(mobileAppSettings.getAndroidConfig()); - } - if (mobileAppSettings.getIosConfig() != null) { - this.iosConfig = JacksonUtil.valueToTree(mobileAppSettings.getIosConfig()); - } - if (mobileAppSettings.getQrCodeConfig() != null) { - this.qrCodeConfig = JacksonUtil.valueToTree(mobileAppSettings.getQrCodeConfig()); - } + this.androidConfig = toJson(mobileAppSettings.getAndroidConfig()); + this.iosConfig = toJson(mobileAppSettings.getIosConfig()); + this.qrCodeConfig = toJson(mobileAppSettings.getQrCodeConfig()); } @Override public MobileAppSettings toData() { - MobileAppSettings mobileAppSettings = new MobileAppSettings(); + MobileAppSettings mobileAppSettings = new MobileAppSettings(new MobileAppSettingsId(getUuid())); + mobileAppSettings.setCreatedTime(createdTime); mobileAppSettings.setTenantId(TenantId.fromUUID(tenantId)); mobileAppSettings.setUseDefaultApp(useDefaultApp); - if (qrCodeConfig != null) { - mobileAppSettings.setAndroidConfig(JacksonUtil.convertValue(androidConfig, AndroidConfig.class)); - } - if (qrCodeConfig != null) { - mobileAppSettings.setIosConfig(JacksonUtil.convertValue(iosConfig, IosConfig.class)); - } - if (qrCodeConfig != null) { - mobileAppSettings.setQrCodeConfig(JacksonUtil.convertValue(qrCodeConfig, QRCodeConfig.class)); - } + mobileAppSettings.setAndroidConfig(fromJson(androidConfig, AndroidConfig.class)); + mobileAppSettings.setIosConfig(fromJson(iosConfig, IosConfig.class)); + mobileAppSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class)); return mobileAppSettings; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java new file mode 100644 index 0000000000..6400cd08af --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.AndroidConfig; +import org.thingsboard.server.common.data.mobile.IosConfig; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.QRCodeConfig; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class MobileAppSettingsDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, MobileAppSettings mobileAppSettings) { + AndroidConfig androidConfig = mobileAppSettings.getAndroidConfig(); + IosConfig iosConfig = mobileAppSettings.getIosConfig(); + QRCodeConfig qrCodeConfig = mobileAppSettings.getQrCodeConfig(); + if (!mobileAppSettings.isUseDefaultApp() && (androidConfig == null || iosConfig == null)) { + throw new DataValidationException("Android/ios settings are required to use custom application!"); + } + if (qrCodeConfig == null) { + throw new DataValidationException("Qr code config is required!"); + } + if (androidConfig != null && androidConfig.isEnabled() && + (androidConfig.getAppPackage() == null || androidConfig.getSha256CertFingerprints() == null)) { + throw new DataValidationException("Application package and sha256 cert fingerprints are required for enabled android settings!"); + } + if (iosConfig != null && iosConfig.isEnabled() && iosConfig.getAppId() == null) { + throw new DataValidationException("Application id is required for enabled ios settings!"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java similarity index 73% rename from dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java index 8c732e0ffc..e4286bb949 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/JpaMobileAppSettingsDao.java @@ -17,32 +17,31 @@ package org.thingsboard.server.dao.sql.mobile; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.mobile.MobileAppSettingsDao; import org.thingsboard.server.dao.model.sql.MobileAppSettingsEntity; -import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; +import java.util.UUID; + @Component @Slf4j @SqlDao -public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService implements MobileAppSettingsDao { +public class JpaMobileAppSettingsDao extends JpaAbstractDao implements MobileAppSettingsDao { @Autowired private MobileAppSettingsRepository mobileAppSettingsRepository; - @Override - public MobileAppSettings save(TenantId tenantId, MobileAppSettings mobileAppSettings) { - return DaoUtil.getData(mobileAppSettingsRepository.save(new MobileAppSettingsEntity(mobileAppSettings))); - } @Override public MobileAppSettings findByTenantId(TenantId tenantId) { - return DaoUtil.getData(mobileAppSettingsRepository.findById(tenantId.getId())); + return DaoUtil.getData(mobileAppSettingsRepository.findByTenantId(tenantId.getId())); } @Override @@ -50,4 +49,13 @@ public class JpaMobileAppDao extends JpaAbstractDaoListeningExecutorService impl mobileAppSettingsRepository.deleteByTenantId(tenantId.getId()); } + @Override + protected Class getEntityClass() { + return MobileAppSettingsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return mobileAppSettingsRepository; + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java index a2339e1354..bb6e62f3fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/mobile/MobileAppSettingsRepository.java @@ -27,6 +27,8 @@ import java.util.UUID; public interface MobileAppSettingsRepository extends JpaRepository { + MobileAppSettingsEntity findByTenantId(@Param("tenantId") UUID tenantId); + @Transactional @Modifying @Query("DELETE FROM MobileAppSettingsEntity r WHERE r.tenantId = :tenantId") diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 72dae5394b..91df2e81bc 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -896,7 +896,9 @@ CREATE TABLE IF NOT EXISTS queue_stats ( ); CREATE TABLE IF NOT EXISTS mobile_app_settings ( - tenant_id UUID NOT NULL, + id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid UNIQUE NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), From 5a8bded35fdd66b84a8f50b735c93e2b95746025 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 30 Apr 2024 18:28:37 +0200 Subject: [PATCH 034/121] JUnit5 migration in msa. Signed-off-by: Oleksandra Matviienko --- msa/black-box-tests/pom.xml | 5 ----- msa/vc-executor/pom.xml | 5 ----- 2 files changed, 10 deletions(-) diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index f1b7525438..f433b1930c 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -63,11 +63,6 @@ spring-boot-starter-test test - - org.junit.vintage - junit-vintage-engine - test - org.testng testng diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 34340ab08c..081ebd1d3c 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -75,11 +75,6 @@ spring-boot-starter-test test - - org.junit.vintage - junit-vintage-engine - test - org.awaitility awaitility From f9348633886edd5bed543a6bcb8463031375f373 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 2 May 2024 12:56:47 +0300 Subject: [PATCH 035/121] fixed api test --- .../server/config/ThingsboardSecurityConfiguration.java | 2 +- .../server/controller/MobileApplicationController.java | 2 -- .../server/controller/MobileApplicationControllerTest.java | 6 ++++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index eda8d3e89d..c83f6500b2 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -77,7 +77,7 @@ public class ThingsboardSecurityConfiguration { public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_ENTRY_POINT = "/api/ws/**"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index 96b1bf4d35..ddd49ab975 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -125,7 +125,6 @@ public class MobileApplicationController extends BaseController { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.WRITE); mobileAppSettings.setTenantId(getTenantId()); - return mobileAppSettingsService.saveMobileAppSettings(currentUser.getTenantId(), mobileAppSettings); } @@ -136,7 +135,6 @@ public class MobileApplicationController extends BaseController { public MobileAppSettings getMobileAppSettings() throws ThingsboardException { SecurityUser currentUser = getCurrentUser(); accessControlService.checkPermission(currentUser, Resource.MOBILE_APP_SETTINGS, Operation.READ); - return mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID); } diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index c4f1301457..ad049295ea 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -163,6 +163,12 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { @Test public void testGetApplicationAssociations() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(false); + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + JsonNode assetLinks = doGet("/.well-known/assetlinks.json", JsonNode.class); assertThat(assetLinks.get(0).get("target").get("package_name").asText()).isEqualTo(ANDROID_PACKAGE_NAME); assertThat(assetLinks.get(0).get("target").get("sha256_cert_fingerprints").get(0).asText()).isEqualTo(ANDROID_APP_SHA256); From fb91898bd3e561483014483c1e2a5cde93af4248 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Thu, 2 May 2024 16:57:25 +0300 Subject: [PATCH 036/121] UI: improvements for connectors config --- .../ellipsis-chip-list.directive.ts | 16 ++++++++++++---- .../mapping-data-keys-panel.component.scss | 2 +- .../dialog/mapping-dialog.component.html | 10 ++++++---- .../gateway/dialog/mapping-dialog.component.ts | 17 +++++++++++++++-- .../assets/locale/locale.constant-en_US.json | 5 +---- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts index e5a6c5a54c..aa4738d707 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts @@ -79,6 +79,10 @@ export class EllipsisChipListDirective implements OnDestroy { chipNodes.forEach((chip) => { this.renderer.setStyle(chip, 'display', 'inline-flex'); + const textLabelContainer = chip.querySelector('.mdc-evolution-chip__text-label'); + + this.applyMaxChipTextWidth(textLabelContainer, (availableWidth / 3)); + if ((usedWidth + (chip.offsetWidth + margin) <= availableWidth) && (visibleChipsCount < this.chipsValue.length)) { visibleChipsCount++; usedWidth += chip.offsetWidth + margin; @@ -104,15 +108,19 @@ export class EllipsisChipListDirective implements OnDestroy { this.renderer.setStyle(ellipsisChip, 'display', 'none'); this.renderer.setStyle(chipNodes[0], 'display', 'inline-flex'); - this.renderer.setStyle(textLabelContainer, 'max-width', computedTextWidth + 'px'); - this.renderer.setStyle(textLabelContainer, 'overflow', 'hidden'); - this.renderer.setStyle(textLabelContainer, 'text-overflow', 'ellipsis'); - this.renderer.setStyle(textLabelContainer, 'white-space', 'nowrap'); + this.applyMaxChipTextWidth(textLabelContainer, computedTextWidth); } else { this.renderer.setStyle(ellipsisChip, 'display', 'none'); } } + private applyMaxChipTextWidth(element: HTMLElement, widthLimit: number): void { + this.renderer.setStyle(element, 'max-width', widthLimit + 'px'); + this.renderer.setStyle(element, 'overflow', 'hidden'); + this.renderer.setStyle(element, 'text-overflow', 'ellipsis'); + this.renderer.setStyle(element, 'white-space', 'nowrap'); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss index 7f6aef05c0..24498863c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss @@ -20,7 +20,7 @@ max-width: 700px; .title-container { - max-width: 14vw; + max-width: 11vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html index 97366bd26b..78ceaa566f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html @@ -595,13 +595,15 @@
gateway.response-timeout
- + warning diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index b083439ef7..d01050b52e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -50,6 +50,7 @@ import { startWith, takeUntil } from 'rxjs/operators'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-mapping-dialog', @@ -105,7 +106,8 @@ export class MappingDialogComponent extends DialogComponent Date: Thu, 2 May 2024 19:33:39 +0300 Subject: [PATCH 037/121] code formatting --- .../server/config/ThingsboardSecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 693dd4bfec..a3111cb137 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -76,7 +76,7 @@ public class ThingsboardSecurityConfiguration { public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**", "/.well-known/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_ENTRY_POINT = "/api/ws/**"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; From a057f42e95425b629d86dfd5687dd14932703321 Mon Sep 17 00:00:00 2001 From: rusikv Date: Thu, 2 May 2024 21:37:11 +0300 Subject: [PATCH 038/121] UI: mobile app QR code UI initial implementation --- .../widget_bundles/home_page_widgets.json | 3 +- .../widget_types/mobile_app_qr_code.json | 21 ++ .../src/app/core/http/mobile-app.service.ts | 45 ++++ ui-ngx/src/app/core/services/menu.service.ts | 8 + ui-ngx/src/app/core/services/utils.service.ts | 5 +- .../mobile-app-qrcode-widget.component.html | 40 ++++ .../mobile-app-qrcode-widget.component.scss | 56 +++++ .../lib/mobile-app-qrcode-widget.component.ts | 111 ++++++++++ .../widget/widget-components.module.ts | 3 + .../home/pages/admin/admin-routing.module.ts | 14 ++ .../modules/home/pages/admin/admin.module.ts | 18 +- .../admin/mobile-app-settings.component.html | 161 ++++++++++++++ .../admin/mobile-app-settings.component.scss | 28 +++ .../admin/mobile-app-settings.component.ts | 197 ++++++++++++++++++ .../app/shared/models/mobile-app.models.ts | 81 +++++++ .../android_store_en_black_badge.svg | 22 ++ .../ios_store_en_black_badge.svg | 46 ++++ .../ios_store_en_white_badge.svg | 46 ++++ .../dashboard/tenant_admin_home_page.json | 53 ++++- .../assets/locale/locale.constant-en_US.json | 27 +++ ui-ngx/src/form.scss | 15 ++ 21 files changed, 987 insertions(+), 13 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/mobile_app_qr_code.json create mode 100644 ui-ngx/src/app/core/http/mobile-app.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts create mode 100644 ui-ngx/src/app/shared/models/mobile-app.models.ts create mode 100644 ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg create mode 100755 ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg create mode 100755 ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index a30d6ee76f..5465f750e5 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -13,6 +13,7 @@ "home_page_widgets.quick_links", "home_page_widgets.documentation_links", "home_page_widgets.dashboards", - "home_page_widgets.usage_info" + "home_page_widgets.usage_info", + "home_page_widgets.mobile_app_qr_code" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json new file mode 100644 index 0000000000..2e3600d05b --- /dev/null +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.mobile_app_qr_code", + "name": "Mobile app QR code", + "deprecated": false, + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 6, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnInit();\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnDestroy();\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "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\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" + }, + "tags": null +} \ No newline at end of file diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts new file mode 100644 index 0000000000..75d5f337bc --- /dev/null +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { MobileAppSettings } from '@shared/models/mobile-app.models'; + +@Injectable({ + providedIn: 'root' +}) +export class MobileAppService { + + constructor( + private http: HttpClient + ) { + } + + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + } + + public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + } + + public getMobileAppDeepLink( config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 1f2d76a8b8..ffb5b5f0a6 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -245,6 +245,14 @@ export class MenuService { path: '/settings/queues', icon: 'swap_calls' }, + { + id: 'mobile_app_settings', + name: 'admin.mobile-app.mobile-app', + fullName: 'admin.mobile-app.mobile-app', + type: 'link', + path: '/settings/mobile-app', + icon: 'smartphone' + } ] }, { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 70b04ee4eb..d7238c450d 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -30,7 +30,7 @@ import { guid, hashCode, isDefined, - isDefinedAndNotNull, + isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined, objToBase64, @@ -386,8 +386,7 @@ export class UtilsService { this.window.performance.now() : Date.now(); } - public getQueryParam(name: string): string { - const url = this.window.location.href; + public getQueryParam(name: string, url = this.window.location.href): string { name = name.replace(/[\[\]]/g, '\\$&'); const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); const results = regex.exec(url); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html new file mode 100644 index 0000000000..53f95cb57e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -0,0 +1,40 @@ + +
admin.mobile-app.connect-mobile-app
+
+
+ +
+ + + + + + + + +
+
+
+ {{ mobileAppSettings.qrCodeConfig.qrCodeLabel }} +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss new file mode 100644 index 0000000000..1d088ec482 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../../../scss/constants'; + +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background-color: transparent; +} + +.tb-title { + align-self: start; + font-weight: 600; + font-size: 20px; + line-height: 24px; + letter-spacing: 0.1px; + color: rgba(0, 0, 0, 0.76); + @media #{$mat-md-lg} { + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + } +} + +.tb-qrcode-label { + font-size: 14px; + color: rgba(0, 0, 0, 0.54); +} + +.tb-qrcode { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08); + border-radius: 6px; +} + +.tb-badge-container { + @media #{$mat-lt-lg} { + display: none; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts new file mode 100644 index 0000000000..c9c8541613 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; +import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; +import { MINUTE } from '@shared/models/time/time.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-mobile-app-qrcode-widget', + templateUrl: './mobile-app-qrcode-widget.component.html', + styleUrls: ['./mobile-app-qrcode-widget.component.scss'] +}) +export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { + + @Input() + ctx: WidgetContext; + + @Input() + @coerceBoolean() + previewMode: boolean; + + @Input() + set mobileAppSettings(settings: MobileAppSettings) { + if (settings) { + this.mobileAppSettingsValue = settings; + } + }; + + get mobileAppSettings() { + return this.mobileAppSettingsValue; + } + + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + + private readonly destroy$ = new Subject(); + + badgeStyle = BadgeStyle; + badgePosition = BadgePosition; + badgeStyleURLMap = badgeStyleURLMap; + + private mobileAppSettingsValue: MobileAppSettings; + private deepLinkTTL: number; + + constructor(protected store: Store, + protected cd: ChangeDetectorRef, + private mobileAppService: MobileAppService, + private utilsService: UtilsService) { + super(store); + } + + ngOnInit(): void { + if (this.ctx) { + this.ctx.$scope.mobileAppQrcodeWidget = this; + } + if (!this.mobileAppSettings) { + this.mobileAppService.getMobileAppSettings().subscribe((settings => { + this.mobileAppSettings = settings; + this.cd.detectChanges(); + })); + } + } + + ngAfterViewInit(): void { + this.getMobileAppDeepLink().subscribe(link => { + this.deepLinkTTL = Number(this.utilsService.getQueryParam('ttl', link)) * MINUTE; + this.updateQRCode(link); + interval(this.deepLinkTTL).pipe( + takeUntil(this.destroy$), + mergeMap(() => this.getMobileAppDeepLink()) + ).subscribe(link => this.updateQRCode(link)); + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + getMobileAppDeepLink(): Observable { + return this.mobileAppService.getMobileAppDeepLink(); + } + + updateQRCode(link: string) { + import('qrcode').then((QRCode) => { + QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 125}); + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 69185b6dd2..e25f0d3925 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -92,6 +92,7 @@ import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-c import { BarChartWidgetComponent } from '@home/components/widget/lib/chart/bar-chart-widget.component'; import { PolarAreaWidgetComponent } from '@home/components/widget/lib/chart/polar-area-widget.component'; import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/radar-chart-widget.component'; +import { MobileAppQrcodeWidgetComponent } from '@home/components/widget/lib/mobile-app-qrcode-widget.component'; @NgModule({ declarations: @@ -112,6 +113,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad NavigationCardsWidgetComponent, NavigationCardWidgetComponent, QrCodeWidgetComponent, + MobileAppQrcodeWidgetComponent, MarkdownWidgetComponent, SelectEntityDialogComponent, LegendComponent, @@ -176,6 +178,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad NavigationCardsWidgetComponent, NavigationCardWidgetComponent, QrCodeWidgetComponent, + MobileAppQrcodeWidgetComponent, MarkdownWidgetComponent, LegendComponent, FlotWidgetComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 63fb9a14de..f05fc05a25 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -40,6 +40,7 @@ import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing. import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; +import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -270,6 +271,19 @@ const routes: Routes = [ } } }, + { + path: 'mobile-app', + component: MobileAppSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.mobile-app.mobile-app', + breadcrumb: { + label: 'admin.mobile-app.mobile-app', + icon: 'smartphone' + } + } + }, { path: 'security-settings', redirectTo: '/security-settings/general' diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 1ab60687a3..f44782e814 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -33,6 +33,8 @@ import { QueueComponent } from '@home/pages/admin/queue/queue.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; +import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; +import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; @NgModule({ declarations: @@ -49,13 +51,15 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut QueueComponent, RepositoryAdminSettingsComponent, AutoCommitAdminSettingsComponent, - TwoFactorAuthSettingsComponent + TwoFactorAuthSettingsComponent, + MobileAppSettingsComponent ], - imports: [ - CommonModule, - SharedModule, - HomeComponentsModule, - AdminRoutingModule - ] + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + AdminRoutingModule, + WidgetComponentsModule + ] }) export class AdminModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html new file mode 100644 index 0000000000..5b1cb710e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html @@ -0,0 +1,161 @@ + + + + + admin.mobile-app.mobile-app-qr-code-widget-settings + + + + +
+ +
+
+
+
admin.mobile-app.applications
+ + {{ 'admin.mobile-app.default' | translate }} + {{ 'admin.mobile-app.custom' | translate }} + +
+
+
+ + {{ 'admin.mobile-app.android' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-package-name' | translate }}
+ + + + warning + + +
+
+
+
+
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
+ + + + warning + + +
+
+
+
+
+ + {{ 'admin.mobile-app.ios' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-id' | translate }}
+ + + + warning + + +
+
+
+
+
+
+
admin.mobile-app.appearance-on-home-page
+ + {{ 'admin.mobile-app.enabled' | translate }} + {{ 'admin.mobile-app.disabled' | translate }} + +
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + + {{ badgeStyle.value | translate }} + + + + + + + {{ badgePosition.value | translate }} + + + +
+
+
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
+
+
+
admin.mobile-app.preview
+ + +
+
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss new file mode 100644 index 0000000000..5f04c04e8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-fixed-width { + min-width: 230px; +} + +.tb-qrcode-preview { + background-color: rgba(0, 0, 0, 0.04); + box-shadow: none; + + .tb-form-panel-title { + color: rgba(0, 0, 0, 0.38); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts new file mode 100644 index 0000000000..c342b68d1f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -0,0 +1,197 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnDestroy } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { Subject, takeUntil } from 'rxjs'; +import { MobileAppService } from '@core/http/mobile-app.service'; +import { + BadgePosition, + badgePositionTranslationsMap, + BadgeStyle, + badgeStyleTranslationsMap, + MobileAppSettings +} from '@shared/models/mobile-app.models'; + +@Component({ + selector: 'tb-mobile-app-settings', + templateUrl: './mobile-app-settings.component.html', + styleUrls: ['mobile-app-settings.component.scss', './settings-card.scss'] +}) +export class MobileAppSettingsComponent extends PageComponent implements HasConfirmForm, OnDestroy { + + mobileAppSettingsForm: FormGroup; + + mobileAppSettings: MobileAppSettings; + + private readonly destroy$ = new Subject(); + + badgePositionTranslationsMap = badgePositionTranslationsMap; + badgeStyleTranslationsMap = badgeStyleTranslationsMap; + + constructor(protected store: Store, + private mobileAppService: MobileAppService, + public fb: FormBuilder) { + super(store); + this.buildMobileAppSettingsForm(); + this.mobileAppService.getMobileAppSettings() + .subscribe(settings => this.processMobileAppSettings(settings)); + this.mobileAppSettingsForm.get('useDefaultApp').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); + } else { + this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } + this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); + this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); + }); + this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + if (value) { + this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + } else { + this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + } + this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); + } + if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); + if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!this.mobileAppSettingsForm.get('iosConfig.enabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + } + }); + this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + if (value) { + this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } else { + this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); + } + this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); + } + if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); + if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!this.mobileAppSettingsForm.get('androidConfig.enabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + } + }); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + }); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable(); + } else { + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable(); + } + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + private buildMobileAppSettingsForm() { + this.mobileAppSettingsForm = this.fb.group({ + useDefaultApp: [true, []], + androidConfig: this.fb.group({ + enabled: [true, []], + appPackage: ['', []], + sha256CertFingerprints: ['', []] + }), + iosConfig: this.fb.group({ + enabled: [true, []], + appId: ['', []] + }), + qrCodeConfig: this.fb.group({ + showOnHomePage: [true, []], + badgeEnabled: [true, []], + badgeStyle: [{value: BadgeStyle.ORIGINAL, disabled: true}, []], + badgePosition: [{value: BadgePosition.RIGHT, disabled: true}, []], + qrCodeLabelEnabled: [true, []], + qrCodeLabel: ['', []] + }) + }); + } + + private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { + this.mobileAppSettings = {...mobileAppSettings}; + this.mobileAppSettingsForm.reset(this.mobileAppSettings); + } + + save(): void { + this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; + this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) + .subscribe((settings) => this.processMobileAppSettings(settings)); + } + + confirmForm(): FormGroup { + return this.mobileAppSettingsForm; + } + +} diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts new file mode 100644 index 0000000000..0787f17747 --- /dev/null +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -0,0 +1,81 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TenantId } from '@shared/models/id/tenant-id'; + +export interface MobileAppSettings { + tenantId: TenantId; + useDefaultApp: boolean; + androidConfig: AndroidConfig; + iosConfig: IosConfig; + qrCodeConfig: QRCodeConfig; +} + +export interface AndroidConfig { + enabled: boolean; + appPackage: string; + sha256CertFingerprints: string +} + +export interface IosConfig { + enabled: boolean; + appId: string; +} + +export interface QRCodeConfig { + showOnHomePage: boolean; + badgeEnabled: boolean; + badgePosition: BadgePosition; + badgeStyle: BadgeStyle; + qrCodeLabelEnabled: boolean; + qrCodeLabel: string; +} + +export interface MobileOSBadgeURL { + iOS: string; + android: string; +} + +export enum BadgePosition { + RIGHT = 'RIGHT', + LEFT = 'LEFT' +} + +export const badgePositionTranslationsMap = new Map([ + [BadgePosition.RIGHT, 'admin.mobile-app.right'], + [BadgePosition.LEFT, 'admin.mobile-app.left'] +]); + +export enum BadgeStyle { + ORIGINAL = 'ORIGINAL', + WHITE = 'WHITE' +} + +export const badgeStyleTranslationsMap = new Map([ + [BadgeStyle.ORIGINAL, 'admin.mobile-app.original'], + [BadgeStyle.WHITE, 'admin.mobile-app.white'] +]); + +export const badgeStyleURLMap = new Map([ + [BadgeStyle.ORIGINAL, { + iOS: 'assets/android-ios-stores-badges/ios_store_en_black_badge.svg', + android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' + }], + [BadgeStyle.WHITE, { + iOS: 'assets/android-ios-stores-badges/ios_store_en_white_badge.svg', + android: 'assets/android-ios-stores-badges/android_store_en_black_badge.svg' + }] +]); diff --git a/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg new file mode 100644 index 0000000000..02b88dab6f --- /dev/null +++ b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg new file mode 100755 index 0000000000..072b425a1a --- /dev/null +++ b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_black_badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg new file mode 100755 index 0000000000..16c0496ca7 --- /dev/null +++ b/ui-ngx/src/assets/android-ios-stores-badges/ios_store_en_white_badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index 85669b8bf4..d850e640b7 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1143,6 +1143,49 @@ "row": 0, "col": 0, "id": "8e71a398-caf5-540d-cec5-6e5dc264343e" + }, + "b9d704a2-f579-dbff-f123-f953d92081fd": { + "typeFullFqn": "system.home_page_widgets.mobile_app_qr_code", + "type": "static", + "sizeX": 6, + "sizeY": 3, + "config": { + "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": { + "cardHtml": "
HTML code here
", + "cardCss": ".card {\n font-weight: bold;\n font-size: 32px;\n color: #999;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}" + }, + "title": "HTML Card", + "dropShadow": false + }, + "row": 0, + "col": 0, + "id": "b9d704a2-f579-dbff-f123-f953d92081fd" } }, "states": { @@ -1169,7 +1212,7 @@ }, "a23185ad-dc46-806c-0e50-5b21fb080ace": { "sizeX": 35, - "sizeY": 58, + "sizeY": 42, "row": 0, "col": 85, "mobileHide": true @@ -1212,6 +1255,12 @@ "row": 0, "col": 43, "mobileOrder": 0 + }, + "b9d704a2-f579-dbff-f123-f953d92081fd": { + "sizeX": 35, + "sizeY": 16, + "row": 42, + "col": 85 } }, "gridSettings": { @@ -1416,4 +1465,4 @@ } }, "name": "Tenant Administrator Home Page" -} \ No newline at end of file +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index b13d169cdc..1a50f9f62b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -419,6 +419,33 @@ "no-auto-commit-entities-prompt": "No entities configured for auto-commit", "delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?", "delete-auto-commit-settings-text": "Be careful, after the confirmation the auto-commit settings will be removed and auto-commit will be disabled for all entities.", + "mobile-app": { + "mobile-app": "Mobile app", + "mobile-app-qr-code-widget-settings": "Mobile app QR code widget settings", + "applications": "Applications", + "default": "Default", + "custom": "Custom", + "android": "Android", + "app-package-name": "App package name", + "app-package-name-required": "App package name is required", + "sha256-certificate-fingerprints": "SHA256 certificate fingerprints", + "sha256-certificate-fingerprints-required": "SHA256 certificate fingerprints is required", + "ios": "iOS", + "app-id": "App ID", + "app-id-required": "App ID is required", + "appearance-on-home-page": "Appearance on Home page", + "enabled": "Enabled", + "disabled": "Disabled", + "badges": "Badges", + "original": "Original", + "white": "White", + "label": "Label", + "right": "Right", + "left": "Left", + "set": "Set", + "preview": "Preview", + "connect-mobile-app": "Connect mobile app" + }, "2fa": { "2fa": "Two-factor authentication", "available-providers": "Available providers", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 1f9ca93c88..a692994144 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -257,9 +257,15 @@ &.row { flex-direction: row; } + &.row-reverse { + flex-direction: row-reverse; + } &.column { flex-direction: column; } + &.center { + justify-content: center; + } &.flex-start { justify-content: flex-start; } @@ -278,6 +284,15 @@ &.fill-width { width: 100%; } + &.fill-height { + height: 100%; + } + &.shrink { + flex: 0; + } + &.no-gap { + gap: 0; + } } .tb-form-panel, .tb-form-row { From 3c9e7b3d027093231ff9b1e92bb1f60a67479bb6 Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 3 May 2024 13:33:06 +0300 Subject: [PATCH 039/121] UI: added passing of column into handleWidgetAction additionalParams of onCellClick for alarms table widget --- .../widget/lib/alarm/alarms-table-widget.component.html | 2 +- .../widget/lib/alarm/alarms-table-widget.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html index 71f7debe84..0aca6e05bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html @@ -83,7 +83,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 7d6bc454bd..1d1aa533cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -796,7 +796,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return res; } - public onCellClick($event: Event, alarm: AlarmDataInfo, columnIndex: number) { + public onCellClick($event: Event, alarm: AlarmDataInfo, key: EntityColumn, columnIndex: number) { this.alarmsDatasource.toggleCurrentAlarm(alarm); const descriptors = this.ctx.actionsApi.getActionDescriptors('cellClick'); let descriptor; @@ -813,7 +813,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, entityName = alarm.entityName; entityLabel = alarm.entityLabel; } - this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, {alarm}, entityLabel); + this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, {alarm, key}, entityLabel); } } From 316305aea8f9e3abcff13b6cc1a40dca06322985 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 3 May 2024 13:06:46 +0200 Subject: [PATCH 040/121] msa tests: JUnit4 org.junit.Assert replaced with assertJ --- .../org/thingsboard/server/msa/AbstractLwm2mClientTest.java | 3 +-- .../server/msa/connectivity/lwm2m/LwM2MTestClient.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java index d0a8b53c32..c0ae8e9776 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractLwm2mClientTest.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.util.Hex; -import org.junit.Assert; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Device; @@ -141,7 +140,7 @@ public class AbstractLwm2mClientTest extends AbstractContainerTest{ log.warn("msa basicTestConnection update -> finishState: [{}] states: {}", finishState, lwM2MTestClient.getClientStates()); return lwM2MTestClient.getClientStates().contains(finishState) || lwM2MTestClient.getClientStates().contains(ON_UPDATE_SUCCESS); }); - Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesRegistrationLwm2mSuccess)); + assertThat(lwM2MTestClient.getClientStates()).containsAll(expectedStatusesRegistrationLwm2mSuccess); } public LwM2MTestClient createNewClient(Security security, diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java index 9b1ab4bd22..c9f193704b 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java @@ -50,7 +50,6 @@ import org.eclipse.leshan.core.request.BootstrapRequest; import org.eclipse.leshan.core.request.DeregisterRequest; import org.eclipse.leshan.core.request.RegisterRequest; import org.eclipse.leshan.core.request.UpdateRequest; -import org.junit.Assert; import org.thingsboard.server.msa.connectivity.lwm2m.Lwm2mTestHelper.LwM2MClientState; import java.io.IOException; @@ -63,6 +62,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; @@ -107,7 +107,7 @@ public class LwM2MTestClient { private Map clientDtlsCid; public void init(Security security, int clientPort) throws InvalidDDFFileException, IOException { - Assert.assertNull("client already initialized", leshanClient); + assertThat(leshanClient).as("client already initialized").isNull(); List models = new ArrayList<>(); for (String resourceName : resources) { From eae2d94e8b7341006608ddab94b32f3cafc1ab19 Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Fri, 3 May 2024 16:50:41 +0300 Subject: [PATCH 041/121] Update ---bug-report.md Added disclaimer --- .github/ISSUE_TEMPLATE/---bug-report.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index f5d093f424..34557d5979 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -12,8 +12,8 @@ A clear and concise description of what the bug is. **Your Server Environment** -* [https://demo.thingsboard.io](demo.thingsboard.io) -* [https://thingsboard.cloud](thingsboard.cloud) +* Live Demo +* ThingsBoard Cloud * own setup * Deployment: monolith or microservices * Deployment type: deb, rpm, exe, docker-compose, k8s, ami @@ -60,3 +60,8 @@ If applicable, please add screenshots to help explain your problem. **Additional context** Please feel free to add any other context about the problem here. + +________________________________________________________________ +**Disclaimer** + +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the Community ethics policy, and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From e4ffd3e39fcac0ebd519503974f296313b4eeb2b Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Fri, 3 May 2024 16:54:21 +0300 Subject: [PATCH 042/121] Update feature_request.md Updated ISSUE_TEMPLATE. Added **Disclaimer** field to a Feature Request template. --- .github/ISSUE_TEMPLATE/feature_request.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 1168adaf15..84da3f39f7 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -18,3 +18,8 @@ A clear and concise description of any alternative solutions or features you've **Additional context** Add any other context or screenshots about the feature request here. + +_____________________________________________________ +**Disclaimer** + +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the Community ethics policy, and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From cbefb1742f2837b57f4af2a16bfeead9748d4aac Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Fri, 3 May 2024 16:55:52 +0300 Subject: [PATCH 043/121] Update question.md Updated ISSUE_TEMPLATE. Added **Disclaimer** field to a Question template. --- .github/ISSUE_TEMPLATE/question.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index dea6318ac5..db847cef34 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -23,3 +23,8 @@ Clear and concise details. * OS: name and version * ThingsBoard: version * Browser: name and version + +___________________________________________________________ +**Disclaimer** + +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the Community ethics policy, and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From 742af6ebf853833244e47356a36f908dda0cd67a Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Fri, 3 May 2024 17:37:04 +0300 Subject: [PATCH 044/121] Update question.md Fixed link --- .github/ISSUE_TEMPLATE/question.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index db847cef34..a7c2b94488 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -27,4 +27,4 @@ Clear and concise details. ___________________________________________________________ **Disclaimer** -We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the Community ethics policy, and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://opensource.guide/building-community/), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From a23b665eedda3ea797859c7bc150b0bbe06da2e3 Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Fri, 3 May 2024 17:39:03 +0300 Subject: [PATCH 045/121] Update feature_request.md Fixed link --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 84da3f39f7..005141b2e5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -22,4 +22,4 @@ Add any other context or screenshots about the feature request here. _____________________________________________________ **Disclaimer** -We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the Community ethics policy, and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://opensource.guide/building-community/), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From bcb3f721f3d54cc3c45926d5f8d3b60bfb5684bd Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Fri, 3 May 2024 17:41:12 +0300 Subject: [PATCH 046/121] Update ---bug-report.md Fixed links --- .github/ISSUE_TEMPLATE/---bug-report.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index 34557d5979..e883305f89 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -12,8 +12,8 @@ A clear and concise description of what the bug is. **Your Server Environment** -* Live Demo -* ThingsBoard Cloud +* [Live Demo](https://demo.thingsboard.io) +* [ThingsBoard Cloud](https://thingsboard.cloud) * own setup * Deployment: monolith or microservices * Deployment type: deb, rpm, exe, docker-compose, k8s, ami @@ -64,4 +64,4 @@ Please feel free to add any other context about the problem here. ________________________________________________________________ **Disclaimer** -We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the Community ethics policy, and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://opensource.guide/building-community/), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From 4ee969fa1ef5a6321e072f66d058909b2cc166ec Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Sun, 5 May 2024 18:12:26 +0300 Subject: [PATCH 047/121] fixed mobile application validator --- .../MobileApplicationControllerTest.java | 22 +++++++++++++++---- .../MobileAppSettingsDataValidator.java | 10 ++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java index ad049295ea..39e2ef6a40 100644 --- a/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/MobileApplicationControllerTest.java @@ -110,7 +110,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { mobileAppSettings.setIosConfig(IosConfig.builder().enabled(false).build()); doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Qr code config is required!"))); + .andExpect(statusReason(containsString("Qr code configuration is required!"))); mobileAppSettings.setQrCodeConfig(QRCodeConfig.builder().showOnHomePage(false).build()); doPost("/api/mobile/app/settings", mobileAppSettings) @@ -121,6 +121,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { public void testShouldNotSaveMobileAppSettingsWithoutRequiredAndroidConf() throws Exception { loginSysAdmin(); MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(false); AndroidConfig androidConfig = AndroidConfig.builder() .enabled(true) .appPackage(null) @@ -130,12 +131,12 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); androidConfig.setAppPackage("test_app_package"); doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for enabled android settings!"))); + .andExpect(statusReason(containsString("Application package and sha256 cert fingerprints are required for custom android application!"))); androidConfig.setSha256CertFingerprints("test_sha_256"); doPost("/api/mobile/app/settings", mobileAppSettings) @@ -146,6 +147,7 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { public void testShouldNotSaveMobileAppSettingsWithoutRequiredIosConf() throws Exception { loginSysAdmin(); MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(false); IosConfig iosConfig = IosConfig.builder() .enabled(true) .appId(null) @@ -154,13 +156,25 @@ public class MobileApplicationControllerTest extends AbstractControllerTest { doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Application id is required for enabled ios settings!"))); + .andExpect(statusReason(containsString("Application id is required for custom ios application!"))); iosConfig.setAppId("test_app_id"); doPost("/api/mobile/app/settings", mobileAppSettings) .andExpect(status().isOk()); } + @Test + public void testShouldSaveMobileAppSettingsForDefaultApp() throws Exception { + loginSysAdmin(); + MobileAppSettings mobileAppSettings = doGet("/api/mobile/app/settings", MobileAppSettings.class); + mobileAppSettings.setUseDefaultApp(true); + mobileAppSettings.setIosConfig(null); + mobileAppSettings.setAndroidConfig(null); + + doPost("/api/mobile/app/settings", mobileAppSettings) + .andExpect(status().isOk()); + } + @Test public void testGetApplicationAssociations() throws Exception { loginSysAdmin(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java index 6400cd08af..c867b08cc4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/MobileAppSettingsDataValidator.java @@ -38,14 +38,14 @@ public class MobileAppSettingsDataValidator extends DataValidator Date: Sun, 5 May 2024 19:43:16 +0300 Subject: [PATCH 048/121] lwm2m: object-id-version --- ...ile-transport-configuration.component.html | 27 ++++--------------- ...ofile-transport-configuration.component.ts | 12 ++++++--- .../lwm2m/lwm2m-profile-config.models.ts | 16 +++++++++-- .../assets/locale/locale.constant-ar_AE.json | 5 +++- .../assets/locale/locale.constant-ca_ES.json | 5 +++- .../assets/locale/locale.constant-cs_CZ.json | 7 +++-- .../assets/locale/locale.constant-en_US.json | 5 +++- .../assets/locale/locale.constant-es_ES.json | 5 +++- .../assets/locale/locale.constant-nl_BE.json | 5 +++- .../assets/locale/locale.constant-pl_PL.json | 5 +++- .../assets/locale/locale.constant-tr_TR.json | 5 +++- .../assets/locale/locale.constant-zh_CN.json | 5 +++- .../assets/locale/locale.constant-zh_TW.json | 5 +++- 13 files changed, 69 insertions(+), 38 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 677d45c6b0..4617c58275 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -91,28 +91,11 @@ - {{ 'device-profile.lwm2m.composite-operations-support' | translate }} - - - - - - - - - - - - - - - - - - - - + + + {{ objectIDVerTranslationMap.get(objectIDVer) | translate}} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index b3516501cb..8655b70b6b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -40,9 +40,11 @@ import { ObjectLwM2M, OBSERVE, PowerMode, + ObjectIDVer, RESOURCES, ServerSecurityConfig, - TELEMETRY + TELEMETRY, + ObjectIDVerTranslationMap } from './lwm2m-profile-config.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils'; @@ -80,6 +82,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro lwm2mDeviceProfileFormGroup: UntypedFormGroup; configurationValue: Lwm2mProfileConfigModels; + + objectIDVers = Object.values(ObjectIDVer); + objectIDVerTranslationMap = ObjectIDVerTranslationMap; + sortFunction: (key: string, value: object) => object; get required(): boolean { @@ -117,7 +123,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro edrxCycle: [{disabled: true, value: 0}, Validators.required], psmActivityTimer: [{disabled: true, value: 0}, Validators.required], pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required], - compositeOperationsSupport: [false] + defaultObjectIDVer: [ObjectIDVer.V1_1, Validators.required] }) }); @@ -276,7 +282,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro pagingTransmissionWindow: this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW, psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER, - compositeOperationsSupport: this.configurationValue.clientLwM2mSettings.compositeOperationsSupport || false + defaultObjectIDVer: this.configurationValue.clientLwM2mSettings.defaultObjectIDVer || ObjectIDVer.V1_1 } }, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index e5c292ef0a..79e9bb6514 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -122,6 +122,18 @@ export const PowerModeTranslationMap = new Map( ] ); +export enum ObjectIDVer { + V1_0 = '1.0', + V1_1 = '1.1' +} + +export const ObjectIDVerTranslationMap = new Map( + [ + [ObjectIDVer.V1_0, 'device-profile.lwm2m.default-object-id-ver.v1-0'], + [ObjectIDVer.V1_1, 'device-profile.lwm2m.default-object-id-ver.v1-1'] + ] +); + export interface ServerSecurityConfig { host?: string; port?: number; @@ -163,7 +175,7 @@ export interface ClientLwM2mSettings { edrxCycle?: number; pagingTransmissionWindow?: number; psmActivityTimer?: number; - compositeOperationsSupport: boolean; + defaultObjectIDVer: string; } export interface ObservableAttributes { @@ -190,7 +202,7 @@ export function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSetting fwUpdateStrategy: 1, swUpdateStrategy: 1, powerMode: PowerMode.DRX, - compositeOperationsSupport: false + defaultObjectIDVer: ObjectIDVer.V1_1 }; } diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 104eb18abd..30de473035 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -2079,7 +2079,10 @@ "min-evaluation-period": "الفترة الدنيا للتقييم", "max-evaluation-period": "الفترة القصوى للتقييم" }, - "composite-operations-support": "يدعم عمليات القراءة/الكتابة/المراقبة المركبة" + "default-object-id-ver": { + "v1-0": "\"1.0\" هي قيمة إصدار الكائن إذا كان إصدار الكائن فارغًا.", + "v1-1": "\"1.1\" هي قيمة إصدار الكائن إذا كان إصدار الكائن فارغًا." + } }, "snmp": { "add-communication-config": "إضافة تكوين الاتصال", diff --git a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json index d530dae430..35f15ed95a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json @@ -1711,7 +1711,10 @@ "min-evaluation-period": "Període mínim d'avaluació", "max-evaluation-period": "Període màxim d'avaluació" }, - "composite-operations-support": "Admet operacions compostes de lectura/escriptura/observació" + "default-object-id-ver": { + "v1-0": "\"1.0\" és el valor de la versió de l'objecte si la versió de l'objecte està buida.", + "v1-1": "\"1.1\" és el valor de la versió de l'objecte si la versió de l'objecte està buida." + } }, "snmp": { "add-communication-config": "Afegeix la configuració de comunicació", diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index fa23dddb45..6ee55ab303 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -1302,8 +1302,11 @@ "min-evaluation-period": "Minimální interval evaluace", "max-evaluation-period": "Maximální interval evaluace" }, - "composite-operations-support": "Podporuje kompozitní Read/Write/Observe operace" - }, + "default-object-id-ver": { + "v1-0": "\"1.0\" je hodnota verze objektu, pokud je verze objektu prázdná.", + "v1-1": "\"1.1\" je hodnota verze objektu, pokud je verze objektu prázdná." + } + }, "snmp": { "add-communication-config": "Přidat konfiguraci komunikace", "add-mapping": "Přidat mapování", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index b13d169cdc..feb6b7ea75 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1899,7 +1899,10 @@ "min-evaluation-period": "Minimum evaluation period", "max-evaluation-period": "Maximum evaluation period" }, - "composite-operations-support": "Supports composite Read/Write/Observe operations" + "default-object-id-ver": { + "v1-0": "\"1.0\" is the object's version value if the object's version is empty.", + "v1-1": "\"1.1\" is the object's version value if the object's version is empty." + } }, "snmp": { "add-communication-config": "Add communication config", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index e80fbf6c3e..0ecc2a54eb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -1836,7 +1836,10 @@ "min-evaluation-period": "Período mínimo de evaluación", "max-evaluation-period": "Período máximo de evaluación" }, - "composite-operations-support": "Soporta operaciones Lectura/Escritura/Observación Compuestas" + "default-object-id-ver": { + "v1-0": "\"1.0\" es el valor de la versión del objeto si la versión del objeto está vacía.", + "v1-1": "\"1.1\" es el valor de la versión del objeto si la versión del objeto está vacía." + } }, "snmp": { "add-communication-config": "Añadir configuración de comunicaciones", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index 4cd3bb1e49..223e896e61 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -1936,7 +1936,10 @@ "min-evaluation-period": "Minimale evaluatieperiode", "max-evaluation-period": "Maximale evaluatieperiode" }, - "composite-operations-support": "Ondersteunt samengestelde lees-/schrijf-/observatiebewerkingen" + "default-object-id-ver": { + "v1-0": "\"1.0\" is de versiewaarde van het object als de versie van het object leeg is..", + "v1-1": "\"1.1\" is de versiewaarde van het object als de versie van het object leeg is." + } }, "snmp": { "add-communication-config": "Communicatieconfiguratie toevoegen", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index 58d4a0168f..d60314ed67 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -1873,7 +1873,10 @@ "min-evaluation-period":"Minimalny okres oceny", "max-evaluation-period":"Maksymalny okres oceny" }, - "composite-operations-support":"Obsługuje operacje złożone Odczyt/Zapis/Obserwacja" + "default-object-id-ver": { + "v1-0": "\"1.0\" to wartość wersji obiektu, jeśli wersja obiektu jest pusta.", + "v1-1": "\"1.1\" to wartość wersji obiektu, jeśli wersja obiektu jest pusta." + } }, "snmp":{ "add-communication-config":"Dodaj konfigurację komunikacji", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index e516196b28..6b425eb2c5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -1317,7 +1317,10 @@ "min-evaluation-period": "Minimum değerlendirme süresi", "max-evaluation-period": "Maksimum değerlendirme süresi" }, - "composite-operations-support": "İç içe Okuma/Yazma/Gözlemleme işlemlerini destekler" + "default-object-id-ver": { + "v1-0": "\"1.0\", nesnenin sürümü boşsa nesnenin sürüm değeridir.", + "v1-1": "\"1.1\", nesnenin sürümü boşsa nesnenin sürüm değeridir." + } }, "snmp": { "add-communication-config": "İletişim yapılandırması ekle", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 886151fdf2..ab4afacbc6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1873,7 +1873,10 @@ "min-evaluation-period": "最小评估周期", "max-evaluation-period": "最大评估周期" }, - "composite-operations-support": "支持复合读取/写入/观察操作" + "default-object-id-ver": { + "v1-0": "如果对象版本为空,则“1.0”是对象版本的值。", + "v1-1": "如果对象版本为空,则“1.1”是对象版本的值。" + } }, "snmp": { "add-communication-config": "添加通信配置", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index eafc22498f..e215c8e87e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -1511,7 +1511,10 @@ "min-evaluation-period": "最小評估期間", "max-evaluation-period": "最大評估期間" }, - "composite-operations-support": "支持複合讀/寫/觀察 操作" + "default-object-id-ver": { + "v1-0": "如果物件的版本為空,則「1.0」是物件的版本值。", + "v1-1": "如果物件的版本為空,則「1.1」是物件的版本值。" + } }, "snmp": { "add-communication-config": "新增通訊配置", From 405fe83e79ba6174f069bed8f2ba04d41789cffc Mon Sep 17 00:00:00 2001 From: nick Date: Sun, 5 May 2024 22:07:06 +0300 Subject: [PATCH 049/121] lwm2m: object-id-version format --- ...e-profile-transport-configuration.component.html | 13 ++++++++----- ui-ngx/src/assets/locale/locale.constant-ar_AE.json | 1 + ui-ngx/src/assets/locale/locale.constant-ca_ES.json | 1 + ui-ngx/src/assets/locale/locale.constant-cs_CZ.json | 1 + ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + ui-ngx/src/assets/locale/locale.constant-es_ES.json | 1 + ui-ngx/src/assets/locale/locale.constant-nl_BE.json | 3 ++- ui-ngx/src/assets/locale/locale.constant-pl_PL.json | 1 + ui-ngx/src/assets/locale/locale.constant-tr_TR.json | 1 + ui-ngx/src/assets/locale/locale.constant-zh_CN.json | 1 + ui-ngx/src/assets/locale/locale.constant-zh_TW.json | 1 + 11 files changed, 19 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 4617c58275..6290048f62 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -91,11 +91,14 @@ - - - {{ objectIDVerTranslationMap.get(objectIDVer) | translate}} - - +
+ device-profile.lwm2m.default-object-id + + + {{ objectIDVerTranslationMap.get(objectIDVer) | translate}} + + +
diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 30de473035..a9a7813b80 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -2079,6 +2079,7 @@ "min-evaluation-period": "الفترة الدنيا للتقييم", "max-evaluation-period": "الفترة القصوى للتقييم" }, + "default-object-id": "إصدار الكائن الافتراضي (السمة)", "default-object-id-ver": { "v1-0": "\"1.0\" هي قيمة إصدار الكائن إذا كان إصدار الكائن فارغًا.", "v1-1": "\"1.1\" هي قيمة إصدار الكائن إذا كان إصدار الكائن فارغًا." diff --git a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json index 35f15ed95a..f6a20d00d9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ca_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-ca_ES.json @@ -1711,6 +1711,7 @@ "min-evaluation-period": "Període mínim d'avaluació", "max-evaluation-period": "Període màxim d'avaluació" }, + "default-object-id": "Versió de l'objecte predeterminat (atribut)", "default-object-id-ver": { "v1-0": "\"1.0\" és el valor de la versió de l'objecte si la versió de l'objecte està buida.", "v1-1": "\"1.1\" és el valor de la versió de l'objecte si la versió de l'objecte està buida." diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 6ee55ab303..75d8bc0360 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -1302,6 +1302,7 @@ "min-evaluation-period": "Minimální interval evaluace", "max-evaluation-period": "Maximální interval evaluace" }, + "default-object-id": "VVýchozí verze objektu (atribut)", "default-object-id-ver": { "v1-0": "\"1.0\" je hodnota verze objektu, pokud je verze objektu prázdná.", "v1-1": "\"1.1\" je hodnota verze objektu, pokud je verze objektu prázdná." diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index feb6b7ea75..e59fb8fdd6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1899,6 +1899,7 @@ "min-evaluation-period": "Minimum evaluation period", "max-evaluation-period": "Maximum evaluation period" }, + "default-object-id": "Default object Version (Attribute)", "default-object-id-ver": { "v1-0": "\"1.0\" is the object's version value if the object's version is empty.", "v1-1": "\"1.1\" is the object's version value if the object's version is empty." diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 0ecc2a54eb..9e6b57536f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -1836,6 +1836,7 @@ "min-evaluation-period": "Período mínimo de evaluación", "max-evaluation-period": "Período máximo de evaluación" }, + "default-object-id": "Versión de objeto predeterminada (atributo)", "default-object-id-ver": { "v1-0": "\"1.0\" es el valor de la versión del objeto si la versión del objeto está vacía.", "v1-1": "\"1.1\" es el valor de la versión del objeto si la versión del objeto está vacía." diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index 223e896e61..acdebb0bfc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -1936,8 +1936,9 @@ "min-evaluation-period": "Minimale evaluatieperiode", "max-evaluation-period": "Maximale evaluatieperiode" }, + "default-object-id": "Standaard objectversie (attribuut)", "default-object-id-ver": { - "v1-0": "\"1.0\" is de versiewaarde van het object als de versie van het object leeg is..", + "v1-0": "\"1.0\" is de versiewaarde van het object als de versie van het object leeg is.", "v1-1": "\"1.1\" is de versiewaarde van het object als de versie van het object leeg is." } }, diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index d60314ed67..85dd037de6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -1873,6 +1873,7 @@ "min-evaluation-period":"Minimalny okres oceny", "max-evaluation-period":"Maksymalny okres oceny" }, + "default-object-id": "Domyślna wersja obiektu (atrybut)", "default-object-id-ver": { "v1-0": "\"1.0\" to wartość wersji obiektu, jeśli wersja obiektu jest pusta.", "v1-1": "\"1.1\" to wartość wersji obiektu, jeśli wersja obiektu jest pusta." diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index 6b425eb2c5..b6cab6695a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -1317,6 +1317,7 @@ "min-evaluation-period": "Minimum değerlendirme süresi", "max-evaluation-period": "Maksimum değerlendirme süresi" }, + "default-object-id": "Varsayılan Nesne Sürümü (Öznitelik)", "default-object-id-ver": { "v1-0": "\"1.0\", nesnenin sürümü boşsa nesnenin sürüm değeridir.", "v1-1": "\"1.1\", nesnenin sürümü boşsa nesnenin sürüm değeridir." diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index ab4afacbc6..8e45dcf705 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1873,6 +1873,7 @@ "min-evaluation-period": "最小评估周期", "max-evaluation-period": "最大评估周期" }, + "default-object-id": "默认对象版本(属性", "default-object-id-ver": { "v1-0": "如果对象版本为空,则“1.0”是对象版本的值。", "v1-1": "如果对象版本为空,则“1.1”是对象版本的值。" diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json index e215c8e87e..b2e874fb2e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_TW.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_TW.json @@ -1511,6 +1511,7 @@ "min-evaluation-period": "最小評估期間", "max-evaluation-period": "最大評估期間" }, + "default-object-id": "預設物件版本(屬性)", "default-object-id-ver": { "v1-0": "如果物件的版本為空,則「1.0」是物件的版本值。", "v1-1": "如果物件的版本為空,則「1.1」是物件的版本值。" From 4eba0ada30e43cf0826f9a03e8ca48996f689e91 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 6 May 2024 09:27:59 +0300 Subject: [PATCH 050/121] UI: Reset translations --- .../assets/locale/locale.constant-ar_AE.json | 5 - .../assets/locale/locale.constant-pl_PL.json | 14188 +++++++++------- 2 files changed, 7734 insertions(+), 6459 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index a9a7813b80..ae02012787 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -2078,11 +2078,6 @@ "step": "خطوة", "min-evaluation-period": "الفترة الدنيا للتقييم", "max-evaluation-period": "الفترة القصوى للتقييم" - }, - "default-object-id": "إصدار الكائن الافتراضي (السمة)", - "default-object-id-ver": { - "v1-0": "\"1.0\" هي قيمة إصدار الكائن إذا كان إصدار الكائن فارغًا.", - "v1-1": "\"1.1\" هي قيمة إصدار الكائن إذا كان إصدار الكائن فارغًا." } }, "snmp": { diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index 85dd037de6..a46c732c6f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -1,5164 +1,6520 @@ { - "access":{ - "unauthorized":"Unauthorized", - "unauthorized-access":"Unauthorized Access", - "unauthorized-access-text":"You should sign in to have access to this resource!", - "access-forbidden":"Access Forbidden", - "access-forbidden-text":"You haven't access rights to this location!
Try to sign in with different user if you still wish to gain access to this location.", - "refresh-token-expired":"Session has expired", - "refresh-token-failed":"Unable to refresh session", - "permission-denied":"Permission Denied", - "permission-denied-text":"You don't have permission to perform this operation!" + "access": { + "unauthorized": "Nieautoryzowany", + "unauthorized-access": "Nieautoryzowany dostęp", + "unauthorized-access-text": "Aby mieć dostęp do tego zasobu, musisz się zalogować!", + "access-forbidden": "Dostęp Zabroniony", + "access-forbidden-text": "Nie masz praw dostępu do tej lokalizacji!
Spróbuj zalogować się przy użyciu innego użytkownika, jeśli nadal chcesz uzyskać dostęp do tej lokalizacji.", + "refresh-token-expired": "Sesja wygasła", + "refresh-token-failed": "Nie można odświeżyć sesji", + "permission-denied": "Odmowa pozwolenia", + "permission-denied-text": "Nie masz uprawnień do wykonania tej operacji!" }, - "account":{ - "account":"Konto", - "notification-settings":"Ustawienia powiadomień" + "account": { + "account": "Konto", + "notification-settings": "Ustawienia notyfikacji" }, - "action":{ - "activate":"Aktywuj", - "suspend":"Zawieś", - "save":"Zapisz", - "saveAs":"Zapisz jako", - "move":"Przenieś", - "cancel":"Anuluj", - "ok":"OK", - "delete":"Usuń", - "add":"Dodaj", - "yes":"Tak", - "no":"Nie", - "update":"Aktualizuj", - "remove":"Usuń", - "select":"Wybierz", - "search":"Szukaj", - "clear-search":"Wyczyść wyszukiwanie", - "assign":"Przypisz", - "unassign":"Cofnij przypisanie", - "share":"Udostępnij", - "make-private":"Uczyń prywatnym", - "apply":"Zastosuj", - "apply-changes":"Zastosuj zmiany", - "edit-mode":"Tryb edycji", - "enter-edit-mode":"Wejdź w tryb edycji", - "decline-changes":"Odrzuć zmiany", - "decline":"Odrzuć", - "close":"Zamknij", - "back":"Wstecz", - "run":"Uruchom", - "sign-in":"Zaloguj się!", - "edit":"Edytuj", - "view":"Zobacz", - "create":"Utwórz", - "drag":"Przeciągnij", - "refresh":"Odśwież", - "undo":"Cofnij", - "copy":"Kopiuj", - "paste":"Wklej", - "copy-reference":"Kopiuj odniesienie", - "paste-reference":"Wklej odniesienie", - "import":"Importuj", - "export":"Eksportuj", - "share-via":"Udostępnij przez {{provider}}", - "continue":"Kontynuuj", - "discard-changes":"Porzuć zmiany", - "download":"Pobierz", - "next":"Dalej", - "next-with-label":"Następne: {{label}}", - "read-more":"Czytaj więcej", - "hide":"Ukryj", - "done":"Gotowe", - "print":"Drukuj", - "restore":"Przywróć", - "confirm":"Potwierdź", - "more":"Więcej", - "less":"Mniej", - "skip":"Pomiń", - "send":"Wyślij", - "reset":"Resetuj", - "show-more":"Pokaż więcej", - "dont-show-again":"Nie pokazuj ponownie", - "see-documentation":"Zobacz dokumentację", - "clear":"Wyczyść", - "upload":"Prześlij", - "delete-anyway":"Usuń mimo wszystko", - "delete-selected":"Usuń wybrane" + "action": { + "activate": "Aktywuj", + "suspend": "Wstrzymaj", + "save": "Zapisz", + "saveAs": "Zapisz jako", + "move": "Przenieś", + "cancel": "Anuluj", + "ok": "OK", + "delete": "Usuń", + "add": "Dodaj", + "yes": "Tak", + "no": "Nie", + "update": "Aktualizuj", + "remove": "Usuń", + "search": "Szukaj", + "clear-search": "Wyczyść wyszukiwanie", + "assign": "Przypisz", + "unassign": "Anuluj przypisanie", + "share": "Udostępnij", + "make-private": "Uczyń prywatnym", + "make-public": "Upublicznij", + "apply": "Zastosuj", + "apply-changes": "Zastosuj zmiany", + "edit-mode": "Tryb edycji", + "enter-edit-mode": "Wejdź w tryb edycji", + "decline-changes": "Odrzuć zmiany", + "open": "Otwórz", + "decline": "Decline", + "close": "Zamknij", + "back": "Wstecz", + "run": "Uruchom", + "sign-in": "Zaloguj się!", + "edit": "Edytuj", + "view": "Pogląd", + "create": "Utwórz", + "drag": "Przeciągnij", + "refresh": "Odśwież", + "undo": "Cofnij", + "copy": "Kopiuj", + "paste": "Wklej", + "copy-reference": "Skopiuj odnośnik", + "paste-reference": "Wklej odnośnik", + "import": "Importuj", + "export": "Eksportuj", + "share-via": "Udostępnij przez {{provider}}", + "select": "Wybierz", + "continue": "Kontynuuj", + "discard-changes": "Odrzuć zmiany", + "download": "Pobierz", + "next": "Dalej", + "next-with-label": "Dalej: {{label}}", + "read-more": "Czytaj więcej", + "hide": "Ukryj", + "done": "Gotowe", + "print": "Drukuj", + "restore": "Przywróć", + "confirm": "Potwierdź", + "more": "Więcej", + "less": "Mniej", + "skip": "Pomiń", + "send": "Wyślij", + "reset": "Reset", + "show-more": "Pokaż więcej", + "dont-show-again": "Nie pokazuj więcej", + "see-documentation": "Zobacz dokumentację", + "clear": "Clear" }, - "aggregation":{ - "aggregation":"Agregacja", - "function":"Funkcja agregacji danych", - "limit":"Maksymalna liczba wartości", - "group-interval":"Interwał grupowania", - "min":"Min", - "max":"Maks", - "avg":"Średnia", - "sum":"Suma", - "count":"Liczba", - "none":"Brak" + "aggregation": { + "aggregation": "Zbiór", + "function": "Funkcja zbioru danych", + "limit": "Wartości maksymalne", + "group-interval": "Interwał grupowania", + "min": "Minimum", + "max": "Maksimum", + "avg": "Średnia", + "sum": "Suma", + "count": "Ilość", + "none": "Brak" }, - "admin":{ - "settings":"Ustawienia", - "general":"Ogólne", - "general-settings":"Ustawienia Ogólne", - "home-settings":"Ustawienia Strony Głównej", - "home":"Strona Główna", - "outgoing-mail":"Serwer poczty", - "outgoing-mail-settings":"Ustawienia Serwera Poczty Wychodzącej", - "system-settings":"Ustawienia Systemowe", - "test-mail-sent":"Testowy email został wysłany pomyślnie!", - "base-url":"Podstawowy URL", - "base-url-required":"Podstawowy URL jest wymagany.", - "prohibit-different-url":"Zabroń używania nazwy hosta z nagłówków żądania klienta", - "prohibit-different-url-hint":"Ta opcja powinna być włączona w środowiskach produkcyjnych. Może powodować problemy bezpieczeństwa, gdy jest wyłączona", - "device-connectivity":{ - "device-connectivity":"Łączność urządzenia", - "http-s":"HTTP(s)", - "mqtt-s":"MQTT(s)", - "coap-s":"COAP(s)", - "http":"HTTP", - "https":"HTTPs", - "mqtt":"MQTT", - "mqtts":"MQTTs", - "coap":"COAP", - "coaps":"COAPs", - "hint":"Jeśli pola hosta lub portu są puste, zostanie użyta domyślna wartość protokołu.", - "host":"Host", - "port":"Port", - "port-pattern":"Port musi być dodatnią liczbą całkowitą.", - "port-range":"Port powinien mieścić się w zakresie od 1 do 65535." + "admin": { + "settings": "Ustawienia", + "general": "Ogólne", + "general-settings": "Ustawienia ogólne", + "home-settings": "Ustawienia strony głównej", + "home": "Strona główna", + "outgoing-mail": "Serwer poczty", + "outgoing-mail-settings": "Ustawienia serwera poczty wychodzącej", + "system-settings": "Ustawienia systemowe", + "test-mail-sent": "Testowa wiadomość została pomyślnie wysłana!", + "base-url": "Bazowy adres URL", + "base-url-required": "Bazowy adres URL jest wymagany.", + "prohibit-different-url": "Zabroń używania nazwy hosta z nagłówków żądań klienta", + "prohibit-different-url-hint": "To ustawienie powinno być włączone w środowiskach produkcyjnych. Po wyłączeniu może powodować problemy z bezpieczeństwem.", + "mail-from": "Adres E-Mail nadawcy", + "mail-from-required": "Adres E-Mail nadawcy jest wymagany.", + "smtp-protocol": "Protokół SMTP", + "smtp-host": "Host SMTP", + "smtp-host-required": "Host SMTP jest wymagany.", + "smtp-port": "Port SMTP", + "smtp-port-required": "Musisz podać port smtp.", + "smtp-port-invalid": "To nie wygląda na prawidłowy port smtp.", + "timeout-msec": "Limit czasu (ms)", + "timeout-required": "Limit czasu jest wymagany.", + "timeout-invalid": "To nie wygląda na prawidłowy limit czasu.", + "enable-tls": "Włącz TLS", + "tls-version": "Wersja TLS", + "enable-proxy": "Włącz serwer proxy", + "proxy-host": "Host proxy", + "proxy-host-required": "Host proxy jest wymagany.", + "proxy-port": "Port proxy", + "proxy-port-required": "Port proxy jest wymagany.", + "proxy-port-range": "Port proxy powinien należeć do zakresu od 1 do 65535.", + "proxy-user": "Użytkownik proxy", + "proxy-password": "Hasło proxy", + "change-password": "Zmień hasło", + "send-test-mail": "Wyślij wiadomość testową", + "use-system-mail-settings": "Użyj ustawień serwera poczty systemowej", + "mail-templates": "Szablony wiadomości", + "mail-template-settings": "Ustawienia szablonów wiadomości", + "use-system-mail-template-settings": "Użyj systemowych szablonów wiadomości", + "mail-template": { + "mail-template": "Szablon wiadomości", + "test": "Testowa wiadomość e-mail", + "activation": "Wiadomość o aktywacji konta", + "account-activated": "Wiadomość o aktywacji konta", + "account-lockout": "Komunikat o blokadzie konta", + "reset-password": "Zresetuj wiadomość dotyczącą hasła", + "password-was-reset": "Komunikat o zresetowaniu hasła", + "user-activated": "Wiadomość aktywowana przez użytkownika", + "user-registered": "Wiadomość zarejestrowana przez użytkownika", + "api-usage-state-enabled": "Stan użycia interfejsu API włączony", + "api-usage-state-warning": "Ostrzeżenie o stanie użycia interfejsu API", + "api-usage-state-disabled": "Stan użycia interfejsu API wyłączony", + "two-fa-verification": "Wiadomość weryfikacyjna 2FA" }, - "mail-from":"Nadawca", - "mail-from-required":"Nadawca jest wymagany.", - "smtp-protocol":"Protokół SMTP", - "smtp-host":"Host SMTP", - "smtp-host-required":"Host SMTP jest wymagany.", - "smtp-port":"Port SMTP", - "smtp-port-required":"Musisz podać port SMTP.", - "smtp-port-invalid":"To nie wygląda na prawidłowy port SMTP.", - "timeout-msec":"Limit czasu (ms)", - "timeout-required":"Limit czasu jest wymagany.", - "timeout-invalid":"To nie wygląda na prawidłowy limit czasu.", - "enable-tls":"Włącz TLS", - "tls-version":"Wersja TLS", - "enable-proxy":"Włącz proxy", - "proxy-host":"Host proxy", - "proxy-host-required":"Host proxy jest wymagany.", - "proxy-port":"Port proxy", - "proxy-port-required":"Port proxy jest wymagany.", - "proxy-port-range":"Port proxy powinien znajdować się w zakresie od 1 do 65535.", - "proxy-user":"Użytkownik proxy", - "proxy-password":"Hasło proxy", - "change-password":"Zmień hasło", - "send-test-mail":"Wyślij mail testowy", - "sms-provider":"Dostawca SMS", - "sms-provider-settings":"Ustawienia dostawcy SMS", - "sms-provider-type":"Typ dostawcy SMS", - "sms-provider-type-required":"Typ dostawcy SMS jest wymagany.", - "sms-provider-type-aws-sns":"Amazon SNS", - "sms-provider-type-twilio":"Twilio", - "sms-provider-type-smpp":"SMPP", - "aws-access-key-id":"ID klucza dostępu AWS", - "aws-access-key-id-required":"ID klucza dostępu AWS jest wymagane", - "aws-secret-access-key":"Tajny klucz dostępu AWS", - "aws-secret-access-key-required":"Tajny klucz dostępu AWS jest wymagany", - "aws-region":"Region AWS", - "aws-region-required":"Region AWS jest wymagany", - "number-from":"Numer telefonu nadawcy", - "number-from-required":"Numer telefonu nadawcy jest wymagany.", - "number-to":"Numer telefonu odbiorcy", - "number-to-required":"Numer telefonu odbiorcy jest wymagany.", - "phone-number-hint":"Numer telefonu w formacie E.164, np. +19995550123", - "phone-number-hint-twilio":"Numer telefonu w formacie E.164/Numer SID telefonu/SID usługi wiadomości, np. +19995550123/PNXXX/MGXXX", - "phone-number-pattern":"Nieprawidłowy numer telefonu. Powinien być w formacie E.164, np. +19995550123.", - "phone-number-pattern-twilio":"Nieprawidłowy numer telefonu. Powinien być w formacie E.164/Numer SID telefonu/SID usługi wiadomości, np. +19995550123/PNXXX/MGXXX.", - "sms-message":"Wiadomość SMS", - "sms-message-required":"Wiadomość SMS jest wymagana.", - "sms-message-max-length":"Wiadomość SMS nie może być dłuższa niż 1600 znaków", - "twilio-account-sid":"SID konta Twilio", - "twilio-account-sid-required":"SID konta Twilio jest wymagany", - "twilio-account-token":"Token konta Twilio", - "twilio-account-token-required":"Token konta Twilio jest wymagany", - "send-test-sms":"Wyślij testowego SMS-a", - "test-sms-sent":"Testowy SMS został wysłany pomyślnie!", - "security-settings":"Ustawienia bezpieczeństwa", - "password-policy":"Polityka haseł", - "minimum-password-length":"Minimalna długość hasła", - "minimum-password-length-required":"Minimalna długość hasła jest wymagana", - "minimum-password-length-range":"Minimalna długość hasła powinna wynosić od 6 do 50", - "maximum-password-length":"Maksymalna długość hasła", - "maximum-password-length-min":"Maksymalna długość hasła powinna wynosić co najmniej 6", - "maximum-password-length-less-min":"Maksymalna długość hasła powinna być większa niż minimalna długość", - "minimum-uppercase-letters":"Minimalna liczba wielkich liter", - "minimum-uppercase-letters-range":"Minimalna liczba wielkich liter nie może być ujemna", - "minimum-lowercase-letters":"Minimalna liczba małych liter", - "minimum-lowercase-letters-range":"Minimalna liczba małych liter nie może być ujemna", - "minimum-digits":"Minimalna liczba cyfr", - "minimum-digits-range":"Minimalna liczba cyfr nie może być ujemna", - "minimum-special-characters":"Minimalna liczba znaków specjalnych", - "minimum-special-characters-range":"Minimalna liczba znaków specjalnych nie może być ujemna", - "password-expiration-period-days":"Okres ważności hasła w dniach", - "password-expiration-period-days-range":"Okres ważności hasła w dniach nie może być ujemny", - "password-reuse-frequency-days":"Częstotliwość ponownego używania hasła w dniach", - "password-reuse-frequency-days-range":"Częstotliwość ponownego używania hasła w dniach nie może być ujemna", - "allow-whitespace":"Zezwól na białe znaki", - "force-reset-password-if-no-valid":"Wymuś zresetowanie hasła, jeśli nie jest ważne", - "force-reset-password-if-no-valid-hint":"Ostrożnie z włączeniem tej funkcji: będzie wymagać od użytkowników z nieaktualnymi hasłami resetowania ich poprzez email.", - "general-policy":"Ogólna polityka", - "max-failed-login-attempts":"Maksymalna liczba nieudanych prób", - "minimum-max-failed-login-attempts-range":"Maksymalna liczba nieudanych prób logowania nie może być ujemna", - "user-lockout-notification-email":"W przypadku zablokowania konta użytkownika, wyślij powiadomienie na email", - "domain-name":"Nazwa domeny", - "domain-name-unique":"Nazwa domeny i protokół muszą być unikalne.", - "domain-name-max-length":"Nazwa domeny powinna być krótsza niż 256", - "error-verification-url":"Nazwa domeny nie powinna zawierać symboli '/' i ':'. Przykład: thingsboard.io", - "connection-settings":"Ustawienia połączenia", - "oauth2":{ - "access-token-uri":"URI tokenu dostępu", - "access-token-uri-required":"URI tokenu dostępu jest wymagane.", - "activate-user":"Aktywuj użytkownika", - "add-domain":"Dodaj domenę", - "delete-domain":"Usuń domenę", - "add-provider":"Dodaj dostawcę", - "delete-provider":"Usuń dostawcę", - "allow-user-creation":"Zezwalaj na tworzenie użytkowników", - "always-fullscreen":"Zawsze na pełnym ekranie", - "authorization-uri":"URI autoryzacji", - "authorization-uri-required":"URI autoryzacji jest wymagane.", - "client-authentication-method":"Metoda uwierzytelnienia klienta", - "client-id":"ID klienta", - "client-id-required":"ID klienta jest wymagane.", - "client-id-max-length":"ID klienta powinno być krótsze niż 256", - "client-secret":"Sekret klienta", - "client-secret-required":"Sekret klienta jest wymagany.", - "client-secret-max-length":"Sekret klienta powinien być krótszy niż 2049", - "custom-setting":"Niestandardowe ustawienia", - "customer-name-pattern":"Wzorzec nazwy klienta", - "customer-name-pattern-max-length":"Wzorzec nazwy klienta powinien być krótszy niż 256", - "default-dashboard-name":"Domyślna nazwa pulpitu nawigacyjnego", - "default-dashboard-name-max-length":"Domyślna nazwa pulpitu nawigacyjnego powinna być krótsza niż 256", - "delete-domain-text":"Uważaj, po potwierdzeniu domena i wszystkie dane dostawcy będą niedostępne.", - "delete-domain-title":"Czy na pewno chcesz usunąć ustawienia domeny '{{domainName}}'?", - "delete-registration-text":"Uważaj, po potwierdzeniu dane dostawcy będą niedostępne.", - "delete-registration-title":"Czy na pewno chcesz usunąć dostawcę '{{name}}'?", - "email-attribute-key":"Klucz atrybutu email", - "email-attribute-key-required":"Klucz atrybutu email jest wymagany.", - "email-attribute-key-max-length":"Klucz atrybutu email powinien być krótszy niż 32", - "first-name-attribute-key":"Klucz atrybutu imienia", - "first-name-attribute-key-max-length":"Klucz atrybutu imienia powinien być krótszy niż 32", - "general":"Ogólne", - "jwk-set-uri":"URI klucza Web JSON", - "last-name-attribute-key":"Klucz atrybutu nazwiska", - "last-name-attribute-key-max-length":"Klucz atrybutu nazwiska powinien być krótszy niż 32", - "login-button-icon":"Ikona przycisku logowania", - "login-button-label":"Etykieta dostawcy", - "login-button-label-placeholder":"Zaloguj się przez $(Etykieta dostawcy)", - "login-button-label-required":"Etykieta jest wymagana.", - "login-provider":"Dostawca logowania", - "mapper":"Mapper", - "new-domain":"Nowa domena", - "oauth2":"OAuth2", - "password-max-length":"Hasło powinno być krótsze niż 256", - "redirect-uri-template":"Szablon URI przekierowania", - "copy-redirect-uri":"Kopiuj URI przekierowania", - "registration-id":"ID rejestracji", - "registration-id-required":"ID rejestracji jest wymagane.", - "registration-id-unique":"ID rejestracji musi być unikalne dla systemu.", - "scope":"Zakres", - "scope-required":"Zakres jest wymagany.", - "tenant-name-pattern":"Wzorzec nazwy dzierżawcy", - "tenant-name-pattern-required":"Wzorzec nazwy dzierżawcy jest wymagany.", - "tenant-name-pattern-max-length":"Wzorzec nazwy dzierżawcy powinien być krótszy niż 256", - "tenant-name-strategy":"Strategia nazewnictwa dzierżawcy", - "type":"Typ mappera", - "uri-pattern-error":"Nieprawidłowy format URI.", - "url":"URL", - "url-pattern":"Nieprawidłowy format URL.", - "url-required":"URL jest wymagany.", - "url-max-length":"URL powinien być krótszy niż 256", - "user-info-uri":"URI informacji o użytkowniku", - "user-info-uri-required":"URI informacji o użytkowniku jest wymagane.", - "username-max-length":"Nazwa użytkownika powinna być krótsza niż 256", - "user-name-attribute-name":"Klucz atrybutu nazwy użytkownika", - "user-name-attribute-name-required":"Klucz atrybutu nazwy użytkownika jest wymagany", - "protocol":"Protokół", - "domain-schema-http":"HTTP", - "domain-schema-https":"HTTPS", - "domain-schema-mixed":"HTTP+HTTPS", - "enable":"Włącz ustawienia OAuth2", - "domains":"Domeny", - "mobile-apps":"Aplikacje mobilne", - "no-mobile-apps":"Brak skonfigurowanych aplikacji", - "mobile-package":"Pakiet aplikacji", - "mobile-package-placeholder":"Np.: my.example.app", - "mobile-package-hint":"Dla Androida: unikalny ID aplikacji. Dla iOS: identyfikator paczki produktu.", - "mobile-package-unique":"Pakiet aplikacji musi być unikalny.", - "mobile-app-secret":"Sekret aplikacji", - "invalid-mobile-app-secret":"Sekret aplikacji musi zawierać tylko znaki alfanumeryczne i mieć od 16 do 2048 znaków.", - "copy-mobile-app-secret":"Kopiuj sekret aplikacji", - "add-mobile-app":"Dodaj aplikację", - "delete-mobile-app":"Usuń informacje o aplikacji", - "providers":"Dostawcy", - "platform-web":"Web", - "platform-android":"Android", - "platform-ios":"iOS", - "all-platforms":"Wszystkie platformy", - "smtp-provider":"Dostawca SMTP", - "allowed-platforms":"Dozwolone platformy", - "authentication":"Autentykacja", - "basic":"Podstawowy", - "provider":"Dostawca", - "redirect-url":"URI przekierowania", - "domain-name":"Nazwa domeny", - "redirect-url-template":"Szablon URI przekierowania", - "microsoft-tenant-id":"ID dzierżawy (tenant) Microsoft", - "microsoft-tenant-id-required":"ID dzierżawy (tenant) Microsoft jest wymagane", - "token-uri":"URI tokenu", - "token-uri-required":"URI tokenu jest wymagane", - "redirect-uri":"URI przekierowania", - "google-provider":"Google", - "microsoft-provider":"Office 365", - "sendgrid-provider":"Sendgrid", - "custom-provider":"Niestandardowy", - "generate-access-token":"Generuj token dostępu", - "update-access-token":"Aktualizuj token dostępu", - "access-token-status":"Status tokenu dostępu:", - "token-status-generated":"wygenerowany", - "token-status-not-generated":"nie wygenerowany" + "mail-subject": "Temat wiadomości", + "mail-body": "Treść wiadomości", + "sms-provider": "Dostawca SMS", + "sms-provider-settings": "Ustawienia dostawcy SMS", + "use-system-sms-settings": "Skorzystaj z systemowych ustawień dostawcy SMS", + "sms-provider-type": "Typ dostawcy SMS", + "sms-provider-type-required": "Typ dostawcy SMS jest wymagany.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "Identyfikator klucza dostępu AWS", + "aws-access-key-id-required": "Identyfikator klucza dostępu AWS jest wymagany", + "aws-secret-access-key": "Tajny klucz dostępu AWS", + "aws-secret-access-key-required": "Tajny klucz dostępu AWS jest wymagany", + "aws-region": "Region AWS", + "aws-region-required": "Region AWS jest wymagany", + "number-from": "Numer telefonu nadawcy", + "number-from-required": "Numer telefonu nadawcy jest wymagany.", + "number-to": "Numer telefonu adresata", + "number-to-required": "Numer telefonu adresata jest wymagany.", + "phone-number-hint": "Numer telefonu w formacie E.164, np. ", + "phone-number-hint-twilio": "Numer telefonu w formacie E.164/identyfikator SID numeru telefonu/SID usługi przesyłania wiadomości, np. +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "Nieprawidłowy numer telefonu. Powinien być w formacie E.164, np. +19995550123.", + "phone-number-pattern-twilio": "Nieprawidłowy numer telefonu. Powinien być w formacie E.164/identyfikator SID numeru telefonu/SID usługi przesyłania wiadomości, np. +19995550123/PNXXX/MGXXX", + "sms-message": "Wiadomość SMS", + "sms-message-required": "Wiadomość SMS jest wymagana.", + "sms-message-max-length": "Wiadomość SMS nie może mieć więcej niż 1600 znaków", + "twilio-account-sid": "Identyfikator SID konta Twilio", + "twilio-account-sid-required": "Identyfikator SID konta Twilio jest wymagany", + "twilio-account-token": "Token konta Twilio", + "twilio-account-token-required": "Token konta Twilio jest wymagany", + "send-test-sms": "Wyślij testowego SMS-a", + "test-sms-sent": "Testowy SMS został pomyślnie wysłany!", + "security-settings": "Ustawienia bezpieczeństwa", + "password-policy": "Polityka haseł", + "minimum-password-length": "Minimalna długość hasła", + "minimum-password-length-required": "Minimalna długość hasła jest wymagana", + "minimum-password-length-range": "Minimalna długość hasła powinna mieścić się w zakresie od 5 do 50", + "minimum-uppercase-letters": "Minimalna liczba wielkich liter", + "minimum-uppercase-letters-range": "Minimalna liczba wielkich liter nie może być ujemna", + "minimum-lowercase-letters": "Minimalna liczba małych liter", + "minimum-lowercase-letters-range": "Minimalna liczba małych liter nie może być ujemna", + "minimum-digits": "Minimalna liczba cyfr", + "minimum-digits-range": "Minimalna liczba cyfr nie może być ujemna", + "minimum-special-characters": "Minimalna liczba znaków specjalnych", + "minimum-special-characters-range": "Minimalna liczba znaków specjalnych nie może być ujemna", + "password-expiration-period-days": "Okres ważności hasła w dniach", + "password-expiration-period-days-range": "Okres ważności hasła w dniach nie może być ujemny", + "password-reuse-frequency-days": "Częstotliwość ponownego użycia hasła w dniach", + "password-reuse-frequency-days-range": "Częstotliwość ponownego użycia hasła w dniach nie może być ujemna", + "allow-whitespace": "Zezwalaj na białe znaki", + "general-policy": "Polityka ogólna", + "max-failed-login-attempts": "Maksymalna liczba nieudanych prób logowania, zanim konto zostanie zablokowane", + "minimum-max-failed-login-attempts-range": "Maksymalna liczba nieudanych prób logowania nie może być ujemna", + "user-lockout-notification-email": "W przypadku blokady konta użytkownika wyślij powiadomienie na e-mail", + "domain-name": "Nazwa domeny", + "domain-name-unique": "Nazwa domeny i protokół muszą być unikalne.", + "domain-name-max-length": "Nazwa domeny powinna być mniejsza niż 256", + "error-verification-url": "Nazwa domeny nie powinna zawierać symboli „/” i „:”. ", + "connection-settings": "Connection settings", + "oauth2": { + "access-token-uri": "URI tokena dostępu", + "access-token-uri-required": "Identyfikator URI tokenu dostępu jest wymagany.", + "activate-user": "Aktywuj użytkownika", + "add-domain": "Dodaj domenę", + "delete-domain": "Usuń domenę", + "add-provider": "Dodaj dostawcę", + "delete-provider": "Usuń dostawcę", + "allow-user-creation": "Zezwalaj na tworzenie użytkowników", + "always-fullscreen": "Zawsze w trybie pełnoekranowym", + "authorization-uri": "URI autoryzacji", + "authorization-uri-required": "Identyfikator URI autoryzacji jest wymagany.", + "client-authentication-method": "Metoda uwierzytelniania klienta", + "client-id": "Identyfikator klienta", + "client-id-required": "Identyfikator klienta jest wymagany.", + "client-id-max-length": "Identyfikator klienta powinien być mniejszy niż 256", + "client-secret": "Tajny klucz klienta", + "client-secret-required": "Tajny klucz klienta jest wymagany.", + "client-secret-max-length": "Tajny klucz klienta powinien być mniejszy niż 2049", + "custom-setting": "Ustawienia niestandardowe", + "customer-name-pattern": "Wzorzec nazwy klienta", + "customer-name-pattern-max-length": "Wzorzec nazwy klienta powinien być mniejszy niż 256", + "parent-customer-name-pattern": "Wzorzec nazwy klienta nadrzędnego", + "user-groups-name-pattern": "Wzorzec nazw grup użytkowników", + "default-dashboard-name": "Domyślna nazwa panelu", + "default-dashboard-name-max-length": "Domyślna nazwa panelu powinna być mniejsza niż 256", + "delete-domain-text": "Uważaj, po potwierdzeniu domena i wszystkie dane dostawcy będą niedostępne.", + "delete-domain-title": "Czy na pewno chcesz usunąć domenę '{{domainName}}'?", + "delete-registration-text": "Uważaj, po potwierdzeniu dane dostawcy będą niedostępne.", + "delete-registration-title": "Czy na pewno chcesz usunąć dostawcę '{{name}}'?", + "email-attribute-key": "Klucz atrybutu adresu e-mail", + "email-attribute-key-required": "Klucz atrybutu adresu e-mail jest wymagany.", + "email-attribute-key-max-length": "Klucz atrybutu adresu e-mail powinien być mniejszy niż 32", + "first-name-attribute-key": "Klucz atrybutu imienia", + "first-name-attribute-key-max-length": "Klucz atrybutu imienia powinien być mniejszy niż 32", + "general": "Ogólne", + "jwk-set-uri": "URI klucza internetowego JSON", + "last-name-attribute-key": "Klucz atrybutu nazwiska", + "last-name-attribute-key-max-length": "Klucz atrybutu nazwiska powinien być mniejszy niż 32", + "login-button-icon": "Ikona przycisku logowania", + "login-button-label": "Etykieta dostawcy", + "login-button-label-placeholder": "Zaloguj się za pomocą $(Provider label)", + "login-button-label-required": "Etykieta jest wymagana.", + "login-provider": "Dostawca logowania", + "mapper": "Maper", + "new-domain": "Nowa domena", + "oauth2": "OAuth2", + "password-max-length": "Hasło powinno być mniejsze niż 256", + "redirect-uri-template": "Szablon przekierowania URI", + "copy-redirect-uri": "Skopiuj identyfikator przekierowania URI", + "registration-id": "Identyfikator rejestracji", + "registration-id-required": "Identyfikator rejestracyjny jest wymagany.", + "registration-id-unique": "Identyfikator rejestracyjny musi być unikalny dla systemu.", + "scope": "Zakres", + "scope-required": "Zakres jest wymagany.", + "tenant-name-pattern": "Wzór nazwy najemcy", + "tenant-name-pattern-required": "Wzorzec nazwy dzierżawy jest wymagany.", + "tenant-name-pattern-max-length": "Wzorzec nazwy dzierżawy powinien być mniejszy niż 256", + "tenant-name-strategy": "Strategia dotycząca nazwy najemcy", + "type": "Typ mapera", + "uri-pattern-error": "Nieprawidłowy format URI.", + "url": "Adres URL", + "url-pattern": "Nieprawidłowy format adresu URL.", + "url-required": "Adres URL jest wymagany.", + "url-max-length": "Adres URL powinien być mniejszy niż 256", + "user-info-uri": "URI informacji o użytkowniku", + "user-info-uri-required": "Identyfikator URI informacji o użytkowniku jest wymagany.", + "username-max-length": "Nazwa użytkownika powinna być mniejsza niż 256", + "user-name-attribute-name": "Klucz atrybutu nazwy użytkownika", + "user-name-attribute-name-required": "Klucz atrybutu nazwy użytkownika jest wymagany", + "protocol": "Protokół", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Włącz ustawienia OAuth2", + "domains": "Domeny", + "mobile-apps": "Aplikacje mobilne", + "no-mobile-apps": "Nie skonfigurowano żadnych aplikacji", + "mobile-package": "Pakiet aplikacji", + "mobile-package-placeholder": "Np.: moja.przykładowa.aplikacja", + "mobile-package-hint": "Dla Androida: Twój własny, unikalny identyfikator aplikacji. Dla iOS: identyfikator Bundle ID", + "mobile-package-unique": "Pakiet aplikacji musi być unikalny.", + "mobile-app-secret": "Tajny klucz aplikacji", + "invalid-mobile-app-secret": "Tajny klucz aplikacji może zawierać wyłącznie znaki alfanumeryczne i mieć długość od 16 do 2048 znaków.", + "copy-mobile-app-secret": "Skopiuj tajny klucz aplikacji", + "add-mobile-app": "Dodaj aplikację", + "delete-mobile-app": "Usuń informacje o aplikacji", + "providers": "Dostawcy", + "platform-web": "Sieć", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "Wszystkie platformy", + "smtp-provider": "SMTP provider", + "allowed-platforms": "Dozwolone platformy", + "authentication": "Authentication", + "basic": "Basic", + "provider": "Provider", + "redirect-url": "Redirect URI", + "domain-name": "Domain name", + "redirect-url-template": "Redirect URI template", + "microsoft-tenant-id": "Directory (tenant) Id", + "microsoft-tenant-id-required": "Directory (tenant) Id is required", + "token-uri": "Token URI", + "token-uri-required": "Token URI is required", + "redirect-uri": "Redirect URI", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "Custom", + "generate-access-token": "Generate access token", + "update-access-token": "Update access token", + "access-token-status": "Access token status:", + "token-status-generated": "generated", + "token-status-not-generated": "not generated" }, - "smpp-provider":{ - "smpp-version":"Wersja SMPP", - "smpp-host":"Host SMPP", - "smpp-host-required":"Host SMPP jest wymagany", - "smpp-port":"Port SMPP", - "smpp-port-required":"Port SMPP jest wymagany", - "system-id":"ID systemu", - "system-id-required":"ID systemu jest wymagane", - "password":"Hasło", - "password-required":"Hasło jest wymagane", - "type-settings":"Ustawienia typu", - "source-settings":"Ustawienia źródła", - "destination-settings":"Ustawienia docelowe", - "additional-settings":"Dodatkowe ustawienia", - "system-type":"Typ systemu", - "bind-type":"Typ powiązania", - "service-type":"Typ usługi", - "source-address":"Adres źródłowy", - "source-ton":"TON źródłowy", - "source-npi":"NPI źródłowy", - "destination-ton":"TON docelowy (Typ Numeru)", - "destination-npi":"NPI docelowy (Identyfikacja Planu Numeracji)", - "address-range":"Zakres adresów", - "coding-scheme":"Schemat kodowania", - "bind-type-tx":"Nadajnik", - "bind-type-rx":"Odbiornik", - "bind-type-trx":"Nadawczo-odbiorczy", - "ton-unknown":"Nieznany", - "ton-international":"Międzynarodowy", - "ton-national":"Krajowy", - "ton-network-specific":"Specyficzny dla sieci", - "ton-subscriber-number":"Numer abonenta", - "ton-alphanumeric":"Alfanumeryczny", - "ton-abbreviated":"Skrócony", - "npi-unknown":"0 - Nieznany", - "npi-isdn":"1 - ISDN/plan numeracji telefonicznej (E163/E164)", - "npi-data-numbering-plan":"3 - Plan numeracji danych (X.121)", - "npi-telex-numbering-plan":"4 - Plan numeracji telexu (F.69)", - "npi-land-mobile":"6 - Mobilny lądowy (E.212)", - "npi-national-numbering-plan":"8 - Krajowy plan numeracji", - "npi-private-numbering-plan":"9 - Prywatny plan numeracji", - "npi-ermes-numbering-plan":"10 - Plan numeracji ERMES (ETSI DE/PS 3 01-3)", - "npi-internet":"13 - Internet (IP)", - "npi-wap-client-id":"18 - Identyfikator klienta WAP (do zdefiniowania przez Forum WAP)", - "scheme-smsc":"0 - Domyślny alfabet SMSC (ASCII dla krótkiego i długiego kodu oraz do GSM dla bezpłatnych)", - "scheme-ia5":"1 - IA5 (ASCII dla krótkiego i długiego kodu, łaciński 9 dla bezpłatnych (ISO-8859-9))", - "scheme-octet-unspecified-2":"2 - Oktet nieokreślony (binarny 8-bitowy)", - "scheme-latin-1":"3 - Łaciński 1 (ISO-8859-1)", - "scheme-octet-unspecified-4":"4 - Oktet nieokreślony (binarny 8-bitowy)", - "scheme-jis":"5 - JIS (X 0208-1990)", - "scheme-cyrillic":"6 - Cyrylica (ISO-8859-5)", - "scheme-latin-hebrew":"7 - Łaciński/hebrajski (ISO-8859-8)", - "scheme-ucs-utf":"8 - UCS2/UTF-16 (ISO/IEC-10646)", - "scheme-pictogram-encoding":"9 - Kodowanie piktogramów", - "scheme-music-codes":"10 - Kody muzyczne (ISO-2022-JP)", - "scheme-extended-kanji-jis":"13 - Rozszerzony Kanji JIS (X 0212-1990)", - "scheme-korean-graphic-character-set":"14 - Zestaw koreańskich znaków graficznych (KS C 5601/KS X 1001)" + "smpp-provider": { + "smpp-version": "Wersja SMPP", + "smpp-host": "Host SMPP", + "smpp-host-required": "Host SMPP jest wymagany", + "smpp-port": "Port SMPP", + "smpp-port-required": "Port SMPP jest wymagany", + "system-id": "Identyfikator systemu", + "system-id-required": "Identyfikator systemu jest wymagany", + "password": "Hasło", + "password-required": "Hasło jest wymagane", + "type-settings": "Ustawienia typu", + "source-settings": "Ustawienia źródła", + "destination-settings": "Ustawienia celu", + "additional-settings": "Ustawienia dodatkowe", + "system-type": "Rodzaj systemu", + "bind-type": "Typ wiązania", + "service-type": "Rodzaj usługi", + "source-address": "Adres źródłowy", + "source-ton": "TON źródłowy", + "source-npi": "NPI źródłowy", + "destination-ton": "TON celu (Type of Number)", + "destination-npi": "NPI celu (Numbering Plan Identification)", + "address-range": "Zakres adresów", + "coding-scheme": "System kodowania", + "bind-type-tx": "Nadajnik", + "bind-type-rx": "Odbiornik", + "bind-type-trx": "Przekaźnik", + "ton-unknown": "Nieznany", + "ton-international": "Międzynarodowy", + "ton-national": "Krajowy", + "ton-network-specific": "Specyficzny dla sieci", + "ton-subscriber-number": "Numer subskrybenta", + "ton-alphanumeric": "Alfanumeryczny", + "ton-abbreviated": "Skrócony", + "npi-unknown": "0 - Nieznane", + "npi-isdn": "1 - Plan numeracji ISDN/telefonu (E163/E164)", + "npi-data-numbering-plan": "3 - Plan numeracji danych (X.121)", + "npi-telex-numbering-plan": "4 - Plan numeracji teleksu (F.69)", + "npi-land-mobile": "6 – Land mobile (E.212)", + "npi-national-numbering-plan": "8 - Krajowy plan numeracji", + "npi-private-numbering-plan": "9 - Plan numeracji prywatnej", + "npi-ermes-numbering-plan": "10 - Plan numeracji ERMES (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - Internet (IP)", + "npi-wap-client-id": "18 - Identyfikator klienta WAP (określony przez Forum WAP)", + "scheme-smsc": "0 - Domyślny alfabet SMSC (ASCII dla krótkiego i długiego kodu oraz GSM dla numeru bezpłatnego)", + "scheme-ia5": "1 - IA5 (ASCII dla krótkiego i długiego kodu, Latin 9 dla numeru bezpłatnego (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 — oktet nieokreślony (8-bitów binarnie)", + "scheme-latin-1": "3 - Łacina 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 — oktet nieokreślony (8-bitowy plik binarny)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 – Cyrylica (ISO-8859-5)", + "scheme-latin-hebrew": "7 – Łacina/Hebrajski (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - Kodowanie piktogramów", + "scheme-music-codes": "10 - Kody muzyczne (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - Rozszerzone Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - Koreański zestaw znaków graficznych (KS C 5601/KS X 1001)" }, - "queue-select-name":"Wybierz nazwę kolejki", - "queue-name":"Nazwa", - "queue-name-required":"Nazwa kolejki jest wymagana!", - "queues":"Kolejki", - "queue-partitions":"Partycje", - "queue-submit-strategy":"Strategia zgłaszania", - "queue-processing-strategy":"Strategia przetwarzania", - "queue-configuration":"Konfiguracja kolejki", - "repository-settings":"Ustawienia repozytorium", - "repository":"Repozytorium", - "repository-url":"URL repozytorium", - "repository-url-required":"URL repozytorium jest wymagany.", - "default-branch":"Domyślna nazwa gałęzi", - "repository-read-only":"Tylko do odczytu", - "show-merge-commits":"Pokaż commity scalające", - "authentication-settings":"Ustawienia autentykacji", - "auth-method":"Metoda uwierzytelniania", - "auth-method-username-password":"Hasło / token dostępu", - "auth-method-username-password-hint":"Użytkownicy GitHub muszą używać tokenów dostępu z uprawnieniami do zapisu w repozytorium.", - "auth-method-private-key":"Klucz prywatny", - "password-access-token":"Hasło / token dostępu", - "change-password-access-token":"Zmień hasło / token dostępu", - "private-key":"Klucz prywatny", - "drop-private-key-file-or":"Przeciągnij i upuść plik klucza prywatnego lub", - "passphrase":"Hasło", - "enter-passphrase":"Wprowadź hasło", - "change-passphrase":"Zmień hasło", - "check-access":"Sprawdź dostęp", - "check-repository-access-success":"Dostęp do repozytorium pomyślnie zweryfikowany!", - "delete-repository-settings-title":"Czy na pewno chcesz usunąć ustawienia repozytorium?", - "delete-repository-settings-text":"Uważaj, po potwierdzeniu ustawienia repozytorium zostaną usunięte, a funkcja kontroli wersji będzie niedostępna.", - "auto-commit-settings":"Ustawienia automatycznego zatwierdzania", - "auto-commit":"Automatyczne zatwierdzanie", - "auto-commit-entities":"Automatyczne zatwierdzanie encji", - "no-auto-commit-entities-prompt":"Brak skonfigurowanych encji do automatycznego zatwierdzania", - "delete-auto-commit-settings-title":"Czy na pewno chcesz usunąć ustawienia automatycznego zatwierdzania?", - "delete-auto-commit-settings-text":"Uważaj, po potwierdzeniu ustawienia automatycznego zatwierdzania zostaną usunięte, a automatyczne zatwierdzanie zostanie wyłączone dla wszystkich encji.", - "2fa":{ - "2fa":"Dwuetapowa autentykacja", - "available-providers":"Dostępni dostawcy", - "issuer-name":"Nazwa wydawcy", - "issuer-name-required":"Nazwa wydawcy jest wymagana.", - "max-verification-failures-before-user-lockout":"Maksymalna liczba nieudanych weryfikacji przed zablokowaniem użytkownika", - "max-verification-failures-before-user-lockout-pattern":"Maksymalna liczba nieudanych weryfikacji musi być dodatnią liczbą całkowitą.", - "number-of-checking-attempts":"Liczba prób sprawdzenia", - "number-of-checking-attempts-pattern":"Liczba prób sprawdzenia musi być dodatnią liczbą całkowitą.", - "number-of-checking-attempts-required":"Liczba prób sprawdzenia jest wymagana.", - "number-of-codes":"Liczba kodów", - "number-of-codes-pattern":"Liczba kodów musi być dodatnią liczbą całkowitą.", - "number-of-codes-required":"Liczba kodów jest wymagana.", - "provider":"Dostawca", - "retry-verification-code-period":"Okres ponownego próbowania kodu weryfikacyjnego (sek)", - "retry-verification-code-period-pattern":"Minimalny czas okresu to 5 sek", - "retry-verification-code-period-required":"Okres ponownego próbowania kodu weryfikacyjnego jest wymagany.", - "total-allowed-time-for-verification":"Całkowity dozwolony czas na weryfikację (sek)", - "total-allowed-time-for-verification-pattern":"Minimalny całkowity dozwolony czas to 60 sek", - "total-allowed-time-for-verification-required":"Całkowity dozwolony czas jest wymagany.", - "use-system-two-factor-auth-settings":"Użyj systemowych ustawień dwuetapowej autentykacji", - "verification-code-check-rate-limit":"Limit częstotliwości sprawdzania kodu weryfikacyjnego", - "verification-code-lifetime":"Czas życia kodu weryfikacyjnego (sek)", - "verification-code-lifetime-pattern":"Czas życia kodu weryfikacyjnego musi być dodatnią liczbą całkowitą.", - "verification-code-lifetime-required":"Czas życia kodu weryfikacyjnego jest wymagany.", - "verification-message-template":"Szablon wiadomości weryfikacyjnej", - "verification-limitations":"Ograniczenia weryfikacji", - "verification-message-template-pattern":"Wiadomość weryfikacyjna musi zawierać wzór: ${code}", - "verification-message-template-required":"Szablon wiadomości weryfikacyjnej jest wymagany.", - "within-time":"W czasie (sek)", - "within-time-pattern":"Czas musi być dodatnią liczbą całkowitą.", - "within-time-required":"Czas jest wymagany." + "queue-select-name": "Wybierz nazwę kolejki", + "queue-name": "Nazwa", + "queue-name-required": "Nazwa kolejki jest wymagana!", + "queues": "Kolejki", + "queue-partitions": "Partycje", + "queue-submit-strategy": "Prześlij strategię", + "queue-processing-strategy": "Strategia przetwarzania", + "queue-configuration": "Konfiguracja kolejki", + "repository-settings": "Ustawienia repozytorium", + "repository": "Repozytorium", + "repository-url": "Adres URL repozytorium", + "repository-url-required": "Adres URL repozytorium jest wymagany.", + "default-branch": "Domyślna nazwa gałęzi", + "repository-read-only": "Tylko do odczytu", + "show-merge-commits": "Pokaż zmiany scalające (merge commits)", + "authentication-settings": "Ustawienia uwierzytelniania", + "auth-method": "Metoda Uwierzytelnienia", + "auth-method-username-password": "Hasło / token dostępu", + "auth-method-username-password-hint": "Użytkownicy GitHuba muszą korzystać z tokenów dostępu z uprawnieniami do zapisu w repozytorium.", + "auth-method-private-key": "Klucz prywatny", + "password-access-token": "Hasło / token dostępu", + "change-password-access-token": "Zmień hasło/token dostępu", + "private-key": "Klucz prywatny", + "drop-private-key-file-or": "Przeciągnij i upuść plik klucza prywatnego lub", + "passphrase": "Hasło", + "enter-passphrase": "Wprowadź hasło", + "change-passphrase": "Zmień hasło", + "check-access": "Sprawdź dostęp", + "check-repository-access-success": "Dostęp do repozytorium pomyślnie zweryfikowany!", + "delete-repository-settings-title": "Czy na pewno chcesz usunąć ustawienia repozytorium?", + "delete-repository-settings-text": "Uważaj, po potwierdzeniu ustawienia repozytorium zostaną usunięte, a funkcja kontroli wersji będzie niedostępna.", + "auto-commit-settings": "Ustawienia automatycznego zatwierdzania", + "auto-commit": "Automatyczne zatwierdzanie", + "auto-commit-entities": "Obiekty automatycznego zatwierdzania", + "no-auto-commit-entities-prompt": "Brak obiektów skonfigurowanych do automatycznego zatwierdzania", + "delete-auto-commit-settings-title": "Czy na pewno chcesz usunąć ustawienia automatycznego zatwierdzania?", + "delete-auto-commit-settings-text": "Bądź ostrożny, po potwierdzeniu ustawienia automatycznego zatwierdzania zostaną usunięte, a automatyczne zatwierdzanie zostanie wyłączone dla wszystkich obiektów.", + "2fa": { + "2fa": "Weryfikacja dwuetapowa", + "available-providers": "Dostępni dostawcy", + "issuer-name": "Nazwa emitenta", + "issuer-name-required": "Nazwa emitenta jest wymagana.", + "max-verification-failures-before-user-lockout": "Maksymalna liczba błędów weryfikacji przed zablokowaniem użytkownika", + "max-verification-failures-before-user-lockout-pattern": "Maksymalna liczba niepowodzeń weryfikacji musi być dodatnią liczbą całkowitą.", + "number-of-checking-attempts": "Liczba prób sprawdzenia", + "number-of-checking-attempts-pattern": "Liczba prób sprawdzenia musi być dodatnią liczbą całkowitą.", + "number-of-checking-attempts-required": "Liczba prób sprawdzenia jest wymagana.", + "number-of-codes": "Liczba kodów", + "number-of-codes-pattern": "Liczba kodów musi być dodatnią liczbą całkowitą.", + "number-of-codes-required": "Liczba kodów jest wymagana.", + "provider": "Dostawca", + "retry-verification-code-period": "Okres ponownej próby weryfikacji kodu (s)", + "retry-verification-code-period-pattern": "Minimalny czas trwania okresu wynosi 5 sekund", + "retry-verification-code-period-required": "Okres ponownej próby weryfikacji kodu jest wymagany.", + "total-allowed-time-for-verification": "Całkowity dozwolony czas weryfikacji (s)", + "total-allowed-time-for-verification-pattern": "Minimalny całkowity dozwolony czas wynosi 60 sekund", + "total-allowed-time-for-verification-required": "Całkowity dozwolony czas jest wymagany.", + "use-system-two-factor-auth-settings": "Użyj systemowych ustawień weryfikacji dwuetapowej", + "verification-code-check-rate-limit": "Limit szybkości sprawdzania kodu weryfikacyjnego", + "verification-code-lifetime": "Okres ważności kodu weryfikacyjnego (s)", + "verification-code-lifetime-pattern": "Okres ważności kodu weryfikacyjnego musi być dodatnią liczbą całkowitą.", + "verification-code-lifetime-required": "Okres ważności kodu weryfikacyjnego jest wymagany.", + "verification-message-template": "Szablon wiadomości weryfikacyjnej", + "verification-limitations": "Ograniczenia weryfikacji", + "verification-message-template-pattern": "Wiadomość weryfikacyjna musi zawierać wzór: ${code}", + "verification-message-template-required": "Szablon wiadomości weryfikacyjnej jest wymagany.", + "within-time": "W ciągu (sek)", + "within-time-pattern": "Czas musi być dodatnią liczbą całkowitą.", + "within-time-required": "Czas jest wymagany." }, - "jwt":{ - "security-settings":"Ustawienia bezpieczeństwa JWT", - "issuer-name":"Nazwa wydawcy", - "issuer-name-required":"Nazwa wydawcy jest wymagana.", - "signings-key":"Klucz podpisujący", - "signings-key-hint":"Kodowany Base64 ciąg reprezentujący co najmniej 256 bitów danych.", - "signings-key-required":"Klucz podpisujący jest wymagany.", - "signings-key-min-length":"Klucz podpisujący musi reprezentować co najmniej 256 bitów danych.", - "signings-key-base64":"Klucz podpisujący musi być w formacie base64.", - "expiration-time":"Czas wygaśnięcia tokena (sek)", - "expiration-time-required":"Czas wygaśnięcia tokena jest wymagany.", - "expiration-time-pattern":"Czas wygaśnięcia tokena musi być dodatnią liczbą całkowitą.", - "expiration-time-min":"Minimalny czas to 60 sekund (1 minuta).", - "refresh-expiration-time":"Czas wygaśnięcia tokena odświeżającego (sek)", - "refresh-expiration-time-required":"Czas wygaśnięcia tokena odświeżającego jest wymagany.", - "refresh-expiration-time-pattern":"Czas wygaśnięcia tokena odświeżającego musi być dodatnią liczbą całkowitą.", - "refresh-expiration-time-min":"Minimalny czas to 900 sekund (15 minut).", - "refresh-expiration-time-less-token":"Czas tokena odświeżającego musi być dłuższy niż czas tokena.", - "generate-key":"Wygeneruj klucz", - "info-header":"Wszyscy użytkownicy będą musieli ponownie się zalogować", - "info-message":"Zmiana klucza podpisującego JWT spowoduje unieważnienie wszystkich wydanych tokenów. Wszyscy użytkownicy będą musieli ponownie się zalogować. Dotyczy to również skryptów korzystających z Rest API/Websockets." + "jwt": { + "security-settings": "Ustawienia zabezpieczeń JWT", + "issuer-name": "Nazwa emitenta", + "issuer-name-required": "Nazwa emitenta jest wymagana.", + "signings-key": "Klucz do podpisu", + "signings-key-hint": "Ciąg zakodowany w formacie Base64 reprezentujący co najmniej 256 bitów danych.", + "signings-key-required": "Klucz do podpisu jest wymagany.", + "signings-key-min-length": "Klucz podpisujący musi mieć co najmniej 256 bitów danych.", + "signings-key-base64": "Klucz podpisujący musi być w formacie base64.", + "expiration-time": "Czas ważności tokena (s)", + "expiration-time-required": "Czas ważności tokena jest wymagany.", + "expiration-time-pattern": "Czas ważności tokena musi być dodatnią liczbą całkowitą.", + "expiration-time-min": "Minimalny czas to 60 sekund (1 minuta).", + "refresh-expiration-time": "Czas wygaśnięcia tokena odświeżenia (s)", + "refresh-expiration-time-required": "Czas ważności tokena odświeżania jest wymagany.", + "refresh-expiration-time-pattern": "Czas wygaśnięcia tokena odświeżenia powinien być dodatnią liczbą całkowitą.", + "refresh-expiration-time-min": "Minimalny czas to 900 sekund (15 minut).", + "refresh-expiration-time-less-token": "Czas odświeżania tokenu musi być dłuższy.", + "generate-key": "Wygeneruj klucz", + "info-header": "Wszyscy użytkownicy będą musieli się ponownie zalogować", + "info-message": "Zmiana klucza podpisującego JWT spowoduje, że wszystkie wydane tokeny staną się nieważne. " }, - "resources":"Zasoby", - "notifications":"Powiadomienia", - "notifications-settings":"Ustawienia powiadomień", - "slack-api-token":"Token API Slack", - "slack":"Slack", - "slack-settings":"Ustawienia Slack" + "resources": "Biblioteki", + "notifications": "Powiadomienia", + "notifications-settings": "Ustawienia powiadomień", + "slack-api-token": "Token API Slacka", + "slack": "Slack", + "slack-settings": "Ustawienia Slacka" }, - "alarm":{ - "alarm":"Alarm", - "alarms":"Alarmy", - "all-alarms":"Wszystkie alarmy", - "select-alarm":"Wybierz alarm", - "no-alarms-matching":"Nie znaleziono alarmów pasujących do '{{entity}}'.", - "alarm-required":"Alarm jest wymagany", - "alarm-filter":"Filtr alarmów", - "filter":"Filtr", - "alarm-status":"Status alarmu", - "alarm-status-list":"Lista statusów alarmów", - "any-status":"Dowolny status", - "search-status":{ - "ANY":"Dowolny", - "ACTIVE":"Aktywny", - "CLEARED":"Wyczyszczony", - "ACK":"Potwierdzony", - "UNACK":"Niepotwierdzony" + "alarm": { + "alarm": "Alarm", + "alarms": "Alarmy", + "all-alarms": "Wszystkie alarmy", + "select-alarm": "Wybierz alarm", + "no-alarms-matching": "Nie znaleziono alarmów pasujących do '{{entity}}'.", + "alarm-required": "Alarm jest wymagany", + "alarm-filter": "Filtr alarmu", + "filter": "Filtr", + "alarm-status": "Stan alarmu", + "alarm-status-list": "Lista stanów alarmów", + "any-status": "Dowolny stan", + "search-status": { + "ANY": "Dowolny", + "ACTIVE": "Aktywny", + "CLEARED": "Wyczyszczony", + "ACK": "Potwierdzony", + "UNACK": "Niepotwierdzony" }, - "display-status":{ - "ACTIVE_UNACK":"Aktywny Niepotwierdzony", - "ACTIVE_ACK":"Aktywny Potwierdzony", - "CLEARED_UNACK":"Wyczyszczony Niepotwierdzony", - "CLEARED_ACK":"Wyczyszczony Potwierdzony" + "display-status": { + "ACTIVE_UNACK": "Aktywny Niepotwierdzony", + "ACTIVE_ACK": "Aktywny Potwierdzony", + "CLEARED_UNACK": "Wyczyszczony Niepotwierdzony", + "CLEARED_ACK": "Wyszczony Potwierdzony" }, - "no-alarms-prompt":"Nie znaleziono alarmów", - "created-time":"Czas utworzenia", - "type":"Typ", - "severity":"Waga", - "originator":"Inicjator", - "originator-type":"Typ inicjatora", - "details":"Szczegóły", - "originator-label":"Etykieta inicjatora", - "assign":"Przypisz", - "assignments":"Przypisania", - "assignee":"Przypisany", - "assignee-id":"ID przypisanego", - "assignee-first-name":"Imię przypisanego", - "assignee-last-name":"Nazwisko przypisanego", - "assignee-email":"Email przypisanego", - "unassigned":"Nieprzypisany", - "assignee-not-set":"Wszystkie", - "status":"Status", - "alarm-details":"Szczegóły alarmu", - "start-time":"Czas rozpoczęcia", - "assign-time":"Czas przypisania", - "end-time":"Czas zakończenia", - "ack-time":"Czas potwierdzenia", - "clear-time":"Czas wyczyszczenia", - "duration":"Czas trwania", - "alarm-severity-list":"Lista ważności alarmów", - "any-severity":"Dowolna ważność", - "severity-critical":"Krytyczna", - "severity-major":"Poważna", - "severity-minor":"Mniejsza", - "severity-warning":"Ostrzeżenie", - "severity-indeterminate":"Nieokreślona", - "acknowledge":"Potwierdź", - "clear":"Wyczyść", - "delete":"Usuń", - "search":"Szukaj alarmów", - "selected-alarms":"{ count, plural, =1 {1 alarm} other {# alarmów} } wybrano", - "no-data":"Brak danych do wyświetlenia", - "polling-interval":"Interwał odpytywania alarmów (sek)", - "polling-interval-required":"Interwał odpytywania alarmów jest wymagany.", - "min-polling-interval-message":"Dozwolony jest minimalny interwał odpytywania 1 sek.", - "aknowledge-alarms-title":"Potwierdź { count, plural, =1 {1 alarm} other {# alarmów} }", - "aknowledge-alarms-text":"Czy na pewno chcesz potwierdzić { count, plural, =1 {1 alarm} other {# alarmów} }?", - "aknowledge-alarm-title":"Potwierdź alarm", - "aknowledge-alarm-text":"Czy na pewno chcesz potwierdzić alarm?", - "selected-alarms-are-acknowledged":"Wybrane alarmy zostały już potwierdzone", - "clear-alarms-title":"Wyczyść { count, plural, =1 {1 alarm} other {# alarmów} }", - "clear-alarms-text":"Czy na pewno chcesz wyczyścić { count, plural, =1 {1 alarm} other {# alarmów} }?", - "clear-alarm-title":"Wyczyść alarm", - "clear-alarm-text":"Czy na pewno chcesz wyczyścić alarm?", - "delete-alarms-title":"Usuń { count, plural, =1 {1 alarm} other {# alarmów} }", - "delete-alarms-text":"Czy na pewno chcesz usunąć { count, plural, =1 {1 alarm} other {# alarmów} }?", - "selected-alarms-are-cleared":"Wybrane alarmy zostały już wyczyszczone", - "alarm-status-filter":"Filtr statusu alarmów", - "alarm-filter-title":"Filtr alarmów", - "assigned":"Przypisane", - "filter-title":"Filtr", - "max-count-load":"Maksymalna liczba alarmów do załadowania (0 - bez limitu)", - "max-count-load-required":"Maksymalna liczba alarmów do załadowania jest wymagana.", - "max-count-load-error-min":"Minimalna wartość to 0.", - "fetch-size":"Rozmiar pobrania", - "fetch-size-required":"Rozmiar pobrania jest wymagany.", - "fetch-size-error-min":"Minimalna wartość to 10.", - "alarm-type-list":"Lista typów alarmów", - "any-type":"Dowolny typ", - "assigned-to-current-user":"Przypisane do aktualnego użytkownika", - "assigned-to-me":"Przypisane do mnie", - "search-propagated-alarms":"Szukaj propagowanych alarmów", - "comments":"Komentarze do alarmu", - "show-more":"Pokaż więcej", - "additional-info":"Dodatkowe informacje", - "alarm-type":"Typ alarmu", - "enter-alarm-type":"Wpisz typ alarmu", - "no-alarm-types-matching":"Nie znaleziono typów alarmów pasujących do '{{entitySubtype}}'.", - "alarm-type-list-empty":"Nie wybrano typów alarmów." + "no-alarms-prompt": "Nie znaleziono alarmów", + "created-time": "Czas utworzenia", + "type": "Typ", + "severity": "Waga", + "originator": "Inicjator", + "originator-type": "Typ inicjatora", + "details": "Szczegóły", + "originator-label": "Etykieta twórcy", + "assign": "Przydziel", + "assignments": "Przydziały", + "assignee": "Przydzielony", + "assignee-id": "Identyfikator przydzielonego", + "assignee-first-name": "Imię przydzielonego", + "assignee-last-name": "Nazwisko przydzielonego", + "assignee-email": "Adres e-mail przydzielonej osoby", + "unassigned": "Nieprzydzielony", + "assignee-not-set": "Wszyscy", + "status": "Status", + "alarm-details": "Szczegóły alarmu", + "start-time": "Czas rozpoczęcia", + "assign-time": "Przypisz czas", + "end-time": "Czas końcowy", + "ack-time": "Potwierdzony czas", + "clear-time": "Wyzerowany czas", + "duration": "Czas trwania", + "alarm-severity-list": "Lista wag alarmów", + "any-severity": "Dowolna waga", + "severity-critical": "Krytyczny", + "severity-major": "Ważny", + "severity-minor": "Mało ważny", + "severity-warning": "Ostrzeżenie", + "severity-indeterminate": "Nieokreślony", + "acknowledge": "Potwierdź", + "clear": "Wyczyść", + "delete": "Delete", + "search": "Wyszukaj alarmy", + "selected-alarms": "Wybrano { count, plural, =1 {1 alarm} other {# alarmów} }", + "no-data": "Brak danych do wyświetlenia", + "polling-interval": "Czas między odpytywaniem alarmów (s)", + "polling-interval-required": "Czas między odpytywaniem nalarmówi jest wymagay.", + "min-polling-interval-message": "Dozwolony jest czas między odpytywaniem wynoszący co najmniej 1 sekundę.", + "aknowledge-alarms-title": "Potwierdź { count, plural, =1 {1 alarm} other {# alarmów} }", + "aknowledge-alarms-text": "Czy na pewno chcesz potwierdzić { count, plural, =1 {1 alarm} other {# alarmów} }?", + "aknowledge-alarm-title": "Potwierdź alarm", + "aknowledge-alarm-text": "Czy na pewno chcesz potwierdzić Alarm?", + "selected-alarms-are-acknowledged": "Selected alarms are already acknowledged", + "clear-alarms-title": "Wyczyść { count, plural, =1 {1 alarm} other {# alarmów} }", + "clear-alarms-text": "Czy na pewno chcesz wyczyścić { count, plural, =1 {1 alarm} other {# alarmów} }?", + "clear-alarm-title": "Wyczyść alarm", + "clear-alarm-text": "Czy na pewno chcesz wyczyścić Alarm?", + "delete-alarms-title": "Delete { count, plural, =1 {1 alarm} other {# alarms} }", + "delete-alarms-text": "Are you sure you want to delete { count, plural, =1 {1 alarm} other {# alarms} }?", + "selected-alarms-are-cleared": "Selected alarms are already cleared", + "alarm-status-filter": "Filtr stanów alarmów", + "alarm-filter-title": "Filtr alarmów", + "assigned": "Przydzielony", + "filter-title": "Filtr", + "max-count-load": "Maksymalna liczba alarmów do załadowania (0 - nieograniczona)", + "max-count-load-required": "Maksymalna liczba alarmów do załadowania jest wymagana.", + "max-count-load-error-min": "Minimalna wartość to 0.", + "fetch-size": "Rozmiar pobierania", + "fetch-size-required": "Rozmiar pobierania jest wymagany.", + "fetch-size-error-min": "Minimalna wartość to 10.", + "alarm-type-list": "Lista typów alarmów", + "any-type": "Dowolny typ", + "assigned-to-current-user": "Przypisany do bieżącego użytkownika", + "assigned-to-me": "Przydzielony do mnie", + "search-propagated-alarms": "Wyszukaj propagowane alarmy", + "comments": "Komentarze alarmów", + "show-more": "Pokaż więcej", + "additional-info": "Dodatkowe informacje", + "alarm-type": "Alarm type", + "enter-alarm-type": "Enter alarm type", + "no-alarm-types-matching": "No alarm types matching '{{entitySubtype}}' were found.", + "alarm-type-list-empty": "No alarm types selected." }, - "alarm-activity":{ - "add":"Dodaj komentarz...", - "alarm-comment":"Komentarz do alarmu", - "comments":"Komentarze", - "delete-alarm-comment":"Czy chcesz usunąć ten komentarz?", - "refresh":"Odśwież", - "oldest-first":"Najstarsze najpierw", - "newest-first":"Najnowsze najpierw", - "activity":"Aktywność", - "export":"Eksportuj do CSV", - "author":"Autor", - "created-date":"Data utworzenia", - "edited-date":"Data edycji", - "text":"Tekst", - "system":"System" + "alarm-activity": { + "add": "Dodaj komentarz...", + "alarm-comment": "Komentarz alarmu", + "comments": "Komentarze", + "delete-alarm-comment": "Czy chcesz usunąć ten komentarz?", + "refresh": "Odśwież", + "oldest-first": "Najpierw najstarsze", + "newest-first": "Najpierw najnowsze", + "activity": "Aktywność", + "export": "Eksportuj do CSV", + "author": "Autor", + "created-date": "Data utworzenia", + "edited-date": "Data edycji", + "text": "Tekst", + "system": "System" }, - "alias":{ - "add":"Dodaj alias", - "edit":"Edytuj alias", - "name":"Nazwa aliasu", - "name-required":"Nazwa aliasu jest wymagana", - "duplicate-alias":"Alias o tej samej nazwie już istnieje.", - "filter-type-single-entity":"Pojedyncza encja", - "filter-type-entity-list":"Lista encji", - "filter-type-entity-name":"Nazwa encji", - "filter-type-entity-type":"Typ encji", - "filter-type-state-entity":"Encja ze stanu pulpitu nawigacyjnego", - "filter-type-state-entity-description":"Encja pobrana z parametrów stanu pulpitu nawigacyjnego", - "filter-type-asset-type":"Typ aktywa", - "filter-type-asset-type-description":"Aktywa typu '{{assetTypes}}'", - "filter-type-asset-type-and-name-description":"Aktywa typu '{{assetTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", - "filter-type-device-type":"Typ urządzenia", - "filter-type-device-type-description":"Urządzenia typu '{{deviceTypes}}'", - "filter-type-device-type-and-name-description":"Urządzenia typu '{{deviceTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", - "filter-type-entity-view-type":"Typ widoku encji", - "filter-type-entity-view-type-description":"Widoki encji typu '{{entityViewTypes}}'", - "filter-type-entity-view-type-and-name-description":"Widoki encji typu '{{entityViewTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", - "filter-type-edge-type":"Typ krawędzi", - "filter-type-edge-type-description":"Krawędzie typu '{{edgeTypes}}'", - "filter-type-edge-type-and-name-description":"Krawędzie typu '{{edgeTypes}}' o nazwie rozpoczynającej się od '{{prefix}}'", - "filter-type-relations-query":"Zapytanie o relacje", - "filter-type-relations-query-description":"{{entities}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-asset-search-query":"Zapytanie wyszukiwania aktywów", - "filter-type-asset-search-query-description":"Aktywa typu {{assetTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-device-search-query":"Zapytanie wyszukiwania urządzeń", - "filter-type-device-search-query-description":"Urządzenia typu {{deviceTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query":"Zapytanie wyszukiwania widoków encji", - "filter-type-entity-view-search-query-description":"Widoki encji typu {{entityViewTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", - "filter-type-apiUsageState":"Stan użycia API", - "filter-type-edge-search-query":"Zapytanie wyszukiwania krawędzi", - "filter-type-edge-search-query-description":"Krawędzie typu {{edgeTypes}} mające relację typu {{relationType}} {{direction}} {{rootEntity}}", - "entity-filter":"Filtr encji", - "resolve-multiple":"Rozwiąż jako wiele encji", - "filter-type":"Typ filtra", - "filter-type-required":"Typ filtra jest wymagany.", - "entity-filter-no-entity-matched":"Nie znaleziono encji pasujących do określonego filtra.", - "no-entity-filter-specified":"Nie określono filtra encji", - "root-state-entity":"Użyj encji stanu pulpitu nawigacyjnego jako korzenia", - "last-level-relation":"Pobierz tylko relacje ostatniego poziomu", - "root-entity":"Encja główna", - "state-entity-parameter-name":"Nazwa parametru encji stanu", - "default-state-entity":"Domyślna encja stanu", - "default-entity-parameter-name":"Domyślnie", - "max-relation-level":"Maksymalny poziom relacji", - "unlimited-level":"Nieograniczony poziom", - "state-entity":"Encja stanu pulpitu nawigacyjnego", - "all-entities":"Wszystkie encje", - "any-relation":"dowolna" + "alias": { + "add": "Dodaj alias", + "edit": "Edytuj alias", + "name": "Nazwa aliasu", + "name-required": "Alias jest wymagany", + "duplicate-alias": "Alias o tej samej nazwie już istnieje.", + "filter-type-single-entity": "Pojedyńczy obiekt", + "filter-type-entity-group": "Grupa obietków", + "filter-type-entity-list": "Lista obiektów", + "filter-type-entity-name": "Nazwa obiektu", + "filter-type-entity-type": "Typ obiektu", + "filter-type-entity-group-list": "Lista grup obiektów", + "filter-type-entity-group-name": "Nazwa grupy obiektu", + "filter-type-entities-by-group-name": "Obiekty według nazwy grupy", + "filter-type-state-entity": "Obiekt ze stanu panelu", + "filter-type-state-entity-description": "Obiekt pobrany z parametrów stanu panelu", + "filter-type-state-entity-owner": "Właściciel obiektu ze stanu panelu", + "filter-type-state-entity-owner-description": "Właściciel obiektu pobrany z parametrów stanu panelu", + "filter-type-asset-type": "Typ zasobu", + "filter-type-asset-type-description": "Zasoby typu '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Zasoby typu '{{assetTypes}}' i z nazwą zaczynającą się od '{{prefix}}'", + "filter-type-device-type": "Typ urządzenia", + "filter-type-device-type-description": "Urządzenia typu '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Urządzenia typu '{{deviceTypes}}' i z nazwą zaczynającą się od '{{prefix}}'", + "filter-type-entity-view-type": "Typ widoku obiektu", + "filter-type-entity-view-type-description": "Widoki obiektów typu '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Widoki obiektów typu '{{entityViewTypes}}' i z nazwą zaczynającą się od '{{prefix}}'", + "filter-type-edge-type": "Typ Edge", + "filter-type-edge-type-description": "Edge typu '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Edge typu '{{edgeTypes}}' i z nazwą zaczynającą się od '{{prefix}}'", + "filter-type-relations-query": "Zapytanie o relacje", + "filter-type-relations-query-description": "{{entities}} które mają {{relationType}} relację {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Zapytanie wyszukiwania Edge", + "filter-type-edge-search-query-description": "Edge typu {{edgeTypes}} które mają {{relationType}} relacja {{direction}} {{rootEntity}}", + "filter-type-scheduler-event": "Zdarzenia planisty", + "filter-type-scheduler-event-type-description": "Zdarzenia planisty typu '{{eventType}}'", + "filter-type-scheduler-event-originator-description": "Zdarzenia planisty określonego inicjatora", + "filter-type-scheduler-event-type-originator-description": "Zdarzenia planisty określonego inicjatora typu '{{eventType}}'", + "filter-type-asset-search-query": "Zapytanie wyszukiwania zasobów", + "filter-type-asset-search-query-description": "Zasoby typów {{assetTypes}} które mają {{relationType}} relację {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Zapytanie wyszukiwania urządzenia", + "filter-type-device-search-query-description": "Urządzenia typów {{deviceTypes}} które mają {{relationType}} relację {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Zapytanie wyszukiwania widoku obiektów", + "filter-type-entity-view-search-query-description": "Widoki obiektów typów {{entityViewTypes}} które mają {{relationType}} relację {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "Stan użycia interfejsu API", + "entity-filter": "Filtr obiektu", + "resolve-multiple": "Rozwiąż jako wiele obiektów", + "filter-type": "Typ filtra", + "filter-type-required": "Typ filtra jest wymagany.", + "entity-filter-no-entity-matched": "Nie znaleziono żadnych obiektów pasujących do określonego filtra.", + "no-entity-filter-specified": "Nie określono filtra obiektów", + "root-state-entity": "Użyj obiektu stanu panelu jako root", + "group-state-entity": "Użyj obiektu stanu panelu jako grupy obiektu", + "group-state-entity-owner": "Użyj obiektu stanu panelu jako właściciela grupy obiektu", + "last-level-relation": "Pobierz tylko relację ostatniego poziomu", + "root-entity": "Obiekt root", + "state-entity-parameter-name": "Nazwa parametru obiektu stanu", + "default-state-entity": "Domyślny obiekt stanu", + "default-state-entity-group": "Domyślna grupa obiektów stanu", + "default-entity-parameter-name": "Domyślnie", + "max-relation-level": "Maksymalny poziom relacji", + "unlimited-level": "Nieograniczony poziom", + "state-entity": "Obiekt stanu panelu", + "entities-of-group-state-entity": "Obiekty z grupy obiektów stanu panelu", + "all-entities": "Wszystkie obiekty", + "any-relation": "każda", + "originator": "Inicjator", + "originator-state-entity": "Użyj obiektu stanu panelu jako inicjatora", + "scheduler-event-type": "Typ zdarzenia planisty" }, - "asset":{ - "asset":"Aktywa", - "assets":"Aktywa", - "management":"Zarządzanie aktywami", - "view-assets":"Wyświetl aktywa", - "add":"Dodaj aktywa", - "asset-type-max-length":"Typ aktywa powinien być krótszy niż 256", - "assign-to-customer":"Przypisz do klienta", - "assign-asset-to-customer":"Przypisz aktywa do klienta", - "assign-asset-to-customer-text":"Proszę wybrać aktywa do przypisania klientowi", - "no-assets-text":"Nie znaleziono aktywów", - "assign-to-customer-text":"Proszę wybrać klienta do przypisania aktywów", - "public":"Publiczne", - "assignedToCustomer":"Przypisane do klienta", - "make-public":"Upublicznij aktywa", - "make-private":"Prywatyzuj aktywa", - "unassign-from-customer":"Usuń przypisanie od klienta", - "delete":"Usuń aktywa", - "asset-public":"Aktywa są publiczne", - "asset-type":"Typ aktywa", - "asset-type-required":"Typ aktywa jest wymagany.", - "select-asset-type":"Wybierz typ aktywa", - "enter-asset-type":"Wprowadź typ profilu aktywa", - "any-asset":"Dowolne aktywa", - "no-asset-types-matching":"Nie znaleziono typów aktywów pasujących do '{{entitySubtype}}'.", - "asset-type-list-empty":"Nie wybrano typów aktywów.", - "asset-types":"Typy aktywów", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "name-max-length":"Nazwa powinna być krótsza niż 256", - "label-max-length":"Etykieta powinna być krótsza niż 256", - "description":"Opis", - "type":"Typ", - "type-required":"Typ jest wymagany.", - "details":"Szczegóły", - "events":"Wydarzenia", - "add-asset-text":"Dodaj nowe aktywa", - "asset-details":"Szczegóły aktywa", - "assign-assets":"Przypisz aktywa", - "assign-assets-text":"Przypisz { count, plural, =1 {1 aktywa} other {# aktywów} } do klienta", - "assign-asset-to-edge-title":"Przypisz aktywa do krawędzi", - "assign-asset-to-edge-text":"Proszę wybrać aktywa do przypisania do krawędzi", - "delete-assets":"Usuń aktywa", - "unassign-assets":"Usuń przypisanie aktywów", - "unassign-assets-action-title":"Usuń przypisanie { count, plural, =1 {1 aktywa} other {# aktywów} } od klienta", - "assign-new-asset":"Przypisz nowe aktywa", - "delete-asset-title":"Czy na pewno chcesz usunąć aktywa '{{assetName}}'?", - "delete-asset-text":"Ostrożnie, po potwierdzeniu aktywa oraz wszystkie powiązane dane staną się nieodwracalne.", - "delete-assets-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 aktywa} other {# aktywów} }?", - "delete-assets-action-title":"Usuń { count, plural, =1 {1 aktywa} other {# aktywów} }", - "delete-assets-text":"Ostrożnie, po potwierdzeniu wszystkie wybrane aktywa oraz wszystkie powiązane dane staną się nieodwracalne.", - "make-public-asset-title":"Czy na pewno chcesz uczynić aktywa '{{assetName}}' publicznymi?", - "make-public-asset-text":"Po potwierdzeniu aktywa oraz wszystkie jego dane staną się publicznie dostępne.", - "make-private-asset-title":"Czy na pewno chcesz uczynić aktywa '{{assetName}}' prywatnymi?", - "make-private-asset-text":"Po potwierdzeniu aktywa oraz wszystkie jego dane staną się prywatne i niedostępne dla innych.", - "unassign-asset-title":"Czy na pewno chcesz usunąć przypisanie aktywów '{{assetName}}'?", - "unassign-asset-text":"Po potwierdzeniu aktywa zostaną usunięte z przypisania i nie będą dostępne dla klienta.", - "unassign-asset":"Usuń przypisanie aktywów", - "unassign-assets-title":"Czy na pewno chcesz usunąć przypisanie { count, plural, =1 {1 aktywa} other {# aktywów} }?", - "unassign-assets-text":"Po potwierdzeniu wszystkie wybrane aktywa zostaną usunięte z przypisania i nie będą dostępne dla klienta.", - "unassign-assets-from-edge":"Usuń przypisanie aktywów od krawędzi", - "copyId":"Kopiuj ID aktywów", - "idCopiedMessage":"ID aktywów zostało skopiowane do schowka", - "select-asset":"Wybierz aktywa", - "no-assets-matching":"Nie znaleziono aktywów pasujących do '{{entity}}'.", - "asset-required":"Aktywa są wymagane", - "name-starts-with":"Wyrażenie nazwy aktywów", - "help-text":"Użyj '%', zgodnie z potrzebą: '%nazwa_zawiera_aktywa%', '%nazwa_kończy_się_aktywami', 'nazwa_rozpoczyna_się_aktywami'.", - "import":"Importuj aktywa", - "asset-file":"Plik aktywów", - "label":"Etykieta", - "search":"Wyszukaj aktywa", - "assign-asset-to-edge":"Przypisz aktywa do krawędzi", - "unassign-asset-from-edge":"Usuń przypisanie aktywów od krawędzi", - "unassign-asset-from-edge-title":"Czy na pewno chcesz usunąć przypisanie aktywów '{{assetName}}' od krawędzi?", - "unassign-asset-from-edge-text":"Po potwierdzeniu aktywa zostaną usunięte z przypisania od krawędzi i nie będą dostępne dla tej krawędzi.", - "unassign-assets-from-edge-title":"Czy na pewno chcesz usunąć przypisanie { count, plural, =1 {1 aktywa} other {# aktywów} } od krawędzi?", - "unassign-assets-from-edge-text":"Po potwierdzeniu wszystkie wybrane aktywa zostaną usunięte z przypisania od krawędzi i nie będą dostępne dla tej krawędzi.", - "selected-assets":"{ count, plural, =1 {1 aktywo} other {# aktywów} } wybrane" + "asset": { + "all": "Wszystko", + "all-assets": "Wszystkie zasoby", + "groups": "Grupy", + "shared": "Współdzielony", + "asset": "Zasób", + "assets": "Zasoby", + "management": "Zarządzanie zasobami", + "view-assets": "Zobacz zasoby", + "add": "Dodaj zasób", + "asset-type-max-length": "Typ zasobu powinien być mniejszy niż 256", + "assign-to-customer": "Przypisz do klienta", + "assign-asset-to-customer": "Przypisz zasoby do klienta", + "assign-asset-to-customer-text": "Wybierz zasoby, które chcesz przypisać klientowi", + "no-assets-text": "Nie znaleziono żadnych zasobów", + "assign-to-customer-text": "Wybierz klienta, któremu chcesz przypisać zasoby", + "public": "Publiczny", + "assignedToCustomer": "Przypisany do klienta", + "make-public": "Ustaw zasób jako publiczny", + "make-private": "Ustaw zasób jako prywatny", + "unassign-from-customer": "Usuń przypisanie do klienta", + "delete": "Usuń zasób", + "asset-public": "Zasób jest publiczny", + "asset-type": "Typ zasobu", + "asset-type-required": "Typ zasobu jest wymagany.", + "select-asset-type": "Wybierz typ zasobu", + "enter-asset-type": "Wpisz typ zasobu", + "any-asset": "Dowolny zasób", + "no-asset-types-matching": "Nie znaleziono zasobów pasujących do '{{entitySubtype}}'.", + "asset-type-list-empty": "Nie wybrano typów zasobów.", + "asset-types": "Typy zasobów", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "label-max-length": "Etykieta powinna być mniejsza niż 256", + "description": "Opis", + "type": "Typ", + "type-required": "Typ jest wymagany.", + "details": "Szczegóły", + "events": "Wydarzenia", + "add-asset-text": "Dodaj nowy zasób", + "asset-details": "Szczegóły zasobu", + "assign-assets": "Przypisz zasoby", + "assign-assets-text": "Przypisz {count, plural, =1 {1 zasób} other {# zasobów} }", + "assign-asset-to-edge-title": "Przypisz zasoby do edge", + "assign-asset-to-edge-text": "Wybierz zasoby, które chcesz przypisać do Edge", + "delete-assets": "Usuń zasoby", + "unassign-assets": "Cofnij przypisanie zasobów", + "unassign-assets-action-title": "Anuluj przypisanie {count, plural, =1 {1 zasobu} other {# zasobów} }", + "assign-new-asset": "Przypisz nowy zasób", + "delete-asset-title": "Czy na pewno chcesz usunąć zasób '{{assetName}}'?", + "delete-asset-text": "Bądź ostrożny, po potwierdzeniu zasobu i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-assets-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 zasób} other {# zasobów} }?", + "delete-assets-action-title": "Usuń {count, plural, =1 {1 zasób} other {# zasobów} }", + "delete-assets-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane zasoby zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "make-public-asset-title": "Czy na pewno chcesz uczynić zasób '{{assetName}}' publicznym?", + "make-public-asset-text": "Po potwierdzeniu zasób i wszystkie jego dane zostaną upublicznione i dostępne dla innych.", + "make-private-asset-title": "Czy na pewno chcesz uczynić zasób '{{assetName}}' prywatnym?", + "make-private-asset-text": "Po potwierdzeniu zasób i wszystkie jego dane staną się prywatne i nie będą dostępne dla innych.", + "unassign-asset-title": "Czy na pewno chcesz cofnąć przypisanie zasobu '{{assetName}}'?", + "unassign-asset-text": "Po potwierdzeniu zasób zostanie usunięty i nie będzie dostępny dla klienta.", + "unassign-asset": "Cofnij przypisanie zasobu", + "unassign-assets-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 zasobu} other {# zasobów} }?", + "unassign-assets-text": "Po potwierdzeniu wszystkie wybrane zasoby zostaną anulowane i nie będą dostępne dla klienta.", + "copyId": "Skopiuj identyfikator zasobu", + "idCopiedMessage": "Identyfikator zasobu został skopiowany do schowka", + "select-asset": "Wybierz zasób", + "no-assets-matching": "Nie znaleziono zasobów pasujących do '{{entity}}'.", + "asset-required": "Zasób jest wymagany", + "name-starts-with": "Wyrażenie nazwy zasobu", + "help-text": "Użyj „%” w zależności od potrzeb: „%asset_name_contains%”, „%asset_name_ends”, „%asset_starts_with”.", + "search": "Wyszukaj zasoby", + "select-group-to-add": "Wybierz grupę docelową, aby dodać wybrane zasoby", + "select-group-to-move": "Wybierz grupę docelową, aby przenieść wybrane zasoby", + "remove-assets-from-group": "Czy na pewno chcesz usunąć {count, plural, =1 {1 zasób} other {# zasobów} } z grupy '{{{entityGroup}}'?", + "group": "Grupa zasobów", + "list-of-groups": "{count, plural, =1 {Jedna grupa zasobów} other {Lista # grup zasobów} }", + "group-name-starts-with": "Grupy zasobów, których nazwy zaczynają się od '{{prefix}}'", + "import": "Importuj zasoby", + "asset-file": "Plik zasobów", + "label": "Etykieta", + "assign-asset-to-edge": "Przypisz zasoby do Edge", + "unassign-asset-from-edge": "Cofnij przypisanie zasobu", + "unassign-asset-from-edge-title": "Czy na pewno chcesz cofnąć przypisanie zasobu '{{assetName}}'?", + "unassign-asset-from-edge-text": "Po potwierdzeniu zasób zostanie usunięty i nie będzie dostępny na urządzeniu Edge.", + "unassign-assets-from-edge-title": "Czy na pewno chcesz cofnąć przypisanie { count, plural, =1 {1 zasobu} other {# zasobów} }?", + "unassign-assets-from-edge-text": "Po potwierdzeniu wszystkie wybrane zasoby zostaną anulowane i nie będą dostępne na urządzeniu Edge.", + "selected-assets": "Wybrano { count, plural, =1 {1 zasób} other {# zasobów} }" }, - "attribute":{ - "attributes":"Atrybuty", - "latest-telemetry":"Najnowsza telemetria", - "no-latest-telemetry":"Brak najnowszej telemetrii", - "attributes-scope":"Zakres atrybutów encji", - "scope-telemetry":"Telemetria", - "scope-latest-telemetry":"Najnowsza telemetria", - "scope-client":"Atrybuty klienta", - "scope-server":"Atrybuty serwera", - "scope-shared":"Atrybuty współdzielone", - "add":"Dodaj atrybut", - "key":"Klucz", - "key-max-length":"Klucz powinien być krótszy niż 256", - "last-update-time":"Czas ostatniej aktualizacji", - "key-required":"Klucz atrybutu jest wymagany.", - "value":"Wartość", - "value-required":"Wartość atrybutu jest wymagana.", - "telemetry-key-required":"Klucz telemetrii jest wymagany", - "telemetry-value-required":"Wartość telemetrii jest wymagana", - "delete-attributes-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 atrybut} other {# atrybutów} }?", - "delete-attributes-text":"Ostrożnie, po potwierdzeniu wszystkie wybrane atrybuty zostaną usunięte.", - "delete-attributes":"Usuń atrybuty", - "enter-attribute-value":"Wprowadź wartość atrybutu", - "show-on-widget":"Pokaż na widżecie", - "widget-mode":"Tryb widżetu", - "next-widget":"Następny widżet", - "prev-widget":"Poprzedni widżet", - "add-to-dashboard":"Dodaj do pulpitu nawigacyjnego", - "add-widget-to-dashboard":"Dodaj widżet do pulpitu nawigacyjnego", - "selected-attributes":"{ count, plural, =1 {1 atrybut} other {# atrybutów} } wybrano", - "selected-telemetry":"{ count, plural, =1 {1 jednostka telemetrii} other {# jednostek telemetrii} } wybrano", - "no-attributes-text":"Nie znaleziono atrybutów", - "no-telemetry-text":"Nie znaleziono telemetrii", - "copy-key":"Kopiuj klucz", - "add-telemetry":"Dodaj telemetrię", - "copy-value":"Kopiuj wartość", - "delete-timeseries":{ - "start-time":"Czas rozpoczęcia", - "ends-on":"Kończy się", - "strategy":"Strategia", - "delete-strategy":"Strategia usuwania", - "all-data":"Usuń wszystkie dane", - "all-data-except-latest-value":"Usuń wszystkie dane oprócz najnowszej wartości", - "latest-value":"Usuń najnowszą wartość", - "all-data-for-time-period":"Usuń wszystkie dane za okres czasu", - "rewrite-latest-value":"Zastąp najnowszą wartość" + "attribute": { + "attributes": "Atrybuty", + "latest-telemetry": "Najnowsza telemetria", + "no-latest-telemetry": "Brak telemetrii", + "attributes-scope": "Zakres atrybutów encji", + "scope-telemetry": "Telemetry", + "scope-latest-telemetry": "Najnowsza telemetria", + "scope-client": "Atrybuty klienta", + "scope-server": "Atrybuty serwera", + "scope-shared": "Wspólne atrybuty", + "add": "Dodaj atrybut", + "add-attribute-prompt": "Proszę dodać atrybut", + "key": "Klucz", + "key-max-length": "Klucz powinien być krótszy niż 256 znaków", + "last-update-time": "Ostatnia aktualizacja", + "key-required": "Klucz atrybutu jest wymagany.", + "value": "Wartość", + "value-required": "Wartość atrybutu jest wymagana.", + "telemetry-key-required": "Telemetry key is required", + "telemetry-value-required": "Telemetry value is required", + "delete-attributes-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 atrybut} other {# atrybutów} }?", + "delete-attributes-text": "Uważaj, po potwierdzeniu wszystkie wybrane atrybuty zostaną usunięte.", + "delete-attributes": "Usuń atrybuty", + "enter-attribute-value": "Wprowadź wartość atrybutu", + "show-on-widget": "Pokaż na widżecie", + "widget-mode": "Tryb widgetu", + "next-widget": "Następny widżet", + "prev-widget": "Poprzedni widżet", + "add-to-dashboard": "Dodaj do panelu", + "add-widget-to-dashboard": "Dodaj widżet do panelu", + "selected-attributes": "Wybrano {count, plural, =1 {1 atrybut} other {# atrybutów} }", + "selected-telemetry": "Wybrano {count, plural, =1 {1 jednostkę telemetrii} other {# jednostek telemetrii} }", + "no-attributes-text": "Nie znaleziono atrybutów", + "no-telemetry-text": "Nie znaleziono danych telemetrycznych", + "copy-key": "Skopiuj klucz", + "add-telemetry": "Dodaj telemetrię", + "copy-value": "Skopiuj wartość", + "delete-timeseries": { + "start-time": "Start time", + "ends-on": "Ends on", + "strategy": "Strategy", + "delete-strategy": "Delete strategy", + "all-data": "Delete all data", + "all-data-except-latest-value": "Delete all data except latest value", + "latest-value": "Delete latest value", + "all-data-for-time-period": "Delete all data for time period", + "rewrite-latest-value": "Rewrite latest value" } }, - "api-usage":{ - "api-features":"Funkcje API", - "api-usage":"Użycie API", - "alarm":"Alarm", - "alarms-created":"Utworzone alarmy", - "alarms-created-daily-activity":"Dzienna aktywność utworzonych alarmów", - "alarms-created-hourly-activity":"Godzinna aktywność utworzonych alarmów", - "alarms-created-monthly-activity":"Miesięczna aktywność utworzonych alarmów", - "data-points":"Punkty danych", - "data-points-storage-days":"Dni przechowywania punktów danych", - "device-api":"API urządzenia", - "email":"Email", - "email-messages":"Wiadomości e-mail", - "email-messages-daily-activity":"Dzienna aktywność wiadomości e-mail", - "email-messages-monthly-activity":"Miesięczna aktywność wiadomości e-mail", - "exceptions":"Wyjątki", - "executions":"Wykonania", - "scripts":"Skrypty", - "scripts-hourly-activity":"Godzinna aktywność skryptów", - "scripts-daily-activity":"Dzienna aktywność skryptów", - "scripts-monthly-activity":"Miesięczna aktywność skryptów", - "javascript":"JavaScript", - "javascript-executions":"Wykonania JavaScript", - "tbel":"TBEL", - "tbel-executions":"Wykonania TBEL", - "latest-error":"Ostatni błąd", - "messages":"Wiadomości", - "notifications":"Powiadomienia", - "notifications-email-sms":"Powiadomienia (Email/SMS)", - "notifications-hourly-activity":"Godzinna aktywność powiadomień", - "permanent-failures":"Stałe awarie ${entityName}", - "permanent-timeouts":"Stałe przekroczenia czasu ${entityName}", - "processing-failures":"Błędy przetwarzania ${entityName}", - "processing-failures-and-timeouts":"Błędy przetwarzania i przekroczenia czasu", - "processing-timeouts":"Przekroczenia czasu przetwarzania ${entityName}", - "queue-stats":"Statystyki kolejki", - "rule-chain":"Łańcuch reguł", - "rule-engine":"Silnik reguł", - "rule-engine-daily-activity":"Dzienna aktywność silnika reguł", - "rule-engine-executions":"Wykonania silnika reguł", - "rule-engine-hourly-activity":"Godzinna aktywność silnika reguł", - "rule-engine-monthly-activity":"Miesięczna aktywność silnika reguł", - "rule-engine-statistics":"Statystyki silnika reguł", - "rule-node":"Węzeł reguł", - "sms":"SMS", - "sms-messages":"Wiadomości SMS", - "sms-messages-daily-activity":"Dzienna aktywność wiadomości SMS", - "sms-messages-monthly-activity":"Miesięczna aktywność wiadomości SMS", - "successful":"Udane ${entityName}", - "telemetry":"Telemetria", - "telemetry-persistence":"Przechowywanie telemetrii", - "telemetry-persistence-daily-activity":"Dzienna aktywność przechowywania telemetrii", - "telemetry-persistence-hourly-activity":"Godzinna aktywność przechowywania telemetrii", - "telemetry-persistence-monthly-activity":"Miesięczna aktywność przechowywania telemetrii", - "transport":"Transport", - "transport-daily-activity":"Dzienna aktywność transportu", - "transport-data-points":"Punkty danych transportu", - "transport-hourly-activity":"Godzinna aktywność transportu", - "transport-messages":"Wiadomości transportu", - "transport-monthly-activity":"Miesięczna aktywność transportu", - "view-details":"Wyświetl szczegóły", - "view-statistics":"Wyświetl statystyki" + "api-usage": { + "api-features": "Funkcje API", + "api-usage": "Użycie API", + "alarm": "Alarm", + "alarms-created": "Utworzono alarmy", + "alarms-created-daily-activity": "Alarmy tworzyły codzienną aktywność", + "alarms-created-hourly-activity": "Alarmy utworzyły aktywność godzinową", + "alarms-created-monthly-activity": "Alarmy utworzyły miesięczną aktywność", + "data-points": "Punkty danych", + "data-points-storage-days": "Dni przechowywania punktów danych", + "device-api": "Interfejs API urządzenia", + "email": "E-mail", + "email-messages": "Wiadomości e-mailowe", + "email-messages-daily-activity": "Codzienna aktywność związana z wiadomościami e-mail", + "email-messages-monthly-activity": "Miesięczna aktywność dotycząca wiadomości e-mail", + "exceptions": "Wyjątki", + "executions": "Wywołania", + "javascript": "JavaScript", + "javascript-executions": "Wykonania JavaScript", + "javascript-functions": "Funkcje JavaScriptu", + "javascript-functions-daily-activity": "Funkcje JavaScript w codziennej działalności", + "javascript-functions-hourly-activity": "Funkcje JavaScript, aktywność godzinowa", + "javascript-functions-monthly-activity": "Funkcje JavaScript, aktywność miesięczna", + "latest-error": "Ostatni błąd", + "messages": "Wiadomości", + "notifications": "Powiadomienia", + "notifications-email-sms": "Powiadomienia (e-mail/sms)", + "notifications-hourly-activity": "Powiadomienia o aktywności godzinowej", + "permanent-failures": "${entityName} Trwałe awarie", + "permanent-timeouts": "${entityName} Stałe przekroczenia limitu czasu", + "processing-failures": "${entityName} Błędy przetwarzania", + "processing-failures-and-timeouts": "Błędy przetwarzania i przekroczenia limitu czasu", + "processing-timeouts": "${entityName} Przekroczono limity czasu przetwarzania", + "queue-stats": "Statystyki kolejek", + "rule-chain": "Łańcuch reguł", + "rule-engine": "Silnik reguł", + "rule-engine-daily-activity": "Dzienna aktywność silnika reguł", + "rule-engine-executions": "Wykonania silnika reguł", + "rule-engine-hourly-activity": "Aktywność godzinowa silnika reguły", + "rule-engine-monthly-activity": "Miesięczna aktywność silnika reguł", + "rule-engine-statistics": "Statystyki silnika reguł", + "rule-node": "Węzeł reguły", + "sms": "SMS-em", + "sms-messages": "Wiadomości SMS", + "sms-messages-daily-activity": "Codzienna aktywność związana z wiadomościami SMS", + "sms-messages-monthly-activity": "Miesięczna aktywność wiadomości SMS", + "successful": "${entityName} Pomyślne", + "telemetry": "Telemetria", + "telemetry-persistence": "Trwałość telemetrii", + "telemetry-persistence-daily-activity": "Telemetria utrzymująca codzienną aktywność", + "telemetry-persistence-hourly-activity": "Aktywność godzinowa trwałości telemetrii", + "telemetry-persistence-monthly-activity": "Miesięczna aktywność dotycząca trwałości telemetrii", + "transport": "Transport", + "transport-daily-activity": "Codzienna aktywność transportowa", + "transport-data-points": "Punkty danych dotyczących transportu", + "transport-hourly-activity": "Godzinowa aktywność transportowa", + "transport-messages": "Komunikaty transportowe", + "transport-monthly-activity": "Miesięczna aktywność transportowa", + "view-details": "Pokaż szczegóły", + "view-statistics": "Zobacz statystyki" }, - "api-limit":{ - "cassandra-queries":"Zapytania do Cassandry", - "entity-version-creation":"Tworzenie wersji encji", - "entity-version-load":"Ładowanie wersji encji", - "notification-requests":"Żądania powiadomień", - "notification-requests-per-rule":"Żądania powiadomień na regułę", - "rest-api-requests":"Żądania REST API", - "rest-api-requests-per-customer":"Żądania REST API na klienta", - "transport-messages":"Wiadomości transportowe", - "transport-messages-per-device":"Wiadomości transportowe na urządzenie", - "ws-updates-per-session":"Aktualizacje WS na sesję" + "api-limit": { + "cassandra-queries": "Cassandra queries", + "entity-version-creation": "Entity version creation", + "entity-version-load": "Entity version load", + "integration-messages": "Integration messages", + "integration-messages-per-device": "Integration messages per device", + "notification-requests": "Notification requests", + "notification-requests-per-rule": "Notification requests per rule", + "reports-generation": "Reports generation", + "rest-api-requests": "REST API requests", + "rest-api-requests-per-customer": "REST API requests per customer", + "transport-messages": "Transport messages", + "transport-messages-per-device": "Transport messages per device", + "ws-updates-per-session": "WS updates per session" }, - "audit-log":{ - "audit":"Audyt", - "audit-logs":"Dzienniki audytu", - "timestamp":"Znacznik czasu", - "entity-type":"Typ encji", - "entity-name":"Nazwa encji", - "user":"Użytkownik", - "type":"Typ", - "status":"Status", - "details":"Szczegóły", - "type-added":"Dodano", - "type-deleted":"Usunięto", - "type-updated":"Zaktualizowano", - "type-attributes-updated":"Zaktualizowano atrybuty", - "type-attributes-deleted":"Usunięto atrybuty", - "type-rpc-call":"Wywołanie RPC", - "type-credentials-updated":"Zaktualizowano dane uwierzytelniające", - "type-assigned-to-customer":"Przypisano do klienta", - "type-unassigned-from-customer":"Usunięto przypisanie od klienta", - "type-assigned-to-edge":"Przypisano do krawędzi", - "type-unassigned-from-edge":"Usunięto przypisanie od krawędzi", - "type-activated":"Aktywowano", - "type-suspended":"Zawieszono", - "type-credentials-read":"Odczytano dane uwierzytelniające", - "type-attributes-read":"Odczytano atrybuty", - "type-relation-add-or-update":"Zaktualizowano relację", - "type-relation-delete":"Usunięto relację", - "type-relations-delete":"Usunięto wszystkie relacje", - "type-alarm-ack":"Potwierdzono", - "type-alarm-clear":"Wyczyszczono", - "type-alarm-assign":"Przypisano", - "type-alarm-unassign":"Usunięto przypisanie", - "type-added-comment":"Dodano komentarz", - "type-updated-comment":"Zaktualizowano komentarz", - "type-deleted-comment":"Usunięto komentarz", - "type-login":"Logowanie", - "type-logout":"Wylogowanie", - "type-lockout":"Blokada", - "status-success":"Sukces", - "status-failure":"Niepowodzenie", - "audit-log-details":"Szczegóły dziennika audytu", - "no-audit-logs-prompt":"Nie znaleziono dzienników", - "action-data":"Dane akcji", - "failure-details":"Szczegóły niepowodzenia", - "search":"Szukaj w dziennikach audytu", - "clear-search":"Wyczyść wyszukiwanie", - "type-assigned-from-tenant":"Przypisano od dzierżawcy", - "type-assigned-to-tenant":"Przypisano do dzierżawcy", - "type-provision-success":"Pomyślne przygotowanie urządzenia", - "type-provision-failure":"Niepowodzenie przygotowania urządzenia", - "type-timeseries-updated":"Zaktualizowano telemetrię", - "type-timeseries-deleted":"Usunięto telemetrię", - "type-sms-sent":"Wysłano SMS" + "audit-log": { + "audit": "Rewizja", + "audit-logs": "Dzienniki audytu", + "timestamp": "Znak czasu", + "entity-type": "Typ obiektu", + "entity-name": "Nazwa obiektu", + "user": "Użytkownik", + "type": "Typ", + "status": "Status", + "details": "Szczegóły", + "type-added": "Dodany", + "type-deleted": "Usunięto", + "type-updated": "Zaktualizowano", + "type-attributes-updated": "Zaktualizowano atrybuty", + "type-attributes-deleted": "Atrybuty usunięte", + "type-rpc-call": "Zadzwoń do RPC", + "type-credentials-updated": "Dane uwierzytelniające zaktualizowane", + "type-assigned-to-customer": "Przypisane do Klienta", + "type-unassigned-from-customer": "Nieprzypisane od klienta", + "type-assigned-to-edge": "Przypisany do Edge", + "type-unassigned-from-edge": "Nieprzypisany do Edge", + "type-activated": "Aktywowany", + "type-suspended": "Zawieszony", + "type-credentials-read": "Poświadczenia przeczytane", + "type-attributes-read": "Odczytane atrybuty", + "type-added-to-entity-group": "Dodano do grupy", + "type-removed-from-entity-group": "Usunięto z grupy", + "type-relation-add-or-update": "Relacja zaktualizowana", + "type-relation-delete": "Relacja usunięta", + "type-relations-delete": "Wszystkie relacje usunięte", + "type-alarm-ack": "Uznany", + "type-alarm-clear": "Wyczyszczone", + "type-alarm-assign": "Przydzielony", + "type-alarm-unassign": "Nieprzypisany", + "type-added-comment": "Dodano komentarz", + "type-updated-comment": "Zaktualizowany komentarz", + "type-deleted-comment": "Usunięty komentarz", + "type-rest-api-rule-engine-call": "Wywołanie API REST mechanizmu reguł", + "type-made-public": "Upublicznione", + "type-made-private": "Zrobione prywatnie", + "type-login": "Zaloguj się", + "type-logout": "Wyloguj", + "type-lockout": "Lokaut", + "status-success": "Powodzenie", + "status-failure": "Awaria", + "audit-log-details": "Szczegóły dziennika audytu", + "no-audit-logs-prompt": "Nie znaleziono logów", + "action-data": "Dane akcji", + "failure-details": "Szczegóły awarii", + "search": "Przeszukaj dzienniki kontroli", + "clear-search": "Wyczyść wyszukiwanie", + "type-assigned-from-tenant": "Przypisane od Najemcy", + "type-assigned-to-tenant": "Przypisane Najemcy", + "type-provision-success": "Urządzenie udostępnione", + "type-provision-failure": "Udostępnienie urządzenia nie powiodło się", + "type-timeseries-updated": "Telemetria zaktualizowana", + "type-timeseries-deleted": "Telemetria usunięta", + "type-owner-changed": "Właściciel się zmienił", + "type-sms-sent": "SMS sent" }, - "confirm-on-exit":{ - "message":"Masz niezapisane zmiany. Czy na pewno chcesz opuścić tę stronę?", - "html-message":"Masz niezapisane zmiany.
Czy na pewno chcesz opuścić tę stronę?", - "title":"Niezapisane zmiany" + "confirm-on-exit": { + "message": "Masz niezapisane zmiany. ", + "html-message": "Masz niezapisane zmiany.
Czy na pewno chcesz opuścić tę stronę?", + "title": "Niezapisane zmiany" }, - "contact":{ - "country":"Kraj", - "city":"Miasto", - "state":"Stan / Prowincja", - "postal-code":"Kod pocztowy", - "postal-code-invalid":"Nieprawidłowy format kodu pocztowego.", - "address":"Adres", - "address2":"Adres 2", - "phone":"Telefon", - "email":"Email", - "no-address":"Brak adresu", - "state-max-length":"Długość stanu powinna być mniejsza niż 256", - "phone-max-length":"Numer telefonu powinien być krótszy niż 256", - "city-max-length":"Nazwa miasta powinna być krótsza niż 256" + "contact": { + "country": "Kraj", + "city": "Miasto", + "state": "Stan/prowincja", + "postal-code": "Kod pocztowy", + "postal-code-invalid": "Nieprawidłowy format kodu pocztowego.", + "address": "Adres", + "address2": "Adres 2", + "phone": "Telefon", + "email": "E-mail", + "no-address": "Brak adresu", + "state-max-length": "Długość stanu powinna być mniejsza niż 256", + "phone-max-length": "Numer telefonu powinien być mniejszy niż 256", + "city-max-length": "Określone miasto powinno być mniejsze niż 256" }, - "common":{ - "username":"Nazwa użytkownika", - "password":"Hasło", - "enter-username":"Wprowadź nazwę użytkownika", - "enter-password":"Wprowadź hasło", - "enter-search":"Wprowadź wyszukiwanie", - "created-time":"Czas utworzenia", - "loading":"Ładowanie...", - "proceed":"Kontynuuj", - "open-details-page":"Otwórz stronę szczegółów", - "not-found":"Nie znaleziono", - "documentation":"Dokumentacja" + "common": { + "username": "Nazwa użytkownika", + "password": "Hasło", + "enter-username": "Wpisz nazwę użytkownika", + "enter-password": "Wprowadź hasło", + "enter-search": "Wprowadź wyszukiwanie", + "created-time": "Stworzony czas", + "loading": "Ładowanie...", + "proceed": "Przystępować", + "open-details-page": "Otwórz stronę szczegółów", + "not-found": "Not found", + "documentation": "Documentation" }, - "content-type":{ - "json":"Json", - "text":"Tekst", - "binary":"Binarny (Base64)" + "converter": { + "converter": "Konwerter danych", + "converters": "Konwertery danych", + "select-converter": "Wybierz konwerter danych", + "no-converters-matching": "Brak znalezionych konwerterów danych pasujących do '{{entity}}'.", + "no-converters-found": "Nie znaleziono konwerterów danych.", + "converter-required": "Konwerter danych jest wymagany", + "delete": "Usuń konwerter", + "management": "Zarządzanie konwerterami danych", + "add-converter-text": "Dodaj nowy konwerter danych", + "no-converters-text": "Nie znaleziono konwerterów danych", + "selected-converters": "Wybrano {count, plural, =1 {1 konwerter danych} other {# konwerterów danych} }", + "delete-converter-title": "Czy na pewno chcesz usunąć konwerter danych '{{converterName}}'?", + "delete-converter-text": "Bądź ostrożny, po potwierdzeniu konwerter danych i wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "delete-converters-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 konwerter danych} other {# konwerterów danych} }?", + "delete-converters-action-title": "Usuń {count, plural, =1 {1 konwerter danych} other {# konwerterów danych} }", + "delete-converters-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane konwertery danych zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "events": "Wydarzenia", + "add": "Dodaj konwerter danych", + "search": "Wyszukaj konwertery danych", + "converter-details": "Szczegóły konwertera danych", + "details": "Szczegóły", + "copyId": "Skopiuj identyfikator konwertera", + "idCopiedMessage": "Identyfikator konwertera został skopiowany do schowka", + "debug-mode": "Tryb debugowania", + "created-time": "Stworzony czas", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "description": "Opis", + "decoder": "Dekoder", + "encoder": "Enkoder", + "test-decoder-fuction": "Sprawdź działanie dekodera", + "test-encoder-fuction": "Testuj działanie enkodera", + "test-with-this-message": "{{test}} with this message", + "decoder-input-params": "Parametry wejściowe dekodera", + "encoder-input-params": "Parametry wejściowe enkodera", + "payload": "Payload", + "payload-content-type": "Typ zawartości payload", + "payload-content": "Zawartość payload", + "message": "Wiadomość", + "message-type": "Typ wiadomości", + "message-type-required": "Typ wiadomości jest wymagany", + "test": "Test", + "metadata": "Metadane", + "metadata-required": "Wpisy metadanych nie mogą być puste.", + "integration-metadata": "Metadane integracji", + "integration-metadata-required": "Wpisy metadanych integracji nie mogą być puste.", + "output": "Wyjście", + "import": "Importuj konwerter", + "export": "Eksportuj konwerter", + "export-failed-error": "Nie można wyeksportować konwertera: {{error}}", + "create-new-converter": "Utwórz nowy konwerter", + "converter-file": "Plik konwertera", + "invalid-converter-file-error": "Nie można zaimportować konwertera: Nieprawidłowa struktura danych konwertera.", + "type": "Typ", + "type-required": "Typ jest wymagany.", + "type-uplink": "Łącze w górę", + "type-downlink": "Łącze w dół", + "update-only-keys-list": "Update only keys list", + "update-only-keys-list-hint": "The values associated with the provided keys will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and telemetry in the converter output.", + "add-key": "Add key" }, - "customer":{ - "customer":"Klient", - "customers":"Klienci", - "management":"Zarządzanie klientami", - "dashboard":"Pulpit nawigacyjny klienta", - "dashboards":"Pulpity nawigacyjne klienta", - "devices":"Urządzenia klienta", - "entity-views":"Widoki encji klienta", - "assets":"Aktywa klienta", - "public-dashboards":"Publiczne pulpity nawigacyjne", - "public-devices":"Publiczne urządzenia", - "public-assets":"Publiczne aktywa", - "public-edges":"Publiczne krawędzie", - "public-entity-views":"Publiczne widoki encji", - "add":"Dodaj klienta", - "delete":"Usuń klienta", - "manage-customer-users":"Zarządzaj użytkownikami klienta", - "manage-customer-devices":"Zarządzaj urządzeniami klienta", - "manage-customer-dashboards":"Zarządzaj pulpitami nawigacyjnymi klienta", - "manage-public-devices":"Zarządzaj publicznymi urządzeniami", - "manage-public-dashboards":"Zarządzaj publicznymi pulpitami nawigacyjnymi", - "manage-customer-assets":"Zarządzaj aktywami klienta", - "manage-public-assets":"Zarządzaj publicznymi aktywami", - "manage-customer-edges":"Zarządzaj krawędziami klienta", - "manage-public-edges":"Zarządzaj publicznymi krawędziami", - "add-customer-text":"Dodaj nowego klienta", - "no-customers-text":"Nie znaleziono klientów", - "customer-details":"Szczegóły klienta", - "delete-customer-title":"Czy na pewno chcesz usunąć klienta '{{customerTitle}}'?", - "delete-customer-text":"Uważaj, po potwierdzeniu klient i wszystkie powiązane dane staną się nieodwracalne.", - "delete-customers-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 klienta} other {# klientów} }?", - "delete-customers-action-title":"Usuń { count, plural, =1 {1 klienta} other {# klientów} }", - "delete-customers-text":"Uważaj, po potwierdzeniu wszyscy wybrani klienci oraz wszystkie powiązane dane staną się nieodwracalne.", - "manage-users":"Zarządzaj użytkownikami", - "manage-assets":"Zarządzaj aktywami", - "manage-devices":"Zarządzaj urządzeniami", - "manage-dashboards":"Zarządzaj pulpitami nawigacyjnymi", - "title":"Tytuł", - "title-required":"Tytuł jest wymagany.", - "title-max-length":"Tytuł powinien być krótszy niż 256", - "description":"Opis", - "details":"Szczegóły", - "events":"Wydarzenia", - "copyId":"Kopiuj ID klienta", - "idCopiedMessage":"ID klienta zostało skopiowane do schowka", - "select-customer":"Wybierz klienta", - "no-customers-matching":"Nie znaleziono klientów pasujących do '{{entity}}'.", - "customer-required":"Klient jest wymagany", - "select-default-customer":"Wybierz domyślnego klienta", - "default-customer":"Domyślny klient", - "default-customer-required":"Domyślny klient jest wymagany do debugowania pulpitu nawigacyjnego na poziomie dzierżawcy", - "search":"Szukaj klientów", - "selected-customers":"{ count, plural, =1 {1 klient} other {# klientów} } wybrano", - "edges":"Instancje krawędzi klienta", - "manage-edges":"Zarządzaj krawędziami" + "content-type": { + "json": "Json", + "text": "Tekst", + "binary": "Binarny (Base64)" }, - "date":{ - "last-update-n-ago":"Ostatnia aktualizacja N temu", - "last-update-n-ago-text":"Ostatnia aktualizacja {{ agoText }}", - "custom-date":"Niestandardowa data", - "format":"Format", - "preview":"Podgląd" + "color": { + "color": "Color", + "color-picker": "Narzędzie do wybierania kolorów", + "primary-colors": "Kolory podstawowe", + "accent-colors": "Kolory akcentujące", + "no-color-selected": "Nie wybrano koloru" }, - "datetime":{ - "date-from":"Data od", - "time-from":"Czas od", - "date-to":"Data do", - "time-to":"Czas do" + "customer": { + "all": "Wszystko", + "all-customers": "Wszyscy klienci", + "groups": "Grupy", + "shared": "Wspólny", + "hierarchy": "Hierarchia", + "customer": "Klient", + "customers": "Klienci", + "management": "Zarządzanie klientami", + "dashboard": "Panel Klienta", + "dashboards": "Panele klientów", + "devices": "Urządzenia Klienta", + "entity-views": "Widoki obiektów klienta", + "assets": "Zasoby Klienta", + "public-dashboards": "Panele publiczne", + "public-devices": "Urządzenia publiczne", + "public-assets": "Zasoby publiczne", + "public-entity-views": "Publiczne widoki obiektów", + "add": "Dodaj klienta", + "delete": "Usuń klienta", + "manage-customer-user-groups": "Zarządzaj grupami użytkowników klientów", + "manage-customer-groups": "Zarządzaj grupami klientów", + "manage-customer-device-groups": "Zarządzaj grupami urządzeń klientów", + "manage-customer-asset-groups": "Zarządzaj grupami zasobów klientów", + "manage-customer-entity-view-groups": "Zarządzaj grupami widoków encji klienta", + "manage-customer-edge-groups": "Zarządzaj grupami Edge klientów", + "manage-customer-dashboard-groups": "Zarządzaj grupami paneli klientów", + "manage-customer-users": "Zarządzaj użytkownikami klientów", + "manage-customers": "Zarządzaj klientami", + "manage-customer-devices": "Zarządzaj urządzeniami klientów", + "manage-customer-entity-views": "Zarządzaj widokami encji klienta", + "manage-customer-dashboards": "Zarządzaj panelami klientów", + "manage-public-devices": "Zarządzaj urządzeniami publicznymi", + "manage-public-dashboards": "Zarządzaj panelami publicznymi", + "manage-customer-assets": "Zarządzaj zasobami klientów", + "manage-customer-edges": "Zarządzaj Edge klientów", + "manage-public-assets": "Zarządzaj zasobami publicznymi", + "add-customer-text": "Dodaj nowego klienta", + "no-customers-text": "Nie znaleziono żadnych klientów", + "customer-details": "Szczegóły klienta", + "delete-customer-title": "Czy na pewno chcesz usunąć klienta '{{customerTitle}}'?", + "delete-customer-text": "Bądź ostrożny, po potwierdzeniu klienta i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-customers-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 klienta} other {# klientów} }?", + "delete-customers-action-title": "Usuń {count, plural, =1 {1 klienta} other {# klientów} }", + "delete-customers-text": "Bądź ostrożny, po potwierdzeniu wszyscy wybrani klienci zostaną usunięci, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "manage-user-groups": "Zarządzaj grupami użytkowników", + "manage-asset-groups": "Zarządzaj grupami zasobów", + "manage-device-groups": "Zarządzaj grupami urządzeń", + "manage-dashboard-groups": "Zarządzaj grupami paneli", + "manage-entity-view-groups": "Zarządzaj grupami widoków encji", + "manage-edge-groups": "Zarządzaj grupami Edge", + "manage-users": "Zarządzaj użytkownikami", + "manage-assets": "Zarządzaj zasobami", + "manage-devices": "Zarządzaj urządzeniami", + "manage-dashboards": "Zarządzaj panelami", + "manage-entity-views": "Zarządzaj widokami encji", + "title": "Tytuł", + "title-required": "Tytuł jest wymagany.", + "title-max-length": "Tytuł powinien być mniejszy niż 256", + "description": "Opis", + "details": "Szczegóły", + "events": "Wydarzenia", + "copyId": "Skopiuj identyfikator klienta", + "idCopiedMessage": "Identyfikator klienta został skopiowany do schowka", + "select-customer": "Wybierz klienta", + "no-customers-matching": "Brak klientów pasujących do '{{entity}}' zostały znalezione.", + "customer-required": "Klient jest wymagany", + "select-group-to-add": "Wybierz grupę docelową, aby dodać wybranych klientów", + "select-group-to-move": "Wybierz grupę docelową, aby przenieść wybranych klientów", + "remove-customers-from-group": "Czy na pewno chcesz usunąć {count, plural, =1 {1 klienta} other {# klientów} } z grupy '{{entityGroup}}'?", + "group": "Grupa klientów", + "list-of-groups": "{count, plural, =1 {Jedna grupa klientów} other {Lista # grup klientów} }", + "group-name-starts-with": "Grupy klientów, których nazwy zaczynają się od '{{prefix}}'", + "select-default-customer": "Wybierz domyślnego klienta", + "default-customer": "Domyślny klient", + "default-customer-required": "Do debugowania panelu na poziomie dzierżawy wymagany jest domyślny klient", + "allow-white-labeling": "Zezwalaj na white labeling", + "search": "Szukaj klientów", + "selected-customers": "Wybrano {count, plural, =1 {1 klienta} other {# klientów} }", + "edges": "Obiekty Edge klienta", + "manage-edges": "Zarządzaj Edge" }, - "dashboard":{ - "dashboard":"Panel", - "dashboards":"Panele", - "management":"Zarządzanie panelami", - "view-dashboards":"Przeglądaj panele", - "add":"Dodaj panel", - "assign-dashboard-to-customer":"Przypisz panel(y) do klienta", - "assign-dashboard-to-customer-text":"Wybierz panele do przypisania do klienta", - "assign-dashboard-to-edge-title":"Przypisz panel(y) do urządzenia", - "assign-to-customer-text":"Wybierz klienta, do którego przypisane będą panele", - "assign-to-customer":"Przypisz klientowi", - "unassign-from-customer":"Odłącz od klienta", - "make-public":"Udostępnij panel publicznie", - "make-private":"Ustaw panel jako prywatny", - "manage-assigned-customers":"Zarządzaj przypisanymi klientami", - "assigned-customers":"Przypisani klienci", - "assign-to-customers":"Przypisz panele klientom", - "assign-to-customers-text":"Wybierz klientów, do których przypisane będą panele", - "unassign-from-customers":"Odłącz panele od klientów", - "unassign-from-customers-text":"Wybierz klientów, od których odłączone zostaną panele", - "no-dashboards-text":"Nie znaleziono paneli", - "no-widgets":"Brak skonfigurowanych widżetów", - "add-widget":"Dodaj nowy widżet", - "add-widget-button-text":"Dodaj widżet", - "title":"Tytuł", - "image":"Obraz panelu", - "mobile-app-settings":"Ustawienia aplikacji mobilnej", - "mobile-order":"Kolejność panelu w aplikacji mobilnej", - "mobile-hide":"Ukryj panel w aplikacji mobilnej", - "update-image":"Aktualizuj obraz panelu", - "take-screenshot":"Zrób zrzut ekranu", - "select-widget-title":"Wybierz widżet", - "select-widget-value":"{{title}}: wybierz widżet", - "select-widget-subtitle":"Lista dostępnych typów widżetów", - "delete":"Usuń panel", - "title-required":"Tytuł jest wymagany.", - "title-max-length":"Tytuł powinien mieć mniej niż 256 znaków", - "description":"Opis", - "details":"Szczegóły", - "dashboard-details":"Szczegóły panelu", - "add-dashboard-text":"Dodaj nowy panel", - "assign-dashboards":"Przypisz panele", - "assign-new-dashboard":"Przypisz nowy panel", - "assign-dashboards-text":"Przypisz { count, plural, =1 {1 panel} other {# panele} } do klientów", - "unassign-dashboards-action-text":"Odłącz { count, plural, =1 {1 panel} other {# panele} } od klientów", - "delete-dashboards":"Usuń panele", - "unassign-dashboards":"Odłącz panele", - "unassign-dashboards-action-title":"Odłącz { count, plural, =1 {1 panel} other {# panele} } od klienta", - "delete-dashboard-title":"Czy na pewno chcesz usunąć panel '{{dashboardTitle}}'?", - "delete-dashboard-text":"Bądź ostrożny, po potwierdzeniu panel i wszystkie związane z nim dane staną się nieodwracalnie utracone.", - "delete-dashboards-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 panel} other {# panele} }?", - "delete-dashboards-action-title":"Usuń { count, plural, =1 {1 panel} other {# panele} }", - "delete-dashboards-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane panele zostaną usunięte, a wszystkie związane z nimi dane staną się nieodwracalnie utracone.", - "unassign-dashboard-title":"Czy na pewno chcesz odłączyć panel '{{dashboardTitle}}'?", - "unassign-dashboard-text":"Po potwierdzeniu panel zostanie odłączony i nie będzie dostępny dla klienta.", - "unassign-dashboard":"Odłącz panel", - "unassign-dashboards-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 panel} other {# panele} }?", - "unassign-dashboards-text":"Po potwierdzeniu wszystkie wybrane panele zostaną odłączone i nie będą dostępne dla klienta.", - "public-dashboard-title":"Panel jest teraz publiczny", - "public-dashboard-text":"Twój panel {{dashboardTitle}} jest teraz publiczny i dostępny za pomocą następującego publicznego linku:", - "public-dashboard-notice":"Uwaga: Nie zapomnij udostępnić powiązanych urządzeń publicznie, aby uzyskać do nich dostęp.", - "make-private-dashboard-title":"Czy na pewno chcesz uczynić panel '{{dashboardTitle}}' prywatnym?", - "make-private-dashboard-text":"Po potwierdzeniu panel zostanie uczyniony prywatnym i nie będzie dostępny dla innych.", - "make-private-dashboard":"Ustaw panel jako prywatny", - "socialshare-text":"'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title":"'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard":"Wybierz panel", - "no-dashboards-matching":"Nie znaleziono paneli pasujących do '{{entity}}'", - "dashboard-required":"Panel jest wymagany.", - "select-existing":"Wybierz istniejący panel", - "create-new":"Utwórz nowy panel", - "new-dashboard-title":"Tytuł nowego panelu", - "open-dashboard":"Otwórz panel", - "set-background":"Ustaw tło", - "background-color":"Kolor tła", - "background-image":"Obraz tła", - "background-size-mode":"Tryb rozmiaru tła", - "no-image":"Brak wybranego obrazu", - "empty-image":"Brak obrazu", - "drop-image":"Upuść obraz lub kliknij, aby wybrać plik do przesłania.", - "maximum-upload-file-size":"Maksymalny rozmiar pliku do przesłania: {{ size }}", - "cannot-upload-file":"Nie można przesłać pliku", - "settings":"Ustawienia", - "layout-settings":"Ustawienia układu", - "columns-count":"Liczba kolumn", - "columns-count-required":"Liczba kolumn jest wymagana.", - "min-columns-count-message":"Dozwolona jest tylko minimalna liczba kolumn wynosząca 10.", - "max-columns-count-message":"Dozwolona jest tylko maksymalna liczba kolumn wynosząca 1000.", - "widgets-margins":"Margines między widżetami", - "margin-required":"Wartość marginesu jest wymagana.", - "min-margin-message":"Dozwolona jest tylko wartość minimalna marginesu wynosząca 0.", - "max-margin-message":"Dozwolona jest tylko wartość maksymalna marginesu wynosząca 50.", - "horizontal-margin":"Margines poziomy", - "horizontal-margin-required":"Wartość marginesu poziomego jest wymagana.", - "min-horizontal-margin-message":"Dozwolona jest tylko wartość minimalna marginesu poziomego wynosząca 0.", - "max-horizontal-margin-message":"Dozwolona jest tylko wartość maksymalna marginesu poziomego wynosząca 50.", - "vertical-margin":"Margines pionowy", - "vertical-margin-required":"Wartość marginesu pionowego jest wymagana.", - "min-vertical-margin-message":"Dozwolona jest tylko wartość minimalna marginesu pionowego wynosząca 0.", - "max-vertical-margin-message":"Dozwolona jest tylko wartość maksymalna marginesu pionowego wynosząca 50.", - "apply-outer-margin":"Zastosuj margines do boków układu", - "autofill-height":"Automatycznie wypełnij wysokość układu", - "mobile-layout":"Ustawienia układu na urządzenia mobilne", - "mobile-row-height":"Wysokość rzędu na urządzenia mobilne, px", - "mobile-row-height-required":"Wartość wysokości rzędu na urządzenia mobilne jest wymagana.", - "min-mobile-row-height-message":"Dozwolona jest tylko minimalna wartość wysokości rzędu na urządzenia mobilne wynosząca 5 pikseli.", - "max-mobile-row-height-message":"Dozwolona jest tylko maksymalna wartość wysokości rzędu na urządzenia mobilne wynosząca 200 pikseli.", - "title-settings":"Ustawienia tytułu", - "display-title":"Wyświetl tytuł panelu", - "title-color":"Kolor tytułu", - "toolbar-settings":"Ustawienia paska narzędziowego", - "hide-toolbar":"Ukryj pasek narzędziowy", - "toolbar-always-open":"Zawsze otwarty pasek narzędziowy", - "display-dashboards-selection":"Wyświetl wybór paneli", - "display-entities-selection":"Wyświetl wybór jednostek", - "display-filters":"Wyświetl filtry", - "display-dashboard-timewindow":"Wyświetl okno czasowe panelu", - "display-dashboard-export":"Wyświetl eksport panelu", - "display-update-dashboard-image":"Wyświetl aktualizację obrazu panelu", - "dashboard-logo-settings":"Ustawienia logo panelu", - "display-dashboard-logo":"Wyświetl logo w trybie pełnoekranowym panelu", - "dashboard-logo-image":"Obraz logo panelu", - "advanced-settings":"Zaawansowane ustawienia", - "dashboard-css":"CSS panelu", - "import":"Importuj panel", - "export":"Eksportuj panel", - "export-failed-error":"Nie można wyeksportować panelu: {{error}}", - "create-new-dashboard":"Utwórz nowy panel", - "dashboard-file":"Plik panelu", - "invalid-dashboard-file-error":"Nie można zaimportować panelu: Nieprawidłowa struktura danych panelu.", - "dashboard-import-missing-aliases-title":"Skonfiguruj aliasy używane przez importowany panel", - "create-new-widget":"Utwórz nowy widżet", - "import-widget":"Importuj widżet", - "widget-file":"Plik widżetu", - "invalid-widget-file-error":"Nie można zaimportować widżetu: Nieprawidłowa struktura danych widżetu.", - "widget-import-missing-aliases-title":"Skonfiguruj aliasy używane przez importowany widżet", - "open-toolbar":"Otwórz pasek narzędziowy panelu", - "close-toolbar":"Zamknij pasek narzędziowy panelu", - "configuration-error":"Błąd konfiguracji", - "alias-resolution-error-title":"Błąd konfiguracji aliasów panelu", - "invalid-aliases-config":"Nie można znaleźć żadnych urządzeń pasujących do niektórych filtrów aliasów.
Skontaktuj się z administratorem w celu rozwiązania tego problemu.", - "select-devices":"Wybierz urządzenia", - "assignedToCustomer":"Przypisane do klienta", - "assignedToCustomers":"Przypisane do klientów", - "public":"Publiczne", - "copyId":"Skopiuj identyfikator panelu", - "idCopiedMessage":"Identyfikator panelu został skopiowany do schowka", - "public-link":"Publiczny link", - "copy-public-link":"Skopiuj publiczny link", - "public-link-copied-message":"Publiczny link do panelu został skopiowany do schowka", - "manage-states":"Zarządzaj stanami panelu", - "states":"Stany panelu", - "states-short":"Stany", - "search-states":"Szukaj stanów panelu", - "selected-states":"{ count, plural, =1 {1 stan panelu} other {# stany panelu} } wybranych", - "edit-state":"Edytuj stan panelu", - "delete-state":"Usuń stan panelu", - "add-state":"Dodaj stan panelu", - "no-states-text":"Nie znaleziono stanów", - "state":"Stan panelu", - "state-name":"Nazwa", - "state-name-required":"Wymagana jest nazwa stanu panelu.", - "state-id":"Identyfikator stanu", - "state-id-required":"Wymagane jest Identyfikator stanu panelu.", - "state-id-exists":"Stan panelu o tym samym identyfikatorze już istnieje.", - "is-root-state":"Główny stan", - "delete-state-title":"Usuń stan panelu", - "delete-state-text":"Czy na pewno chcesz usunąć stan panelu o nazwie '{{stateName}}'?", - "show-details":"Pokaż szczegóły", - "hide-details":"Ukryj szczegóły", - "select-state":"Wybierz docelowy stan", - "state-controller":"Kontroler stanu", - "state-controller-default":"statyczny (przestarzały)", - "search":"Szukaj paneli", - "selected-dashboards":"{ count, plural, =1 {1 panel} other {# panele} } wybranych", - "home-dashboard":"Panel główny", - "home-dashboard-hide-toolbar":"Ukryj pasek narzędzi panelu głównego", - "unassign-dashboard-from-edge-text":"Po potwierdzeniu panel zostanie odłączony i nie będzie dostępny dla krawędzi.", - "unassign-dashboards-from-edge-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 panel} other {# panele} }?", - "unassign-dashboards-from-edge-text":"Po potwierdzeniu wszystkie wybrane panele zostaną odłączone i nie będą dostępne dla krawędzi.", - "assign-dashboard-to-edge":"Przypisz panele do krawędzi", - "assign-dashboard-to-edge-text":"Proszę wybrać panele do przypisania do krawędzi", - "non-existent-dashboard-state-error":"Stan panelu o identyfikatorze \"{{ stateId }}\" nie istnieje", - "edit-mode":"Tryb edycji", - "duplicate-state-action":"Zduplikuj stan" + "customers-hierarchy": { + "customers-hierarchy": "Hierarchia klientów", + "open-nav-tree": "Otwórz drzewo nawigacji", + "return-to-top-level": "Wróć na najwyższy poziom" }, - "datakey":{ - "settings":"Ustawienia", - "general":"Ogólne", - "advanced":"Zaawansowane", - "key":"Klucz", - "label":"Etykieta", - "color":"Kolor", - "units":"Symbol specjalny do wyświetlenia obok wartości", - "decimals":"Liczba cyfr po przecinku", - "data-generation-func":"Funkcja generacji danych", - "use-data-post-processing-func":"Użyj funkcji przetwarzania danych po generacji", - "configuration":"Konfiguracja klucza danych", - "timeseries":"Szereg czasowy", - "attributes":"Atrybuty", - "entity-field":"Pole encji", - "alarm":"Pola alarmowe", - "timeseries-required":"Wymagane są serie czasowe encji.", - "timeseries-or-attributes-required":"Wymagane są serie czasowe/cechy encji.", - "alarm-fields-timeseries-or-attributes-required":"Wymagane są pola alarmowe lub serie czasowe/cechy encji.", - "maximum-timeseries-or-attributes":"Maksymalnie { count, plural, =1 {1 seria czasowa/cecha jest dozwolona.} other {# serie czasowe/cechy są dozwolone} }", - "alarm-fields-required":"Wymagane są pola alarmowe.", - "function-types":"Typy funkcji", - "function-type":"Typ funkcji", - "function-types-required":"Wymagane są typy funkcji.", - "data-keys":"Klucze danych", - "data-key":"Klucz danych", - "data-keys-required":"Wymagane są klucze danych.", - "data-key-required":"Wymagany jest klucz danych.", - "alarm-keys":"Klucze danych alarmowych", - "alarm-key":"Klucz danych alarmowych", - "alarm-key-functions":"Funkcje klucza alarmowego", - "alarm-key-function":"Funkcja klucza alarmowego", - "latest-keys":"Ostatnie klucze danych", - "latest-key":"Ostatni klucz danych", - "latest-key-functions":"Funkcje ostatniego klucza", - "latest-key-function":"Funkcja ostatniego klucza", - "timeseries-keys":"Klucze danych szeregów czasowych", - "timeseries-key":"Klucz danych szeregów czasowych", - "timeseries-key-functions":"Funkcje klucza szeregów czasowych", - "timeseries-key-function":"Funkcja klucza szeregów czasowych", - "maximum-function-types":"Maksymalnie { count, plural, =1 {1 typ funkcji jest dozwolony.} other {# typy funkcji są dozwolone} }", - "time-description":"znacznik czasu bieżącej wartości;", - "value-description":"bieżąca wartość;", - "prev-value-description":"wynik poprzedniego wywołania funkcji;", - "time-prev-description":"znacznik czasu poprzedniej wartości;", - "prev-orig-value-description":"oryginalna poprzednia wartość;", - "aggregation":"Agregacja", - "aggregation-type-hint-common":"Ze względów wydajnościowych obliczenia wartości agregowanej są dostępne tylko dla stałych interwałów czasowych, takich jak \"bieżący dzień\", \"bieżący miesiąc\", itp., i nie są dostępne dla interwałów przesuwnych, takich jak 'ostatnie 30 minut' lub 'ostatnie 24 godziny'.", - "aggregation-type-none-hint":"Weź ostatnią wartość.", - "aggregation-type-min-hint":"Znajdź minimalną wartość między punktami danych w wybranym oknie czasowym.", - "aggregation-type-max-hint":"Znajdź maksymalną wartość między punktami danych w wybranym oknie czasowym.", - "aggregation-type-avg-hint":"Oblicz średnią wartość między punktami danych w wybranym oknie czasowym.", - "aggregation-type-sum-hint":"Dodaj wszystkie wartości punktów danych w wybranym oknie czasowym.", - "aggregation-type-count-hint":"Całkowita liczba punktów danych w wybranym oknie czasowym.", - "delta-calculation":"Obliczenia delta", - "enable-delta-calculation":"Włącz obliczenia delta", - "enable-delta-calculation-hint":"Po włączeniu, wartość klucza danych jest obliczana na podstawie wartości agregowanych w wybranym oknie czasowym i okresie porównawczym. Ze względów wydajnościowych obliczenia delta są dostępne tylko dla okien czasowych historycznych, a nie dla wartości czasu rzeczywistego. Na przykład możesz obliczyć różnicę między zużyciem energii wczoraj a zużyciem energii przedwczoraj.", - "delta-calculation-result":"Wynik obliczeń delta", - "delta-calculation-result-previous-value":"Poprzednia wartość", - "delta-calculation-result-delta-absolute":"Delta (bezwzględna)", - "delta-calculation-result-delta-percent":"Delta (procentowa)", - "source":"Źródło", - "latest":"Najnowsze", - "latest-value":"Najnowsza wartość", - "delta":"delta", - "percent":"procent", - "absolute":"bezwzględna" + "custom-menu": { + "custom-menu": "Niestandardowe menu", + "custom-menu-hint": "Zdefiniuj poniżej JSON niestandardowego menu. Ten JSON zawiera listę elementów niestandardowego menu." }, - "datasource":{ - "type":"Typ źródła danych", - "name":"Nazwa", - "label":"Etykieta", - "add-datasource-prompt":"Proszę dodać źródło danych" + "custom-translation": { + "custom-translation": "Tłumaczenie niestandardowe", + "translation-map": "Mapa tłumaczeń", + "key": "Klucz do tłumaczenia", + "import": "Importuj tłumaczenie", + "export": "Eksportuj tłumaczenie", + "export-data": "Eksportuj dane tłumaczeń", + "import-data": "Importuj dane tłumaczeń", + "translation-file": "Plik tłumaczeniowy", + "invalid-translation-file-error": "Nie można zaimportować pliku tłumaczenia: Nieprawidłowa struktura danych tłumaczenia.", + "custom-translation-hint": "Zdefiniuj poniżej tłumaczenie niestandardowe JSON. ", + "download-locale-file": "Pobierz plik ustawień regionalnych" }, - "details":{ - "details":"Szczegóły", - "edit-mode":"Tryb edycji", - "edit-json":"Edytuj JSON", - "toggle-edit-mode":"Przełącz tryb edycji" + "date": { + "last-update-n-ago": "Last update N ago", + "last-update-n-ago-text": "Last update {{ agoText }}", + "custom-date": "Custom date", + "format": "Format", + "preview": "Preview" }, - "device":{ - "device":"Urządzenie", - "device-required":"Urządzenie jest wymagane.", - "devices":"Urządzenia", - "management":"Zarządzanie urządzeniem", - "view-devices":"Zobacz urządzenia", - "device-alias":"Alias urządzenia", - "device-type-max-length":"Typ urządzenia powinien mieć mniej niż 256 znaków", - "aliases":"Aliasy urządzeń", - "no-alias-matching":"'{{alias}}' nie znaleziono.", - "no-aliases-found":"Nie znaleziono aliasów.", - "no-key-matching":"'{{key}}' nie znaleziono.", - "no-keys-found":"Nie znaleziono kluczy.", - "create-new-alias":"Utwórz nowy!", - "create-new-key":"Utwórz nowy!", - "duplicate-alias-error":"Znaleziono duplikat aliasu '{{alias}}'.
Aliasy urządzeń muszą być unikalne w ramach panelu.", - "configure-alias":"Konfiguruj alias '{{alias}}'", - "no-devices-matching":"Nie znaleziono urządzeń pasujących do '{{entity}}'.", - "alias":"Alias", - "alias-required":"Alias urządzenia jest wymagany.", - "remove-alias":"Usuń alias urządzenia", - "add-alias":"Dodaj alias urządzenia", - "name-starts-with":"Wyrażenie nazwy urządzenia", - "help-text":"Użyj '%' według potrzeb: '%device_name_contains%', '%device_name_ends%', 'device_starts_with'.", - "device-list":"Lista urządzeń", - "use-device-name-filter":"Użyj filtra", - "device-list-empty":"Brak wybranych urządzeń.", - "device-name-filter-required":"Filtr nazwy urządzenia jest wymagany.", - "device-name-filter-no-device-matched":"Nie znaleziono urządzeń rozpoczynających się od '{{device}}'.", - "add":"Dodaj urządzenie", - "assign-to-customer":"Przypisz do klienta", - "assign-device-to-customer":"Przypisz urządzenia do klienta", - "assign-device-to-customer-text":"Proszę wybrać urządzenia do przypisania do klienta", - "assign-device-to-edge-title":"Przypisz urządzenia do krawędzi", - "assign-device-to-edge-text":"Proszę wybrać urządzenia do przypisania do krawędzi", - "make-public":"Udostępnij publicznie", - "make-private":"Udostępnij prywatnie", - "no-devices-text":"Nie znaleziono urządzeń", - "assign-to-customer-text":"Proszę wybrać klienta, do którego przypisane będą urządzenia", - "device-details":"Szczegóły urządzenia", - "add-device-text":"Dodaj nowe urządzenie", - "credentials":"Poświadczenia", - "manage-credentials":"Zarządzaj poświadczeniami", - "delete":"Usuń urządzenie", - "assign-devices":"Przypisz urządzenia", - "assign-devices-text":"Przypisz { count, plural, =1 {1 urządzenie} other {# urządzeń} } do klienta", - "delete-devices":"Usuń urządzenia", - "unassign-from-customer":"Odłącz od klienta", - "unassign-devices":"Odłącz urządzenia", - "unassign-devices-action-title":"Odłącz { count, plural, =1 {1 urządzenie} other {# urządzeń} } od klienta", - "unassign-device-from-edge-title":"Czy na pewno chcesz odłączyć urządzenie '{{deviceName}}'?", - "unassign-device-from-edge-text":"Po potwierdzeniu urządzenie zostanie odłączone i nie będzie dostępne przez krawędź.", - "unassign-devices-from-edge":"Odłącz urządzenia od krawędzi", - "assign-new-device":"Przypisz nowe urządzenie", - "make-public-device-title":"Czy na pewno chcesz udostępnić publicznie urządzenie '{{deviceName}}'?", - "make-public-device-text":"Po potwierdzeniu urządzenie i wszystkie jego dane będą dostępne publicznie dla innych.", - "make-private-device-title":"Czy na pewno chcesz udostępnić prywatnie urządzenie '{{deviceName}}'?", - "make-private-device-text":"Po potwierdzeniu urządzenie i wszystkie jego dane będą dostępne prywatnie i nie będą dostępne dla innych.", - "view-credentials":"Zobacz poświadczenia", - "delete-device-title":"Czy na pewno chcesz usunąć urządzenie '{{deviceName}}'?", - "delete-device-text":"Bądź ostrożny, po potwierdzeniu urządzenie i wszystkie powiązane dane staną się nieodwracalnie utracone.", - "delete-devices-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 urządzenie} other {# urządzeń} }?", - "delete-devices-action-title":"Usuń { count, plural, =1 {1 urządzenie} other {# urządzeń} }", - "delete-devices-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane urządzenia zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalnie utracone.", - "unassign-device-title":"Czy na pewno chcesz odłączyć urządzenie '{{deviceName}}'?", - "unassign-device-text":"Po potwierdzeniu urządzenie zostanie odłączone i nie będzie dostępne przez klienta.", - "unassign-device":"Odłącz urządzenie", - "unassign-devices-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 urządzenie} other {# urządzeń} }?", - "unassign-devices-text":"Po potwierdzeniu wszystkie wybrane urządzenia zostaną odłączone i nie będą dostępne przez klienta.", - "device-credentials":"Poświadczenia urządzenia", - "loading-device-credentials":"Ładowanie poświadczeń urządzenia...", - "credentials-type":"Typ poświadczeń", - "access-token":"Token dostępu", - "access-token-required":"Token dostępu jest wymagany.", - "access-token-invalid":"Długość tokenu dostępu musi wynosić od 1 do 32 znaków.", - "certificate-pem-format":"Certyfikat w formacie PEM", - "certificate-pem-format-required":"Certyfikat jest wymagany.", - "copy-access-token":"Skopiuj token dostępu", - "copy-certificate":"Skopiuj certyfikat", - "copy-client-id":"Skopiuj identyfikator klienta", - "copy-user-name":"Skopiuj nazwę użytkownika", - "copy-password":"Skopiuj hasło", - "generate-client-id":"Generuj identyfikator klienta", - "generate-user-name":"Generuj nazwę użytkownika", - "generate-password":"Generuj hasło", - "generate-access-token":"Generuj token dostępu", - "lwm2m-security-config":{ - "identity":"Identyfikator klienta", - "identity-required":"Identyfikator klienta jest wymagany.", - "identity-tooltip":"Identyfikator PSK to dowolny identyfikator PSK o długości do 128 bajtów, zgodny z opisem w standardzie [RFC7925].\nIdentyfikator PSK MUSI być najpierw przekształcony w ciąg znaków, a następnie zakodowany na bajty przy użyciu UTF-8.", - "client-key":"Klucz klienta", - "client-key-required":"Klucz klienta jest wymagany.", - "client-key-tooltip-prk":"Klucz publiczny RPK lub identyfikator muszą być zgodne ze standardem [RFC7250] i zakodowane w formacie Base64!", - "client-key-tooltip-psk":"Klucz PSK musi być zgodny ze standardem [RFC4279] i formatem HexDec: 32, 64, 128 znaków!", - "endpoint":"Nazwa klienta Endpoint", - "endpoint-required":"Nazwa klienta Endpoint jest wymagana.", - "client-public-key":"Klucz publiczny klienta", - "client-public-key-hint":"Jeśli klucz publiczny klienta jest pusty, używane będzie zaufane certyfikat", - "client-public-key-tooltip":"Klucz publiczny X509 musi być w formacie zakodowanym DER X509v3 i obsługiwać wyłącznie algorytm EC, a następnie zakodowany w formacie Base64!", - "mode":"Tryb konfiguracji zabezpieczeń", - "client-tab":"Konfiguracja zabezpieczeń klienta", - "client-certificate":"Certyfikat klienta", - "bootstrap-tab":"Klient rozruchowy", - "bootstrap-server":"Serwer rozruchowy", - "lwm2m-server":"Serwer LwM2M", - "client-publicKey-or-id":"Klucz publiczny klienta lub Identyfikator", - "client-publicKey-or-id-required":"Klucz publiczny klienta lub Identyfikator są wymagane.", - "client-publicKey-or-id-tooltip-psk":"Identyfikator PSK to dowolny identyfikator PSK o długości do 128 bajtów, zgodny z opisem w standardzie [RFC7925].\nIdentyfikator PSK MUSI być najpierw przekształcony w ciąg znaków, a następnie zakodowany na bajty przy użyciu UTF-8.", - "client-publicKey-or-id-tooltip-rpk":"Klucz publiczny RPK lub identyfikator muszą być zgodne ze standardem [RFC7250] i zakodowane w formacie Base64!", - "client-publicKey-or-id-tooltip-x509":"Klucz publiczny X509 musi być w formacie zakodowanym DER X509v3 i obsługiwać wyłącznie algorytm EC, a następnie zakodowany w formacie Base64!", - "client-secret-key":"Tajny klucz klienta", - "client-secret-key-required":"Tajny klucz klienta jest wymagany.", - "client-secret-key-tooltip-psk":"Klucz PSK musi być zgodny ze standardem [RFC4279] i formatem HexDec: 32, 64, 128 znaków!", - "client-secret-key-tooltip-prk":"Tajny klucz RPK musi być w formacie PKCS_8 (kodowanie DER, standard [RFC5958]) i zakodowany w formacie Base64!", - "client-secret-key-tooltip-x509":"Tajny klucz X509 musi być w formacie PKCS_8 (kodowanie DER, standard [RFC5958]) i zakodowany w formacie Base64!" + "datetime": { + "date-from": "Data od", + "time-from": "Czas od", + "date-to": "Data do", + "time-to": "Czas na" + }, + "dashboard": { + "all": "Wszystko", + "all-dashboards": "Wszystkie panele", + "groups": "Grupy", + "shared": "Wspólny", + "dashboard": "Panel", + "dashboards": "Panele", + "management": "Zarządzanie panelem", + "view-dashboards": "Wyświetl panele", + "add": "Dodaj panel", + "assign-dashboard-to-customer": "Przypisz panel(e) do klienta", + "assign-dashboard-to-customer-text": "Proszę wybrać panele, które chcesz przypisać do klienta", + "assign-to-customer-text": "Wybierz klienta, któremu chcesz przypisać panel(e)", + "assign-to-customer": "Przypisz do klienta", + "unassign-from-customer": "Usuń przypisanie od klienta", + "make-public": "Ustaw panel jako publiczny", + "make-private": "Ustaw panel jako prywatny", + "manage-assigned-customers": "Zarządzaj przydzielonymi klientami", + "assigned-customers": "Przydzieleni klienci", + "assign-to-customers": "Przypisz panele klientom", + "assign-to-customers-text": "Wybierz klientów, którym chcesz przypisać panele", + "unassign-from-customers": "Usuń przypisanie paneli od klientów", + "unassign-from-customers-text": "Wybierz klientów, których przypisanie chcesz usunąć z paneli", + "no-dashboards-text": "Nie znaleziono paneli", + "no-widgets": "Nie skonfigurowano żadnych widżetów", + "add-widget": "Dodaj nowy widżet", + "add-widget-button-text": "Add widget", + "title": "Tytuł", + "image": "Obraz panelu", + "mobile-app-settings": "Ustawienia aplikacji mobilnej", + "mobile-order": "Kolejność panelu w aplikacji mobilnej", + "mobile-hide": "Ukryj panel w aplikacji mobilnej", + "update-image": "Zaktualizuj obraz panelu", + "take-screenshot": "Zrobić zrzut ekranu", + "select-widget-title": "Wybierz widżet", + "select-widget-value": "{{title}}: wybierz widżet", + "select-widget-subtitle": "Lista dostępnych typów widgetów", + "delete": "Usuń panel", + "title-required": "Tytuł jest wymagany.", + "title-max-length": "Tytuł powinien być mniejszy niż 256", + "description": "Opis", + "details": "Szczegóły", + "dashboard-details": "Szczegóły panelu", + "add-dashboard-text": "Dodaj nowy panel", + "assign-dashboards": "Przypisz panele", + "assign-new-dashboard": "Przypisz nowy panel", + "assign-dashboards-text": "Przypisz {count, plural, =1 {1 panel} other {# panelów} } do klientów.", + "unassign-dashboards-action-text": "Anuluj przypisanie {count, plural, =1 {1 panelu} other {# paneli} } do klientów.", + "delete-dashboards": "Usuń panele", + "unassign-dashboards": "Usuń przypisanie paneli", + "unassign-dashboards-action-title": "Anuluj przypisanie {count, plural, =1 {1 panelu} other {# paneli} } do klientów.", + "delete-dashboard-title": "Czy na pewno chcesz usunąć panel '{{dashboardTitle}}'?", + "delete-dashboard-text": "Bądź ostrożny, po potwierdzeniu panelu i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-dashboards-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 panel} other {# paneli} }?", + "delete-dashboards-action-title": "Usuń {count, plural, =1 {1 panel} other {# paneli} }", + "delete-dashboards-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane panele zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "unassign-dashboard-title": "Czy na pewno chcesz usunąć przypisanie panelu '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Po potwierdzeniu przypisanie panelu zostanie usunięte i nie będzie dostępne dla klienta.", + "unassign-dashboard": "Anuluj przypisanie panelu", + "unassign-dashboards-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 panelu} other {# paneli} }?", + "unassign-dashboards-text": "Po potwierdzeniu wszystkie wybrane panele zostaną usunięte i nie będą dostępne dla klienta.", + "public-dashboard-title": "Panel jest teraz publiczny", + "public-dashboard-text": "Twój panel {{dashboardTitle}} jest teraz publiczny i dostępny poprzez następny publiczny link:", + "public-dashboard-notice": "Notatka: Nie zapomnij o upublicznieniu powiązanych urządzeń, aby uzyskać dostęp do ich danych.", + "public-dashboard-link": "Link do publicznego panelu", + "public-dashboard-link-text": "Twój publiczny panel {{dashboardTitle}} jest dostępny poprzez next public połączyć:", + "public-dashboard-link-notice": "Notatka: Nie zapomnij o upublicznieniu powiązanych urządzeń, zasobów i widoków obiektów, aby uzyskać dostęp do ich danych.", + "make-private-dashboard-title": "Czy na pewno chcesz uczynić panel '{{dashboardTitle}}' prywatnym?", + "make-private-dashboard-text": "Po potwierdzeniu panel stanie się prywatny i nie będzie dostępny dla innych.", + "make-private-dashboard": "Ustaw panel jako prywatny", + "socialshare-text": "'{{dashboardTitle}}' zasilany przez ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' zasilany przez ThingsBoard", + "select-dashboard": "Wybierz panel", + "no-dashboards-matching": "Nie znaleziono pasujących paneli do '{{entity}}'.", + "dashboard-required": "Panel jest wymagany.", + "select-existing": "Wybierz istniejący panel", + "create-new": "Utwórz nowy panel", + "new-dashboard-title": "Nowy tytuł panelu", + "open-dashboard": "Otwórz panel", + "set-background": "Ustaw tło", + "background-color": "Kolor tła", + "background-image": "Zdjęcie w tle", + "background-size-mode": "Tryb rozmiaru tła", + "no-image": "Nie wybrano obrazu", + "empty-image": "Brak obrazka", + "drop-image": "Upuść obraz lub kliknij, aby wybrać plik do przesłania.", + "maximum-upload-file-size": "Maksymalny rozmiar przesyłanego pliku: {{ size }}", + "cannot-upload-file": "Nie można przesłać pliku", + "settings": "Ustawienia", + "layout-settings": "Ustawienia układu", + "columns-count": "Liczba kolumn", + "columns-count-required": "Liczba kolumn jest wymagana.", + "min-columns-count-message": "Dozwolona jest tylko minimalna liczba kolumn wynosząca 10.", + "max-columns-count-message": "Dozwolona jest tylko maksymalna liczba kolumn wynosząca 1000.", + "widgets-margins": "Margines między widżetami", + "margin-required": "Wartość marginesu jest wymagana.", + "min-margin-message": "Jako minimalną wartość marginesu dozwolone jest tylko 0.", + "max-margin-message": "Jako maksymalna wartość marginesu dozwolona jest tylko 50.", + "horizontal-margin": "Margines poziomy", + "horizontal-margin-required": "Wartość marginesu poziomego jest wymagana.", + "min-horizontal-margin-message": "Dozwolona jest tylko wartość 0 jako minimalna wartość marginesu poziomego.", + "max-horizontal-margin-message": "Dozwolona jest tylko wartość 50 jako maksymalna wartość marginesu poziomego.", + "vertical-margin": "Margines pionowy", + "vertical-margin-required": "Wartość marginesu pionowego jest wymagana.", + "min-vertical-margin-message": "Dozwolone jest tylko 0 jako minimalna wartość marginesu pionowego.", + "max-vertical-margin-message": "Dozwolona jest tylko wartość 50 jako maksymalna wartość marginesu pionowego.", + "apply-outer-margin": "Zastosuj marginesy po bokach układu", + "autofill-height": "Automatyczne dopełnienie do wysokości strony", + "mobile-layout": "Ustawienia układu mobilnego", + "mobile-row-height": "Wysokość wiersza mobilnego, px", + "mobile-row-height-required": "Wartość wysokości wiersza mobilnego jest wymagana.", + "min-mobile-row-height-message": "Jako minimalna wysokość wiersza na urządzeniach mobilnych dozwolone jest tylko 5 pikseli.", + "max-mobile-row-height-message": "Maksymalna wysokość wiersza mobilnego może wynosić tylko 200 pikseli.", + "title-settings": "Ustawienia tytułu", + "display-title": "Wyświetl tytuł panelu kontrolnego", + "title-color": "Kolor tytułu", + "toolbar-settings": "Ustawienia paska narzędzi", + "hide-toolbar": "Ukryj pasek narzędzi", + "toolbar-always-open": "Pozostaw pasek narzędzi otwarty", + "display-dashboards-selection": "Wyświetl wybór paneli", + "display-entities-selection": "Wyświetl wybór obiektów", + "display-filters": "Wyświetl filtry", + "display-dashboard-timewindow": "Wyświetl okno czasowe", + "display-dashboard-export": "Eksport wyświetlacza", + "display-update-dashboard-image": "Wyświetl obraz panelu aktualizacji", + "dashboard-logo-settings": "Ustawienia logo panelu", + "display-dashboard-logo": "Wyświetlaj logo w trybie pełnoekranowym deski rozdzielczej", + "dashboard-logo-image": "Obraz logo panelu", + "advanced-settings": "Zaawansowane ustawienia", + "dashboard-css": "CSS panelu kontrolnego", + "import": "Importuj panel", + "export": "Eksportuj panel", + "export-failed-error": "Nie można wyeksportować panelu: {{error}}", + "export-pdf": "Eksportuj jako plik PDF", + "export-png": "Eksportuj jako PNG", + "export-jpg": "Eksportuj jako JPEG", + "export-json-config": "Eksportuj konfigurację JSON", + "download-dashboard-progress": "Generowanie panelu {{reportType}} ...", + "create-new-dashboard": "Utwórz nowy panel", + "dashboard-file": "Plik panelu", + "invalid-dashboard-file-error": "Nie można zaimportować panelu: Nieprawidłowa struktura danych panelu.", + "dashboard-import-missing-aliases-title": "Skonfiguruj aliasy używane przez zaimportowany panel", + "create-new-widget": "Utwórz nowy widget", + "import-widget": "Importuj widżet", + "widget-file": "Plik widgetu", + "invalid-widget-file-error": "Nie można zaimportować widżetu: Nieprawidłowa struktura danych widżetu.", + "widget-import-missing-aliases-title": "Skonfiguruj aliasy używane przez zaimportowany widget", + "open-toolbar": "Otwórz pasek narzędzi panelu", + "close-toolbar": "Zamknij pasek narzędzi", + "configuration-error": "Błąd konfiguracji", + "alias-resolution-error-title": "Błąd konfiguracji aliasów panelu", + "invalid-aliases-config": "Nie można znaleźć żadnych urządzeń pasujących do niektórych filtrów aliasów.
Aby rozwiązać ten problem, skontaktuj się z administratorem.", + "select-devices": "Wybierz urządzenia", + "assignedToCustomer": "Przypisany do klienta", + "assignedToCustomers": "Przypisane do klientów", + "public": "Publiczny", + "copyId": "Skopiuj identyfikator panelu", + "idCopiedMessage": "Identyfikator panelu został skopiowany do schowka", + "public-link": "Link publiczny", + "copy-public-link": "Skopiuj link publiczny", + "public-link-copied-message": "Link publiczny panelu został skopiowany do schowka", + "manage-states": "Zarządzaj stanami panelu", + "states": "Stany panelu", + "states-short": "States", + "search-states": "Wyszukaj stany panelu", + "selected-states": "Wybrano {count, plural, =1 {1 stan panelu} other {# stanów paneli} }", + "edit-state": "Edytuj stan panelu", + "delete-state": "Usuń stan panelu", + "add-state": "Dodaj stan panelu", + "no-states-text": "Nie znaleziono żadnych stanów", + "state": "Stan panelu", + "state-name": "Nazwa", + "state-name-required": "Nazwa stanu panelu jest wymagana.", + "state-id": "Identyfikator stanu", + "state-id-required": "Identyfikator stanu panelu jest wymagany.", + "state-id-exists": "Stan panelu o tym samym identyfikatorze już istnieje.", + "is-root-state": "Root state", + "delete-state-title": "Usuń stan panelu", + "delete-state-text": "Czy na pewno chcesz usunąć stan panelu o nazwie '{{stateName}}'?", + "show-details": "Pokaż szczegóły", + "hide-details": "Ukryj szczegóły", + "select-state": "Wybierz stan docelowy", + "state-controller": "Kontroler stanu", + "search": "Przeszukaj dashboardy", + "selected-dashboards": "Wybrano {count, plural, =1 {1 panel} other {# dashboardów} }", + "home-dashboard": "Panel główny", + "home-dashboard-hide-toolbar": "Ukryj pasek narzędzi panelu głównego", + "select-group-to-add": "Wybierz grupę docelową, aby dodać wybrane panele", + "select-group-to-move": "Wybierz grupę docelową, aby przenieść wybrane panele", + "remove-dashboards-from-group": "Czy na pewno chcesz usunąć {count, plural, =1 {1 panel} other {# paneli} } z grupy '{{entityGroup}}'?", + "group": "Grupa paneli", + "list-of-groups": "{count, plural, =1 {Jedna grupa paneli} other {Lista # grup paneli} }", + "group-name-starts-with": "Grupy paneli, których nazwy zaczynają się od '{{prefix}}'", + "unassign-dashboard-from-edge-text": "Po potwierdzeniu panel zostanie usunięty i nie będzie dostępny na urządzeniu Edge.", + "unassign-dashboards-from-edge-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 panelu} other {# paneli} }?", + "unassign-dashboards-from-edge-text": "Po potwierdzeniu wszystkie wybrane panele zostaną usunięte i nie będą dostępne na urządzeniu Edge.", + "assign-dashboard-to-edge": "Przypisz panele do Edge", + "assign-dashboard-to-edge-text": "Wybierz panele, które chcesz przypisać do Edge", + "non-existent-dashboard-state-error": "Stan panelu z identyfikatorem \"{{ stateId }}\" nie znaleziono", + "edit-mode": "Edit mode" + }, + "datakey": { + "settings": "Ustawienia", + "general": "Ogólne", + "advanced": "Zaawansowane", + "key": "Klucz", + "label": "Etykieta", + "color": "Kolor", + "units": "Specjalny symbol wyświetlany obok wartości", + "decimals": "Liczba cyfr po przecinku", + "data-generation-func": "Funkcja generowania danych", + "use-data-post-processing-func": "Użyj funkcji przetwarzania końcowego danych", + "configuration": "Konfiguracja klucza danych", + "timeseries": "Szereg czasowy", + "attributes": "Atrybuty", + "entity-field": "Pole obiektu", + "alarm": "Pola alarmowe", + "timeseries-required": "Szeregi czasowe encji są wymagane.", + "timeseries-or-attributes-required": "Szeregi czasowe/atrybuty elementu są wymagane.", + "alarm-fields-timeseries-or-attributes-required": "Pola alarmów lub szeregi czasowe/atrybuty encji są wymagane.", + "maximum-timeseries-or-attributes": "Maksymalnie {count, plural, =1 {1 seria czasowa/atrybut jest dozwolona.} other {# serii czasowych/atrybutów jest dozwolonych} }.", + "alarm-fields-required": "Pola alarmowe są wymagane.", + "function-types": "Typy funkcji", + "function-type": "Typ funkcji", + "function-types-required": "Typy funkcji są wymagane.", + "data-keys": "Data keys", + "data-key": "Data key", + "data-keys-required": "Data keys are required.", + "data-key-required": "Data key is required.", + "alarm-keys": "Klucze danych alarmowych", + "alarm-key": "Klucz danych alarmowych", + "alarm-key-functions": "Funkcje klawiszy alarmowych", + "alarm-key-function": "Funkcja klawisza alarmu", + "latest-keys": "Najnowsze klucze danych", + "latest-key": "Najnowszy klucz danych", + "latest-key-functions": "Najnowsze kluczowe funkcje", + "latest-key-function": "Najnowsza funkcja klucza", + "timeseries-keys": "Klucze danych szeregów czasowych", + "timeseries-key": "Klucz danych szeregów czasowych", + "timeseries-key-functions": "Kluczowe funkcje szeregów czasowych", + "timeseries-key-function": "Funkcja klucza szeregów czasowych", + "maximum-function-types": "Maksymalnie {count, plural, =1 {dozwolony jest 1 typ funkcji.} other {dozwolonych jest # typów funkcji.} }.", + "time-description": "znacznik czasu bieżącej wartości;", + "value-description": "aktualna wartość;", + "prev-value-description": "wynik poprzedniego wywołania funkcji;", + "time-prev-description": "znacznik czasu poprzedniej wartości;", + "prev-orig-value-description": "oryginalna poprzednia wartość;", + "aggregation": "Zbiór", + "aggregation-type-hint-common": "Ze względu na wydajność obliczanie wartości zagregowanych jest dostępne tylko dla stałych przedziałów czasu, takich jak „bieżący dzień”, „bieżący miesiąc” itp., i nie jest dostępne dla przedziałów okna przesuwnego, takich jak „ostatnie 30 minut” lub „ostatnie 24 godziny”.", + "aggregation-type-none-hint": "Weź najnowszą wartość.", + "aggregation-type-min-hint": "Znajdź minimalną wartość wśród punktów danych w wybranym oknie czasowym.", + "aggregation-type-max-hint": "Znajdź maksymalną wartość wśród punktów danych w wybranym oknie czasowym.", + "aggregation-type-avg-hint": "Oblicz średnią wartość spośród punktów danych w wybranym oknie czasowym.", + "aggregation-type-sum-hint": "Zsumuj wszystkie wartości punktów danych w wybranym oknie czasowym.", + "aggregation-type-count-hint": "Całkowita liczba punktów danych w wybranym oknie czasowym.", + "delta-calculation": "Obliczanie delty", + "enable-delta-calculation": "Włącz obliczanie delty", + "enable-delta-calculation-hint": "Po włączeniu wartość klucza danych jest obliczana na podstawie zagregowanych wartości dla wybranego okna czasowego i określonego okresu porównania. ", + "delta-calculation-result": "Wynik obliczenia delty", + "delta-calculation-result-previous-value": "Poprzednia wartość", + "delta-calculation-result-delta-absolute": "Delta (absolutna)", + "delta-calculation-result-delta-percent": "Delta (procent)", + "source": "Source", + "latest": "Latest", + "latest-value": "Latest value", + "delta": "delta", + "percent": "percent", + "absolute": "absolute" + }, + "datasource": { + "type": "Typ źródła danych", + "name": "Nazwa", + "label": "Etykieta", + "add-datasource-prompt": "Proszę dodać źródło danych" + }, + "details": { + "details": "Szczegóły", + "edit-mode": "Tryb edycji", + "edit-json": "Edytuj JSON", + "toggle-edit-mode": "Przełącz tryb edycji" + }, + "device": { + "all": "Wszystko", + "all-devices": "Wszystkie urządzenia", + "groups": "Grupy", + "shared": "Wspólny", + "device": "Urządzenie", + "device-required": "Urządzenie jest wymagane.", + "devices": "Urządzenia", + "management": "Zarządzanie urządzeniami", + "view-devices": "Wyświetl urządzenia", + "device-alias": "Alias urządzenia", + "device-type-max-length": "Typ urządzenia powinien być mniejszy niż 256", + "aliases": "Aliasy urządzeń", + "no-alias-matching": "'{{alias}}' nie znaleziono.", + "no-aliases-found": "Nie znaleziono aliasów.", + "no-key-matching": "'{{key}}' nie znaleziono.", + "no-keys-found": "Nie znaleziono kluczy.", + "create-new-alias": "Utwórz nowy!", + "create-new-key": "Utwórz nowy!", + "duplicate-alias-error": "Znaleziono zduplikowany alias '{{alias}}'.
Aliasy urządzeń muszą być unikalne w obrębie panelu kontrolnego.", + "configure-alias": "Skonfiguruj '{{alias}}' Alias", + "no-devices-matching": "Brak urządzeń pasujących do '{{entity}}' zostały znalezione.", + "alias": "Alias", + "alias-required": "Alias urządzenia jest wymagany.", + "remove-alias": "Usuń alias urządzenia", + "add-alias": "Dodaj alias urządzenia", + "name-starts-with": "Wyrażenie nazwy urządzenia", + "help-text": "Użyj „%” w zależności od potrzeb: „%device_name_contains%”, „%device_name_ends”, „device_starts_with”.", + "device-list": "Lista urządzeń", + "use-device-name-filter": "Użyj filtra", + "device-list-empty": "Nie wybrano żadnych urządzeń.", + "device-name-filter-required": "Filtr nazw urządzeń jest wymagany.", + "device-name-filter-no-device-matched": "Brak urządzeń zaczynających się od '{{device}}' zostały znalezione.", + "add": "Dodaj urządzenie", + "assign-to-customer": "Przypisz do klienta", + "assign-device-to-customer": "Przypisz urządzenia do klienta", + "assign-device-to-customer-text": "Proszę wybrać urządzenia, które chcesz przypisać do klienta", + "make-public": "Ustaw urządzenie jako publiczne", + "make-private": "Ustaw urządzenie jako prywatne", + "no-devices-text": "Nie znaleziono urządzeń", + "assign-to-customer-text": "Wybierz klienta, aby przypisać urządzenie(a)", + "device-details": "Szczegóły urządzenia", + "add-device-text": "Dodaj nowe urządzenie", + "credentials": "Dane uwierzytelniające", + "manage-credentials": "Zarządzaj danymi uwierzytelniającymi", + "delete": "Usuń urządzenie", + "assign-devices": "Przypisz urządzenia", + "assign-devices-text": "Przypisz {count, plural, =1 {1 urządzenie} other {# urządzeń} } do klienta", + "delete-devices": "Usuń urządzenia", + "unassign-from-customer": "Usuń przypisanie od klienta", + "unassign-devices": "Usuń przypisanie urządzeń", + "unassign-devices-action-title": "Anuluj przypisanie {count, plural, =1 {1 urządzenie} other {# urządzeń} } od klienta.", + "unassign-device-from-edge-title": "Czy na pewno chcesz usunąć przypisanie urządzenia '{{deviceName}}'?", + "unassign-device-from-edge-text": "Po potwierdzeniu urządzenie zostanie anulowane i nie będzie dostępne na urządzeniu Edge.", + "unassign-devices-from-edge": "Usuń przypisanie urządzeń z Edge", + "assign-new-device": "Przypisz nowe urządzenie", + "make-public-device-title": "Czy na pewno chcesz uczynić urządzenie {{deviceName}} publicznym ?", + "make-public-device-text": "Po potwierdzeniu urządzenie i wszystkie jego dane zostaną upublicznione i dostępne dla innych.", + "make-private-device-title": "Czy na pewno chcesz uczynić urządzenie '{{deviceName}}' prywatnym?", + "make-private-device-text": "Po potwierdzeniu urządzenie i wszystkie jego dane staną się prywatne i nie będą dostępne dla innych.", + "view-credentials": "Wyświetl dane uwierzytelniające", + "delete-device-title": "Czy na pewno chcesz usunąć urządzenie '{{deviceName}}'?", + "delete-device-text": "Bądź ostrożny, po potwierdzeniu urządzenia i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-devices-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 urządzenie} other {# urządzeń} }?", + "delete-devices-action-title": "Usuń {count, plural, =1 {1 urządzenie} other {# urządzeń} }", + "delete-devices-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane urządzenia zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "unassign-device-title": "Czy na pewno chcesz usunąć przypisanie urządzenia '{{deviceName}}'?", + "unassign-device-text": "Po potwierdzeniu urządzenie zostanie anulowane i nie będzie dostępne dla klienta.", + "unassign-device": "Usuń przypisanie urządzenia", + "unassign-devices-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 urządzenia} other {# urządzeń} }?", + "unassign-devices-text": "Po potwierdzeniu wszystkie wybrane urządzenia zostaną usunięte i nie będą dostępne dla klienta.", + "device-credentials": "Poświadczenia urządzenia.", + "loading-device-credentials": "Ładowanie danych logowania urządzenia...", + "credentials-type": "Typ poświadczeń", + "access-token": "Token dostępu", + "access-token-required": "Token dostępu jest wymagany.", + "access-token-invalid": "Długość tokenu dostępu musi wynosić od 1 do 32 znaków.", + "certificate-pem-format": "Certyfikat w formacie PEM", + "certificate-pem-format-required": "Certyfikat jest wymagany.", + "copy-access-token": "Skopiuj token dostępu", + "copy-certificate": "Skopiuj certyfikat", + "copy-client-id": "Skopiuj identyfikator klienta", + "copy-user-name": "Skopiuj nazwę użytkownika", + "copy-password": "Skopiuj hasło", + "generate-client-id": "Wygeneruj identyfikator klienta", + "generate-user-name": "Wygeneruj nazwę użytkownika", + "generate-password": "Wygeneruj hasło", + "generate-access-token": "Wygeneruj token dostępu", + "lwm2m-security-config": { + "identity": "Tożsamość Klienta", + "identity-required": "Tożsamość klienta jest wymagana.", + "identity-tooltip": "Identyfikator PSK jest dowolnym identyfikatorem PSK o długości do 128 bajtów, zgodnie z opisem w normie [RFC7925].\n Identyfikator PSK MUSI najpierw zostać przekonwertowany do łańcuchu znaków i zakodowanym w oktety używając UTF-8.", + "client-key": "Klucz klienta", + "client-key-required": "Klucz klienta jest wymagany.", + "client-key-tooltip-prk": "Klucz publiczny lub identyfikator RPK musi być w standardzie [RFC7250] i zakodowany w formacie Base64!", + "client-key-tooltip-psk": "Klucz PSK musi być w standardzie [RFC4279] i formacie HexDec: 32, 64, 128 znaków!", + "endpoint": "Nazwa klienta punktu końcowego", + "endpoint-required": "Nazwa klienta punktu końcowego jest wymagana.", + "client-public-key": "Klucz publiczny klienta", + "client-public-key-hint": "Jeśli klucz publiczny klienta jest pusty, zostanie użyty zaufany certyfikat", + "client-public-key-tooltip": "Klucz publiczny X509 musi być w formacie X509v3 zakodowanym w DER i obsługiwać wyłącznie algorytm EC, a następnie zakodowany w formacie Base64!", + "mode": "Tryb konfiguracji zabezpieczeń", + "client-tab": "Konfiguracja zabezpieczeń klienta", + "client-certificate": "Certyfikat klienta", + "bootstrap-tab": "Klient Bootstrapa", + "bootstrap-server": "Serwer startowy", + "lwm2m-server": "Serwer LwM2M", + "client-publicKey-or-id": "Klucz publiczny klienta lub identyfikator jest wymagany.", + "client-publicKey-or-id-required": "Klucz publiczny lub identyfikator klienta są wymagane.", + "client-publicKey-or-id-tooltip-psk": "Identyfikator PSK jest dowolnym identyfikatorem PSK o długości do 128 bajtów, zgodnie z opisem w normie [RFC7925].\nIdentyfikator PSK MUSI najpierw zostać przekonwertowany na łańcuch znaków i potem zakodowany do oktetów używając UTF-8", + "client-publicKey-or-id-tooltip-rpk": "Klucz publiczny lub identyfikator RPK musi być w standardzie [RFC7250] i zakodowany w formacie Base64!", + "client-publicKey-or-id-tooltip-x509": "Klucz publiczny X509 musi być w formacie X509v3 zakodowanym w DER i obsługiwać wyłącznie algorytm EC, a następnie zakodowany w formacie Base64", + "client-secret-key": "Tajny klucz klienta", + "client-secret-key-required": "Tajny klucz klienta jest wymagany.", + "client-secret-key-tooltip-psk": "Klucz PSK musi być w standardzie [RFC4279] i formacie HexDec: 32, 64, 128 znaków!", + "client-secret-key-tooltip-prk": "Tajny klucz RPK musi być w formacie PKCS_8 (kodowanie DER, standard [RFC5958]), a następnie zakodowany w formacie Base64!", + "client-secret-key-tooltip-x509": "Tajny klucz X509 musi być w formacie PKCS_8 (kodowanie DER, standard [RFC5958]), a następnie zakodowany w formacie Base64!" }, - "client-id":"ID klienta", - "client-id-pattern":"Zawiera nieprawidłowy znak.", - "user-name":"Nazwa użytkownika", - "user-name-required":"Nazwa użytkownika jest wymagana.", - "client-id-or-user-name-necessary":"ID klienta i/lub Nazwa użytkownika są wymagane", - "password":"Hasło", - "secret":"Sekret", - "secret-required":"Sekret jest wymagany.", - "device-type":"Profil urządzenia", - "device-type-required":"Typ urządzenia jest wymagany.", - "select-device-type":"Wybierz typ urządzenia", - "enter-device-type":"Wprowadź profil urządzenia", - "any-device":"Dowolne urządzenie", - "no-device-types-matching":"Nie znaleziono profili urządzeń pasujących do '{{entitySubtype}}'.", - "device-type-list-empty":"Nie wybrano profili urządzeń!", - "device-profile-type-list-empty":"Należy wybrać co najmniej jeden profil urządzenia.", - "device-types":"Profile urządzeń", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "name-max-length":"Nazwa powinna być krótsza niż 256", - "label-max-length":"Etykieta powinna być krótsza niż 256", - "description":"Opis", - "label":"Etykieta", - "events":"Zdarzenia", - "details":"Szczegóły", - "copyId":"Kopiuj ID urządzenia", - "copyAccessToken":"Kopiuj token dostępu", - "copy-mqtt-authentication":"Kopiuj poświadczenia MQTT", - "idCopiedMessage":"ID urządzenia zostało skopiowane do schowka", - "accessTokenCopiedMessage":"Token dostępu do urządzenia został skopiowany do schowka", - "mqtt-authentication-copied-message":"Poświadczenia MQTT urządzenia zostały skopiowane do schowka", - "assignedToCustomer":"Przypisane do klienta", - "unable-delete-device-alias-title":"Nie można usunąć aliasu urządzenia", - "unable-delete-device-alias-text":"Alias urządzenia '{{deviceAlias}}' nie może być usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", - "is-gateway":"Jest bramą", - "overwrite-activity-time":"Nadpisz czas aktywności podłączonego urządzenia", - "device-filter":"Filtr urządzenia", - "device-filter-title":"Filtr urządzenia", - "filter-title":"Filtr", - "device-state":"Stan urządzenia", - "state":"Stan", - "any":"Dowolny", - "active":"Aktywny", - "inactive":"Nieaktywny", - "public":"Publiczny", - "device-public":"Urządzenie jest publiczne", - "select-device":"Wybierz urządzenie", - "import":"Importuj urządzenie", - "device-file":"Plik urządzenia", - "search":"Wyszukaj urządzenia", - "selected-devices":"{ count, plural, =1 {1 urządzenie} other {# urządzeń} } wybranych", - "device-configuration":"Konfiguracja urządzenia", - "transport-configuration":"Konfiguracja transportu", - "wizard":{ - "device-details":"Szczegóły urządzenia" + "client-id": "Identyfikator klienta", + "client-id-pattern": "Zawiera nieprawidłowy znak.", + "user-name": "Nazwa użytkownika", + "user-name-required": "Nazwa użytkownika jest wymagana.", + "client-id-or-user-name-necessary": "Identyfikator klienta i/lub nazwa użytkownika są niezbędne", + "password": "Hasło", + "secret": "Sekret", + "secret-required": "Sekret jest wymagany.", + "device-type": "Rodzaj urządzenia", + "device-type-required": "Typ urządzenia jest wymagany.", + "select-device-type": "Wybierz typ urządzenia", + "enter-device-type": "Wpisz typ urządzenia", + "any-device": "Dowolne urządzenie", + "no-device-types-matching": "Brak typów urządzeń pasujących do '{{entitySubtype}}' zostały znalezione.", + "device-type-list-empty": "Nie wybrano typów urządzeń.", + "device-types": "Typy urządzeń", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "label-max-length": "Etykieta powinna być mniejsza niż 256", + "description": "Opis", + "label": "Etykieta", + "events": "Wydarzenia", + "details": "Szczegóły", + "copyId": "Skopiuj identyfikator urządzenia", + "copyAccessToken": "Skopiuj token dostępu", + "copy-mqtt-authentication": "Skopiuj poświadczenia MQTT", + "idCopiedMessage": "Identyfikator urządzenia został skopiowany do schowka", + "accessTokenCopiedMessage": "Token dostępu do urządzenia został skopiowany do schowka", + "mqtt-authentication-copied-message": "Uwierzytelnianie urządzenia MQTT zostało skopiowane do schowka", + "assignedToCustomer": "Przypisany do klienta", + "unable-delete-device-alias-title": "Nie można usunąć aliasu urządzenia", + "unable-delete-device-alias-text": "Alias urządzenia '{{deviceAlias}}' nie może zostać usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", + "is-gateway": "Gateway", + "overwrite-activity-time": "Zastąp czas aktywności podłączonego urządzenia", + "device-filter": "Filtruj urządzenia", + "device-filter-title": "Filtruj urządzenia", + "filter-title": "Filtr", + "device-state": "Stan urządzenia", + "state": "Stan", + "any": "Każdy", + "active": "Aktywny", + "inactive": "Nieaktywny", + "public": "Publiczny", + "device-public": "Urządzenie jest publiczne", + "select-device": "Wybierz urządzenie", + "select-group-to-add": "Wybierz grupę docelową, aby dodać wybrane urządzenia", + "select-group-to-move": "Wybierz grupę docelową, aby przenieść wybrane urządzenia", + "remove-devices-from-group": "Czy na pewno chcesz usunąć {count, plural, =1 {1 urządzenie} other {# urządzeń} } z grupy '{{entityGroup}}'?", + "group": "Grupa urządzeń", + "list-of-groups": "{count, plural, =1 {Jedna grupa urządzeń} other {Lista # grup urządzeń} }", + "group-name-starts-with": "Grupy urządzeń, których nazwy zaczynają się od '{{prefix}}'", + "import": "Importuj urządzenie", + "device-file": "Plik urządzenia", + "search": "Wyszukaj urządzenia", + "selected-devices": "Wybrano {count, plural, =1 {1 urządzenie} other {# urządzeń} }", + "device-configuration": "Konfiguracja urzadzenia", + "transport-configuration": "Konfiguracja transportu", + "wizard": { + "device-details": "Szczegóły urządzenia", + "device-wizard": "Kreator urządzeń", + "new-device-profile": "Utwórz nowy profil urządzenia", + "existing-device-profile": "Wybierz istniejący profil urządzenia", + "specific-configuration": "Konkretna konfiguracja", + "customer-to-assign-device": "Klientowi przypisanie urządzenia", + "add-credentials": "Dodaj dane uwierzytelniające" }, - "unassign-devices-from-edge-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 urządzenie} other {# urządzeń} }?", - "unassign-devices-from-edge-text":"Po potwierdzeniu wszystkie wybrane urządzenia zostaną odłączone i nie będą dostępne dla krawędzi.", - "time":"Czas", - "connectivity":{ - "check-connectivity":"Sprawdź łączność", - "device-created-check-connectivity":"Urządzenie utworzone. Sprawdźmy łączność!", - "loading-check-connectivity-command":"Ładowanie poleceń sprawdzania łączności...", - "use-following-instructions":"Skorzystaj z poniższych instrukcji, aby wysyłać telemetrię w imieniu urządzenia za pomocą konsoli", - "execute-following-command":"Wykonaj poniższe polecenie", - "install-curl-windows":"Począwszy od systemu Windows 10 b17063, cURL jest dostępny domyślnie", - "install-curl-macos":"Począwszy od Mac OS X 10.2 6C115 (Jaguar), cURL jest dostępny domyślnie", - "install-mqtt-windows":"Użyj instrukcji do pobrania, zainstalowania, skonfigurowania i uruchomienia mosquitto_pub", - "install-coap-client":"Użyj instrukcji do pobrania, zainstalowania, skonfigurowania i uruchomienia klienta coap", - "install-necessary-client-tools":"Zainstaluj niezbędne narzędzia klienta", - "mqtts-x509-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą MQTT z uwierzytelnianiem X509", - "coaps-x509-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą CoAP przez DTLS z uwierzytelnianiem X509", - "snmp-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą SNMP.", - "sparkplug-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą MQTT Sparkplug.", - "lwm2m-command":"Skorzystaj z poniższej dokumentacji, aby połączyć urządzenie za pomocą LWM2M." + "unassign-devices-from-edge-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 urządzenia} other {# urządzeń} }?", + "unassign-devices-from-edge-text": "Po potwierdzeniu wszystkie wybrane urządzenia zostaną usunięte i nie będą dostępne na urządzeniu Edge.", + "time": "Time", + "connectivity": { + "check-connectivity": "Check connectivity", + "device-created-check-connectivity": "Device created. Let's check connectivity!", + "loading-check-connectivity-command": "Loading check connectivity commands...", + "use-following-instructions": "Use the following instructions for sending telemetry on behalf of the device using shell", + "execute-following-command": "Execute the following command", + "install-curl-windows": "Starting Windows 10 b17063, cURL is available by default", + "install-curl-macos": "Starting Mac OS X 10.2 6C115 (Jaguar), cURL is available by default", + "install-mqtt-windows": "Use the instructions to download, install, setup and run mosquitto_pub", + "install-coap-client": "Use the instructions to download, install, setup and run coap-client", + "install-necessary-client-tools": "Install necessary client tools", + "mqtts-x509-command": "Use the following documentation to connect the device via MQTT with authorization X509", + "coaps-x509-command": "Use the following documentation to connect the device via CoAP over DTLS with authorization X509", + "snmp-command": "Use the following documentation to connect the device through the SNMP.", + "sparkplug-command": "Use the following documentation to connect the device through the MQTT Sparkplug.", + "lwm2m-command": "Use the following documentation to connect the device through the LWM2M." } }, - "asset-profile":{ - "asset-profile":"Profil zasobu", - "asset-profiles":"Profile zasobów", - "all-asset-profiles":"Wszystkie", - "add":"Dodaj profil zasobu", - "edit":"Edytuj profil zasobu", - "asset-profile-details":"Szczegóły profilu zasobu", - "no-asset-profiles-text":"Nie znaleziono profili zasobów", - "search":"Szukaj profili zasobów", - "selected-asset-profiles":"{ count, plural, =1 {1 profil zasobu} other {# profile zasobów} } wybrano", - "no-asset-profiles-matching":"Nie znaleziono profilu zasobu pasującego do '{{entity}}'.", - "asset-profile-required":"Wymagany profil zasobu", - "idCopiedMessage":"Identyfikator profilu zasobu został skopiowany do schowka", - "set-default":"Ustaw jako domyślny profil zasobu", - "delete":"Usuń profil zasobu", - "copyId":"Kopiuj identyfikator profilu zasobu", - "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", - "new-device-profile-name":"Nazwa profilu zasobu", - "new-device-profile-name-required":"Wymagana jest nazwa profilu zasobu.", - "name":"Nazwa", - "name-required":"Wymagana jest nazwa.", - "image":"Zdjęcie profilu zasobu", - "description":"Opis", - "default":"Domyślny", - "default-rule-chain":"Domyślny łańcuch reguł", - "default-edge-rule-chain":"Domyślny łańcuch reguł na krawędzi", - "default-edge-rule-chain-hint":"Używane na krawędzi jako łańcuch reguł do przetwarzania przychodzących danych dla zasobów tego profilu zasobu", - "mobile-dashboard":"Mobilna konsola", - "mobile-dashboard-hint":"Używane przez aplikację mobilną jako konsola szczegółów zasobu", - "select-queue-hint":"Wybierz z listy rozwijanej.", - "delete-asset-profile-title":"Czy na pewno chcesz usunąć profil zasobu '{{assetProfileName}}'?", - "delete-asset-profile-text":"Bądź ostrożny, po potwierdzeniu profil zasobu i wszystkie związane dane staną się nieodwracalnie utracone.", - "delete-asset-profiles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 profil zasobu} other {# profile zasobów} }?", - "delete-asset-profiles-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile zasobów zostaną usunięte, a wszystkie związane dane staną się nieodwracalnie utracone.", - "set-default-asset-profile-title":"Czy na pewno chcesz ustawić profil zasobu '{{assetProfileName}}' jako domyślny?", - "set-default-asset-profile-text":"Po potwierdzeniu profil zasobu zostanie oznaczony jako domyślny i będzie używany dla nowych zasobów bez określonego profilu.", - "no-asset-profiles-found":"Nie znaleziono profili zasobów.", - "create-new-asset-profile":"Utwórz nowy!", - "create-asset-profile":"Utwórz nowy profil zasobu", - "import":"Importuj profil zasobu", - "export":"Eksportuj profil zasobu", - "export-failed-error":"Nie można wyeksportować profilu zasobu: {{error}}", - "asset-profile-file":"Plik profilu zasobu", - "invalid-asset-profile-file-error":"Nie można zaimportować profilu zasobu: Nieprawidłowa struktura danych profilu zasobu." + "asset-profile": { + "asset-profile": "Profil zasobu", + "asset-profiles": "Profile zasobu", + "all-asset-profiles": "Wszystko", + "add": "Dodaj profil zasobu", + "edit": "Edytuj profil zasobu", + "asset-profile-details": "Szczegóły profilu zasobów", + "no-asset-profiles-text": "Nie znaleziono profili zasobów", + "search": "Wyszukaj profile zasobów", + "selected-asset-profiles": "Wybrano {count, plural, =1 {1 profil zasobu} other {# profili zasobów} }", + "no-asset-profiles-matching": "Nie znaleziono dopasowania profilu zasobu '{{entity}}'", + "asset-profile-required": "Profil zasobu jest wymagany", + "idCopiedMessage": "Identyfikator profilu zasobu został skopiowany do schowka", + "set-default": "Ustaw profil zasobów jako domyślny", + "delete": "Usuń profil zasobu", + "copyId": "Skopiuj identyfikator profilu zasobu", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "new-device-profile-name": "Nazwa profilu zasobu", + "new-device-profile-name-required": "Nazwa profilu zasobu jest wymagana.", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana", + "image": "Obraz profilowy zasobu", + "description": "Opis", + "default": "Domyślny", + "default-rule-chain": "Domyślny łańcuch reguł", + "default-edge-rule-chain": "Domyślny łańcuch reguł Edge", + "default-edge-rule-chain-hint": "Używany na Edge jako łańcuch reguł do przetwarzania danych przychodzących dla zasobów tego profilu zasobów", + "mobile-dashboard": "Mobilny panel", + "mobile-dashboard-hint": "Używany przez aplikację mobilną jako panel ze szczegółami zasobów", + "select-queue-hint": "Wybierz z listy rozwijanej.", + "delete-asset-profile-title": "Czy na pewno chcesz usunąć profil zasobu '{{assetProfileName}}'?", + "delete-asset-profile-text": "Bądź ostrożny, po potwierdzeniu profilu zasobu i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-asset-profiles-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 profil zasobu} other {# profilów zasobów} }?", + "delete-asset-profiles-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile zasobów zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "set-default-asset-profile-title": "Czy na pewno chcesz uczynić profil zasobu '{{assetProfileName}}' domyślnym?", + "set-default-asset-profile-text": "Po potwierdzeniu profil zasobu zostanie oznaczony jako domyślny i będzie używany do nowych zasobów bez określonego profilu.", + "no-asset-profiles-found": "Nie znaleziono profili zasobów.", + "create-new-asset-profile": "Utwórz nowy!", + "create-asset-profile": "Utwórz nowy profil zasobu", + "import": "Importuj profil zasobu", + "export": "Eksportuj profil zasobów", + "export-failed-error": "Nie można wyeksportować profilu zasobu: {{error}}", + "asset-profile-file": "Plik profilu zasobów", + "invalid-asset-profile-file-error": "Nie można zaimportować profilu zasobu: nieprawidłowa struktura danych profilu zasobu." }, - "device-profile":{ - "device-profile":"Profil urządzenia", - "device-profiles":"Profile urządzeń", - "all-device-profiles":"Wszystkie", - "add":"Dodaj profil urządzenia", - "edit":"Edytuj profil urządzenia", - "device-profile-details":"Szczegóły profilu urządzenia", - "no-device-profiles-text":"Nie znaleziono profili urządzeń", - "search":"Szukaj profili urządzeń", - "selected-device-profiles":"{ count, plural, =1 {1 profil urządzenia} other {# profile urządzeń} } wybrano", - "no-device-profiles-matching":"Nie znaleziono profilu urządzenia pasującego do '{{entity}}'.", - "device-profile-required":"Wymagany profil urządzenia", - "idCopiedMessage":"Identyfikator profilu urządzenia został skopiowany do schowka", - "set-default":"Ustaw jako domyślny profil urządzenia", - "delete":"Usuń profil urządzenia", - "copyId":"Kopiuj identyfikator profilu urządzenia", - "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", - "name":"Nazwa", - "name-required":"Wymagana jest nazwa.", - "type":"Typ profilu", - "type-required":"Wymagany jest typ profilu.", - "type-default":"Domyślny", - "image":"Zdjęcie profilu urządzenia", - "transport-type":"Typ transportu", - "transport-type-required":"Wymagany jest typ transportu.", - "transport-type-default":"Domyślny", - "transport-type-default-hint":"Obsługuje podstawowe transporty MQTT, HTTP i CoAP", - "transport-type-mqtt":"MQTT", - "transport-type-mqtt-hint":"Umożliwia zaawansowane ustawienia transportu MQTT", - "transport-type-coap":"CoAP", - "transport-type-coap-hint":"Umożliwia zaawansowane ustawienia transportu CoAP", - "transport-type-lwm2m":"LWM2M", - "transport-type-lwm2m-hint":"Typ transportu LWM2M", - "transport-type-snmp":"SNMP", - "transport-type-snmp-hint":"Określ konfigurację transportu SNMP", - "transport-type-http":"HTTP", - "description":"Opis", - "default":"Domyślny", - "profile-configuration":"Konfiguracja profilu", - "transport-configuration":"Konfiguracja transportu", - "default-rule-chain":"Domyślny łańcuch reguł", - "default-edge-rule-chain":"Domyślny łańcuch reguł na krawędzi", - "default-edge-rule-chain-hint":"Używane na krawędzi jako łańcuch reguł do przetwarzania przychodzących danych dla urządzeń tego profilu urządzenia", - "mobile-dashboard":"Mobilna konsola", - "mobile-dashboard-hint":"Używane przez aplikację mobilną jako konsola szczegółów urządzenia", - "select-queue-hint":"Wybierz z listy rozwijanej.", - "delete-device-profile-title":"Czy na pewno chcesz usunąć profil urządzenia '{{deviceProfileName}}'?", - "delete-device-profile-text":"Bądź ostrożny, po potwierdzeniu profil urządzenia i wszystkie związane dane, w tym powiązane aktualizacje OTA, staną się nieodwracalnie utracone.", - "delete-device-profiles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 profil urządzenia} other {# profile urządzeń} }?", - "delete-device-profiles-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile urządzeń zostaną usunięte, a wszystkie związane dane, w tym powiązane aktualizacje OTA, staną się nieodwracalnie utracone.", - "set-default-device-profile-title":"Czy na pewno chcesz ustawić profil urządzenia '{{deviceProfileName}}' jako domyślny?", - "set-default-device-profile-text":"Po potwierdzeniu profil urządzenia zostanie oznaczony jako domyślny i będzie używany dla nowych urządzeń bez określonego profilu.", - "no-device-profiles-found":"Nie znaleziono profili urządzeń.", - "create-new-device-profile":"Utwórz nowy!", - "mqtt-device-topic-filters":"Filtr tematów urządzenia MQTT", - "mqtt-device-topic-filters-unique":"Filtry tematów urządzenia MQTT muszą być unikalne.", - "mqtt-device-topic-filters-spark-plug":"Węzeł krawędziowy MQTT Sparkplug B (EoN).", - "mqtt-device-topic-filters-spark-plug-hint":"Zezwól na połączenia z węzłami EoN z ładunkiem i formatem tematu Sparkplug B.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names":"Metryki SparkPlug do przechowywania jako atrybuty.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint":"Nazwy metryk SparkPlug, które będą przechowywane jako atrybuty urządzenia. Wszystkie inne metryki będą przechowywane jako telemetria urządzenia.", - "mqtt-device-payload-type":"Typ ładunka urządzenia MQTT", - "mqtt-device-payload-type-json":"JSON", - "mqtt-device-payload-type-proto":"Protobuf", - "mqtt-enable-compatibility-with-json-payload-format":"Włącz zgodność z innymi formatami ładunków.", - "mqtt-enable-compatibility-with-json-payload-format-hint":"Gdy jest włączona, platforma będzie domyślnie używać formatu ładunku Protobuf. Jeśli parsowanie się nie powiedzie, platforma spróbuje użyć formatu ładunku JSON. Przydatne dla wstecznej kompatybilności podczas aktualizacji oprogramowania układowego. Na przykład początkowa wersja oprogramowania używa Json, podczas gdy nowa wersja używa Protobuf. Podczas procesu aktualizacji oprogramowania dla floty urządzeń konieczne jest jednoczesne obsługiwanie zarówno Protobuf, jak i JSON. Tryb zgodności wprowadza niewielki spadek wydajności, więc zaleca się wyłączenie tego trybu, gdy wszystkie urządzenia zostaną zaktualizowane.", - "mqtt-use-json-format-for-default-downlink-topics":"Użyj formatu Json dla domyślnych tematów zwrotnych", - "mqtt-use-json-format-for-default-downlink-topics-hint":"Gdy jest włączone, platforma będzie używać formatu ładunku Json do przesyłania atrybutów i RPC za pomocą następujących tematów: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. Ta ustawienie nie wpływa na subskrypcje atrybutów i rpc wysyłane za pomocą nowych (v2) tematów: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Gdzie $request_id to identyfikator żądania.", - "mqtt-send-ack-on-validation-exception":"Wyślij PUBACK w przypadku niepowodzenia walidacji komunikatu PUBLISH", - "mqtt-send-ack-on-validation-exception-hint":"Domyślnie platforma zakończy sesję MQTT w przypadku niepowodzenia walidacji komunikatu. Gdy jest włączone, platforma wyśle potwierdzenie publikacji zamiast zakończenia sesji.", - "snmp-add-mapping":"Dodaj mapowanie SNMP", - "snmp-mapping-not-configured":"Nie skonfigurowano mapowania OID na szeregi czasowe/telemetrię", - "snmp-timseries-or-attribute-name":"Nazwa szeregów czasowych/cechy do mapowania", - "snmp-timseries-or-attribute-type":"Typ szeregów czasowych/cechy do mapowania", - "snmp-method-pdu-type-get-request":"GetRequest", - "snmp-method-pdu-type-get-next-request":"GetNextRequest", - "snmp-oid":"OID", - "transport-device-payload-type-json":"JSON", - "transport-device-payload-type-proto":"Protobuf", - "mqtt-payload-type-required":"Wymagany jest typ ładunka.", - "coap-device-type":"Typ urządzenia CoAP", - "coap-device-payload-type":"Typ ładunka urządzenia CoAP", - "coap-device-type-required":"Wymagany jest typ urządzenia CoAP.", - "coap-device-type-default":"Domyślny", - "coap-device-type-efento":"Efento NB-IoT", - "support-level-wildcards":"Obsługiwane są pojedyncze [+] i wielopoziomowe [#] znaki wieloznaczne.", - "telemetry-topic-filter":"Filtr tematu telemetrii", - "telemetry-topic-filter-required":"Wymagany jest filtr tematu telemetrii.", - "attributes-topic-filter":"Filtr tematu publikacji atrybutów", - "attributes-subscribe-topic-filter":"Filtr tematu subskrypcji atrybutów", - "attributes-topic-filter-required":"Wymagany jest filtr tematu publikacji atrybutów.", - "attributes-subscribe-topic-filter-required":"Wymagany jest filtr tematu subskrypcji atrybutów", - "telemetry-proto-schema":"Schemat protokołu telemetrii", - "telemetry-proto-schema-required":"Wymagany jest schemat protokołu telemetrii.", - "attributes-proto-schema":"Schemat protokołu atrybutów", - "attributes-proto-schema-required":"Wymagany jest schemat protokołu atrybutów.", - "rpc-response-proto-schema":"Schemat protokołu odpowiedzi RPC", - "rpc-response-proto-schema-required":"Wymagany jest schemat protokołu odpowiedzi RPC.", - "rpc-response-topic-filter":"Filtr tematu odpowiedzi RPC", - "rpc-response-topic-filter-required":"Wymagany jest filtr tematu odpowiedzi RPC.", - "rpc-request-proto-schema":"Schemat protokołu żądania RPC", - "rpc-request-proto-schema-required":"Wymagany jest schemat protokołu żądania RPC.", - "rpc-request-proto-schema-hint":"Wiadomość żądania RPC powinna zawsze zawierać pola: string method = 1; int32 requestId = 2; oraz params = 3 dowolnego typu danych.", - "not-valid-pattern-topic-filter":"Nieprawidłowy filtr tematu wzorca", - "not-valid-single-character":"Nieprawidłowe użycie znaku wieloznacznego jednego poziomu", - "not-valid-multi-character":"Nieprawidłowe użycie znaku wieloznacznego wielu poziomów", - "single-level-wildcards-hint":"[+] nadaje się do dowolnego poziomu filtra tematu. Na przykład: v1/devices/+/telemetry lub +/devices/+/attributes.", - "multi-level-wildcards-hint":"[#] może zastąpić sam filtr tematu i musi być ostatnim symbolem tematu. Na przykład: # lub v1/devices/me/#.", - "alarm-rules":"Zasady alarmowe", - "alarm-rules-with-count":"Zasady alarmowe ({{count}})", - "no-alarm-rules":"Brak skonfigurowanych zasad alarmowych", - "add-alarm-rule":"Dodaj zasadę alarmową", - "edit-alarm-rule":"Edytuj zasadę alarmową", - "alarm-type":"Typ alarmu", - "alarm-type-required":"Wymagany jest typ alarmu.", - "alarm-type-unique":"Typ alarmu musi być unikalny w ramach zasad alarmowych profilu urządzenia.", - "alarm-type-max-length":"Typ alarmu powinien być krótszy niż 256", - "create-alarm-pattern":"Utwórz alarm {{alarmType}}", - "create-alarm-rules":"Utwórz zasady alarmowe", - "no-create-alarm-rules":"Brak skonfigurowanych warunków utworzenia", - "add-create-alarm-rule-prompt":"Proszę dodać warunek utworzenia alarmu", - "clear-alarm-rule":"Wyczyść zasadę alarmową", - "no-clear-alarm-rule":"Brak skonfigurowanego warunku wyczyszczenia", - "add-create-alarm-rule":"Dodaj warunek utworzenia alarmu", - "add-clear-alarm-rule":"Dodaj warunek wyczyszczenia", - "select-alarm-severity":"Wybierz stopień pilności alarmu", - "alarm-severity-required":"Wymagany jest stopień pilności alarmu.", - "condition-duration":"Czas trwania warunku", - "condition-duration-value":"Wartość czasu trwania", - "condition-duration-time-unit":"Jednostka czasu", - "condition-duration-value-range":"Wartość czasu trwania powinna zawierać się w zakresie od 1 do 2147483647.", - "condition-duration-value-pattern":"Wartość czasu trwania powinna być liczbą całkowitą.", - "condition-duration-value-required":"Wymagana jest wartość czasu trwania.", - "condition-duration-time-unit-required":"Wymagana jest jednostka czasu.", - "advanced-settings":"Zaawansowane ustawienia", - "alarm-rule-additional-info":"Dodatkowe informacje", - "edit-alarm-rule-additional-info":"Edytuj dodatkowe informacje", - "alarm-rule-additional-info-placeholder":"Proszę podać swoje uwagi i dostosowania tutaj, aby wyświetlić je w szczegółach alarmu w sekcji Dodatkowe informacje", - "alarm-rule-additional-info-hint":"Wskazówka: użyj ${kluczNazwy} do zastąpienia wartości kluczy atrybutu lub telemetrii używanych w warunku zasady alarmowej.", - "alarm-rule-mobile-dashboard":"Panel nawigacyjny na urządzenia przenośne", - "alarm-rule-mobile-dashboard-hint":"Używane przez aplikację mobilną jako panel nawigacyjny szczegółów alarmu", - "alarm-rule-no-mobile-dashboard":"Nie wybrano panelu nawigacyjnego", - "propagate-alarm":"Rozprzestrzeniaj alarm do powiązanych jednostek", - "alarm-rule-relation-types-list":"Typy relacji do rozprzestrzeniania", - "alarm-rule-relation-types-list-hint":"Jeśli nie zostaną wybrane typy relacji do rozprzestrzeniania, alarmy będą rozprzestrzeniane bez filtrowania według typu relacji.", - "propagate-alarm-to-owner":"Rozprzestrzeniaj alarm do właściciela jednostki (Klienta lub Najemcy)", - "propagate-alarm-to-tenant":"Rozprzestrzeniaj alarm do Najemcy", - "alarm-rule-condition":"Warunek zasady alarmowej", - "enter-alarm-rule-condition-prompt":"Proszę dodać warunek zasady alarmowej", - "edit-alarm-rule-condition":"Edytuj warunek zasady alarmowej", - "device-provisioning":"Proces przydzielania urządzenia", - "provision-strategy":"Strategia przydzielania", - "provision-strategy-required":"Wymagana jest strategia przydzielania.", - "provision-strategy-disabled":"Wyłączone", - "provision-strategy-created-new":"Zezwól na tworzenie nowych urządzeń", - "provision-strategy-check-pre-provisioned":"Sprawdź wcześniej przydzielone urządzenia", - "provision-device-key":"Klucz przydzielania urządzenia", - "provision-device-key-required":"Wymagany jest klucz przydzielania urządzenia.", - "copy-provision-key":"Kopiuj klucz przydzielania", - "provision-key-copied-message":"Klucz przydzielania został skopiowany do schowka", - "provision-device-secret":"Sekret przydzielania urządzenia", - "provision-device-secret-required":"Wymagany jest sekret przydzielania urządzenia.", - "copy-provision-secret":"Kopiuj sekret przydzielania", - "provision-secret-copied-message":"Sekret przydzielania został skopiowany do schowka", - "provision-strategy-x509":{ - "certificate-chain":"Łańcuch certyfikatów X509", - "certificate-chain-hint":"Strategia certyfikatów X.509 jest używana do przydzielania urządzeń za pomocą certyfikatów klientów w dwukierunkowej komunikacji TLS.", - "allow-create-new-devices":"Utwórz nowe urządzenia", - "allow-create-new-devices-hint":"Jeśli zaznaczone, nowe urządzenia zostaną utworzone, a certyfikat klienta będzie używany jako dane uwierzytelniające urządzenia.", - "certificate-value":"Certyfikat w formacie PEM", - "certificate-value-required":"Wymagany jest certyfikat w formacie PEM", - "cn-regex-variable":"Zmienna wyrażenia regularnego CN", - "cn-regex-variable-required":"Wymagana jest zmienna wyrażenia regularnego CN", - "cn-regex-variable-hint":"Wymagane do pobrania nazwy urządzenia z wspólnego nazewnictwa certyfikatu X509 urządzenia." + "device-profile": { + "device-profile": "Profil urządzenia", + "device-profiles": "Profile urządzeń", + "all-device-profiles": "Wszystkie", + "add": "Dodaj profil urządzenia", + "edit": "Edytuj profil urządzenia", + "device-profile-details": "Szczegóły profilu urządzenia", + "no-device-profiles-text": "Nie znaleziono profili urządzeń", + "search": "Wyszukaj profile urządzeń", + "selected-device-profiles": "Wybrano {count, plural, =1 {1 profil urządzenia} other {# profili urządzeń} }", + "no-device-profiles-matching": "Nie znaleziono profilu urządzenia pasującego do '{{entity}}'", + "device-profile-required": "Profil urządzenia jest wymagany", + "idCopiedMessage": "Identyfikator profilu urządzenia został skopiowany do schowka", + "set-default": "Ustaw profil urządzenia jako domyślny", + "delete": "Usuń profil urządzenia", + "copyId": "Skopiuj identyfikator profilu urządzenia", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana", + "type": "Typ profilu", + "type-required": "Typ profilu jest wymagany.", + "type-default": "Domyślny", + "image": "Obraz profilu urządzenia", + "transport-type": "Rodzaj transportu", + "transport-type-required": "Rodzaj transportu jest wymagany.", + "transport-type-default": "Domyślny", + "transport-type-default-hint": "Obsługuje podstawowy transport MQTT, HTTP i CoAP", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Włącza zaawansowane ustawienia transportu MQTT", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "Włącza zaawansowane ustawienia transportu CoAP", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "Typ transportu LWM2M", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "Określ konfigurację transportu SNMP", + "transport-type-http": "HTTP", + "description": "Opis", + "default": "Domyślny", + "profile-configuration": "Konfiguracja profilu", + "transport-configuration": "Konfiguracja transportu", + "default-rule-chain": "Domyślny łańcuch reguł", + "default-edge-rule-chain": "Domyślny łańcuch reguł Edge", + "default-edge-rule-chain-hint": "Używany na Edge jako łańcuch reguł do przetwarzania danych przychodzących dla urządzeń o tym profilu urządzenia", + "mobile-dashboard": "Mobilny panel", + "mobile-dashboard-hint": "Używany przez aplikację mobilną jako panel ze szczegółami urządzenia", + "select-queue-hint": "Wybierz z listy rozwijanej.", + "delete-device-profile-title": "Czy na pewno chcesz usunąć profil urządzenia '{{deviceProfileName}}'?", + "delete-device-profile-text": "Zachowaj ostrożność, po potwierdzeniu profilu urządzenia i wszystkich powiązanych danych, w tym powiązanych aktualizacji OTA, nie będzie można odzyskać.", + "delete-device-profiles-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 profil urządzenia} other {# profili urządzeń} }?", + "delete-device-profiles-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile urządzeń zostaną usunięte, a wszystkie powiązane dane, w tym powiązane aktualizacje OTA, staną się niemożliwe do odzyskania.", + "set-default-device-profile-title": "Czy na pewno chcesz uczynić profil urządzenia '{{deviceProfileName}}' domyślnym?", + "set-default-device-profile-text": "Po potwierdzeniu profil urządzenia zostanie oznaczony jako domyślny i będzie używany w przypadku nowych urządzeń bez określonego profilu.", + "no-device-profiles-found": "Nie znaleziono profili urządzeń.", + "create-new-device-profile": "Utwórz nowy!", + "mqtt-device-topic-filters": "Filtry tematów urządzeń MQTT", + "mqtt-device-topic-filters-unique": "Filtry tematów urządzeń MQTT muszą być unikalne.", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Węzeł brzegowy sieci (EoN).", + "mqtt-device-topic-filters-spark-plug-hint": "Zezwalaj na połączenia z węzłów EoN z ładunkiem Sparkplug B i formatem tematu.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "Metryki SparkPlug do przechowywania jako atrybuty.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Nazwy metryk SparkPlug, które będą przechowywane jako atrybuty urządzenia. Wszystkie pozostałe metryki będą przechowywane jako telemetria urządzeń.", + "mqtt-device-payload-type": "Zawartość wiadomości urządzenia MQTT", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "Włącz zgodność z innymi formatami ładunku.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "Po włączeniu platforma domyślnie będzie używać formatu ładunku Protobuf. Jeśli parsowanie się nie powiedzie, platforma spróbuje użyć formatu ładunku JSON. Przydatne dla wstecznej kompatybilności podczas aktualizacji oprogramowania sprzętowego. Na przykład, początkowa wersja oprogramowania sprzętowego wykorzystuje Json, podczas gdy nowa wersja wykorzystuje Protobuf. Podczas procesu aktualizacji oprogramowania układowego dla floty urządzeń wymagana jest jednoczesna obsługa zarówno Protobuf, jak i JSON. Tryb zgodności wprowadza niewielki spadek wydajności, dlatego zaleca się wyłączenie tego trybu po zaktualizowaniu wszystkich urządzeń.", + "mqtt-use-json-format-for-default-downlink-topics": "Użyj formatu Json dla domyślnych tematów łącza w dół", + "mqtt-use-json-format-for-default-downlink-topics-hint": "Po włączeniu platforma będzie używać formatu ładunku Json do przekazywania atrybutów i RPC za pośrednictwem następujących tematów: v1/devices/me/attributes/response/$request_id, v1/urządzenia/ja/atrybuty, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. $id_żądania jest całkowitym identyfikatorem żądania.", + "mqtt-send-ack-on-validation-exception": "Wyślij PUBACK w przypadku niepowodzenia sprawdzania wiadomości PUBLISH", + "mqtt-send-ack-on-validation-exception-hint": "Domyślnie platforma zamknie sesję MQTT w przypadku niepowodzenia sprawdzania wiadomości. Po włączeniu tej opcji platforma wyśle potwierdzenie publikacji zamiast zamykać sesję.", + "snmp-add-mapping": "Dodaj mapowanie SNMP", + "snmp-mapping-not-configured": "Nie skonfigurowano mapowania OID na szeregi czasowe/telemetrię", + "snmp-timseries-or-attribute-name": "Seria czasowa/nazwa atrybutu do mapowania", + "snmp-timseries-or-attribute-type": "Typy szeregów czasowych/atrybutów do mapowania", + "snmp-method-pdu-type-get-request": "Pobierz żądanie", + "snmp-method-pdu-type-get-next-request": "Pobierz następne żądanie", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Typ ładunku jest wymagany.", + "coap-device-type": "Typ urządzenia CoAP", + "coap-device-payload-type": "Ładunek urządzenia CoAP", + "coap-device-type-required": "Typ urządzenia CoAP jest wymagany.", + "coap-device-type-default": "Domyślny", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "Symbole wieloznaczne pojedyńcze [+] i wielopoziomowe [#] są obsługiwane.", + "telemetry-topic-filter": "Filtr tematu telemetrii", + "telemetry-topic-filter-required": "Filtr tematu telemetrii jest wymagany.", + "attributes-topic-filter": "Filtr tematu publikacji atrybutów", + "attributes-subscribe-topic-filter": "Filtr tematu subskrybcji atrybutów", + "attributes-topic-filter-required": "Filtr tematu publikowania atrybutów jest wymagany.", + "attributes-subscribe-topic-filter-required": "Temat subskrypcji atrybutów jest wymagany", + "telemetry-proto-schema": "Protoschemat telemetrii", + "telemetry-proto-schema-required": "Protoschemat telemetrii jest wymagany.", + "attributes-proto-schema": "Atrybuty schematu proto", + "attributes-proto-schema-required": "Schemat proto atrybutów jest wymagany.", + "rpc-response-proto-schema": "Schemat proto odpowiedzi RPC", + "rpc-response-proto-schema-required": "Schemat proto odpowiedzi RPC jest wymagany.", + "rpc-response-topic-filter": "Filtr tematu odpowiedzi RPC", + "rpc-response-topic-filter-required": "Filtr tematu odpowiedzi RPC jest wymagany.", + "rpc-request-proto-schema": "Schemat proto żądania RPC", + "rpc-request-proto-schema-required": "Schemat proto żądania RPC jest wymagany.", + "rpc-request-proto-schema-hint": "Komunikat żądania RPC powinien zawsze zawierać pola: string method = 1; ", + "not-valid-pattern-topic-filter": "Nieprawidłowy filtr tematu wzorca", + "not-valid-single-character": "Nieprawidłowe użycie jednopoziomowego znaku wieloznacznego", + "not-valid-multi-character": "Nieprawidłowe użycie wielopoziomowego znaku wieloznacznego", + "single-level-wildcards-hint": "[+] odpowiada każdemu poziomowi filtra tematycznego. Np.: v1/devices/+/telemetry lub +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] zastępuje końcówkę filtru tematu (musi byc ostatnim symbolem). Mp.: # lub v1/devices/me/#.", + "alarm-rules": "Zasady alarmowania", + "alarm-rules-with-count": "Reguły alarmowe ({{count}})", + "no-alarm-rules": "Nie skonfigurowano żadnych reguł alarmowych", + "add-alarm-rule": "Dodaj regułę alarmową", + "edit-alarm-rule": "Edytuj regułę alarmu", + "alarm-type": "Typ alarmu", + "alarm-type-required": "Typ alarmu jest wymagany.", + "alarm-type-unique": "Typ alarmu musi być unikalny w ramach reguł alarmowych profilu urządzenia.", + "alarm-type-max-length": "Typ alarmu powinien być mniejszy niż 256", + "create-alarm-pattern": "Tworzyć {{alarmType}} alarm", + "create-alarm-rules": "Utwórz reguły alarmowe", + "no-create-alarm-rules": "Nie skonfigurowano warunków tworzenia", + "add-create-alarm-rule-prompt": "Dodaj regułę tworzenia alarmu", + "clear-alarm-rule": "Wyczyść regułę alarmową", + "no-clear-alarm-rule": "Nie skonfigurowano jasnego warunku", + "add-create-alarm-rule": "Dodaj warunek utworzenia", + "add-clear-alarm-rule": "Dodaj jasny warunek", + "select-alarm-severity": "Wybierz wagę alarmu", + "alarm-severity-required": "Waga alarmu jest wymagana.", + "condition-duration": "Czas trwania warunku", + "condition-duration-value": "Wartość czasu trwania", + "condition-duration-time-unit": "Jednostka czasu", + "condition-duration-value-range": "Wartość czasu trwania powinna mieścić się w zakresie od 1 do 2147483647.", + "condition-duration-value-pattern": "Wartość czasu trwania powinna być liczbami całkowitymi.", + "condition-duration-value-required": "Wartość czasu trwania jest wymagana.", + "condition-duration-time-unit-required": "Jednostka czasu jest wymagana.", + "advanced-settings": "Zaawansowane ustawienia", + "alarm-rule-details": "Szczegóły", + "alarm-rule-details-hint": "Wskazówka: użyj ${NazwaKlucza} w celu zastąpienia wartości atrybutu lub kluczy telemetrycznych używanych w warunku reguły alarmowej.", + "add-alarm-rule-details": "Dodaj szczegóły", + "alarm-rule-mobile-dashboard": "Mobilny panel", + "alarm-rule-mobile-dashboard-hint": "Używany przez aplikację mobilną jako panel ze szczegółami alarmów", + "alarm-rule-no-mobile-dashboard": "Nie wybrano żadnego panelu", + "propagate-alarm": "Propaguj alarm do powiązanych obiektów", + "alarm-rule-relation-types-list": "Typy relacji do propagowania", + "alarm-rule-relation-types-list-hint": "Jeśli nie wybrano opcji Propaguj typy relacji, alarmy będą propagowane bez filtrowania według typu relacji.", + "propagate-alarm-to-owner": "Prześlij alarm do właściciela obiektu (Klienta lub Najemcy)", + "propagate-alarm-to-owner-hierarchy": "Propaguj alarm w hierarchii właścicieli jednostek", + "propagate-alarm-to-tenant": "Przekaż alarm do Najemcy", + "alarm-details": "Szczegóły alarmu", + "alarm-rule-condition": "Warunek reguły alarmowej", + "enter-alarm-rule-condition-prompt": "Dodaj warunek reguły alarmowej", + "edit-alarm-rule-condition": "Edytuj warunek reguły alarmowej", + "device-provisioning": "Udostępnianie urządzeń", + "provision-strategy": "Strategia zaopatrzenia", + "provision-strategy-required": "Strategia świadczenia usług jest wymagana.", + "provision-strategy-disabled": "Wyłączona", + "provision-strategy-created-new": "Zezwalaj na tworzenie nowych urządzeń", + "provision-strategy-check-pre-provisioned": "Sprawdź, czy urządzenia są wstępnie obsługiwane", + "provision-device-key": "Udostępnij klucz urządzenia", + "provision-device-key-required": "Klucz urządzenia udostępniającego jest wymagany.", + "copy-provision-key": "Skopiuj klucz świadczenia", + "provision-key-copied-message": "Klucz udostępniania został skopiowany do schowka", + "provision-device-secret": "Udostępnij sekret urządzenia", + "provision-device-secret-required": "Klucz tajny urządzenia do udostępniania jest wymagany.", + "copy-provision-secret": "Skopiuj tajemnicę świadczenia", + "provision-secret-copied-message": "Sekret świadczenia został skopiowany do schowka", + "provision-strategy-x509": { + "certificate-chain": "Łańcuch certyfikatów X509", + "certificate-chain-hint": "Strategia certyfikatów X.509 służy do udostępniania urządzeniom certyfikatów klienta w dwukierunkowej komunikacji TLS.", + "allow-create-new-devices": "Twórz nowe urządzenia", + "allow-create-new-devices-hint": "Jeśli wybrane zostaną utworzone nowe urządzenia, a certyfikat klienta będzie używany jako dane uwierzytelniające urządzenia.", + "certificate-value": "Certyfikat w formacie PEM", + "certificate-value-required": "Certyfikat w formacie PEM jest wymagany", + "cn-regex-variable": "Zmienna wyrażenia regularnego CN", + "cn-regex-variable-required": "Zmienna wyrażenia regularnego CN jest wymagana", + "cn-regex-variable-hint": "Wymagane do pobrania nazwy urządzenia z nazwy zwyczajowej certyfikatu X509 urządzenia." }, - "condition":"Warunek", - "condition-type":"Typ warunku", - "condition-type-simple":"Prosty", - "condition-type-duration":"Czas trwania", - "condition-during":"Podczas {{during}}", - "condition-during-dynamic":"Podczas \"{{attribute}}\" ({{during}})", - "condition-type-repeating":"Powtarzający się", - "condition-type-required":"Wymagany jest typ warunku.", - "condition-repeating-value":"Liczba zdarzeń", - "condition-repeating-value-range":"Liczba zdarzeń powinna mieścić się w zakresie od 1 do 2147483647.", - "condition-repeating-value-pattern":"Liczba zdarzeń powinna być liczbą całkowitą.", - "condition-repeating-value-required":"Wymagana jest liczba zdarzeń.", - "condition-repeat-times":"Powtarza się { count, plural, =1 {1 raz} other {# razy} }", - "condition-repeat-times-dynamic":"Powtarza się \"{attribute}\" ({ count, plural, =1 {1 raz} other {# razy} })", - "schedule-type":"Typ harmonogramu", - "schedule-type-required":"Wymagany jest typ harmonogramu.", - "schedule":"Harmonogram", - "edit-schedule":"Edytuj harmonogram alarmu", - "schedule-any-time":"Aktywny przez cały czas", - "schedule-specific-time":"Aktywny o określonej godzinie", - "schedule-custom":"Niestandardowy", - "schedule-day":{ - "monday":"Poniedziałek", - "tuesday":"Wtorek", - "wednesday":"Środa", - "thursday":"Czwartek", - "friday":"Piątek", - "saturday":"Sobota", - "sunday":"Niedziela" + "condition": "Stan", + "condition-type": "Typ warunku", + "condition-type-simple": "Prosty", + "condition-type-duration": "Czas trwania", + "condition-during": "Podczas {{during}}", + "condition-during-dynamic": "Podczas \"{{ attribute }}\" ({{during}})", + "condition-type-repeating": "Powtórzenie", + "condition-type-required": "Typ warunku jest wymagany.", + "condition-repeating-value": "Liczba wydarzeń", + "condition-repeating-value-range": "Liczba zdarzeń powinna mieścić się w zakresie od 1 do 2147483647.", + "condition-repeating-value-pattern": "Liczba zdarzeń powinna być liczbami całkowitymi.", + "condition-repeating-value-required": "Liczba zdarzeń jest wymagana.", + "condition-repeat-times": "Powtarza się {count, plural, =1 {1 raz} other {# razy} }", + "condition-repeat-times-dynamic": "Powtarza „{ atrybut }” ({ count, plural, =1 {1 raz} other {# razy} })", + "schedule-type": "Typ harmonogramu", + "schedule-type-required": "Typ harmonogramu jest wymagany.", + "schedule": "Harmonogram", + "edit-schedule": "Edytuj harmonogram alarmów", + "schedule-any-time": "Cały czas aktywny", + "schedule-specific-time": "Aktywny w określonym czasie", + "schedule-custom": "Własny", + "schedule-day": { + "monday": "Poniedziałek", + "tuesday": "Wtorek", + "wednesday": "Środa", + "thursday": "Czwartek", + "friday": "Piątek", + "saturday": "Sobota", + "sunday": "Niedziela" }, - "schedule-days":"Dni", - "schedule-time":"Czas", - "schedule-time-from":"Od", - "schedule-time-to":"Do", - "schedule-days-of-week-required":"Należy wybrać przynajmniej jeden dzień tygodnia.", - "create-device-profile":"Utwórz nowy profil urządzenia", - "import":"Importuj profil urządzenia", - "export":"Eksportuj profil urządzenia", - "export-failed-error":"Nie można wyeksportować profilu urządzenia: {{error}}", - "device-profile-file":"Plik profilu urządzenia", - "invalid-device-profile-file-error":"Nie można zaimportować profilu urządzenia: Nieprawidłowa struktura danych profilu urządzenia.", - "power-saving-mode":"Tryb oszczędzania energii", - "power-saving-mode-type":{ - "default":"Użyj trybu oszczędzania energii profilu urządzenia", - "psm":"Tryb oszczędzania energii (PSM)", - "drx":"Przerwane odbieranie (DRX)", - "edrx":"Rozszerzone przerwane odbieranie (eDRX)" + "schedule-days": "Dni", + "schedule-time": "Czas", + "schedule-time-from": "Z", + "schedule-time-to": "Do", + "schedule-days-of-week-required": "Należy wybrać przynajmniej jeden dzień tygodnia.", + "create-device-profile": "Utwórz nowy profil urządzenia", + "import": "Importuj profil urządzenia", + "export": "Eksportuj profil urządzenia", + "export-failed-error": "Nie można wyeksportować profilu urządzenia: {{error}}", + "device-profile-file": "Plik profilu urządzenia", + "invalid-device-profile-file-error": "Nie można zaimportować profilu urządzenia: nieprawidłowa struktura danych profilu urządzenia.", + "power-saving-mode": "Tryb oszczędzania energii", + "power-saving-mode-type": { + "default": "Użyj trybu oszczędzania energii w profilu urządzenia", + "psm": "Tryb oszczędzania energii (PSM)", + "drx": "Odbiór nieciągły (DRX)", + "edrx": "Rozszerzony odbiór nieciągły (eDRX)" }, - "edrx-cycle":"Cykl eDRX", - "edrx-cycle-required":"Cykl eDRX jest wymagany.", - "edrx-cycle-pattern":"Cykl eDRX musi być liczbą całkowitą dodatnią.", - "edrx-cycle-min":"Minimalna liczba cykli eDRX to {{ min }} sekundy.", - "paging-transmission-window":"Okno transmisji strony", - "paging-transmission-window-required":"Okno transmisji strony jest wymagane.", - "paging-transmission-window-pattern":"Okno transmisji strony musi być liczbą całkowitą dodatnią.", - "paging-transmission-window-min":"Minimalna liczba okna transmisji strony to {{ min }} sekundy.", - "psm-activity-timer":"Licznik aktywności PSM", - "psm-activity-timer-required":"Licznik aktywności PSM jest wymagany.", - "psm-activity-timer-pattern":"Licznik aktywności PSM musi być liczbą całkowitą dodatnią.", - "psm-activity-timer-min":"Minimalna liczba licznika aktywności PSM to {{ min }} sekundy.", - "lwm2m":{ - "object-list":"Lista obiektów", - "object-list-empty":"Brak wybranych obiektów.", - "no-objects-found":"Nie znaleziono obiektów.", - "no-objects-matching":"Nie znaleziono obiektów pasujących do '{{object}}'.", - "model-tab":"Model LWM2M", - "add-new-instances":"Dodaj nowe instancje", - "instances-list":"Lista instancji", - "instances-list-required":"Lista instancji jest wymagana.", - "instance-id-pattern":"Identyfikator instancji musi być liczbą całkowitą dodatnią.", - "instance-id-max":"Maksymalna wartość identyfikatora instancji to {{max}}.", - "instance":"Instancja", - "resource-label":"#ID Nazwa zasobu", - "observe-label":"Obserwuj", - "attribute-label":"Atrybut", - "telemetry-label":"Telemetria", - "edit-observe-select":"Aby edytować obserwację, wybierz telemetrię lub atrybut", - "edit-attributes-select":"Aby edytować atrybuty, wybierz telemetrię lub atrybut", - "no-attributes-set":"Brak ustawionych atrybutów", - "key-name":"Nazwa klucza", - "key-name-required":"Nazwa klucza jest wymagana", - "attribute-name":"Nazwa atrybutu", - "attribute-name-required":"Nazwa atrybutu jest wymagana.", - "attribute-value":"Wartość atrybutu", - "attribute-value-required":"Wartość atrybutu jest wymagana.", - "attribute-value-pattern":"Wartość atrybutu musi być liczbą całkowitą dodatnią.", - "edit-attributes":"Edytuj atrybuty: {{ name }}", - "view-attributes":"Wyświetl atrybuty: {{ name }}", - "add-attribute":"Dodaj atrybut", - "edit-attribute":"Edytuj atrybut", - "view-attribute":"Wyświetl atrybut", - "remove-attribute":"Usuń atrybut", - "delete-server-text":"Bądź ostrożny, po potwierdzeniu konfiguracja serwera stanie się nieodwracalna.", - "delete-server-title":"Czy na pewno chcesz usunąć serwer?", - "mode":"Tryb konfiguracji zabezpieczeń", - "bootstrap-tab":"Bootstrap", - "bootstrap-server-legend":"Serwer Bootstrap (ShortId...)", - "lwm2m-server-legend":"Serwer LwM2M (ShortId...)", - "server":"Serwer", - "short-id":"Krótki identyfikator serwera", - "short-id-tooltip":"Krótki identyfikator serwera. Używany jako łącze do powiązania instancji obiektu serwera.\nTen identyfikator jednoznacznie identyfikuje każdy serwer LwM2M skonfigurowany dla klienta LwM2M.\nZasób MUSI być ustawiony, gdy Zasób Bootstrap-Server ma wartość „false”.\nWartości ID:0 i ID:65535 NIE MOGĄ być używane do identyfikacji serwera LwM2M.", - "short-id-required":"Krótki identyfikator serwera jest wymagany.", - "short-id-range":"Krótki identyfikator serwera powinien mieć wartość z zakresu od 1 do 65534.", - "short-id-pattern":"Krótki identyfikator serwera musi być liczbą całkowitą dodatnią.", - "lifetime":"Czas życia rejestracji klienta", - "lifetime-required":"Czas życia rejestracji klienta jest wymagany.", - "lifetime-pattern":"Czas życia rejestracji klienta musi być liczbą całkowitą dodatnią.", - "default-min-period":"Minimalny okres między dwoma powiadomieniami (s)", - "default-min-period-tooltip":"Domyślna wartość, jaką klient LwM2M powinien używać dla Minimalnego Okresu Obserwacji w przypadku braku uwzględnienia tego parametru w Obserwacji.", - "default-min-period-required":"Minimalny okres jest wymagany.", - "default-min-period-pattern":"Minimalny okres musi być liczbą całkowitą dodatnią.", - "notification-storing":"Przechowywanie powiadomień przy wyłączonym lub offline", - "binding":"Powiązanie", - "binding-type":{ - "u":"U: Klient jest dostępny za pośrednictwem powiązania UDP w dowolnym czasie.", - "m":"M: Klient jest dostępny za pośrednictwem powiązania MQTT w dowolnym czasie.", - "h":"H: Klient jest dostępny za pośrednictwem powiązania HTTP w dowolnym czasie.", - "t":"T: Klient jest dostępny za pośrednictwem powiązania TCP w dowolnym czasie.", - "s":"S: Klient jest dostępny za pośrednictwem powiązania SMS w dowolnym czasie.", - "n":"N: Klient MUSI wysłać odpowiedź na takie żądanie za pośrednictwem powiązania Non-IP (jest obsługiwane od LWM2M 1.1).", - "uq":"UQ: Połączenie UDP w trybie kolejki (nie jest obsługiwane od LWM2M 1.1)", - "uqs":"UQS: zarówno połączenia UDP, jak i SMS aktywne; UDP w trybie kolejki, SMS w trybie standardowym (nie jest obsługiwane od LWM2M 1.1)", - "tq":"TQ: Połączenie TCP w trybie kolejki (nie jest obsługiwane od LWM2M 1.1)", - "tqs":"TQS: zarówno połączenia TCP, jak i SMS aktywne; TCP w trybie kolejki, SMS w trybie standardowym (nie jest obsługiwane od LWM2M 1.1)", - "sq":"SQ: Połączenie SMS w trybie kolejki (nie jest obsługiwane od LWM2M 1.1)" - }, - "binding-tooltip":"To jest lista w zasobie \"binding\" obiektu serwera LwM2M - /1/x/7. Wskazuje obsługiwane tryby powiązań w kliencie LwM2M. Ta wartość POWINNA być taka sama jak wartość w zasobie \"Supported Binding and Modes\" w obiekcie urządzenia (/3/0/16). Chociaż obsługiwane są różne środki transportu, tylko jedno powiązanie transportowe może być używane podczas całej sesji transportowej. Na przykład, gdy obsługiwane są zarówno UDP, jak i SMS, klient LwM2M i serwer LwM2M mogą wybrać komunikację zarówno przez UDP, jak i SMS przez całą sesję transportową.", - "bootstrap-server":"Serwer rozruchowy", - "lwm2m-server":"Serwer LwM2M", - "include-bootstrap-server":"Załącz aktualizacje serwera rozruchowego", - "bootstrap-update-title":"Masz już skonfigurowany serwer rozruchowy. Czy na pewno chcesz wyłączyć aktualizacje?", - "bootstrap-update-text":"Bądź ostrożny, po potwierdzeniu konfiguracja danych serwera rozruchowego stanie się nieodwracalna.", - "server-host":"Host", - "server-host-required":"Host jest wymagany.", - "server-port":"Port", - "server-port-required":"Port jest wymagany.", - "server-port-pattern":"Port musi być liczbą całkowitą dodatnią.", - "server-port-range":"Port powinien być w zakresie od 1 do 65535.", - "server-public-key":"Klucz publiczny serwera", - "server-public-key-required":"Klucz publiczny serwera jest wymagany.", - "client-hold-off-time":"Czas wstrzymania klienta", - "client-hold-off-time-required":"Czas wstrzymania klienta jest wymagany.", - "client-hold-off-time-pattern":"Czas wstrzymania klienta musi być liczbą całkowitą dodatnią.", - "client-hold-off-time-tooltip":"Czas wstrzymania klienta do użycia tylko z serwerem rozruchowym", - "account-after-timeout":"Konto po przekroczeniu czasu oczekiwania", - "account-after-timeout-required":"Konto po przekroczeniu czasu oczekiwania jest wymagane.", - "account-after-timeout-pattern":"Konto po przekroczeniu czasu oczekiwania musi być liczbą całkowitą dodatnią.", - "account-after-timeout-tooltip":"Wartość konta po przekroczeniu czasu oczekiwania podana przez ten zasób serwera rozruchowego.", - "server-type":"Typ serwera", - "add-new-server-title":"Dodaj nową konfigurację serwera", - "add-server-config":"Dodaj konfigurację serwera", - "add-lwm2m-server-config":"Dodaj serwer LwM2M", - "no-config-servers":"Brak skonfigurowanych serwerów", - "others-tab":"Inne ustawienia", - "client-strategy":"Strategia klienta podczas łączenia", - "client-strategy-label":"Strategia", - "client-strategy-only-observe":"Tylko żądanie obserwacji do klienta po połączeniu początkowym", - "client-strategy-read-all":"Odczytaj wszystkie zasoby i żądanie obserwacji do klienta po rejestracji", - "fw-update":"Aktualizacja oprogramowania", - "fw-update-strategy":"Strategia aktualizacji firmware'u", - "fw-update-strategy-data":"Przesuń aktualizację firmware'u jako plik binarny za pomocą Obiektu 19 i Zasobu 0 (Dane)", - "fw-update-strategy-package":"Przesuń aktualizację firmware'u jako plik binarny za pomocą Obiektu 5 i Zasobu 0 (Pakiet)", - "fw-update-strategy-package-uri":"Automatycznie generuj unikalny adres URL CoAP do pobrania pakietu i przesyłaj aktualizację firmware'u jako Obiekt 5 i Zasób 1 (Pakiet URI)", - "sw-update":"Aktualizacja oprogramowania", - "sw-update-strategy":"Strategia aktualizacji oprogramowania", - "sw-update-strategy-package":"Przesuń plik binarny za pomocą Obiektu 9 i Zasobu 2 (Pakiet)", - "sw-update-strategy-package-uri":"Automatycznie generuj unikalny adres URL CoAP do pobrania pakietu i przesyłaj aktualizację oprogramowania za pomocą Obiektu 9 i Zasób 3 (Pakiet URI)", - "fw-update-resource":"Zasób CoAP aktualizacji firmware'u", - "fw-update-resource-required":"Zasób CoAP aktualizacji firmware'u jest wymagany.", - "sw-update-resource":"Zasób CoAP aktualizacji oprogramowania", - "sw-update-resource-required":"Zasób CoAP aktualizacji oprogramowania jest wymagany.", - "config-json-tab":"Profil konfiguracji urządzenia Json", - "attributes-name":{ - "min-period":"Minimalny okres", - "max-period":"Maksymalny okres", - "greater-than":"Większe niż", - "less-than":"Mniejsze niż", - "step":"Krok", - "min-evaluation-period":"Minimalny okres oceny", - "max-evaluation-period":"Maksymalny okres oceny" + "edrx-cycle": "Cykl eDRX", + "edrx-cycle-required": "Cykl eDRX jest wymagany.", + "edrx-cycle-pattern": "Cykl eDRX musi być dodatnią liczbą całkowitą.", + "edrx-cycle-min": "Minimalna liczba cykli eDRX wynosi {{ min }} sekundy.", + "paging-transmission-window": "Okno transmisji stronicowania", + "paging-transmission-window-required": "Okno transmisji stronicowania jest wymagane.", + "paging-transmission-window-pattern": "Okno transmisji stronicowania musi być dodatnią liczbą całkowitą.", + "paging-transmission-window-min": "Minimalna liczba okien transmisji przywoławczej wynosi {{ min }} sekundy.", + "psm-activity-timer": "Licznik aktywności PSM", + "psm-activity-timer-required": "Licznik czasu aktywności PSM jest wymagany.", + "psm-activity-timer-pattern": "Licznik czasu aktywności PSM musi być dodatnią liczbą całkowitą.", + "psm-activity-timer-min": "Minimalna liczba timerów aktywności PSM wynosi {{ min }} sekundy.", + "lwm2m": { + "object-list": "Lista obiektów", + "object-list-empty": "Nie wybrano żadnych obiektów.", + "no-objects-found": "Nie znaleziono żadnych obiektów.", + "no-objects-matching": "Nie znaleziono obiektów pasujących do '{{object}}'", + "model-tab": "Model LWM2M", + "add-new-instances": "Dodaj nowe instancje", + "instances-list": "Lista instancji", + "instances-list-required": "Lista instancji jest wymagana.", + "instance-id-pattern": "Identyfikator instancji musi być dodatnią liczbą całkowitą.", + "instance-id-max": "Maksymalna wartość identyfikatora instancji {{max}}.", + "instance": "Instancja", + "resource-label": "Nazwa biblioteki #ID", + "observe-label": "Obserwuj", + "attribute-label": "Atrybut", + "telemetry-label": "Telemetria", + "edit-observe-select": "Aby edytować obserwację, wybierz telemetrię lub atrybut", + "edit-attributes-select": "Aby edytować atrybuty, wybierz telemetrię lub atrybut", + "no-attributes-set": "Nie ustawiono żadnych atrybutów", + "key-name": "Nazwa klucza", + "key-name-required": "Nazwa klucza jest wymagana", + "attribute-name": "Atrybut nazwy", + "attribute-name-required": "Atrybut nazwy jest wymagany.", + "attribute-value": "Wartość atrybutu", + "attribute-value-required": "Wartość atrybutu jest wymagana.", + "attribute-value-pattern": "Wartość atrybutu musi być dodatnią liczbą całkowitą.", + "edit-attributes": "Edytuj atrybuty: {{ name }}", + "view-attributes": "Zobacz atrybuty: {{ name }}", + "add-attribute": "Dodaj atrybut", + "edit-attribute": "Edytuj atrybut", + "view-attribute": "Zobacz atrybut", + "remove-attribute": "Usuń atrybut", + "delete-server-text": "Bądź ostrożny, po potwierdzeniu konfiguracja serwera stanie się niemożliwa do przywrócenia.", + "delete-server-title": "Czy na pewno chcesz usunąć serwer?", + "mode": "Tryb konfiguracji zabezpieczeń", + "bootstrap-tab": "Bootstrap", + "bootstrap-server-legend": "Serwer startowy (ShortId...)", + "lwm2m-server-legend": "Serwer LwM2M (ShortId...)", + "server": "serwer", + "short-id": "Krótki identyfikator serwera", + "short-id-tooltip": "Krótki identyfikator serwera. ", + "short-id-required": "Krótki identyfikator serwera jest wymagany.", + "short-id-range": "Krótki identyfikator serwera powinien mieścić się w zakresie od 1 do 65534.", + "short-id-pattern": "Krótki identyfikator serwera musi być dodatnią liczbą całkowitą.", + "lifetime": "Okres rejestracji klienta", + "lifetime-required": "Okres rejestracji klienta jest wymagany.", + "lifetime-pattern": "Okres istnienia rejestracji klienta musi być dodatnią liczbą całkowitą.", + "default-min-period": "Minimalny okres między dwoma powiadomieniami", + "default-min-period-tooltip": "Wartość domyślna, którą Klient LwM2M powinien zastosować dla Minimalnego Okresu Obserwacji w przypadku braku tego parametru w Obserwacji.", + "default-min-period-required": "Minimalny okres jest wymagany.", + "default-min-period-pattern": "Minimalny okres musi być dodatnią liczbą całkowitą.", + "notification-storing": "Przechowywanie powiadomień, gdy jest wyłączone lub offline", + "binding": "Wiążący", + "binding-type": { + "u": "U: Klient jest dostępny w dowolnym momencie poprzez powiązanie UDP.", + "m": "M: Klient jest dostępny w dowolnym momencie poprzez powiązanie MQTT.", + "h": "H: Klient jest dostępny w dowolnym momencie poprzez powiązanie HTTP.", + "t": "T: Klient jest dostępny w dowolnym momencie poprzez powiązanie TCP.", + "s": "S: Klient jest dostępny w każdej chwili za pośrednictwem wiadomości SMS.", + "n": "N: Klient MUSI wysłać odpowiedź na takie żądanie poprzez powiązanie inne niż IP (jest obsługiwane od wersji LWM2M 1.1).", + "uq": "UQ: połączenie UDP w trybie kolejki (nie jest obsługiwane od wersji LWM2M 1.1)", + "uqs": "UQS: aktywne połączenia UDP i SMS; ", + "tq": "TQ: połączenie TCP w trybie kolejki (nie jest obsługiwane od wersji LWM2M 1.1)", + "tqs": "TQS: aktywne połączenia TCP i SMS; ", + "sq": "SQ: połączenie SMS w trybie kolejki (nie jest obsługiwane od wersji LWM2M 1.1)" }, - "default-object-id": "Domyślna wersja obiektu (atrybut)", - "default-object-id-ver": { - "v1-0": "\"1.0\" to wartość wersji obiektu, jeśli wersja obiektu jest pusta.", - "v1-1": "\"1.1\" to wartość wersji obiektu, jeśli wersja obiektu jest pusta." + "binding-tooltip": "To jest lista w zasobie „wiążącym” obiektu serwera LwM2M - /1/x/7.\n", + "bootstrap-server": "Serwer startowy", + "lwm2m-server": "Serwer LwM2M", + "include-bootstrap-server": "Dołącz aktualizacje serwera Bootstrap", + "bootstrap-update-title": "Skonfigurowałeś już serwer Bootstrap. ", + "bootstrap-update-text": "Bądź ostrożny, po potwierdzeniu danych konfiguracyjnych serwera Bootstrap nie będzie można odzyskać.", + "server-host": "Host", + "server-host-required": "Host jest wymagany.", + "server-port": "Port", + "server-port-required": "Port jest wymagany.", + "server-port-pattern": "Port musi być dodatnią liczbą całkowitą.", + "server-port-range": "Port powinien należeć do zakresu od 1 do 65535.", + "server-public-key": "Klucz publiczny serwera", + "server-public-key-required": "Klucz publiczny serwera jest wymagany.", + "client-hold-off-time": "Wstrzymaj czas", + "client-hold-off-time-required": "Czas wstrzymania jest wymagany.", + "client-hold-off-time-pattern": "Czas wstrzymania musi być dodatnią liczbą całkowitą.", + "client-hold-off-time-tooltip": "Czas wstrzymania klienta do użytku wyłącznie z serwerem Bootstrap", + "account-after-timeout": "Account after the timeout", + "account-after-timeout-required": "Account after the timeout jest wymagane.", + "account-after-timeout-pattern": "Account after the timeout musi być dodatnią liczbą całkowitą.", + "account-after-timeout-tooltip": "Bootstrap-Server Account after the timeout value given by this resource.", + "server-type": "Rodzaj serwera", + "add-new-server-title": "Dodaj nową konfigurację serwera", + "add-server-config": "Dodaj konfigurację serwera", + "add-lwm2m-server-config": "Dodaj serwer LwM2M", + "no-config-servers": "Nie skonfigurowano żadnych serwerów", + "others-tab": "Inne ustawienia", + "client-strategy": "Strategia klienta podczas łączenia", + "client-strategy-label": "Strategia", + "client-strategy-only-observe": "Obserwuj żądanie skierowane do klienta dopiero po pierwszym połączeniu", + "client-strategy-read-all": "Przeczytaj wszystkie zasoby", + "fw-update": "Aktualizacja firmware", + "fw-update-strategy": "Strategia aktualizacji oprogramowania sprzętowego", + "fw-update-strategy-data": "Wypchnij aktualizację oprogramowania sprzętowego jako plik binarny przy użyciu obiektu 19 i zasobu 0 (dane)", + "fw-update-strategy-package": "Wypchnij aktualizację oprogramowania sprzętowego jako plik binarny przy użyciu obiektu 5 i zasobu 0 (pakiet)", + "fw-update-strategy-package-uri": "Automatycznie wygeneruj unikalny adres URL CoAP, aby pobrać pakiet i przesłać aktualizację oprogramowania sprzętowego jako obiekt 5 i zasób 1 (identyfikator URI pakietu)", + "sw-update": "Aktualizacja oprogramowania", + "sw-update-strategy": "Strategia aktualizacji oprogramowania", + "sw-update-strategy-package": "Wypchnij plik binarny przy użyciu Obiektu 9 i Zasobu 2 (pakiet)", + "sw-update-strategy-package-uri": "Automatycznie wygeneruj unikalny adres URL CoAP, aby pobrać pakiet i przesłać aktualizację oprogramowania przy użyciu Obiektu 9 i Zasobu 3 (URI pakietu)", + "fw-update-resource": "Zasób aktualizacji oprogramowania sprzętowego CoAP", + "fw-update-resource-required": "Zasób aktualizacji oprogramowania sprzętowego CoAP jest wymagany.", + "sw-update-resource": "Zasób aktualizacji oprogramowania CoAP", + "sw-update-resource-required": "Zasób aktualizacji oprogramowania CoAP jest wymagany.", + "config-json-tab": "Urządzenie z profilem konfiguracji Json", + "attributes-name": { + "min-period": "Minimalny okres", + "max-period": "Maksymalny okres", + "greater-than": "Lepszy niż", + "less-than": "Mniej niż", + "step": "Krok", + "min-evaluation-period": "Minimalny okres oceny", + "max-evaluation-period": "Maksymalny okres oceny" } }, - "snmp":{ - "add-communication-config":"Dodaj konfigurację komunikacji", - "add-mapping":"Dodaj mapowanie", - "authentication-passphrase":"Hasło uwierzytelniania", - "authentication-passphrase-required":"Wymagane hasło uwierzytelniania.", - "authentication-protocol":"Protokół uwierzytelniania", - "authentication-protocol-required":"Wymagany protokół uwierzytelniania.", - "communication-configs":"Konfiguracje komunikacji", - "community":"Ciąg społeczności", - "community-required":"Wymagany ciąg społeczności.", - "context-name":"Nazwa kontekstu", - "data-key":"Klucz danych", - "data-key-required":"Wymagany klucz danych.", - "data-type":"Typ danych", - "data-type-required":"Wymagany typ danych.", - "engine-id":"ID silnika", - "host":"Host", - "host-required":"Wymagany host.", - "oid":"OID", - "oid-pattern":"Nieprawidłowy format OID.", - "oid-required":"Wymagany OID.", - "please-add-communication-config":"Proszę dodać konfigurację komunikacji", - "please-add-mapping-config":"Proszę dodać konfigurację mapowania", - "port":"Port", - "port-format":"Nieprawidłowy format portu.", - "port-required":"Wymagany port.", - "privacy-passphrase":"Hasło prywatności", - "privacy-passphrase-required":"Wymagane hasło prywatności.", - "privacy-protocol":"Protokół prywatności", - "privacy-protocol-required":"Wymagany protokół prywatności.", - "protocol-version":"Wersja protokołu", - "protocol-version-required":"Wymagana wersja protokołu.", - "querying-frequency":"Częstotliwość zapytań, ms", - "querying-frequency-invalid-format":"Częstotliwość zapytań musi być liczbą całkowitą dodatnią.", - "querying-frequency-required":"Wymagana częstotliwość zapytań.", - "retries":"Ponowienia", - "retries-invalid-format":"Liczba ponowień musi być liczbą całkowitą dodatnią.", - "retries-required":"Wymagane ponowienia.", - "scope":"Zakres", - "scope-required":"Wymagany zakres.", - "security-name":"Nazwa zabezpieczeń", - "security-name-required":"Wymagana nazwa zabezpieczeń.", - "timeout-ms":"Limit czasu, ms", - "timeout-ms-invalid-format":"Limit czasu musi być liczbą całkowitą dodatnią.", - "timeout-ms-required":"Wymagany limit czasu.", - "user-name":"Nazwa użytkownika", - "user-name-required":"Wymagana nazwa użytkownika." - } + "snmp": { + "add-communication-config": "Dodaj konfigurację komunikacji", + "add-mapping": "Dodaj mapowanie", + "authentication-passphrase": "Hasło uwierzytelniające", + "authentication-passphrase-required": "Hasło uwierzytelniające jest wymagane.", + "authentication-protocol": "Protokół uwierzytelnienia", + "authentication-protocol-required": "Protokół uwierzytelniania jest wymagany.", + "communication-configs": "Konfiguracje komunikacji", + "community": "Community string", + "community-required": "Community string jest wymagane.", + "context-name": "Nazwa kontekstu", + "data-key": "Klucz danych", + "data-key-required": "Klucz danych jest wymagany.", + "data-type": "Typ danych", + "data-type-required": "Typ danych jest wymagany.", + "engine-id": "Identyfikator silnika", + "host": "Gospodarz", + "host-required": "Host jest wymagany.", + "oid": "OID", + "oid-pattern": "Nieprawidłowy format OID.", + "oid-required": "OID jest wymagany.", + "please-add-communication-config": "Proszę dodać konfigurację komunikacji", + "please-add-mapping-config": "Proszę dodać konfigurację mapowania", + "port": "Port", + "port-format": "Nieprawidłowy format portu.", + "port-required": "Port jest wymagany.", + "privacy-passphrase": "Hasło prywatności", + "privacy-passphrase-required": "Hasło prywatności jest wymagane.", + "privacy-protocol": "Protokół prywatności", + "privacy-protocol-required": "Protokół prywatności jest wymagany.", + "protocol-version": "Wersja protokołu", + "protocol-version-required": "Wersja protokołu jest wymagana.", + "querying-frequency": "Częstotliwość zapytań, ms", + "querying-frequency-invalid-format": "Częstotliwość zapytań musi być dodatnią liczbą całkowitą.", + "querying-frequency-required": "Częstotliwość zapytań jest wymagana.", + "retries": "Liczba ponownych prób", + "retries-invalid-format": "Liczba ponownych prób musi być dodatnią liczbą całkowitą.", + "retries-required": "Liczba ponownych prób jest wymagana.", + "scope": "Zakres", + "scope-required": "Zakres jest wymagany.", + "security-name": "Nazwa bezpieczeństwa", + "security-name-required": "Nazwa bezpieczeństwa jest wymagana.", + "timeout-ms": "Limit czasu, ms", + "timeout-ms-invalid-format": "Limit czasu musi być dodatnią liczbą całkowitą.", + "timeout-ms-required": "Limit czasu jest wymagany.", + "user-name": "Nazwa użytkownika", + "user-name-required": "Nazwa użytkownika jest wymagana." + }, + "new-device-profile-name": "Nazwa profilu urządzenia", + "new-device-profile-name-required": "Nazwa profilu urządzenia jest wymagana." + }, + "dialog": { + "close": "Zamknij okno dialogowe", + "error-message-title": "Komunikat o błędzie:", + "error-details-title": "Szczegóły błędu" }, - "dialog":{ - "close":"Zamknij okno dialogowe", - "error-message-title":"Wiadomość o błędzie:", - "error-details-title":"Szczegóły błędu" + "direction": { + "column": "Kolumna", + "row": "Wiersz" }, - "direction":{ - "column":"Kolumna", - "row":"Wiersz" + "edge": { + "all": "Wszystko", + "all-edges": "Wszystkie Edge", + "groups": "Grupy", + "shared": "Wspólny", + "edge": "Edge", + "edge-instances": "Instancje Edge", + "instances": "Instancje", + "edge-file": "Plik Edge", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "label-max-length": "Etykieta powinna być mniejsza niż 256", + "type-max-length": "Typ powinien być mniejszy niż 256", + "management": "Zarządzanie Edge", + "no-edges-matching": "Nie znaleziono Edge pasujących do '{{entity}}'.", + "add": "Dodaj Edge", + "no-edges-text": "Nie znaleziono Edge", + "edge-details": "Szczegóły Edge", + "add-edge-text": "Dodaj nowy Edge", + "delete": "Usuń Edge", + "delete-edge-title": "Czy na pewno chcesz usunąć Edge '{{edgeName}}'?", + "delete-edge-text": "Bądź ostrożny, po potwierdzeniu Edge i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-edges-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 Edge} other {# Edge} }?", + "delete-edges-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane Edgei zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "name": "Nazwa", + "name-starts-with": "Nazwa Edge zaczyna się od", + "name-required": "Nazwa jest wymagana.", + "edge-license-key": "Klucz licencyjny Edge", + "edge-license-key-required": "Klucz licencyjny Edge jest wymagany.", + "edge-license-key-max-length": "Klucz licencyjny Edge powinien być mniejszy niż 31", + "edge-license-key-hint": "WAŻNA UWAGA: domyślny klucz licencyjny Edge udostępniany wyłącznie w celach testowych.
Ten klucz jest aktywny tylko dla 30 dni po aktywacji. Funkcja White-labeling jest wyłączona.
Aby uzyskać klucz licencyjny, przejdź do strona z cenami i wybierz najlepszą opcję licencji dla swojego przypadku.", + "cloud-endpoint": "Punkt końcowy w chmurze", + "cloud-endpoint-required": "Punkt końcowy w chmurze jest wymagany.", + "cloud-endpoint-max-length": "Punkt końcowy chmury powinien być mniejszy niż 256", + "cloud-endpoint-hint": "Edge wymaga dostępu HTTP(s) do Cloud (ThingsBoard PE) w celu zweryfikowania klucza licencyjnego. Podaj adres URL chmury, z którą Edge może się połączyć.", + "description": "Opis", + "details": "Szczegóły", + "events": "Wydarzenia", + "copy-id": "Skopiuj identyfikator Edge", + "id-copied-message": "Identyfikator Edge został skopiowany do schowka", + "sync": "Synchronizuj Edge", + "edge-required": "Wymagany Edge", + "edge-type": "Typ Edge", + "edge-type-required": "Typ Edge jest wymagany.", + "event-action": "Akcja eventowa", + "entity-id": "Identyfikator obiektu", + "select-edge-type": "Wybierz typ Edge", + "assign-to-customer": "Przypisz do klienta", + "assign-to-customer-text": "Proszę wybrać klienta do przypisania Edge", + "assign-edge-to-customer": "Przypisz Edge do klienta", + "assign-edge-to-customer-text": "Prosimy o wybranie Edge do przypisania klientowi", + "assignedToCustomer": "Przypisany do klienta", + "edge-public": "Edge jest publiczny", + "assigned-to-customer": "Przypisane do: {{customerTitle}}", + "unassign-from-customer": "Usuń przypisanie od klienta", + "unassign-edge-title": "Czy na pewno chcesz usunąć przypisanie Edge '{{edgeName}}'?", + "unassign-edge-text": "Po potwierdzeniu Edge zostanie anulowana i nie będzie dostępna dla klienta.", + "unassign-edges-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 Edge} other {# Edge} }?", + "unassign-edges-text": "Po potwierdzeniu wszystkie wybrane Edge zostaną usunięte i nie będą dostępne dla klienta.", + "make-public": "Upublicznij Edge", + "make-public-edge-title": "Czy na pewno chcesz uczynić Edge {{edgeName}} publiczną?", + "make-public-edge-text": "Po potwierdzeniu urządzenie Edge i wszystkie jego dane zostaną upublicznione i dostępne dla innych.", + "make-private": "Ustaw Edge jako prywatny", + "public": "Publiczny", + "make-private-edge-title": "Czy na pewno chcesz uczynić Edge {{edgeName}} prywatną?", + "make-private-edge-text": "Po potwierdzeniu urządzenie Edge i wszystkie jego dane staną się prywatne i nie będą dostępne dla innych.", + "import": "Importuj Edge", + "install-connect-instructions": "zainstalować", + "install-connect-instructions-edge-created": "Edge created! Check Install & Connect Instructions", + "loading-edge-instructions": "Loading edge instructions...", + "label": "Etykieta", + "load-entity-error": "Nie udało się załadować danych. ", + "assign-new-edge": "Przypisz nowy Edge", + "unassign-from-edge": "Usuń przypisanie z Edge", + "edge-key": "Klucz Edge", + "copy-edge-key": "Skopiuj klucz Edge", + "edge-key-copied-message": "Klawisz Edge został skopiowany do schowka", + "edge-secret": "Sekret Edge", + "copy-edge-secret": "Skopiuj sekret Edge", + "edge-secret-copied-message": "Sekret Edge został skopiowany do schowka", + "manage-assets": "Zarządzaj zasobami", + "manage-devices": "Zarządzaj urządzeniami", + "manage-entity-views": "Zarządzaj widokami encji", + "manage-dashboards": "Zarządzaj panelami", + "manage-rulechains": "Zarządzaj łańcuchami reguł", + "assets": "Zasoby Edge", + "devices": "Urządzenia Edge", + "entity-views": "Widoki elementów Edge", + "dashboard": "Panel Edge", + "dashboards": "Panele Edge", + "rulechain-templates": "Szablony łańcucha reguł", + "edge-rulechain-templates": "Szablony łańcucha reguł Edge", + "converter-templates": "Szablony konwerterów", + "edge-converter-templates": "Szablony konwerterów Edge", + "integration-templates": "Szablony integracji", + "edge-integration-templates": "Szablony integracji Edge", + "rulechains": "Łańcuchy reguł Edge", + "integrations": "Integracje", + "search": "Szukaj Edge", + "selected-edges": "Wybrano {count, plural, =1 {1 Edge} other {# Edge} }", + "any-edge": "Dowolna Edge", + "no-edge-types-matching": "Nie znaleziono pasujących typów Edge '{{entitySubtype}}'", + "edge-type-list-empty": "Nie wybrano typów Edge.", + "edge-types": "Typy Edge", + "enter-edge-type": "Podaj typ Edge", + "deployed": "Deployed", + "pending": "Oczekujący", + "downlinks": "Linki w dół", + "no-downlinks-prompt": "Nie znaleziono żadnych łączy w dół", + "sync-process-started-successfully": "Proces synchronizacji rozpoczął się pomyślnie!", + "missing-related-rule-chains-title": "W Edge brakuje powiązanych łańcuchów reguł", + "missing-related-rule-chains-text": "Przypisane do łańcuchów reguł Edge używają węzłów reguł, które przekazują komunikaty do łańcuchów reguł, które nie są przypisane do tego Edge.

Lista brakujących łańcuchów reguł:
{{missingRuleChains}}", + "widget-datasource-error": "Ten widget obsługuje wyłącznie źródło danych encji EDGE", + "assign-to-edge": "Przypisz do Edge", + "assign-to-edge-title": "Przypisz grupy jednostek do Edge", + "manage-edge-user-groups": "Zarządzaj grupami użytkowników Edge", + "manage-edge-asset-groups": "Zarządzaj grupami zasobów Edge", + "manage-edge-device-groups": "Zarządzaj grupami urządzeń Edge", + "manage-edge-entity-view-groups": "Zarządzaj grupami widoków jednostek Edge", + "manage-edge-dashboard-groups": "Zarządzaj grupami paneli Edge", + "manage-edge-rule-chains": "Zarządzaj łańcuchami reguł Edge", + "manage-edge-integrations": "Zarządzaj integracjami Edge", + "manage-edge-scheduler-events": "Zarządzaj zdarzeniami harmonogramu Edge", + "select-group-to-add": "Wybierz grupę docelową, aby dodać wybrane Edge", + "select-group-to-move": "Wybierz grupę docelową, aby przesunąć wybrane Edge", + "remove-edges-from-group": "Czy na pewno chcesz usunąć {count, plural, =1 {1 Edge} other {# Edgeów} }?", + "group": "Grupa Edge", + "list-of-groups": "{count, plural, =1 {Jedna grupa Edge} other {Lista # grup Edge} }", + "group-name-starts-with": "Grupy Edge, których nazwy zaczynają się od '{{prefix}}'", + "unassign-entity-group-from-edge-title": "Czy na pewno chcesz usunąć przypisanie grupy encji '{{ entityGroupName }}'?", + "unassign-entity-group-from-edge-text": "Po potwierdzeniu grupa encji zostanie usunięta i nie będzie dostępna na urządzeniu Edgeowym.", + "unassign-entity-group-from-edge": "Usuń przypisanie grupy jednostek do Edge", + "unassign-entity-groups-from-edge-title": "Czy na pewno chcesz anulować przypisanie {{count}} grupy obiektów?", + "unassign-entity-groups-from-edge-text": "Po potwierdzeniu grupy encji zostaną anulowane i nie będą dostępne na urządzeniu Edge.", + "unassign-entity-groups-from-edge": "Usuń przypisanie grup jednostek z Edge", + "unassign-scheduler-events-from-edge": "Usuń przypisanie zdarzeń harmonogramu z Edge", + "unassign-scheduler-event-from-edge-title": "Czy na pewno chcesz usunąć przypisanie zdarzenia harmonogramu '{{schedulerEventName}}'?", + "unassign-scheduler-event-from-edge-text": "Po potwierdzeniu zdarzenie harmonogramu zostanie anulowane i nie będzie dostępne na urządzeniu Edge.", + "unassign-scheduler-events-from-edge-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 zdarzenie planisty} other {# zdarzeń planisty} }?", + "unassign-scheduler-events-from-edge-text": "Po potwierdzeniu wszystkie wybrane zdarzenia harmonogramu zostaną usunięte i nie będą dostępne na urządzeniu Edge.", + "manage-user-groups": "Zarządzaj grupami użytkowników", + "manage-asset-groups": "Zarządzaj grupami zasobów", + "manage-device-groups": "Zarządzaj grupami urządzeń", + "manage-dashboard-groups": "Zarządzaj grupami paneli", + "manage-entity-view-groups": "Zarządzaj grupami widoków encji", + "manage-scheduler-events": "Zarządzaj zdarzeniami harmonogramu", + "manage-integrations": "Zarządzaj integracjami", + "assign-scheduler-event-to-edge-title": "Przypisz zdarzenia harmonogramu do Edge", + "assign-scheduler-event-to-edge-text": "Wybierz zdarzenia harmonogramu, które chcesz przypisać do urządzenia Edge", + "assign-entity-groups-to-edge-text": "Wybierz grupy elementów, które chcesz przypisać do Edge", + "assign-integration-to-edge-title": "Przypisz integracje do Edge", + "assign-integration-to-edge-text": "Wybierz integracje, które chcesz przypisać do urządzenia Edge", + "missing-attributes-title": "Edge ma brakujące atrybuty", + "missing-attributes-text": "Integracje przypisane do Edge mają symbole zastępcze atrybutów, które nie są dostępne na tej Edge.

Lista brakujących atrybutów Edge:
{{missingEdgeAttributes}}", + "missing-all-related-attributes-title": "Edge mają brakujące atrybuty", + "missing-all-related-attributes-text": "Edge powiązane z integracją nie mają atrybutów, które są obecne w konfiguracji integracji jako symbole zastępcze atrybutów.

Lista brakujących atrybutów Edge:
{{missingEdgeAttributes}}", + "quick-overview-widget-header": "{{edgeName}} szybki przegląd" }, - "edge":{ - "edge":"Krawędź", - "edge-instances":"Instancje krawędzi", - "instances":"Instancje", - "edge-file":"Plik krawędzi", - "name-max-length":"Nazwa powinna być krótsza niż 256", - "label-max-length":"Etykieta powinna być krótsza niż 256", - "type-max-length":"Typ powinien być krótszy niż 256", - "management":"Zarządzanie krawędzią", - "no-edges-matching":"Brak krawędzi pasujących do '{{entity}}'", - "add":"Dodaj krawędź", - "no-edges-text":"Brak znalezionych krawędzi", - "edge-details":"Szczegóły krawędzi", - "add-edge-text":"Dodaj nową krawędź", - "delete":"Usuń krawędź", - "delete-edge-title":"Czy na pewno chcesz usunąć krawędź '{{edgeName}}'?", - "delete-edge-text":"Bądź ostrożny, po potwierdzeniu krawędź i wszystkie związane z nią dane staną się nieodwracalnie utracone.", - "delete-edges-title":"Czy na pewno chcesz usunąć krawędź { count, plural, =1 {1 krawędź} other {# krawędzi} }?", - "delete-edges-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane krawędzie zostaną usunięte, a wszystkie związane z nimi dane staną się nieodwracalnie utracone.", - "name":"Nazwa", - "name-starts-with":"Nazwa krawędzi zaczyna się od", - "name-required":"Nazwa jest wymagana.", - "description":"Opis", - "details":"Szczegóły", - "events":"Zdarzenia", - "copy-id":"Kopiuj identyfikator krawędzi", - "id-copied-message":"Identyfikator krawędzi został skopiowany do schowka", - "sync":"Synchronizuj krawędź", - "edge-required":"Wymagana krawędź", - "edge-type":"Typ krawędzi", - "edge-type-required":"Wymagany jest typ krawędzi.", - "event-action":"Akcja zdarzenia", - "entity-id":"Identyfikator jednostki", - "select-edge-type":"Wybierz typ krawędzi", - "assign-to-customer":"Przypisz do klienta", - "assign-to-customer-text":"Proszę wybrać klienta, do którego przypisane będą krawędzie", - "assign-edge-to-customer":"Przypisz krawędź do klienta", - "assign-edge-to-customer-text":"Proszę wybrać krawędzie do przypisania do klienta", - "assignedToCustomer":"Przypisane do klienta", - "edge-public":"Krawędź jest publiczna", - "assigned-to-customer":"Przypisane do: {{customerTitle}}", - "unassign-from-customer":"Odłącz od klienta", - "unassign-edge-title":"Czy na pewno chcesz odłączyć krawędź '{{edgeName}}'?", - "unassign-edge-text":"Po potwierdzeniu krawędź zostanie odłączona i nie będzie dostępna dla klienta.", - "unassign-edges-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 krawędź} other {# krawędzie} }?", - "unassign-edges-text":"Po potwierdzeniu wszystkie wybrane krawędzie zostaną odłączone i nie będą dostępne dla klienta.", - "make-public":"Udostępnij publicznie", - "make-public-edge-title":"Czy na pewno chcesz udostępnić publicznie krawędź '{{edgeName}}'?", - "make-public-edge-text":"Po potwierdzeniu krawędź i wszystkie jej dane zostaną udostępnione publicznie i będą dostępne dla innych.", - "make-private":"Udostępnij prywatnie", - "public":"Publiczne", - "make-private-edge-title":"Czy na pewno chcesz udostępnić prywatnie krawędź '{{edgeName}}'?", - "make-private-edge-text":"Po potwierdzeniu krawędź i wszystkie jej dane zostaną udostępnione prywatnie i nie będą dostępne dla innych.", - "import":"Importuj krawędź", - "install-connect-instructions":"Instrukcje instalacji i połączenia", - "install-connect-instructions-edge-created":"Krawędź utworzona! Sprawdź instrukcje instalacji i połączenia", - "loading-edge-instructions":"Ładowanie instrukcji krawędzi...", - "label":"Etykieta", - "load-entity-error":"Nie udało się załadować danych. Jednostka została usunięta.", - "assign-new-edge":"Przypisz nową krawędź", - "unassign-from-edge":"Odłącz od krawędzi", - "edge-key":"Klucz krawędzi", - "copy-edge-key":"Kopiuj klucz krawędzi", - "edge-key-copied-message":"Klucz krawędzi został skopiowany do schowka", - "edge-secret":"Sekret krawędzi", - "copy-edge-secret":"Kopiuj sekret krawędzi", - "edge-secret-copied-message":"Sekret krawędzi został skopiowany do schowka", - "manage-assets":"Zarządzaj zasobami", - "manage-devices":"Zarządzaj urządzeniami", - "manage-entity-views":"Zarządzaj widokami jednostek", - "manage-dashboards":"Zarządzaj pulpitem nawigacyjnym", - "manage-rulechains":"Zarządzaj łańcuchami reguł", - "assets":"Zasoby krawędzi", - "devices":"Urządzenia krawędziowe", - "entity-views":"Widoki jednostek krawędziowych", - "dashboard":"Pulpit nawigacyjny krawędzi", - "dashboards":"Panele nawigacyjne krawędziowe", - "rulechain-templates":"Szablony łańcucha reguł", - "edge-rulechain-templates":"Szablony łańcucha reguł krawędziowych", - "rulechains":"Łańcuchy reguł krawędziowych", - "search":"Wyszukaj krawędzie", - "selected-edges":"{ count, plural, =1 {1 krawędź} other {# krawędzie} } wybrana", - "any-edge":"Dowolna krawędź", - "no-edge-types-matching":"Brak typów krawędzi pasujących do '{{entitySubtype}}'", - "edge-type-list-empty":"Brak wybranych typów krawędzi.", - "edge-types":"Typy krawędzi", - "enter-edge-type":"Wprowadź typ krawędzi", - "deployed":"Wdrożone", - "pending":"Oczekujące", - "downlinks":"Downlinki", - "no-downlinks-prompt":"Brak znalezionych downlinków", - "sync-process-started-successfully":"Proces synchronizacji został pomyślnie uruchomiony!", - "missing-related-rule-chains-title":"Krawędź ma brakujące powiązane łańcuchy reguł", - "missing-related-rule-chains-text":"Przypisane do krawędzi łańcuchy reguł używają węzłów reguł, które przekazują wiadomości do łańcuchów reguł, które nie są przypisane do tej krawędzi.

Lista brakujących łańcuchów reguł:
{{missingRuleChains}}", - "upgrade-instructions":"Instrukcje aktualizacji", - "widget-datasource-error":"Ten widżet obsługuje tylko źródło danych krawędziowe" + "edge-event": { + "type-dashboard": "Panel", + "type-asset": "Zasób", + "type-device": "Urządzenie", + "type-device-profile": "Profil urządzenia", + "type-asset-profile": "Profil zasobu", + "type-entity-view": "Widok obiektu", + "type-alarm": "Alarm", + "type-entity-group": "Grupa obiektów", + "type-rule-chain": "Łańcuch reguł", + "type-rule-chain-metadata": "Metadane łańcucha reguł", + "type-edge": "Edge", + "type-user": "Użytkownik", + "type-tenant": "Tenant", + "type-tenant-profile": "Tenant Profile", + "type-customer": "Klient", + "type-relation": "Relacja", + "type-widgets-bundle": "Pakiet widżetów", + "type-widgets-type": "Typ widżetów", + "type-admin-settings": "Ustawienia administratora", + "type-ota-package": "Ota Package", + "type-queue": "Queue", + "type-scheduler-event": "Wydarzenie w harmonogramie", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "Logowanie White Labeling", + "type-custom-translation": "Tłumaczenie niestandardowe", + "type-role": "Rola", + "type-group-permission": "Zezwolenie grupowe", + "type-integration": "Integracja", + "type-converter": "Przetwornik", + "action-type-added": "Dodany", + "action-type-deleted": "Usunięto", + "action-type-updated": "Zaktualizowano", + "action-type-post-attributes": "Atrybuty postów", + "action-type-attributes-updated": "Zaktualizowano atrybuty", + "action-type-attributes-deleted": "Atrybuty usunięte", + "action-type-timeseries-updated": "Zaktualizowano serie czasowe", + "action-type-credentials-updated": "Zaktualizowano dane uwierzytelniające", + "action-type-assigned-to-customer": "Przypisane do Klienta", + "action-type-unassigned-from-customer": "Nieprzypisane od klienta", + "action-type-relation-add-or-update": "Dodaj lub zaktualizuj relację", + "action-type-relation-deleted": "Relacja usunięta", + "action-type-rpc-call": "Zadzwoń do RPC", + "action-type-alarm-ack": "Potwierdzenie alarmu", + "action-type-alarm-clear": "Alarm usunięty", + "action-type-alarm-assigned": "Alarm Assigned", + "action-type-alarm-unassigned": "Alarm Unassigned", + "action-type-assigned-to-edge": "Przypisany do Edge", + "action-type-unassigned-from-edge": "Nieprzypisane do Edge", + "action-type-credentials-request": "Żądanie poświadczeń", + "action-type-entity-merge-request": "Żądanie połączenia jednostek", + "action-type-added-to-entity-group": "Dodano do grupy jednostek", + "action-type-removed-from-entity-group": "Usunięto z grupy jednostek", + "action-type-change-owner": "Zmiana właściciela", + "action-type-relations-deleted": "Relacje usunięte" }, - "edge-event":{ - "type-dashboard":"Panel nawigacyjny", - "type-asset":"Zasób", - "type-device":"Urządzenie", - "type-device-profile":"Profil urządzenia", - "type-asset-profile":"Profil zasobu", - "type-entity-view":"Widok jednostki", - "type-alarm":"Alarm", - "type-rule-chain":"Łańcuch reguł", - "type-rule-chain-metadata":"Metadane łańcucha reguł", - "type-edge":"Krawędź", - "type-user":"Użytkownik", - "type-tenant":"Najemca", - "type-tenant-profile":"Profil najemcy", - "type-customer":"Klient", - "type-relation":"Relacja", - "type-widgets-bundle":"Pakiet widżetów", - "type-widgets-type":"Typ widżetu", - "type-admin-settings":"Ustawienia administratora", - "type-ota-package":"Pakiet OTA", - "type-queue":"Kolejka", - "action-type-added":"Dodane", - "action-type-deleted":"Usunięte", - "action-type-updated":"Zaktualizowane", - "action-type-post-attributes":"Opublikuj atrybuty", - "action-type-attributes-updated":"Zaktualizowane atrybuty", - "action-type-attributes-deleted":"Usunięte atrybuty", - "action-type-timeseries-updated":"Zaktualizowane serie czasowe", - "action-type-credentials-updated":"Zaktualizowane dane uwierzytelniające", - "action-type-assigned-to-customer":"Przypisane do klienta", - "action-type-unassigned-from-customer":"Odłączone od klienta", - "action-type-relation-add-or-update":"Dodaj lub aktualizuj relację", - "action-type-relation-deleted":"Usunięta relacja", - "action-type-rpc-call":"Wywołanie RPC", - "action-type-alarm-ack":"Potwierdzenie alarmu", - "action-type-alarm-clear":"Wyczyść alarm", - "action-type-alarm-assigned":"Przypisany alarm", - "action-type-alarm-unassigned":"Odłączony alarm", - "action-type-assigned-to-edge":"Przypisane do krawędzi", - "action-type-unassigned-from-edge":"Odłączone od krawędzi", - "action-type-credentials-request":"Żądanie danych uwierzytelniających", - "action-type-entity-merge-request":"Żądanie scalenia jednostki" + "error": { + "unable-to-connect": "Nie można połączyć się z serwerem! ", + "unhandled-error-code": "Nieobsługiwany kod błędu: {{errorCode}}", + "unknown-error": "Nieznany błąd" }, - "error":{ - "unable-to-connect":"Nie można połączyć się z serwerem! Sprawdź swoje połączenie internetowe.", - "unhandled-error-code":"Niestandardowy kod błędu: {{errorCode}}", - "unknown-error":"Nieznany błąd" + "entity": { + "entity": "Obiekt", + "entities": "Obiekty", + "entities-count": "Liczba obiektów", + "alarms-count": "Liczba alarmów", + "aliases": "Aliasy obiektów", + "aliases-short": "Aliases", + "entity-alias": "Alias obiektów", + "unable-delete-entity-alias-title": "Nie można usunąć aliasu obiektu", + "unable-delete-entity-alias-text": "Alias obiektu '{{entityAlias}}' nie może zostać usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", + "duplicate-alias-error": "Znaleziono zduplikowany alias '{{alias}}'.
Aliasy obiektu muszą być unikalne w obrębie panelu kontrolnego.", + "missing-entity-filter-error": "Brakuje filtra dla aliasu '{{alias}}'.", + "configure-alias": "Skonfiguruj '{{alias}}' Alias", + "alias": "Alias", + "alias-required": "Alias obiektu jest wymagany.", + "remove-alias": "Usuń alias obiektu", + "add-alias": "Dodaj alias obiektu", + "entity-list": "Lista obiektu", + "entity-type": "Typ obiektu", + "entity-types": "Typy obiektu", + "entity-type-list": "Lista typów obiektu", + "any-entity": "Dowolny obiekt", + "add-entity-type": "Add entity type", + "enter-entity-type": "Wpisz typ obiektu", + "no-entities-matching": "Nie znaleziono obiektu pasujących do '{{entity}}'.", + "no-entity-types-matching": "Nie znaleziono typów obiektu pasujących do '{{entityType}}'", + "name-starts-with": "Wyrażenie nazwy", + "help-text": "Użyj „%” w zależności od potrzeb: „%entity_name_contains%”, „%entity_name_ends”, „entity_starts_with”.", + "use-entity-name-filter": "Użyj filtra", + "entity-list-empty": "Nie wybrano żadnych obiektu.", + "entity-type-list-required": "At least one entity type should be selected.", + "entity-name-filter-required": "Filtr nazwy obiektu jest wymagany.", + "entity-name-filter-no-entity-matched": "Nie znaleziono obiektu zaczynających się od '{{entity}}'.", + "all-subtypes": "Wszystko", + "select-entities": "Wybierz obiekty", + "no-aliases-found": "Nie znaleziono aliasów.", + "no-alias-matching": "'{{alias}}' nie znaleziono.", + "create-new-alias": "Utwórz nowy!", + "create-new": "Create new", + "key": "Klucz", + "key-name": "Nazwa klucza", + "no-keys-found": "Nie znaleziono kluczy.", + "no-key-matching": "'{{key}}' nie znaleziono.", + "create-new-key": "Utwórz nowy!", + "type": "Typ", + "type-required": "Typ obiektu jest wymagany.", + "type-device": "Urządzenie", + "type-devices": "Urządzenia", + "list-of-devices": "{count, plural, =1 {Jedno urządzenie} other {Lista # urządzeń} }", + "device-name-starts-with": "Urządzenia, których nazwy zaczynają się od '{{prefix}}'", + "type-device-profile": "Profil urządzenia", + "type-device-profiles": "Profile urządzeń", + "clear-selected-profiles": "Clear selected profiles", + "list-of-device-profiles": "{count, plural, =1 {Profil jednego urządzenia} other {Lista profili # urządzeń} }", + "device-profile-name-starts-with": "Profile urządzeń, których nazwy zaczynają się od '{{prefix}}'", + "type-asset-profile": "Profil zasobu", + "type-asset-profiles": "Profile zasobów", + "list-of-asset-profiles": "{count, plural, =1 {Jeden profil zasobu} other {Lista profili # zasobów} }", + "asset-profile-name-starts-with": "Profile zasobów, których nazwy zaczynają się od '{{prefix}}'", + "type-asset": "Zasób", + "type-assets": "Zasoby", + "list-of-assets": "{count, plural, =1 {Jeden zasób} other {Lista # zasobów} }", + "asset-name-starts-with": "Zasoby, których nazwy zaczynają się od '{{prefix}}'", + "type-entity-view": "Widok obiektu", + "type-entity-views": "Widoki obiektu", + "list-of-entity-views": "{count, plural, =1 {Widok jednego obiektu} other {Lista # widoków obiektu} }", + "entity-view-name-starts-with": "Widoki obiektu, których nazwy zaczynają się od '{{prefix}}'", + "type-rule": "Reguła", + "type-rules": "Reguły", + "list-of-rules": "{count, plural, =1 {Jedna reguła} other {Lista # reguł} }", + "rule-name-starts-with": "Reguły, których nazwy zaczynają się od '{{prefix}}'", + "type-plugin": "Wtyczka", + "type-plugins": "Wtyczki", + "list-of-plugins": "{count, plural, =1 {Jedna wtyczka} other {Lista # wtyczek} }", + "plugin-name-starts-with": "Wtyczki, których nazwy zaczynają się od '{{prefix}}'", + "type-tenant": "Tenant", + "type-tenants": "Tenant", + "list-of-tenants": "{count, plural, =1 {Jeden tenant} other {Lista # tenantów} }", + "tenant-name-starts-with": "Tenanci, których nazwy zaczynają się na '{{prefix}}'", + "type-tenant-profile": "Profil tenanta", + "type-tenant-profiles": "Profile tenantów", + "list-of-tenant-profiles": "{count, plural, =1 {Profil jednego tenanta} other {Lista # profili tenantów} }", + "tenant-profile-name-starts-with": "Profile tenantów, których nazwy zaczynają się od '{{prefix}}'", + "type-customer": "Klient", + "type-customers": "Klienci", + "list-of-customers": "{count, plural, =1 {Jeden klient} other {Lista # klientów} }", + "customer-name-starts-with": "Klienci, których imiona zaczynają się na '{{prefix}}'", + "type-user": "Użytkownik", + "type-users": "Użytkownicy", + "list-of-users": "{count, plural, =1 {Jeden użytkownik} other {Lista # użytkowników} }", + "user-name-starts-with": "Użytkownicy, których nazwy zaczynają się od '{{prefix}}'", + "type-dashboard": "Panel", + "type-dashboards": "Panele", + "list-of-dashboards": "{count, plural, =1 {Jeden panel} other {Lista # paneli} }", + "dashboard-name-starts-with": "Panele, których nazwy zaczynają się od '{{prefix}}'", + "type-alarm": "Alarm", + "type-alarms": "Alarmy", + "list-of-alarms": "{count, plural, =1 {Jeden alarm} other {Lista # alarmów} }", + "alarm-name-starts-with": "Alarmy, których nazwy zaczynają się od '{{prefix}}'", + "type-rulechain": "Łańcuch reguł", + "type-rulechains": "Łańcuchy reguł", + "list-of-rulechains": "{count, plural, =1 {Jeden łańcuch reguł} other {Lista # łańcuchów reguł} }", + "rulechain-name-starts-with": "Łańcuchy reguł, których nazwy zaczynają się od '{{prefix}}'", + "type-scheduler-event": "Zdarzenie planisty", + "type-scheduler-events": "Zdarzenia planisty", + "list-of-scheduler-events": "{count, plural, =1 {Jedno zdarzenie planisty} other {Lista # zdarzeń planisty} }", + "scheduler-event-name-starts-with": "Zdarzenia planisty, których nazwy zaczynają się od '{{prefix}}'", + "type-blob-entity": "Obiekt typu blob", + "type-blob-entities": "Obiekty typu blob", + "list-of-blob-entities": "{count, plural, =1 {Jeden obiekt typu blob} other {Lista # obiektów typu blob} }", + "blob-entity-name-starts-with": "Obiekty typu blob, których nazwy zaczynają się od '{{prefix}}'", + "type-rulenode": "Węzeł reguły", + "type-rulenodes": "Węzły reguł", + "list-of-rulenodes": "{count, plural, =1 {Jeden węzeł reguły} other {Lista # węzłów reguł} }", + "rulenode-name-starts-with": "Węzły reguł, których nazwy zaczynają się od '{{prefix}}'", + "type-current-customer": "Aktualny klient", + "type-current-tenant": "Aktualny tenant", + "type-current-user": "Aktualny użytkownik", + "type-current-user-owner": "Aktualny właściciel użytkownika", + "type-widgets-bundle": "Pakiet widżetów", + "type-widgets-bundles": "Pakiety widgetów", + "list-of-widgets-bundles": "{count, plural, =1 {Jeden pakiet widżetów} other {Lista # pakietów widżetów} }", + "type-widget-type": "Widget type", + "type-widget-types": "Widget types", + "list-of-widget-types": "{ count, plural, =1 {One widget type} other {List of # widget types} }", + "search": "Wyszukaj obiekty", + "selected-entities": "Wybrano {count, plural, =1 {1 obiekt} other {# obiektów} }", + "entity-name": "Nazwa obiektu", + "entity-label": "Etykieta obiektu", + "details": "Szczegóły obiektu", + "no-entities-prompt": "Nie znaleziono żadnych obiektu", + "no-data": "brak danych do wyświetlenia", + "columns-to-display": "Kolumny do wyświetlenia", + "type-api-usage-state": "Stan użycia interfejsu API", + "type-entity-group": "Grupa obiektu", + "type-converter": "Konwerter danych", + "type-converters": "Konwertery danych", + "list-of-converters": "{count, plural, =1 {Jeden konwerter danych} other {Lista # konwerterów danych} }", + "converter-name-starts-with": "Konwertery danych, których nazwy zaczynają się od '{{prefix}}'", + "type-integration": "Integracja", + "type-integrations": "Integracje", + "list-of-integrations": "{count, plural, =1 {Jedna integracja} other {Lista # integracji} }", + "integration-name-starts-with": "Integracje, których nazwy zaczynają się od '{{prefix}}'", + "type-role": "Rola", + "type-roles": "Role", + "list-of-roles": "{count, plural, =1 {Jedna rola} other {Lista # ról} }", + "role-name-starts-with": "Role, których nazwy zaczynają się od '{{prefix}}'", + "type-group-permission": "Zezwolenie grupowe", + "type-edge": "Edge", + "type-edges": "Edge", + "list-of-edges": "{count, plural, =1 {Jeden Edge} other {Lista # Edgeów} }", + "edge-name-starts-with": "Edge, których nazwy zaczynają się od '{{prefix}}'", + "type-tb-resource": "Biblioteka", + "type-ota-package": "Pakiet OTA", + "type-rpc": "RPC", + "type-queue": "Kolejka", + "type-notification": "Powiadomienie", + "type-notification-rule": "Zasada powiadamiania", + "type-notification-rules": "Notification rules", + "list-of-notification-rules": "{ count, plural, =1 {One notification rule} other {List of # notification rules} }", + "type-notification-target": "Odbiorca powiadomienia", + "type-notification-targets": "Notification recipients", + "list-of-notification-targets": "{ count, plural, =1 {One notification recipient} other {List of # notification recipients} }", + "type-notification-request": "Żądanie powiadomienia", + "type-notification-template": "Szablon powiadomienia", + "type-notification-templates": "Notification templates", + "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", + "customer-name": "Nazwa klienta", + "sub-customer-name": "Nazwa podklienta", + "parent-customer-name": "Nazwa klienta nadrzędnego", + "parent-sub-customer-name": "Nazwa nadrzędnego klienta podrzędnego", + "include-customer-entities": "Uwzględnij obiekty klientów", + "include-sub-customer-entities": "Uwzględnij obiekty podklientów", + "groups": "Grupy", + "entity-type-list-empty": "Nie wybrano typów obiektu." }, - "entity":{ - "entity":"Encja", - "entities":"Encje", - "entities-count":"Liczba encji", - "alarms-count":"Liczba alarmów", - "aliases":"Aliasy encji", - "aliases-short":"Aliasy", - "entity-alias":"Alias encji", - "unable-delete-entity-alias-title":"Nie można usunąć aliasu encji", - "unable-delete-entity-alias-text":"Alias encji '{{entityAlias}}' nie może zostać usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", - "duplicate-alias-error":"Znaleziono zduplikowany alias '{{alias}}'.
Aleasy encji muszą być unikalne w obrębie deski rozdzielczej.", - "missing-entity-filter-error":"Brakuje filtru dla aliasu '{{alias}}'.", - "configure-alias":"Skonfiguruj alias '{{alias}}'", - "alias":"Alias", - "alias-required":"Wymagany jest alias encji.", - "remove-alias":"Usuń alias encji", - "add-alias":"Dodaj alias encji", - "entity-list":"Lista encji", - "entity-type":"Typ encji", - "entity-types":"Typy encji", - "entity-type-list":"Lista typów encji", - "any-entity":"Dowolna encja", - "add-entity-type":"Dodaj typ encji", - "enter-entity-type":"Wprowadź typ encji", - "no-entities-matching":"Nie znaleziono encji pasujących do '{{entity}}'.", - "no-entity-types-matching":"Nie znaleziono typów encji pasujących do '{{entityType}}'.", - "name-starts-with":"Wyrażenie nazwy", - "help-text":"Użyj '%', zgodnie z potrzebą: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'.", - "use-entity-name-filter":"Użyj filtra", - "entity-list-empty":"Nie wybrano żadnych encji.", - "entity-type-list-required":"Należy wybrać co najmniej jeden typ encji.", - "entity-name-filter-required":"Wymagany jest filtr nazwy encji.", - "entity-name-filter-no-entity-matched":"Nie znaleziono encji rozpoczynających się od '{{entity}}'.", - "all-subtypes":"Wszystkie", - "select-entities":"Wybierz encje", - "no-aliases-found":"Nie znaleziono aliasów.", - "no-alias-matching":"'{{alias}}' nie znaleziono.", - "create-new-alias":"Utwórz nowy!", - "create-new":"Utwórz nowy", - "key":"Klucz", - "key-name":"Nazwa klucza", - "no-keys-found":"Nie znaleziono kluczy.", - "no-key-matching":"'{{key}}' nie znaleziono.", - "create-new-key":"Utwórz nowy!", - "type":"Typ", - "type-required":"Wymagany jest typ encji.", - "type-device":"Urządzenie", - "type-devices":"Urządzenia", - "list-of-devices":"{ count, plural, =1 {Jedno urządzenie} other {Lista # urządzeń} }", - "device-name-starts-with":"Urządzenia, których nazwy zaczynają się od '{{prefix}}'", - "type-device-profile":"Profil urządzenia", - "type-device-profiles":"Profile urządzeń", - "clear-selected-profiles":"Wyczyść wybrane profile", - "list-of-device-profiles":"{ count, plural, =1 {Jeden profil urządzenia} other {Lista # profili urządzeń} }", - "device-profile-name-starts-with":"Profile urządzeń, których nazwy zaczynają się od '{{prefix}}'", - "type-asset-profile":"Profil zasobów", - "type-asset-profiles":"Profile zasobów", - "list-of-asset-profiles":"{ count, plural, =1 {Jeden profil zasobów} other {Lista # profili zasobów} }", - "asset-profile-name-starts-with":"Profile zasobów, których nazwy zaczynają się od '{{prefix}}'", - "type-asset":"Zasób", - "type-assets":"Zasoby", - "list-of-assets":"{ count, plural, =1 {Jeden zasób} other {Lista # zasobów} }", - "asset-name-starts-with":"Zasoby, których nazwy zaczynają się od '{{prefix}}'", - "type-entity-view":"Widok encji", - "type-entity-views":"Widoki encji", - "list-of-entity-views":"{ count, plural, =1 {Jeden widok encji} other {Lista # widoków encji} }", - "entity-view-name-starts-with":"Widoki encji, których nazwy zaczynają się od '{{prefix}}'", - "type-rule":"Reguła", - "type-rules":"Reguły", - "list-of-rules":"{ count, plural, =1 {Jedna reguła} other {Lista # reguł} }", - "rule-name-starts-with":"Reguły, których nazwy zaczynają się od '{{prefix}}'", - "type-plugin":"Wtyczka", - "type-plugins":"Wtyczki", - "list-of-plugins":"{ count, plural, =1 {Jedna wtyczka} other {Lista # wtyczek} }", - "plugin-name-starts-with":"Wtyczki, których nazwy zaczynają się od '{{prefix}}'", - "type-tenant":"Najemca", - "type-tenants":"Najemcy", - "list-of-tenants":"{ count, plural, =1 {Jeden najemca} other {Lista # najemców} }", - "tenant-name-starts-with":"Najemcy, których nazwy zaczynają się od '{{prefix}}'", - "type-tenant-profile":"Profil najemcy", - "type-tenant-profiles":"Profile najemcy", - "list-of-tenant-profiles":"{ count, plural, =1 {Jeden profil najemcy} other {Lista # profili najemcy} }", - "tenant-profile-name-starts-with":"Profile najemcy, których nazwy zaczynają się od '{{prefix}}'", - "type-customer":"Klient", - "type-customers":"Klienci", - "list-of-customers":"{ count, plural, =1 {Jeden klient} other {Lista # klientów} }", - "customer-name-starts-with":"Klienci, których nazwy zaczynają się od '{{prefix}}'", - "type-user":"Użytkownik", - "type-users":"Użytkownicy", - "list-of-users":"{ count, plural, =1 {Jeden użytkownik} other {Lista # użytkowników} }", - "user-name-starts-with":"Użytkownicy, których nazwy zaczynają się od '{{prefix}}'", - "type-dashboard":"Deska rozdzielcza", - "type-dashboards":"Deski rozdzielcze", - "list-of-dashboards":"{ count, plural, =1 {Jedna deska rozdzielcza} other {Lista # desek rozdzielczych} }", - "dashboard-name-starts-with":"Deski rozdzielcze, których nazwy zaczynają się od '{{prefix}}'", - "type-alarm":"Alarm", - "type-alarms":"Alarmy", - "list-of-alarms":"{ count, plural, =1 {Jeden alarm} other {Lista # alarmów} }", - "alarm-name-starts-with":"Alarmy, których nazwy zaczynają się od '{{prefix}}'", - "type-rulechain":"Łańcuch reguł", - "type-rulechains":"Łańcuchy reguł", - "list-of-rulechains":"{ count, plural, =1 {Jeden łańcuch reguł} other {Lista # łańcuchów reguł} }", - "rulechain-name-starts-with":"Łańcuchy reguł, których nazwy zaczynają się od '{{prefix}}'", - "type-rulenode":"Węzeł reguły", - "type-rulenodes":"Węzły reguł", - "list-of-rulenodes":"{ count, plural, =1 {Jeden węzeł reguły} other {Lista # węzłów reguł} }", - "rulenode-name-starts-with":"Węzły reguł, których nazwy zaczynają się od '{{prefix}}'", - "type-current-customer":"Bieżący Klient", - "type-current-tenant":"Bieżący Najemca", - "type-current-user":"Bieżący Użytkownik", - "type-current-user-owner":"Bieżący Właściciel Użytkownika", - "type-widgets-bundle":"Zestaw Widżetów", - "type-widgets-bundles":"Zestawy Widżetów", - "list-of-widgets-bundles":"{ count, plural, =1 {Jeden zestaw widżetów} other {Lista # zestawów widżetów} }", - "type-widget":"Widżet", - "type-widgets":"Widżety", - "list-of-widgets":"{ count, plural, =1 {Jeden widżet} other {Lista # widżetów} }", - "search":"Szukaj jednostek", - "selected-entities":"{ count, plural, =1 {1 jednostka} other {# jednostki} } wybranych", - "entity-name":"Nazwa jednostki", - "entity-label":"Etykieta jednostki", - "details":"Szczegóły jednostki", - "no-entities-prompt":"Nie znaleziono jednostek", - "no-data":"Brak danych do wyświetlenia", - "columns-to-display":"Kolumny do wyświetlenia", - "type-api-usage-state":"Stan korzystania z API", - "type-edge":"Edge", - "type-edges":"Edges", - "list-of-edges":"{ count, plural, =1 {Jeden edge} other {Lista # edges} }", - "edge-name-starts-with":"Edges, których nazwy zaczynają się od '{{prefix}}'", - "type-tb-resource":"Zasób", - "type-tb-resources":"Zasoby", - "list-of-tb-resources":"{ count, plural, =1 {Jeden zasób} other {Lista # zasobów} }", - "type-ota-package":"Pakiet OTA", - "type-rpc":"RPC", - "type-queue":"Kolejka", - "type-notification":"Powiadomienie", - "type-notification-rule":"Reguła powiadomień", - "type-notification-rules":"Reguły powiadomień", - "list-of-notification-rules":"{ count, plural, =1 {Jedna reguła powiadomień} other {Lista # reguł powiadomień} }", - "type-notification-target":"Odbiorca powiadomienia", - "type-notification-targets":"Odbiorcy powiadomień", - "list-of-notification-targets":"{ count, plural, =1 {Jeden odbiorca powiadomienia} other {Lista # odbiorców powiadomień} }", - "type-notification-request":"Żądanie powiadomienia", - "type-notification-template":"Szablon powiadomienia", - "type-notification-templates":"Szablony powiadomień", - "list-of-notification-templates":"{ count, plural, =1 {Jeden szablon powiadomienia} other {Lista # szablonów powiadomień} }" + "entity-group": { + "entity-group": "Grupa obiektów", + "details": "Szczegóły", + "columns": "Kolumny", + "add-column": "Dodaj kolumnę", + "column-value": "Wartość", + "column-value-required": "Wartość kolumny jest wymagana.", + "column-title": "Tytuł", + "default-sort-order": "Domyślny porządek sortowania", + "default-sort-order-required": "Domyślny porządek sortowania jest wymagany.", + "hide-in-mobile-view": "Komórka ukryta", + "use-cell-style-function": "Użyj funkcji stylu komórki", + "use-cell-content-function": "Użyj funkcji zawartości komórki", + "edit-column": "Edytuj kolumnę", + "column-details": "Szczegóły kolumny", + "actions": "działania", + "settings": "Ustawienia", + "search": "Wyszukaj grupy obiektu", + "delete": "Usuń grupę obiektu", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "description": "Opis", + "add": "Dodaj grupę obiektów", + "open-entity-group": "Otwórz grupę obiektów", + "add-entity-group-text": "Dodaj nową grupę obiektów", + "no-entity-groups-text": "Nie znaleziono grup obiektów", + "entity-group-details": "Szczegóły grupy obiektów", + "selected-entity-groups": "Wybrano {count, plural, =1 {1 grupę obiektów} other {# grup obiektów} }", + "delete-entity-groups": "Usuń grupy obiektów", + "delete-entity-group-title": "Czy na pewno chcesz usunąć grupę obiektów '{{entityGroupName}}'?", + "delete-entity-group-text": "Bądź ostrożny, po potwierdzeniu grupy obiektów i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-entity-groups-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 grupę obiektów} other {# grup obiektów} }?", + "delete-entity-groups-action-title": "Usuń {count, plural, =1 {1 grupę obiektów} other {# grup obiektów} }", + "delete-entity-groups-text": "Uważaj, po potwierdzeniu wszystkie wybrane grupy obiektów zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "device-groups": "Grupy urządzeń", + "shared-device-groups": "Współdzielone grupy urządzeń", + "asset-groups": "Grupy zasobów", + "shared-asset-groups": "Współdzielone grupy zasobów", + "customer-groups": "Grupy klientów", + "shared-customer-groups": "Wspólne grupy klientów", + "device-group": "Grupa urządzeń", + "asset-group": "Grupa zasobów", + "user-group": "Grupa użytkowników", + "user-groups": "Grupy użytkowników", + "customer-group": "Grupa klientów", + "entity-view-groups": "Grupy widoków obiektów", + "shared-entity-view-groups": "Grupy widoków encji udostępnionych", + "entity-view-group": "Grupa widoków encji", + "edge-groups": "Grupy Edge", + "shared-edge-groups": "Wspólne grupy Edge", + "edge-group": "Grupa Edge", + "dashboard-groups": "Grupy paneli", + "shared-dashboard-groups": "Udostępnione grupy paneli", + "dashboard-group": "Grupa paneli", + "fetch-more": "Zdobądź więcej", + "column-type": { + "column-type": "Typ kolumny", + "client-attribute": "Atrybut klienta", + "shared-attribute": "Wspólny atrybut", + "server-attribute": "Atrybut serwera", + "timeseries": "Szereg czasowy", + "entity-field": "Pole obiektu" + }, + "column-type-required": "Typ kolumny jest wymagany.", + "entity-field": { + "created-time": "Czas utworzenia", + "name": "Nazwa", + "type": "Typ", + "device_profile": "Profil urządzenia", + "asset_profile": "Profil zasobu", + "assigned_customer": "Przypisany Klient", + "authority": "Autorytet", + "first_name": "Imię", + "last_name": "Nazwisko", + "email": "E-mail", + "title": "Tytuł", + "country": "Kraj", + "state": "Państwo", + "city": "Miasto", + "address": "Adres", + "address2": "Adres 2", + "zip": "Kod pocztowy", + "phone": "Telefon", + "label": "Etykieta" + }, + "sort-order": { + "asc": "Rosnąco", + "desc": "Malejąco", + "none": "Brak" + }, + "details-mode": { + "on-row-click": "Kliknij wiersz", + "on-action-button-click": "Kliknij przycisk szczegółów", + "disabled": "Wyłączony" + }, + "change-owner": "Zmiana właściciela", + "select-target-owner": "Wybierz właściciela docelowego", + "no-owners-matching": "Nie znaleziono pasujących właścicieli do '{{owner}}'", + "target-owner-required": "Właściciel obiektu docelowego jest wymagany.", + "confirm-change-owner-title": "Czy na pewno chcesz zmienić właściciela dla {count, plural, =1 {1 wybranego obiektu} other {# wybranych obiektów} }?", + "confirm-change-owner-text": "Uważaj, po potwierdzeniu wszystkie wybrane obiekty zostaną usunięte z bieżącego właściciela i umieszczone w grupie „Wszystkie” docelowego właściciela.", + "add-to-group": "Dodaj do grupy", + "move-to-group": "Przejdź do grupy", + "select-entity-group": "Wybierz grupę jednostek", + "no-entity-groups-matching": "Brak grup jednostek pasujących do '{{entityGroup}}' zostały znalezione.", + "create-new-entity-group": "Utwórz nową!", + "target-entity-group-required": "Docelowa grupa jednostek jest wymagana.", + "select-user-group": "Wybierz grupę użytkowników", + "no-user-groups-matching": "Brak grup użytkowników pasujących do '{{entityGroup}}' zostały znalezione.", + "target-user-group-required": "Docelowa grupa użytkowników jest wymagana.", + "remove-from-group": "Usuń z grupy", + "group-table-title": "Tytuł tabeli grupowej", + "enable-search": "Włącz wyszukiwanie jednostek", + "enable-add": "Włącz dodawanie jednostek", + "enable-delete": "Włącz usuwanie jednostek", + "enable-selection": "Włącz wybór elementów", + "enable-group-transfer": "Włącz działania związane z transferem grupowym", + "display-pagination": "Wyświetl paginację", + "default-page-size": "Domyślny rozmiar strony", + "enable-assignment-actions": "Włącz akcje przypisania", + "enable-credentials-management": "Włącz zarządzanie danymi uwierzytelniającymi", + "enable-login-as-user": "Włącz logowanie jako akcję użytkownika", + "enable-users-management": "Włącz zarządzanie użytkownikami", + "enable-customers-management": "Włącz zarządzanie klientami", + "enable-assets-management": "Włącz zarządzanie zasobami", + "enable-devices-management": "Włącz zarządzanie urządzeniami", + "enable-entity-views-management": "Włącz zarządzanie widokami encji", + "enable-edges-management": "Włącz zarządzanie Edge", + "enable-dashboards-management": "Włącz zarządzanie panelami", + "enable-scheduler-events-management": "Włącz zarządzanie zdarzeniami harmonogramu", + "open-details-on": "Otwórz szczegóły obiektu na", + "select-existing": "Wybierz istniejącą grupę jednostek", + "create-new": "Utwórz nową grupę jednostek", + "new-entity-group-name": "Nowa nazwa grupy jednostek", + "entity-group-list": "Lista grup jednostek", + "entity-group-list-empty": "Nie wybrano grup jednostek.", + "name-starts-with": "Nazwa grupy jednostek zaczyna się od", + "entity-group-name-filter-required": "Filtr nazwy grupy jednostek jest wymagany.", + "roles": "Role", + "permissions": "Uprawnienia", + "public": "Publiczny", + "entity-group-public": "Grupa jednostek jest publiczna", + "make-public": "Upublicznij grupę encji", + "make-private": "Ustaw grupę encji jako prywatną", + "make-public-entity-group-title": "Czy na pewno chcesz utworzyć grupę encji '{{entityGroupName}}' „publiczny”?", + "make-public-entity-group-text": "Po zatwierdzeniu grupa obiektów i wszystkie jej obiekty zostaną upublicznione i dostępne dla innych.", + "make-private-entity-group-title": "Czy na pewno chcesz utworzyć grupę encji '{{entityGroupName}}'prywatny?", + "make-private-entity-group-text": "Po potwierdzeniu grupa encji i wszystkie jej encje staną się prywatne i nie będą dostępne dla innych.", + "share": "Udostępnij grupę jednostek", + "copyId": "Skopiuj identyfikator grupy jednostek", + "idCopiedMessage": "Identyfikator grupy jednostek został skopiowany do schowka", + "entity-group-name": "Nazwa grupy jednostek", + "all-users": "Wszyscy użytkownicy", + "owner": "Właściciel", + "owner-required": "Właściciel jest wymagany.", + "manage-owner-and-groups": "Zarządzaj właścicielami i grupami", + "owner-and-groups": "Właściciel i grupy" }, - "entity-field":{ - "created-time":"Czas utworzenia", - "name":"Nazwa", - "type":"Typ", - "first-name":"Imię", - "last-name":"Nazwisko", - "email":"E-mail", - "title":"Tytuł", - "country":"Kraj", - "state":"Stan", - "city":"Miasto", - "address":"Adres", - "address2":"Adres 2", - "zip":"Kod pocztowy", - "phone":"Telefon", - "label":"Etykieta" + "entity-field": { + "created-time": "Stworzony czas", + "name": "Nazwa", + "type": "Typ", + "first-name": "Imię", + "last-name": "Nazwisko", + "email": "E-mail", + "title": "Tytuł", + "country": "Kraj", + "state": "Państwo", + "city": "Miasto", + "address": "Adres", + "address2": "Adres 2", + "zip": "Kod pocztowy", + "phone": "Telefon", + "label": "Etykieta", + "configuration": "Konfiguracja", + "schedule": "Harmonogram", + "originatorId": "Identyfikator nadawcy", + "originatorType": "Typ inicjatora" }, - "entity-view":{ - "entity-view":"Widok encji", - "entity-view-required":"Widok encji jest wymagany.", - "entity-views":"Widoki encji", - "management":"Zarządzanie widokiem encji", - "view-entity-views":"Wyświetl widoki encji", - "entity-view-alias":"Alias widoku encji", - "aliases":"Aliasy widoku encji", - "no-alias-matching":"'{{alias}}' nie znaleziono.", - "no-aliases-found":"Nie znaleziono aliasów.", - "no-key-matching":"'{{key}}' nie znaleziono.", - "no-keys-found":"Nie znaleziono kluczy.", - "create-new-alias":"Utwórz nowy!", - "create-new-key":"Utwórz nowy!", - "duplicate-alias-error":"Znaleziono zduplikowany alias '{{alias}}'.
Aliasy widoku encji muszą być unikalne w obrębie panelu.", - "configure-alias":"Konfiguruj alias '{{alias}}'", - "no-entity-views-matching":"Brak widoków encji pasujących do '{{entity}}'.", - "public":"Publiczny", - "alias":"Alias", - "alias-required":"Wymagany jest alias widoku encji.", - "remove-alias":"Usuń alias widoku encji", - "add-alias":"Dodaj alias widoku encji", - "name-starts-with":"Wyrażenie nazwy widoku encji", - "help-text":"Użyj '%' według potrzeb: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with'.", - "entity-view-list":"Lista widoków encji", - "use-entity-view-name-filter":"Użyj filtra", - "entity-view-list-empty":"Brak wybranych widoków encji.", - "entity-view-name-filter-required":"Wymagany jest filtr nazwy widoku encji.", - "entity-view-name-filter-no-entity-view-matched":"Nie znaleziono widoków encji rozpoczynających się od '{{entityView}}'.", - "add":"Dodaj widok encji", - "entity-view-public":"Widok encji jest publiczny", - "assign-to-customer":"Przypisz do klienta", - "assign-entity-view-to-customer":"Przypisz Widoki Encji Do Klienta", - "assign-entity-view-to-customer-text":"Wybierz widoki encji do przypisania do klienta", - "assign-entity-view-to-edge-title":"Przypisz Widoki Encji Do Krawędzi", - "no-entity-views-text":"Nie znaleziono widoków encji", - "assign-to-customer-text":"Wybierz klienta, do którego chcesz przypisać widoki encji", - "entity-view-details":"Szczegóły widoku encji", - "add-entity-view-text":"Dodaj nowy widok encji", - "delete":"Usuń widok encji", - "assign-entity-views":"Przypisz widoki encji", - "assign-entity-views-text":"Przypisz { count, plural, =1 {1 widok encji} other {# widoków encji} } do klienta", - "delete-entity-views":"Usuń widoki encji", - "unassign-from-customer":"Odłącz od klienta", - "unassign-entity-views":"Odłącz widoki encji", - "unassign-entity-views-action-title":"Odłącz { count, plural, =1 {1 widok encji} other {# widoków encji} } od klienta", - "assign-new-entity-view":"Przypisz nowy widok encji", - "delete-entity-view-title":"Czy na pewno chcesz usunąć widok encji '{{entityViewName}}'?", - "delete-entity-view-text":"Bądź ostrożny, po potwierdzeniu widok encji i wszystkie związane z nim dane staną się nieodwracalnie utracone.", - "delete-entity-views-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 widok encji} other {# widoków encji} }?", - "delete-entity-views-action-title":"Usuń { count, plural, =1 {1 widok encji} other {# widoków encji} }", - "delete-entity-views-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane widoki encji zostaną usunięte, a wszystkie związane z nimi dane staną się nieodwracalnie utracone.", - "unassign-entity-view-title":"Czy na pewno chcesz odłączyć widok encji '{{entityViewName}}'?", - "unassign-entity-view-text":"Po potwierdzeniu widok encji zostanie odłączony i nie będzie dostępny dla klienta.", - "unassign-entity-view":"Odłącz widok encji", - "unassign-entity-views-title":"Czy na pewno chcesz odłączyć { count, plural, =1 {1 widok encji} other {# widoków encji} }?", - "unassign-entity-views-text":"Po potwierdzeniu wszystkie wybrane widoki encji zostaną odłączone i nie będą dostępne dla klienta.", - "entity-view-type":"Typ widoku encji", - "entity-view-type-required":"Wymagany jest typ widoku encji.", - "select-entity-view-type":"Wybierz typ widoku encji", - "enter-entity-view-type":"Wprowadź typ widoku encji", - "any-entity-view":"Dowolny widok encji", - "no-entity-view-types-matching":"Brak typów widoków encji pasujących do '{{entitySubtype}}'.", - "entity-view-type-list-empty":"Brak wybranych typów widoków encji.", - "entity-view-types":"Typy widoków encji", - "created-time":"Czas utworzenia", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków.", - "type-max-length":"Typ widoku jednostki powinien mieć mniej niż 256 znaków.", - "description":"Opis", - "events":"Zdarzenia", - "details":"Szczegóły", - "copyId":"Kopiuj identyfikator widoku jednostki", - "idCopiedMessage":"Identyfikator widoku jednostki został skopiowany do schowka.", - "assignedToCustomer":"Przypisane do klienta", - "unable-entity-view-device-alias-title":"Nie można usunąć aliasu widoku jednostki", - "unable-entity-view-device-alias-text":"Alias urządzenia '{{entityViewAlias}}' nie może zostać usunięty, ponieważ jest używany przez następujące widgety:
{{widgetsList}}", - "select-entity-view":"Wybierz widok jednostki", - "make-public":"Udostępnij widok jednostki publicznie", - "make-private":"Udostępnij widok jednostki prywatnie", - "start-date":"Data rozpoczęcia", - "start-ts":"Czas rozpoczęcia", - "end-date":"Data zakończenia", - "end-ts":"Czas zakończenia", - "date-limits":"Ograniczenia dat", - "client-attributes":"Atrybuty klienta", - "shared-attributes":"Współdzielone atrybuty", - "server-attributes":"Atrybuty serwera", - "timeseries":"Szereg czasowy", - "client-attributes-placeholder":"Atrybuty klienta", - "shared-attributes-placeholder":"Współdzielone atrybuty", - "server-attributes-placeholder":"Atrybuty serwera", - "timeseries-placeholder":"Szereg czasowy", - "target-entity":"Docelowa jednostka", - "attributes-propagation":"Propagacja atrybutów", - "attributes-propagation-hint":"Widok jednostki będzie automatycznie kopiować określone atrybuty z Docelowej Jednostki za każdym razem, gdy zapisujesz lub aktualizujesz ten widok jednostki. Ze względów wydajnościowych atrybuty Docelowej Jednostki nie są propagowane do widoku jednostki przy każdej zmianie atrybutu. Możesz włączyć automatyczną propagację, konfigurując węzeł reguły „kopiuj do widoku” w swoim łańcuchu reguł i łącząc wiadomości „Post attributes” i „Attributes Updated” z nowym węzłem reguły.", - "timeseries-data":"Dane szeregu czasowego", - "timeseries-data-hint":"Skonfiguruj klucze danych szeregu czasowego Docelowej Jednostki, które będą dostępne w widoku jednostki. Te dane szeregu czasowego są tylko do odczytu.", - "search":"Szukaj widoków jednostki", - "selected-entity-views":"{ count, plural, =1 {1 widok jednostki} other {# widoki jednostki} } wybrano", - "make-public-entity-view-title":"Czy na pewno chcesz udostępnić widok jednostki '{{entityViewName}}' publicznie?", - "make-public-entity-view-text":"Po potwierdzeniu widok jednostki i wszystkie jego dane staną się publiczne i dostępne dla innych.", - "make-private-entity-view-title":"Czy na pewno chcesz uczynić widok jednostki '{{entityViewName}}' prywatnym?", - "make-private-entity-view-text":"Po potwierdzeniu widok jednostki i wszystkie jego dane staną się prywatne i nie będą dostępne dla innych.", - "assign-entity-view-to-edge":"Przypisz widok jednostki do urządzenia", - "assign-entity-view-to-edge-text":"Wybierz widoki jednostki do przypisania do urządzenia", - "unassign-entity-view-from-edge-title":"Czy na pewno chcesz odpiąć widok jednostki '{{entityViewName}}'?", - "unassign-entity-view-from-edge-text":"Po potwierdzeniu widok jednostki zostanie odpięty i nie będzie dostępny dla urządzenia.", - "unassign-entity-views-from-edge-action-title":"Odpiąć { count, plural, =1 {1 widok jednostki} other {# widoki jednostki} } od urządzenia", - "unassign-entity-view-from-edge":"Odpiąć widok jednostki", - "unassign-entity-views-from-edge-title":"Czy na pewno chcesz odpiąć { count, plural, =1 {1 widok jednostki} other {# widoki jednostki} }?", - "unassign-entity-views-from-edge-text":"Po potwierdzeniu wszystkie wybrane widoki jednostki zostaną odpięte i nie będą dostępne dla urządzenia." + "entity-view": { + "all": "Wszystko", + "all-entity-views": "Wszystkie widoki obiektów", + "groups": "Grupy", + "shared": "Wspólny", + "entity-view": "Widok obiektu", + "entity-view-required": "Widok obiektu jest wymagany.", + "entity-views": "Widoki obiektów", + "management": "Zarządzanie widokiem obiektu", + "view-entity-views": "Wyświetl widoki obiektów", + "entity-view-alias": "Alias widoku obiektu", + "aliases": "Aliasy widoków obiektów", + "no-alias-matching": "Nie znaleziono '{{alias}}'.", + "no-aliases-found": "Nie znaleziono aliasów.", + "no-key-matching": "Nie znaleziono '{{key}}'.", + "no-keys-found": "Nie znaleziono kluczy.", + "create-new-alias": "Utwórz nowy!", + "create-new-key": "Utwórz nowy!", + "duplicate-alias-error": "Znaleziono zduplikowany alias '{{alias}}'.
Aliasy widoków obiektów muszą być unikalne w panelu.", + "configure-alias": "Skonfiguruj alias '{{alias}}'", + "no-entity-views-matching": "Nie znaleziono widoków obiektów pasujących do '{{entity}}'.", + "public": "Publiczny", + "alias": "Alias", + "alias-required": "Alias widoku obiektu jest wymagany.", + "remove-alias": "Usuń alias widoku obiektu", + "add-alias": "Dodaj alias widoku obiektu", + "name-starts-with": "Wyrażenie nazwy widoku obiektu", + "help-text": "Użyj „%” w zależności od potrzeb: „%entity-view_name_contains%”, „%entity-view_name_ends”, „entity-view_starts_with”.", + "entity-view-list": "Lista widoków obiektów", + "use-entity-view-name-filter": "Użyj filtra", + "entity-view-list-empty": "Nie wybrano żadnych widoków obiektów.", + "entity-view-name-filter-required": "Filtr nazwy widoku obiektu jest wymagany.", + "entity-view-name-filter-no-entity-view-matched": "Nie znaleziono widoków obiektów zaczynających się od '{{entityView}}'.", + "add": "Dodaj widok obiektu", + "entity-view-public": "Widok obiektu jest publiczny", + "assign-to-customer": "Przypisz do klienta", + "assign-entity-view-to-customer": "Przypisz widoki obiektów do klienta", + "assign-entity-view-to-customer-text": "Wybierz widoki obiektów, które chcesz przypisać klientowi", + "no-entity-views-text": "Nie znaleziono widoków obiektów", + "assign-to-customer-text": "Wybierz klienta, aby przypisać widoki obiektów", + "entity-view-details": "Szczegóły widoku obiektu", + "add-entity-view-text": "Dodaj nowy widok obiektu", + "delete": "Usuń widok obiektu", + "assign-entity-views": "Przypisz widoki obiektów", + "assign-entity-views-text": "Przypisz {count, plural, =1 {1 widok obiektu} other {# widoków obiektów} }", + "delete-entity-views": "Usuń widoki obiektów", + "make-public": "Ustaw widok obiektu jako publiczny", + "make-private": "Ustaw widok obiektu jako prywatny", + "unassign-from-customer": "Usuń przypisanie od klienta", + "unassign-entity-views": "Usuń przypisanie widoków obiektów", + "unassign-entity-views-action-title": "Anuluj przypisanie {count, plural, =1 {1 widoku obiektu} other {# widoków obiektów} }", + "assign-new-entity-view": "Przypisz nowy widok obiektu", + "delete-entity-view-title": "Czy na pewno chcesz usunąć widok obiektu '{{entityViewName}}'?", + "delete-entity-view-text": "Bądź ostrożny, po potwierdzeniu widok obiektu i wszystkie związane z nim dane nie będzie można odzyskać.", + "delete-entity-views-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 widok obiektu} other {# widoków obiektów} }?", + "delete-entity-views-action-title": "Usuń {count, plural, =1 {1 widok obiektu} other {# widoków obiektów} }", + "delete-entity-views-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane widoki obiektów zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "make-public-entity-view-title": "Czy na pewno chcesz uczynić widok obiektu '{{entityViewName}}' publicznym?", + "make-public-entity-view-text": "Po potwierdzeniu widok obiektu i wszystkie jego dane zostaną upublicznione i dostępne dla innych.", + "make-private-entity-view-title": "Czy na pewno chcesz uczynić widok obiektu '{{entityViewName}}' prywatnym?", + "make-private-entity-view-text": "Po potwierdzeniu widok obiektu i wszystkie jego dane staną się prywatne i nie będą dostępne dla innych.", + "unassign-entity-view-title": "Czy na pewno chcesz cofnąć przypisanie widoku obiektu '{{entityViewName}}'?", + "unassign-entity-view-text": "Po potwierdzeniu widok obiektu zostanie usunięty i nie będzie dostępny dla klienta.", + "unassign-entity-view": "Usuń przypisanie widoku obiektu", + "unassign-entity-views-title": "Czy na pewno chcesz cofnąć przypisanie { count, plural, =1 {1 widoku obiektu} other {# widoków obiektów} }?", + "unassign-entity-views-text": "Po potwierdzeniu wszystkie wybrane widoki obiektów zostaną usunięte i nie będą dostępne dla klienta.", + "entity-view-type": "Typ widoku obiektu", + "entity-view-type-required": "Typ widoku obiektu jest wymagany.", + "select-entity-view-type": "Wybierz typ widoku obiektu", + "enter-entity-view-type": "Wprowadź typ widoku obiektu", + "any-entity-view": "Dowolny widok obiektu", + "no-entity-view-types-matching": "Nie znaleziono typów widoków obiektów pasujących do '{{entitySubtype}}'.", + "entity-view-type-list-empty": "Nie wybrano typów widoków obiektów.", + "entity-view-types": "Typy widoków obiektów", + "created-time": "Czas utworzenia", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "type-max-length": "Typ widoku obiektu powinien być mniejszy niż 256", + "description": "Opis", + "events": "Zdarzenia", + "details": "Szczegóły", + "copyId": "Skopiuj identyfikator widoku obiektu", + "idCopiedMessage": "Identyfikator widoku obiektu został skopiowany do schowka", + "assignedToCustomer": "Przypisany do klienta", + "unable-entity-view-device-alias-title": "Nie można usunąć aliasu widoku obiektu", + "unable-entity-view-device-alias-text": "Alias urządzenia '{{entityViewAlias}}' nie może zostać usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", + "select-entity-view": "Wybierz widok obiektu", + "start-date": "Data rozpoczęcia", + "start-ts": "Czas rozpoczęcia", + "end-date": "Data końcowa", + "end-ts": "Czas końcowy", + "date-limits": "Limity dat", + "client-attributes": "Atrybuty klienta", + "shared-attributes": "Atrybuty wspólne", + "server-attributes": "Atrybuty serwera", + "timeseries": "Szereg czasowy", + "client-attributes-placeholder": "Atrybuty klienta", + "shared-attributes-placeholder": "Atrybuty wspólne", + "server-attributes-placeholder": "Atrybuty serwera", + "timeseries-placeholder": "Szereg czasowy", + "target-entity": "Obiekt docelowy", + "attributes-propagation": "Propagacja atrybutów", + "attributes-propagation-hint": "Widok obiektu automatycznie skopiuje określone atrybuty z obiektu docelowego za każdym razem, gdy zapiszesz lub zaktualizujesz ten widok obiektu. Ze względu na wydajność atrybuty obiektu docelowego nie są propagowane do widoku obiektu przy każdej zmianie atrybutu. Możesz włączyć automatyczną propagację, konfigurując węzeł reguły „skopiuj, aby wyświetlić” w łańcuchu reguł i łącząc wiadomości „Opublikuj atrybuty” i „Atrybuty zaktualizowane” z nowym węzłem reguły.", + "timeseries-data": "Dane szeregów czasowych", + "timeseries-data-hint": "Skonfiguruj klucze danych szeregów czasowych obiektu docelowego, które będą dostępne w widoku obiektu. Dane szeregów czasowych są tylko do odczytu.", + "select-group-to-add": "Wybierz grupę docelową, aby dodać wybrane widoki obiektów", + "select-group-to-move": "Wybierz grupę docelową, aby przenieść wybrane widoki obiektów", + "remove-entity-views-from-group": "Czy na pewno chcesz usunąć {count, plural, =1 {1 widok obiektu} other {# widoków obiektów} } z grupy '{{entityGroup}}'?", + "group": "Grupa widoków obiektów", + "list-of-groups": "{count, plural, =1 {Jedna grupa widoków obiektów} other {Lista # grup widoków obiektów} }", + "group-name-starts-with": "Grupy widoków obiektów, których nazwy zaczynają się od '{{prefix}}'", + "search": "Wyszukaj widoki obiektów", + "selected-entity-views": "Wybrano {count, plural, =1 {1 widok obiektu} other {# widoków obiektów} }", + "assign-entity-view-to-edge": "Przypisz widoki obiektów do Edge", + "assign-entity-view-to-edge-text": "Wybierz widoki obiektów, które chcesz przypisać do Edge", + "unassign-entity-view-from-edge-title": "Czy na pewno chcesz cofnąć przypisanie widoku obiektu '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "Po potwierdzeniu widok obiektu zostanie usunięty i nie będzie dostępny dla Edge.", + "unassign-entity-views-from-edge-action-title": "Anuluj przypisanie {count, plural, =1 {1 widoku obiektu} other {# widoków obiektów} } do Edge", + "unassign-entity-view-from-edge": "Usuń przypisanie widoku obiektu", + "unassign-entity-views-from-edge-title": "Czy na pewno chcesz cofnąć przypisanie {count, plural, =1 {1 widoku obiektu} other {# widoków obiektów} }?", + "unassign-entity-views-from-edge-text": "Po potwierdzeniu wszystkie wybrane widoki obiektów zostaną anulowane i nie będą dostępne dla urządzenia Edge." }, - "event":{ - "event-type":"Typ zdarzenia", - "events-filter":"Filtr zdarzeń", - "clean-events":"Wyczyść zdarzenia", - "type-error":"Błąd", - "type-lc-event":"Zdarzenie cyklu życia", - "type-stats":"Statystyki", - "type-debug-rule-node":"Debug", - "type-debug-rule-chain":"Debug", - "no-events-prompt":"Nie znaleziono zdarzeń", - "error":"Błąd", - "alarm":"Alarm", - "event-time":"Czas zdarzenia", - "server":"Serwer", - "body":"Treść", - "method":"Metoda", - "type":"Typ", - "message":"Wiadomość", - "message-id":"Identyfikator wiadomości", - "copy-message-id":"Kopiuj identyfikator wiadomości", - "message-type":"Typ wiadomości", - "data-type":"Typ danych", - "relation-type":"Typ relacji", - "metadata":"Metadane", - "data":"Dane", - "event":"Zdarzenie", - "status":"Status", - "success":"Sukces", - "failed":"Niepowodzenie", - "messages-processed":"Wiadomości przetworzone", - "max-messages-processed":"Maksymalna ilość wiadomości przetworzonych", - "min-messages-processed":"Minimalna ilość wiadomości przetworzonych", - "errors-occurred":"Wystąpiły błędy", - "max-errors-occurred":"Maksymalna ilość wystąpiłych błędów", - "min-errors-occurred":"Minimalna ilość wystąpiłych błędów", - "min-value":"Minimalna wartość to 0.", - "all-events":"Wszystkie", - "has-error":"Zawiera błąd", - "entity-id":"Identyfikator jednostki", - "copy-entity-id":"Kopiuj identyfikator jednostki", - "entity-type":"Typ jednostki", - "clear-filter":"Wyczyść filtr", - "clear-request-title":"Wyczyść wszystkie zdarzenia", - "clear-request-text":"Czy na pewno chcesz wyczyścić wszystkie zdarzenia?", - "started":"Rozpoczęte", - "updated":"Zaktualizowane", - "stopped":"Zatrzymane" + "event": { + "events": "Wydarzenia", + "event-type": "Typ wydarzenia", + "events-filter": "Filtr zdarzeń", + "clean-events": "Wyczyść zdarzenia", + "type-error": "Błąd", + "type-lc-event": "Wydarzenie cyklu życia", + "type-stats": "Statystyka", + "type-debug-converter": "Debug", + "type-debug-integration": "Debug", + "type-debug-rule-node": "Debug", + "type-debug-rule-chain": "Debug", + "no-events-prompt": "Nie znaleziono żadnych wydarzeń", + "error": "Błąd", + "alarm": "Alarm", + "event-time": "Czas wydarzenia", + "server": "serwer", + "body": "Ciało", + "method": "metoda", + "type": "Typ", + "in": "W", + "out": "Na zewnątrz", + "metadata": "Metadane", + "message": "Wiadomość", + "entity": "Obiekt", + "message-id": "ID wiadomości", + "copy-message-id": "Skopiuj identyfikator wiadomości", + "message-type": "Typ wiadomości", + "data-type": "Typ danych", + "relation-type": "Typ relacji", + "data": "Dane", + "event": "Wydarzenie", + "status": "Status", + "success": "Powodzenie", + "failed": "Niepowodzenie", + "messages-processed": "Wiadomości przetworzone", + "max-messages-processed": "Maksymalna liczba przetworzonych wiadomości", + "min-messages-processed": "Przetworzona minimalna liczba wiadomości", + "errors-occurred": "Wystąpiły błędy", + "max-errors-occurred": "Wystąpiło maksimum błędów", + "min-errors-occurred": "Wystąpiło minimum błędów", + "min-value": "Minimalna wartość to 0.", + "all-events": "Wszystko", + "has-error": "Zawiera błąd", + "entity-id": "Identyfikator jednostki", + "copy-entity-id": "Skopiuj identyfikator jednostki", + "entity-type": "Typ encji", + "clear-filter": "Czysty filtr", + "clear-request-title": "Wyczyść wszystkie zdarzenia", + "clear-request-text": "Czy na pewno chcesz wyczyścić wszystkie zdarzenia?", + "started": "Rozpoczęto", + "updated": "Zaktualizowano", + "stopped": "Zatrzymano" }, - "extension":{ - "extensions":"Rozszerzenia", - "selected-extensions":"{ count, plural, =1 {1 rozszerzenie} other {# rozszerzeń} } wybranych", - "type":"Typ", - "key":"Klucz", - "value":"Wartość", - "id":"Identyfikator", - "extension-id":"Identyfikator rozszerzenia", - "extension-type":"Typ rozszerzenia", - "transformer-json":"JSON *", - "unique-id-required":"Identyfikator rozszerzenia już istnieje.", - "delete":"Usuń rozszerzenie", - "add":"Dodaj rozszerzenie", - "edit":"Edytuj rozszerzenie", - "delete-extension-title":"Czy na pewno chcesz usunąć rozszerzenie '{{extensionId}}'?", - "delete-extension-text":"Bądź ostrożny, po potwierdzeniu rozszerzenie i wszystkie związane z nim dane staną się nieodwracalnie utracone.", - "delete-extensions-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 rozszerzenie} other {# rozszerzeń} }?", - "delete-extensions-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane rozszerzenia zostaną usunięte.", - "converters":"Konwertery", - "converter-id":"Identyfikator konwertera", - "configuration":"Konfiguracja", - "converter-configurations":"Konfiguracje konwertera", - "token":"Token bezpieczeństwa", - "add-converter":"Dodaj konwerter", - "add-config":"Dodaj konfigurację konwertera", - "device-name-expression":"Wyrażenie nazwy urządzenia", - "device-type-expression":"Wyrażenie typu urządzenia", - "custom":"Niestandardowe", - "to-double":"Do Double", - "transformer":"Transformator", - "json-required":"Wymagany jest JSON transformatora.", - "json-parse":"Nie można sparsować JSON transformatora.", - "attributes":"Atrybuty", - "add-attribute":"Dodaj atrybut", - "add-map":"Dodaj element mapowania", - "timeseries":"Szereg czasowy", - "add-timeseries":"Dodaj szereg czasowy", - "field-required":"Pole jest wymagane", - "brokers":"Brokerzy", - "add-broker":"Dodaj brokera", - "host":"Host", - "port":"Port", - "port-range":"Port powinien mieć zakres od 1 do 65535.", - "ssl":"SSL", - "credentials":"Dane uwierzytelniające", - "username":"Nazwa użytkownika", - "password":"Hasło", - "retry-interval":"Interwał ponawiania w milisekundach", - "anonymous":"Anonimowy", - "basic":"Podstawowy", - "pem":"PEM", - "ca-cert":"Plik certyfikatu CA *", - "private-key":"Plik klucza prywatnego *", - "cert":"Plik certyfikatu *", - "no-file":"Nie wybrano pliku.", - "drop-file":"Upuść plik lub kliknij, aby wybrać plik do przesłania.", - "mapping":"Mapowanie", - "topic-filter":"Filtr tematów", - "converter-type":"Typ konwertera", - "converter-json":"Json", - "json-name-expression":"Wyrażenie nazwy urządzenia JSON", - "topic-name-expression":"Wyrażenie nazwy tematu urządzenia", - "json-type-expression":"Wyrażenie typu urządzenia JSON", - "topic-type-expression":"Wyrażenie typu tematu urządzenia", - "attribute-key-expression":"Wyrażenie klucza atrybutu", - "attr-json-key-expression":"Wyrażenie klucza JSON atrybutu", - "attr-topic-key-expression":"Wyrażenie klucza tematu atrybutu", - "request-id-expression":"Wyrażenie identyfikatora żądania", - "request-id-json-expression":"Wyrażenie identyfikatora JSON żądania", - "request-id-topic-expression":"Wyrażenie identyfikatora tematu żądania", - "response-topic-expression":"Wyrażenie tematu odpowiedzi", - "value-expression":"Wyrażenie wartości", - "topic":"Temat", - "timeout":"Timeout w milisekundach", - "converter-json-required":"Wymagany jest JSON konwertera.", - "converter-json-parse":"Nie można sparsować JSON konwertera.", - "filter-expression":"Wyrażenie filtru", - "connect-requests":"Żądania połączenia", - "add-connect-request":"Dodaj żądanie połączenia", - "disconnect-requests":"Żądania rozłączenia", - "add-disconnect-request":"Dodaj żądanie rozłączenia", - "attribute-requests":"Żądania atrybutów", - "add-attribute-request":"Dodaj żądanie atrybutu", - "attribute-updates":"Aktualizacje atrybutów", - "add-attribute-update":"Dodaj aktualizację atrybutu", - "server-side-rpc":"RPC po stronie serwera", - "add-server-side-rpc-request":"Dodaj żądanie RPC po stronie serwera", - "device-name-filter":"Filtr nazwy urządzenia", - "attribute-filter":"Filtr atrybutu", - "method-filter":"Filtr metody", - "request-topic-expression":"Wyrażenie tematu żądania", - "response-timeout":"Timeout odpowiedzi w milisekundach", - "topic-expression":"Wyrażenie tematu", - "client-scope":"Zakres klienta", - "add-device":"Dodaj urządzenie", - "opc-server":"Serwery OPC", - "opc-add-server":"Dodaj serwer OPC", - "opc-add-server-prompt":"Proszę dodać serwer OPC", - "opc-application-name":"Nazwa aplikacji", - "opc-application-uri":"URI aplikacji", - "opc-scan-period-in-seconds":"Okres skanowania w sekundach", - "opc-security":"Bezpieczeństwo", - "opc-identity":"Tożsamość", - "opc-keystore":"Schronisko klucza", - "opc-type":"Typ", - "opc-keystore-type":"Typ schroniska klucza", - "opc-keystore-location":"Lokalizacja *", - "opc-keystore-password":"Hasło", - "opc-keystore-alias":"Alias", - "opc-keystore-key-password":"Hasło klucza", - "opc-device-node-pattern":"Wzorzec węzła urządzenia", - "opc-device-name-pattern":"Wzorzec nazwy urządzenia", - "modbus-server":"Serwery/slave'y Modbus", - "modbus-add-server":"Dodaj serwer/slave'a Modbus", - "modbus-add-server-prompt":"Proszę dodać serwer/slave'a Modbus", - "modbus-transport":"Transport", - "modbus-tcp-reconnect":"Automatyczne ponowne połączenie", - "modbus-rtu-over-tcp":"RTU przez TCP", - "modbus-port-name":"Nazwa portu szeregowego", - "modbus-encoding":"Kodowanie", - "modbus-parity":"Parzystość", - "modbus-baudrate":"Prędkość transmisji", - "modbus-databits":"Bity danych", - "modbus-stopbits":"Bity stopu", - "modbus-databits-range":"Bity danych powinny mieć zakres od 7 do 8.", - "modbus-stopbits-range":"Bity stopu powinny mieć zakres od 1 do 2.", - "modbus-unit-id":"Identyfikator jednostki", - "modbus-unit-id-range":"Identyfikator jednostki powinien mieć zakres od 1 do 247.", - "modbus-device-name":"Nazwa urządzenia", - "modbus-poll-period":"Okres sondowania (ms)", - "modbus-attributes-poll-period":"Okres sondowania atrybutów (ms)", - "modbus-timeseries-poll-period":"Okres sondowania szeregów czasowych (ms)", - "modbus-poll-period-range":"Okres sondowania powinien być wartością dodatnią.", - "modbus-tag":"Tag", - "modbus-function":"Funkcja", - "modbus-register-address":"Adres rejestru", - "modbus-register-address-range":"Adres rejestru powinien mieć zakres od 0 do 65535.", - "modbus-register-bit-index":"Indeks bitu", - "modbus-register-bit-index-range":"Indeks bitu powinien mieć zakres od 0 do 15.", - "modbus-register-count":"Liczba rejestrów", - "modbus-register-count-range":"Liczba rejestrów powinna być wartością dodatnią.", - "modbus-byte-order":"Kolejność bajtów", - "sync":{ - "status":"Status", - "sync":"Synchronizowane", - "not-sync":"Niesynchronizowane", - "last-sync-time":"Ostatnia synchronizacja", - "not-available":"Niedostępne" + "extension": { + "extensions": "Rozszerzenia", + "selected-extensions": "Wybrano {count, plural, =1 {1 rozszerzenie} other {# rozszerzeń} }", + "type": "Typ", + "key": "Klucz", + "value": "Wartość", + "id": "ID", + "extension-id": "Identyfikator rozszerzenia", + "extension-type": "Typ rozszerzenia", + "transformer-json": "JSON *", + "unique-id-required": "Bieżący identyfikator rozszerzenia już istnieje.", + "delete": "Usuń rozszerzenie", + "add": "Dodaj rozszerzenie", + "edit": "Edytuj rozszerzenie", + "view": "Wyświetl rozszerzenie", + "delete-extension-title": "Czy na pewno chcesz usunąć rozszerzenie '{{extensionId}}'?", + "delete-extension-text": "Bądź ostrożny, po potwierdzeniu rozszerzenia i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-extensions-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 rozszerzenie} other {# rozszerzeń} }?", + "delete-extensions-text": "Uważaj, po potwierdzeniu wszystkie wybrane rozszerzenia zostaną usunięte.", + "converters": "Konwertery", + "converter-id": "Identyfikator konwertera", + "configuration": "Konfiguracja", + "converter-configurations": "Konfiguracje konwertera", + "token": "Token bezpieczeństwa", + "add-converter": "Dodaj konwerter", + "add-config": "Dodaj konfigurację konwertera", + "device-name-expression": "Wyrażenie nazwy urządzenia", + "device-type-expression": "Wyrażenie typu urządzenia", + "custom": "Zwyczaj", + "to-double": "Podwoić", + "transformer": "Transformator", + "json-required": "Transformator JSON jest wymagany.", + "json-parse": "Nie można przeanalizować pliku JSON transformatora.", + "attributes": "Atrybuty", + "add-attribute": "Dodaj atrybut", + "add-map": "Dodaj element mapowania", + "timeseries": "Szereg czasowy", + "add-timeseries": "Dodaj szeregi czasowe", + "field-required": "To pole jest wymagane", + "brokers": "Brokerzy", + "add-broker": "Dodaj brokera", + "host": "Host", + "port": "Port", + "port-range": "Port powinien należeć do zakresu od 1 do 65535.", + "ssl": "Ssl", + "credentials": "Dane uwierzytelniające", + "username": "Nazwa użytkownika", + "password": "Hasło", + "retry-interval": "Interwał ponawiania prób w milisekundach", + "sas": "Podpis dostępu współdzielonego", + "anonymous": "Anonimowy", + "basic": "Podstawowy", + "pem": "PEM", + "ca-cert": "Plik certyfikatu urzędu certyfikacji *", + "private-key": "Plik klucza prywatnego *", + "cert": "Plik certyfikatu *", + "no-file": "Nie wybrano pliku.", + "drop-file": "Upuść plik lub kliknij, aby wybrać plik do przesłania.", + "mapping": "Mapowanie", + "topic-filter": "Filtr tematyczny", + "converter-type": "Typ konwertera", + "converter-json": "Jsona", + "json-name-expression": "Wyrażenie JSON nazwy urządzenia", + "topic-name-expression": "Wyrażenie tematu nazwy urządzenia", + "json-type-expression": "Wyrażenie JSON typu urządzenia", + "topic-type-expression": "Wyrażenie tematu typu urządzenia", + "attribute-key-expression": "Wyrażenie klucza atrybutu", + "attr-json-key-expression": "Wyrażenie JSON klucza atrybutu", + "attr-topic-key-expression": "Atrybut kluczowego wyrażenia tematu", + "request-id-expression": "Wyrażenie identyfikatora żądania", + "request-id-json-expression": "Wyrażenie JSON identyfikatora żądania", + "request-id-topic-expression": "Wyrażenie tematu identyfikatora żądania", + "response-topic-expression": "Wyrażenie tematu odpowiedzi", + "value-expression": "Wyrażenie wartości", + "topic": "Temat", + "timeout": "Limit czasu w milisekundach", + "converter-json-required": "Konwerter json jest wymagany.", + "converter-json-parse": "Nie można przeanalizować konwertera JSON.", + "filter-expression": "Wyrażenie filtru", + "connect-requests": "Połącz żądania", + "add-connect-request": "Dodaj prośbę o połączenie", + "disconnect-requests": "Rozłącz żądania", + "add-disconnect-request": "Dodaj prośbę o rozłączenie", + "attribute-requests": "Żądania atrybutów", + "add-attribute-request": "Dodaj żądanie atrybutu", + "attribute-updates": "Aktualizacje atrybutów", + "add-attribute-update": "Dodaj aktualizację atrybutu", + "server-side-rpc": "RPC po stronie serwera", + "add-server-side-rpc-request": "Dodaj żądanie RPC po stronie serwera", + "device-name-filter": "Filtr nazw urządzeń", + "attribute-filter": "Filtr atrybutów", + "method-filter": "Filtr metody", + "request-topic-expression": "Poproś o wyrażenie tematu", + "response-timeout": "Limit czasu odpowiedzi w milisekundach", + "topic-expression": "Wyrażenie tematyczne", + "client-scope": "Zakres klienta", + "add-device": "Dodaj urządzenie", + "opc-server": "Serwery", + "opc-add-server": "Dodaj serwer", + "opc-add-server-prompt": "Proszę dodać serwer", + "opc-application-name": "Nazwa aplikacji", + "opc-application-uri": "Nazwa aplikacji", + "opc-scan-period-in-seconds": "Okres skanowania w sekundach", + "opc-security": "Bezpieczeństwo", + "opc-identity": "Tożsamość", + "opc-keystore": "Magazyn kluczy", + "opc-type": "Typ", + "opc-keystore-type": "Typ", + "opc-keystore-location": "Lokalizacja *", + "opc-keystore-password": "Hasło", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Kluczowe hasło", + "opc-device-node-pattern": "Wzór węzła urządzenia", + "opc-device-name-pattern": "Wzorzec nazwy urządzenia", + "modbus-server": "Serwery/niewolnicy", + "modbus-add-server": "Dodaj serwer/podrzędny", + "modbus-add-server-prompt": "Proszę dodać serwer/podrzędny", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Automatycznie połącz ponownie", + "modbus-rtu-over-tcp": "RTU przez TCP", + "modbus-port-name": "Nazwa portu szeregowego", + "modbus-encoding": "Kodowanie", + "modbus-parity": "Parytet", + "modbus-baudrate": "Szybkość transmisji", + "modbus-databits": "Bity danych", + "modbus-stopbits": "Zatrzymaj bity", + "modbus-databits-range": "Bity danych powinny mieścić się w zakresie od 7 do 8.", + "modbus-stopbits-range": "Bity stopu powinny należeć do zakresu od 1 do 2.", + "modbus-unit-id": "Identyfikator jednostki", + "modbus-unit-id-range": "Identyfikator jednostki powinien mieścić się w zakresie od 1 do 247.", + "modbus-device-name": "Nazwa urządzenia", + "modbus-poll-period": "Okres odpytywania (ms)", + "modbus-attributes-poll-period": "Atrybuty Okres odpytywania (ms)", + "modbus-timeseries-poll-period": "Okres odpytywania szeregów czasowych (ms)", + "modbus-poll-period-range": "Okres odpytywania powinien mieć wartość dodatnią.", + "modbus-tag": "Etykietka", + "modbus-function": "Funkcjonować", + "modbus-register-address": "Adres rejestracyjny", + "modbus-register-address-range": "Adres rejestru powinien mieścić się w zakresie od 0 do 65535.", + "modbus-register-bit-index": "Indeks bitowy", + "modbus-register-bit-index-range": "Indeks bitowy powinien mieścić się w zakresie od 0 do 15.", + "modbus-register-count": "Liczba zarejestrowanych", + "modbus-register-count-range": "Liczba rejestrów powinna mieć wartość dodatnią.", + "modbus-byte-order": "Kolejność bajtów", + "sync": { + "status": "Status", + "sync": "Synchronizuj", + "not-sync": "Brak synchronizacji", + "last-sync-time": "Czas ostatniej synchronizacji", + "not-available": "Niedostępne" }, - "export-extensions-configuration":"Eksportuj konfigurację rozszerzeń", - "import-extensions-configuration":"Importuj konfigurację rozszerzeń", - "import-extensions":"Importuj rozszerzenia", - "import-extension":"Importuj rozszerzenie", - "export-extension":"Eksportuj rozszerzenie", - "file":"Plik rozszerzeń", - "invalid-file-error":"Błąd - nieprawidłowy plik rozszerzenia" + "export-extensions-configuration": "Eksportuj konfigurację rozszerzeń", + "import-extensions-configuration": "Importuj konfigurację rozszerzeń", + "import-extensions": "Importuj rozszerzenia", + "import-extension": "Rozszerzenie importu", + "export-extension": "Rozszerzenie eksportu", + "file": "Plik rozszerzeń", + "invalid-file-error": "Nieprawidłowy plik rozszerzenia", + "text": "TEKST", + "json": "JSON", + "binary": "DWÓJKOWY", + "hex": "KLĄTWA" }, - "feature":{ - "advanced-features":"Zaawansowane funkcje" + "feature": { + "advanced-features": "Zaawansowane funkcje" }, - "filter":{ - "add":"Dodaj filtr", - "edit":"Edytuj filtr", - "name":"Nazwa filtra", - "name-required":"Wymagana nazwa filtra.", - "duplicate-filter":"Filtr o tej samej nazwie już istnieje.", - "filters":"Filtry", - "unable-delete-filter-title":"Nie można usunąć filtra", - "unable-delete-filter-text":"Nie można usunąć filtra '{{filter}}', ponieważ jest używany przez następujące widżety:
{{widgetsList}}", - "duplicate-filter-error":"Znaleziono zduplikowany filtr '{{filter}}'.
Filtry muszą być unikalne w obrębie pulpitu nawigacyjnego.", - "missing-key-filters-error":"Kluczowe filtry są brakujące w filtrze '{{filter}}'.", - "filter":"Filtr", - "editable":"Możliwość edycji", - "no-filters-found":"Nie znaleziono filtrów.", - "no-filter-text":"Brak określonego filtra", - "add-filter-prompt":"Proszę dodać filtr", - "no-filter-matching":"'{{filter}}' nie znaleziono.", - "create-new-filter":"Utwórz nowy!", - "create-new":"Utwórz nowy", - "filter-required":"Filtr jest wymagany.", - "operation":{ - "operation":"Operacja", - "equal":"równa się", - "not-equal":"nie równa się", - "starts-with":"zaczyna się od", - "ends-with":"kończy się na", - "contains":"zawiera", - "not-contains":"nie zawiera", - "greater":"większa niż", - "less":"mniejsza niż", - "greater-or-equal":"większa lub równa", - "less-or-equal":"mniejsza lub równa", - "and":"i", - "or":"lub", - "in":"w", - "not-in":"nie w" + "filter": { + "add": "Dodaj filtr", + "edit": "Edytuj filtr", + "name": "Nazwa filtra", + "name-required": "Nazwa filtra jest wymagana.", + "duplicate-filter": "Filtr o tej samej nazwie już istnieje.", + "filters": "Filtry", + "unable-delete-filter-title": "Nie można usunąć filtra", + "unable-delete-filter-text": "Filtruj '{{filter}}' nie może zostać usunięty, ponieważ jest używany przez następujące widżety:
{{widgetsList}}", + "duplicate-filter-error": "Znaleziono zduplikowany filtr '{{filter}}'.
Filtry muszą być unikalne w panelu kontrolnym.", + "missing-key-filters-error": "Brak kluczowych filtrów dla filtra '{{filter}}'.", + "filter": "Filtr", + "editable": "Edytowalne", + "no-filters-found": "Nie znaleziono filtrów.", + "no-filter-text": "Nie określono filtra", + "add-filter-prompt": "Proszę dodać filtr", + "no-filter-matching": "'{{filter}}' nie znaleziono.", + "create-new-filter": "Utwórz nowy!", + "create-new": "Create new", + "filter-required": "Filtr jest wymagany.", + "operation": { + "operation": "Operacja", + "equal": "równy", + "not-equal": "nie równe", + "starts-with": "zaczynać z", + "ends-with": "kończy się", + "contains": "zawiera", + "not-contains": "nie zawiera", + "greater": "Lepszy niż", + "less": "mniej niż", + "greater-or-equal": "większy lub równy", + "less-or-equal": "mniejszy lub równy", + "and": "I", + "or": "Lub", + "in": "W", + "not-in": "nie w" }, - "ignore-case":"ignoruj wielkość liter", - "value":"Wartość", - "remove-filter":"Usuń filtr", - "duplicate-filter-action":"Zduplikuj filtr", - "preview":"Podgląd filtra", - "no-filters":"Brak skonfigurowanych filtrów", - "add-filter":"Dodaj filtr", - "add-complex-filter":"Dodaj filtr złożony", - "add-complex":"Dodaj złożony", - "complex-filter":"Filtr złożony", - "edit-complex-filter":"Edytuj filtr złożony", - "edit-filter-user-params":"Edytuj parametry użytkownika predykatu filtra", - "filter-user-params":"Parametry użytkownika predykatu filtra", - "user-parameters":"Parametry użytkownika", - "display-label":"Etykieta do wyświetlenia", - "autogenerated-label":"Automatycznie generowana etykieta", - "order-priority":"Priorytet porządkowania pola", - "key-filter":"Filtr klucza", - "key-filters":"Filtry klucza", - "key-name":"Nazwa klucza", - "key-name-required":"Wymagana jest nazwa klucza.", - "key-type":{ - "key-type":"Typ klucza", - "attribute":"Atrybut", - "timeseries":"Szereg czasowy", - "entity-field":"Pole encji", - "constant":"Stała", - "client-attribute":"Atrybut klienta", - "server-attribute":"Atrybut serwera", - "shared-attribute":"Atrybut wspólny" + "ignore-case": "zignoruj wielkość liter", + "value": "Wartość", + "remove-filter": "Usuń filtr", + "duplicate-filter-action": "Zduplikowany filtr", + "preview": "Podgląd filtra", + "no-filters": "Nie skonfigurowano żadnych filtrów", + "add-filter": "Dodaj filtr", + "add-complex-filter": "Dodaj złożony filtr", + "add-complex": "Dodaj kompleks", + "complex-filter": "Złożony filtr", + "edit-complex-filter": "Edytuj złożony filtr", + "edit-filter-user-params": "Edytuj parametry użytkownika predykatu filtra", + "filter-user-params": "Filtruj parametry użytkownika predykatu", + "user-parameters": "Parametry użytkownika", + "display-label": "Etykieta do wyświetlenia", + "autogenerated-label": "Automatyczne generowanie etykiety", + "order-priority": "Priorytet zamówienia pola", + "key-filter": "Filtr kluczowy", + "key-filters": "Kluczowe filtry", + "key-name": "Nazwa klucza", + "key-name-required": "Nazwa klucza jest wymagana.", + "key-type": { + "key-type": "Typ klucza", + "attribute": "Atrybut", + "timeseries": "Szereg czasowy", + "entity-field": "Pole obiektu", + "constant": "Stały" }, - "value-type":{ - "value-type":"Value type", - "string":"String", - "numeric":"Numeric", - "boolean":"Boolean", - "date-time":"Datetime" + "value-type": { + "value-type": "Typ wartości", + "string": "Strunowy", + "numeric": "Numeryczny", + "boolean": "Wartość logiczna", + "date-time": "Data i godzina" }, - "value-type-required":"Wymagany jest typ wartości klucza.", - "key-value-type-change-title":"Czy na pewno chcesz zmienić typ wartości klucza?", - "key-value-type-change-message":"Jeśli potwierdzisz nowy typ wartości, wszystkie wprowadzone filtry klucza zostaną usunięte.", - "no-key-filters":"Brak skonfigurowanych filtrów klucza", - "add-key-filter":"Dodaj filtr klucza", - "remove-key-filter":"Usuń filtr klucza", - "edit-key-filter":"Edytuj filtr klucza", - "date":"Data", - "time":"Czas", - "current-tenant":"Bieżący najemca", - "current-customer":"Bieżący klient", - "current-user":"Bieżący użytkownik", - "current-device":"Bieżące urządzenie", - "default-value":"Wartość domyślna", - "dynamic-source-type":"Typ źródła dynamicznego", - "dynamic-value":"Wartość dynamiczna", - "no-dynamic-value":"Brak wartości dynamicznej", - "source-attribute":"Atrybut źródła", - "switch-to-dynamic-value":"Przełącz na wartość dynamiczną", - "switch-to-default-value":"Przełącz na wartość domyślną", - "inherit-owner":"Dziedzicz od właściciela", - "source-attribute-not-set":"Jeśli atrybut źródła nie jest ustawiony" + "value-type-required": "Typ wartości klucza jest wymagany.", + "key-value-type-change-title": "Czy na pewno chcesz zmienić typ wartości klucza?", + "key-value-type-change-message": "Jeśli potwierdzisz nowy typ wartości, wszystkie wprowadzone filtry kluczowe zostaną usunięte.", + "no-key-filters": "Nie skonfigurowano żadnych filtrów kluczowych", + "add-key-filter": "Dodaj filtr kluczy", + "remove-key-filter": "Usuń kluczowy filtr", + "edit-key-filter": "Edytuj filtr kluczy", + "date": "Data", + "time": "Czas", + "current-tenant": "Obecny najemca", + "current-customer": "Obecny klient", + "current-user": "Aktualny użytkownik", + "current-device": "Bieżące urządzenie", + "default-value": "Domyślna wartość", + "dynamic-source-type": "Typ źródła dynamicznego", + "dynamic-value": "Wartość dynamiczna", + "no-dynamic-value": "Brak wartości dynamicznej", + "source-attribute": "Atrybut źródłowy", + "switch-to-dynamic-value": "Przełącz na wartość dynamiczną", + "switch-to-default-value": "Przełącz na wartość domyślną", + "inherit-owner": "Dziedzicz po właścicielu", + "source-attribute-not-set": "Jeśli atrybut źródła nie jest ustawiony" }, - "fullscreen":{ - "expand":"Rozwiń na pełny ekran", - "exit":"Wyjdź z trybu pełnoekranowego", - "toggle":"Przełącz tryb pełnoekranowy", - "fullscreen":"Pełny ekran" + "fullscreen": { + "expand": "Rozwiń do pełnego ekranu", + "exit": "Wyłączyć tryb pełnoekranowy", + "toggle": "Przełącz tryb pełnoekranowy", + "fullscreen": "Pełny ekran" }, - "function":{ - "function":"Funkcja" + "function": { + "function": "Funkcjonować" }, - "gateway":{ - "add-entry":"Dodaj konfigurację", - "advanced":"Zaawansowane", - "checking-device-activity":"Sprawdzanie aktywności urządzenia", - "command":"Komendy Docker", - "command-copied-message":"Komenda Docker została skopiowana do schowka", - "configuration":"Konfiguracja", - "connector-add":"Dodaj nowy konektor", - "connector-enabled":"Włącz konektor", - "connector-name":"Nazwa konektora", - "connector-name-required":"Nazwa konektora jest wymagana.", - "connector-type":"Typ konektora", - "connector-type-required":"Typ konektora jest wymagany.", - "connectors":"Konektory", - "connectors-config":"Konfiguracja konektorów", - "connectors-table-enabled":"Włączony", - "connectors-table-name":"Nazwa", - "connectors-table-type":"Typ", - "connectors-table-status":"Status", - "connectors-table-actions":"Akcje", - "connectors-table-key":"Klucz", - "connectors-table-class":"Klasa", - "rpc-command-send":"Wyślij", - "rpc-command-result":"Odpowiedź", - "rpc-command-edit-params":"Edytuj parametry", - "gateway-configuration":"Konfiguracja ogólna", - "docker-label":"Użyj poniższej instrukcji, aby uruchomić bramę IoT w Docker Compose z uwierzytelnieniem dla wybranego urządzenia", - "install-docker-compose":"Skorzystaj z instrukcji, aby pobrać, zainstalować i skonfigurować Docker Compose", - "download-configuration-file":"Pobierz plik konfiguracyjny", - "download-docker-compose":"Pobierz plik docker-compose.yml dla Twojej bramy", - "launch-gateway":"Uruchom bramę", - "launch-docker-compose":"Uruchom bramę, używając poniższej komendy w terminalu z folderu zawierającego plik docker-compose.yml", - "create-new-gateway":"Utwórz nową bramę", - "create-new-gateway-text":"Jesteś pewien, że chcesz utworzyć nową bramę o nazwie: '{{gatewayName}}'?", - "created-time":"Czas utworzenia", - "configuration-delete-dialog-header":"Konfiguracje zostaną usunięte", - "configuration-delete-dialog-body":"Wyłączenie Konfiguracji Zdalnej jest możliwe tylko przy fizycznym dostępie do Bramy. Wszystkie wcześniejsze konfiguracje zostaną usunięte.

\nAby wyłączyć konfigurację, wprowadź poniżej nazwę Bramy", - "configuration-delete-dialog-input":"Nazwa Bramy", - "configuration-delete-dialog-input-required":"Nazwa Bramy jest obowiązkowa", - "configuration-delete-dialog-confirm":"Wyłącz", - "delete":"Usuń konfigurację", - "download-tip":"Pobierz plik konfiguracyjny", - "drop-file":"Upuść plik tutaj lub", - "gateway":"Brama", - "gateway-exists":"Urządzenie o tej samej nazwie już istnieje.", - "gateway-name":"Nazwa Bramy", - "gateway-name-required":"Nazwa Bramy jest wymagana.", - "gateway-saved":"Konfiguracja Bramy została pomyślnie zapisana.", - "grpc":"GRPC", - "grpc-keep-alive-timeout":"Limit czasu Keep Alive (w ms)", - "grpc-keep-alive-timeout-required":"Limit czasu Keep Alive jest wymagany", - "grpc-keep-alive-timeout-min":"Limit czasu Keep Alive nie może być mniejszy niż 1", - "grpc-keep-alive-timeout-pattern":"Limit czasu Keep Alive jest nieprawidłowy", - "grpc-keep-alive":"Keep Alive (w ms)", - "grpc-keep-alive-required":"Keep Alive jest wymagany", - "grpc-keep-alive-min":"Keep Alive nie może być mniejszy niż 1", - "grpc-keep-alive-pattern":"Keep Alive jest nieprawidłowy", - "grpc-min-time-between-pings":"Minimalny czas między pingami (w ms)", - "grpc-min-time-between-pings-required":"Minimalny czas między pingami jest wymagany", - "grpc-min-time-between-pings-min":"Minimalny czas między pingami nie może być mniejszy niż 1", - "grpc-min-time-between-pings-pattern":"Minimalny czas między pingami jest nieprawidłowy", - "grpc-min-ping-interval-without-data":"Minimalny interwał pingowania bez danych (w ms)", - "grpc-min-ping-interval-without-data-required":"Minimalny interwał pingowania bez danych jest wymagany", - "grpc-min-ping-interval-without-data-min":"Minimalny interwał pingowania bez danych nie może być mniejszy niż 1", - "grpc-min-ping-interval-without-data-pattern":"Minimalny interwał pingowania bez danych jest nieprawidłowy", - "grpc-max-pings-without-data":"Maksymalna liczba pingów bez danych", - "grpc-max-pings-without-data-required":"Maksymalna liczba pingów bez danych jest wymagana", - "grpc-max-pings-without-data-min":"Maksymalna liczba pingów bez danych nie może być mniejsza niż 1", - "grpc-max-pings-without-data-pattern":"Maksymalna liczba pingów bez danych jest nieprawidłowa", - "inactivity-check-period-seconds":"Okres sprawdzania braku aktywności (w sekundach)", - "inactivity-check-period-seconds-required":"Okres sprawdzania braku aktywności jest wymagany", - "inactivity-check-period-seconds-min":"Okres sprawdzania braku aktywności nie może być mniejszy niż 1", - "inactivity-check-period-seconds-pattern":"Okres sprawdzania braku aktywności jest nieprawidłowy", - "inactivity-timeout-seconds":"Okres braku aktywności (w sekundach)", - "inactivity-timeout-seconds-required":"Okres braku aktywności jest wymagany", - "inactivity-timeout-seconds-min":"Okres braku aktywności nie może być mniejszy niż 1", - "inactivity-timeout-seconds-pattern":"Okres braku aktywności jest nieprawidłowy", - "json-parse":"Nieprawidłowy format JSON.", - "json-required":"To pole nie może być puste.", - "logs":{ - "logs":"Logi", - "days":"dni", - "hours":"godzin", - "minutes":"minut", - "seconds":"sekund", - "date-format":"Format daty", - "date-format-required":"Wymagany format daty", - "log-format":"Format logów", - "log-type":"Typ logu", - "log-format-required":"Wymagany format logów", - "remote":"Zdalne logowanie", - "remote-logs":"Zdalne logi", - "local":"Lokalne logowanie", - "level":"Poziom logowania", - "file-path":"Ścieżka pliku", - "file-path-required":"Wymagana ścieżka pliku", - "saving-period":"Okres zapisywania logów", - "saving-period-min":"Okres zapisywania logów nie może być krótszy niż 1", - "saving-period-required":"Wymagany okres zapisywania logów", - "backup-count":"Liczba kopii zapasowych", - "backup-count-min":"Liczba kopii zapasowych nie może być mniejsza niż 1", - "backup-count-required":"Wymagana liczba kopii zapasowych" + "gateway": { + "add-entry": "Dodaj konfigurację", + "advanced": "Advanced", + "checking-device-activity": "Checking device activity", + "command": "Docker commands", + "command-copied-message": "Docker command has been copied to clipboard", + "configuration": "Configuration", + "connector-add": "Dodaj nowe złącze", + "connector-enabled": "Włącz złącze", + "connector-name": "Nazwa złącza", + "connector-name-required": "Nazwa złącza jest wymagana.", + "connector-type": "Typ złącza", + "connector-type-required": "Typ złącza jest wymagany.", + "connectors": "Konfiguracja złączy", + "connectors-config": "Connectors configuration", + "connectors-table-enabled": "Enabled", + "connectors-table-name": "Name", + "connectors-table-type": "Type", + "connectors-table-status": "Status", + "connectors-table-actions": "Actions", + "connectors-table-key": "Key", + "connectors-table-class": "Class", + "rpc-command-send": "Send", + "rpc-command-result": "Result", + "rpc-command-edit-params": "Edit parameters", + "gateway-configuration": "General Configuration", + "docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.", + "create-new-gateway": "Utwórz nowy gateway", + "create-new-gateway-text": "Czy na pewno chcesz utworzyć nowy gateway o nazwie: '{{gatewayName}}'?", + "created-time": "Created time", + "configuration-delete-dialog-header": "Configurations will be deleted", + "configuration-delete-dialog-body": "Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below", + "configuration-delete-dialog-input": "Gateway name", + "configuration-delete-dialog-input-required": "Gateway name is mandatory", + "configuration-delete-dialog-confirm": "Turn Off", + "delete": "Usuń konfigurację", + "download-tip": "Pobierz plik konfiguracyjny", + "drop-file": "Drop file here or", + "gateway": "Wejście", + "gateway-exists": "Urządzenie o tej samej nazwie już istnieje.", + "gateway-name": "Nazwa Gateway", + "gateway-name-required": "Nazwa Gateway'a jest wymagana.", + "gateway-saved": "Konfiguracja Gatewey'a została pomyślnie zapisana.", + "grpc": "GRPC", + "grpc-keep-alive-timeout": "Keep alive timeout (in ms)", + "grpc-keep-alive-timeout-required": "Keep alive timeout is required", + "grpc-keep-alive-timeout-min": "Keep alive timeout can not be less then 1", + "grpc-keep-alive-timeout-pattern": "Keep alive timeout is not valid", + "grpc-keep-alive": "Keep alive (in ms)", + "grpc-keep-alive-required": "Keep alive is required", + "grpc-keep-alive-min": "Keep alive can not be less then 1", + "grpc-keep-alive-pattern": "Keep alive is not valid", + "grpc-min-time-between-pings": "Min time between pings (in ms)", + "grpc-min-time-between-pings-required": "Min time between pings is required", + "grpc-min-time-between-pings-min": "Min time between pings can not be less then 1", + "grpc-min-time-between-pings-pattern": "Min time between pings is not valid", + "grpc-min-ping-interval-without-data": "Min ping interval without data (in ms)", + "grpc-min-ping-interval-without-data-required": "Min ping interval without data is required", + "grpc-min-ping-interval-without-data-min": "Min ping interval without data can not be less then 1", + "grpc-min-ping-interval-without-data-pattern": "Min ping interval without data is not valid", + "grpc-max-pings-without-data": "Max pings without data", + "grpc-max-pings-without-data-required": "Max pings without data is required", + "grpc-max-pings-without-data-min": "Max pings without data can not be less then 1", + "grpc-max-pings-without-data-pattern": "Max pings without data is not valid", + "inactivity-check-period-seconds": "Inactivity check period (in sec)", + "inactivity-check-period-seconds-required": "Inactivity check period is required", + "inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1", + "inactivity-check-period-seconds-pattern": "Inactivity check period is not valid", + "inactivity-timeout-seconds": "Inactivity timeout (in sec)", + "inactivity-timeout-seconds-required": "Inactivity timeout is required", + "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1", + "inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid", + "json-parse": "Nieprawidłowy JSON.", + "json-required": "Pole nie może być puste.", + "logs": { + "logs": "Logs", + "days": "days", + "hours": "hours", + "minutes": "minutes", + "seconds": "seconds", + "date-format": "Date format", + "date-format-required": "Date format required", + "log-format": "Log format", + "log-type": "Log type", + "log-format-required": "Log format required", + "remote": "Remote logging", + "remote-logs": "Remote logs", + "local": "Local logging", + "level": "Log level", + "file-path": "File path", + "file-path-required": "File path required", + "saving-period": "Log saving period", + "saving-period-min": "Log saving period can not be less then 1", + "saving-period-required": "Log saving period required", + "backup-count": "Backup count", + "backup-count-min": "Backup count can not be less then 1", + "backup-count-required": "Backup count required" }, - "min-pack-send-delay":"Minimalny opóźnienie wysyłki paczki (w ms)", - "min-pack-send-delay-required":"Wymagane minimalne opóźnienie wysyłki paczki", - "min-pack-send-delay-min":"Minimalne opóźnienie wysyłki paczki nie może być mniejsze niż 0", - "no-connectors":"Brak konektorów", - "no-data":"Brak konfiguracji", - "no-gateway-found":"Nie znaleziono bramy.", - "no-gateway-matching":"Brak dopasowania do '{{item}}'", - "path-logs":"Ścieżka do plików dziennika", - "path-logs-required":"Wymagana ścieżka", - "permit-without-calls":"Zezwalaj na keep alive bez wywołań", - "remote":"Zdalna konfiguracja", - "remote-logging-level":"Poziom logowania", - "remove-entry":"Usuń konfigurację", - "remote-shell":"Zdalny shell", - "remote-configuration":"Zdalna konfiguracja", - "other":"Inne", - "save-tip":"Zapisz plik konfiguracyjny", - "security-type":"Typ zabezpieczeń", - "security-types":{ - "access-token":"Token dostępu", - "username-password":"Nazwa użytkownika i hasło", - "tls":"TLS", - "tls-access-token":"TLS + Token dostępu", - "tls-private-key":"TLS + Klucz prywatny" + "min-pack-send-delay": "Min pack send delay (in ms)", + "min-pack-send-delay-required": "Min pack send delay is required", + "min-pack-send-delay-min": "Min pack send delay can not be less then 0", + "no-connectors": "Brak złączy", + "no-data": "Brak konfiguracji", + "no-gateway-found": "Nie znaleziono gateway'a.", + "no-gateway-matching": " '{{item}}' nie znaleziono.", + "path-logs": "Ścieżka do plików dziennika", + "path-logs-required": "Ścieżka jest wymagana.", + "permit-without-calls": "Keep alive permit without calls", + "remote": "Zdalna konfiguracja", + "remote-logging-level": "Poziom logowania", + "remove-entry": "Usuń konfigurację", + "remote-shell": "Remote shell", + "remote-configuration": "Remote Configuration", + "other": "Other", + "save-tip": "Zapisz plik konfiguracyjny", + "security-type": "Rodzaj zabezpieczenia", + "security-types": { + "access-token": "Token dostępu", + "username-password": "Username and Password", + "tls": "TLS", + "tls-access-token": "TLS + Access Token", + "tls-private-key": "TLS + Private Key" }, - "server-port":"Port serwera", - "statistics":{ - "statistic":"Statystyka", - "statistics":"Statystyki", - "statistic-commands-empty":"Brak dostępnych statystyk", - "commands":"Polecenia", - "send-period":"Okres wysyłania statystyk (w sekundach)", - "send-period-required":"Okres wysyłania statystyk jest wymagany", - "send-period-min":"Okres wysyłania statystyk nie może być krótszy niż 60", - "send-period-pattern":"Okres wysyłania statystyk jest nieprawidłowy", - "check-connectors-configuration":"Sprawdź konfigurację łączników (w sekundach)", - "check-connectors-configuration-required":"Sprawdzenie konfiguracji łączników jest wymagane", - "check-connectors-configuration-min":"Sprawdzenie konfiguracji łączników nie może być krótsze niż 1", - "check-connectors-configuration-pattern":"Sprawdzenie konfiguracji łączników jest nieprawidłowe", - "add":"Dodaj polecenie", - "timeout":"Limit czasu", - "timeout-ms":"Limit czasu (w ms)", - "timeout-required":"Limit czasu jest wymagany", - "timeout-min":"Limit czasu nie może być krótszy niż 1", - "timeout-pattern":"Limit czasu jest nieprawidłowy", - "attribute-name":"Nazwa atrybutu", - "attribute-name-required":"Nazwa atrybutu jest wymagana", - "command":"Polecenie", - "command-required":"Polecenie jest wymagane", - "command-pattern":"Polecenie jest nieprawidłowe", - "remove":"Usuń polecenie" + "server-port": "Server port", + "statistics": { + "statistic": "Statistic", + "statistics": "Statistics", + "statistic-commands-empty": "No statistics available", + "commands": "Commands", + "send-period": "Statistic send period (in sec)", + "send-period-required": "Statistic send period is required", + "send-period-min": "Statistic send period can not be less then 60", + "send-period-pattern": "Statistic send period is not valid", + "check-connectors-configuration": "Check connectors configuration (in sec)", + "check-connectors-configuration-required": "Check connectors configuration is required", + "check-connectors-configuration-min": "Check connectors configuration can not be less then 1", + "check-connectors-configuration-pattern": "Check connectors configuration is not valid", + "add": "Add command", + "timeout": "Timeout", + "timeout-ms": "Timeout (in ms)", + "timeout-required": "Timeout is required", + "timeout-min": "Timeout can not be less then 1", + "timeout-pattern": "Timeout is not valid", + "attribute-name": "Attribute name", + "attribute-name-required": "Attribute name is required", + "command": "Command", + "command-required": "Command is required", + "remove": "Remove command" }, - "storage":"Magazyn", - "storage-max-file-records":"Maksymalna liczba rekordów w pliku", - "storage-max-files":"Maksymalna liczba plików", - "storage-max-files-min":"Minimalna liczba wynosi 1.", - "storage-max-files-pattern":"Liczba jest nieprawidłowa.", - "storage-max-files-required":"Wymagana jest liczba.", - "storage-max-records":"Maksymalna liczba rekordów w magazynie", - "storage-max-records-min":"Minimalna liczba rekordów wynosi 1.", - "storage-max-records-pattern":"Liczba jest nieprawidłowa.", - "storage-max-records-required":"Wymagana jest maksymalna liczba rekordów.", - "storage-read-record-count":"Liczba odczytanych rekordów w magazynie", - "storage-read-record-count-min":"Minimalna liczba rekordów wynosi 1.", - "storage-read-record-count-pattern":"Liczba jest nieprawidłowa.", - "storage-read-record-count-required":"Wymagana jest liczba odczytanych rekordów.", - "storage-max-read-record-count":"Maksymalna liczba odczytanych rekordów w magazynie", - "storage-max-read-record-count-min":"Minimalna liczba rekordów wynosi 1.", - "storage-max-read-record-count-pattern":"Liczba jest nieprawidłowa.", - "storage-max-read-record-count-required":"Wymagana jest maksymalna liczba odczytanych rekordów.", - "storage-data-folder-path":"Ścieżka folderu danych", - "storage-data-folder-path-required":"Wymagana jest ścieżka folderu danych.", - "storage-pack-size":"Maksymalny rozmiar pakietu zdarzeń", - "storage-pack-size-min":"Minimalna liczba wynosi 1.", - "storage-pack-size-pattern":"Liczba jest nieprawidłowa.", - "storage-pack-size-required":"Wymagany jest maksymalny rozmiar pakietu zdarzeń.", - "storage-path":"Ścieżka magazynu", - "storage-path-required":"Wymagana jest ścieżka magazynu.", - "storage-type":"Typ magazynu", - "storage-types":{ - "file-storage":"Magazyn plików", - "memory-storage":"Magazyn pamięci", - "sqlite":"SQLite" + "storage": "Składowanie", + "storage-max-file-records": "Maksymalna liczba rekordów w pliku", + "storage-max-files": "Maksymalna liczba plików", + "storage-max-files-min": "Minimalna liczba to 1.", + "storage-max-files-pattern": "Numer jest nieprawidłowy.", + "storage-max-files-required": "Numer jest wymagany.", + "storage-max-records": "Maksymalna liczba rekordów w pamięci", + "storage-max-records-min": "Minimalna liczba rekordów to 1.", + "storage-max-records-pattern": "Numer jest nieprawidłowy.", + "storage-max-records-required": "Maksymalna liczba rekordów jest wymagana.", + "storage-read-record-count": "Read record count in storage", + "storage-read-record-count-min": "Minimum number of records is 1.", + "storage-read-record-count-pattern": "Number is not valid.", + "storage-read-record-count-required": "Read record count is required.", + "storage-max-read-record-count": "Max read record count in storage", + "storage-max-read-record-count-min": "Minimum number of records is 1.", + "storage-max-read-record-count-pattern": "Number is not valid.", + "storage-max-read-record-count-required": "Max Read record count is required.", + "storage-data-folder-path": "Data folder path", + "storage-data-folder-path-required": "Data folder path is required.", + "storage-pack-size": "Maksymalny rozmiar pakietu wydarzeń", + "storage-pack-size-min": "Minimalna liczba to 1.", + "storage-pack-size-pattern": "Numer jest nieprawidłowy.", + "storage-pack-size-required": "Maksymalny rozmiar pakietu wydarzeń jest wymagany.", + "storage-path": "Ścieżka przechowywania", + "storage-path-required": "Ścieżka do przechowywania jest wymagana.", + "storage-type": "Typ składowania", + "storage-types": { + "file-storage": "Nośnik danych", + "memory-storage": "Przechowywanie pamięci", + "sqlite": "SQLITE" }, - "thingsboard":"ThingsBoard", - "general":"Ogólne", - "thingsboard-host":"Host ThingsBoard", - "thingsboard-host-required":"Host jest wymagany.", - "thingsboard-port":"Port ThingsBoard", - "thingsboard-port-max":"Maksymalny numer portu to 65535.", - "thingsboard-port-min":"Minimalny numer portu to 1.", - "thingsboard-port-pattern":"Port jest niepoprawny.", - "thingsboard-port-required":"Port jest wymagany.", - "tidy":"Porządkuj", - "tidy-tip":"Porządkuj konfigurację JSON", - "title-connectors-json":"Konfiguracja konektora {{typeName}}", - "tls-path-ca-certificate":"Ścieżka do certyfikatu CA na gatewayu", - "tls-path-client-certificate":"Ścieżka do certyfikatu klienta na gatewayu", - "messages-ttl-check-in-hours":"Sprawdź TTL wiadomości w godzinach", - "messages-ttl-check-in-hours-required":"Sprawdzenie TTL wiadomości w godzinach jest wymagane.", - "messages-ttl-check-in-hours-min":"Minimalna liczba to 1.", - "messages-ttl-check-in-hours-pattern":"Liczba jest niepoprawna.", - "messages-ttl-in-days":"TTL wiadomości w dniach", - "messages-ttl-in-days-required":"TTL wiadomości w dniach jest wymagane.", - "messages-ttl-in-days-min":"Minimalna liczba to 1.", - "messages-ttl-in-days-pattern":"Liczba jest niepoprawna.", - "mqtt-qos":"QoS", - "mqtt-qos-required":"QoS jest wymagane", - "mqtt-qos-range":"Wartości QoS są w zakresie od 0 do 1", - "tls-path-private-key":"Ścieżka do prywatnego klucza na gatewayu", - "toggle-fullscreen":"Przełącz pełny ekran", - "transformer-json-config":"Konfiguracja JSON*", - "update-config":"Dodaj/aktualizuj konfigurację JSON", - "hints":{ - "remote-configuration":"Umożliwia zdalną konfigurację i zarządzanie bramką", - "remote-shell":"Umożliwia zdalne sterowanie systemem operacyjnym bramki z widżetu Zdalnej Powłoki", - "host":"Nazwa hosta lub adres IP serwera ThingsBoard", - "port":"Port usługi MQTT na serwerze ThingsBoard", - "token":"Token dostępowy dla bramki na serwerze ThingsBoard", - "client-id":"Identyfikator klienta MQTT dla bramki na serwerze ThingsBoard", - "username":"Nazwa użytkownika MQTT dla bramki na serwerze ThingsBoard", - "password":"Hasło MQTT dla bramki na serwerze ThingsBoard", - "ca-cert":"Ścieżka do pliku certyfikatu CA", - "date-form":"Format daty w komunikatach dziennika", - "data-folder":"Ścieżka do folderu, który będzie zawierał dane (względna lub bezwzględna)", - "log-format":"Format komunikatu dziennika", - "remote-log":"Umożliwia zdalne logowanie i odczyt dziennika z bramki", - "backup-count":"Jeśli liczba kopii zapasowych > 0, po dokonaniu zmiany rolki, nie więcej niż określona liczba plików kopii zapasowych jest przechowywana - najstarsze zostaną usunięte", - "storage":"Zapewnia konfigurację zapisu przychodzących danych przed ich wysłaniem na platformę", - "max-file-count":"Maksymalna liczba plików, które zostaną utworzone", - "max-read-count":"Liczba wiadomości do pobrania z magazynu i wysłania do ThingsBoard", - "max-records":"Maksymalna liczba rekordów przechowywanych w jednym pliku", - "read-record-count":"Liczba wiadomości do pobrania z magazynu i wysłania do ThingsBoard", - "max-records-count":"Maksymalna liczba danych w magazynie przed wysłaniem do ThingsBoard", - "ttl-check-hour":"Jak często brama sprawdza dane pod kątem przestarzałości", - "ttl-messages-day":"Maksymalna liczba dni, przez które dane będą przechowywane w magazynie", - "commands":"Polecenia do zbierania dodatkowych statystyk", - "attribute":"Klucz telemetrii statystyk", - "timeout":"Limit czasu na wykonanie polecenia", - "command":"Wynik wykonania polecenia, będzie używany jako wartość telemetrii", - "check-device-activity":"Umożliwia monitorowanie aktywności każdego podłączonego urządzenia", - "inactivity-timeout":"Czas po którym brama rozłączy urządzenie", - "inactivity-period":"Okresowość sprawdzania aktywności urządzenia", - "minimal-pack-delay":"Opóźnienie między wysyłką paczek wiadomości (zmniejszenie tego ustawienia zwiększa zużycie procesora)", - "qos":"Jakość usługi w komunikacji MQTT (0 - przynajmniej raz, 1 - co najmniej raz)", - "server-port":"Port sieciowy, na którym serwer GRPC nasłuchuje nadchodzących połączeń.", - "grpc-keep-alive-timeout":"Maksymalny czas oczekiwania serwera na odpowiedź keepalive ping przed uznaniem połączenia za martwe.", - "grpc-keep-alive":"Czas między dwoma kolejnymi wiadomościami keepalive ping, gdy nie ma aktywnego wywołania RPC.", - "grpc-min-time-between-pings":"Minimalny czas oczekiwania serwera między wysłaniem wiadomości keepalive ping.", - "grpc-max-pings-without-data":"Maksymalna liczba wiadomości keepalive ping, które serwer może wysłać, nie odbierając żadnych danych, zanim uzna połączenie za martwe.", - "grpc-min-ping-interval-without-data":"Minimalny czas oczekiwania serwera między wysłaniem wiadomości keepalive ping, gdy nie są przesyłane ani odbierane żadne dane.", - "permit-without-calls":"Zezwalaj serwerowi na utrzymanie połączenia GRPC aktywnego, nawet gdy nie ma aktywnych wywołań RPC." + "thingsboard": "ThingsBoard", + "general": "General", + "thingsboard-host": "Gospodarz ThingsBoard", + "thingsboard-host-required": "Host jest wymagany.", + "thingsboard-port": "Port ThingsBoard", + "thingsboard-port-max": "Maksymalny numer portu to 65535.", + "thingsboard-port-min": "Minimalny numer portu to 1.", + "thingsboard-port-pattern": "Port jest nieprawidłowy.", + "thingsboard-port-required": "Port jest wymagany.", + "tidy": "Uporządkuj", + "tidy-tip": "Uporządkowana konfiguracja JSON", + "title-connectors-json": "Złącze {{typeName}} konfiguracja", + "tls-path-ca-certificate": "Ścieżka do certyfikatu CA na gateway", + "tls-path-client-certificate": "Ścieżka do certyfikatu klienta na gateway", + "messages-ttl-check-in-hours": "Messages TTL check in hours", + "messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.", + "messages-ttl-check-in-hours-min": "Min number is 1.", + "messages-ttl-check-in-hours-pattern": "Number is not valid.", + "messages-ttl-in-days": "Messages TTL in days", + "messages-ttl-in-days-required": "Messages TTL in days is required.", + "messages-ttl-in-days-min": "Min number is 1.", + "messages-ttl-in-days-pattern": "Number is not valid.", + "mqtt-qos": "QoS", + "mqtt-qos-required": "QoS is required", + "mqtt-qos-range": "QoS values range is from 0 to 1", + "tls-path-private-key": "Ścieżka do klucza prywatnego na bramce", + "toggle-fullscreen": "Przełącz tryb pełnoekranowy", + "transformer-json-config": "Konfiguracja JSON*", + "update-config": "Dodaj/zaktualizuj konfigurację JSON", + "hints": { + "remote-configuration": "Enables remote configuration and management of the gateway", + "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", + "host": "Hostname or IP address of ThingsBoard server", + "port": "Port of MQTT service on ThingsBoard server", + "token": "Access token for the gateway from ThingsBoard server", + "client-id": "MQTT client id for the gateway form ThingsBoard server", + "username": "MQTT username for the gateway form ThingsBoard server", + "password": "MQTT password for the gateway form ThingsBoard server", + "ca-cert": "Path to CA certificate file", + "date-form": "Date format in log message", + "data-folder": "Path to folder, that will contains data (Relative or Absolute)", + "log-format": "Log message format", + "remote-log": "Enables remote logging and logs reading from the gateway", + "backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted", + "storage": "Provides configuration for saving incoming data before it is sent to the platform", + "max-file-count": "Maximum count of file that will be created", + "max-read-count": "Count of messages to get from storage and send to ThingsBoard", + "max-records": "Maximum count of records that will be stored in one file", + "read-record-count": "Count of messages to get from storage and send to ThingsBoard", + "max-records-count": "Maximum count of data in storage before send to ThingsBoard", + "ttl-check-hour": "How often will Gateway check data for obsolescence", + "ttl-messages-day": "Maximum days that storage will save data", + "commands": "Commands for collecting additional statistic", + "attribute": "Statistic telemetry key", + "timeout": "Timeout for command executing", + "command": "The result of the command execution, will be used as the value for telemetry", + "check-device-activity": "Enables monitor the activity of each connected device", + "inactivity-timeout": "Time after whose the gateway will disconnect device", + "inactivity-period": "Periodicity of device activity check", + "minimal-pack-delay": "Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)", + "qos": "Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)", + "server-port": "Network port on which GRPC server will listen for incoming connections.", + "grpc-keep-alive-timeout": "Maximum time the server should wait for a keepalive ping response before considering the connection dead.", + "grpc-keep-alive": "Duration between two successive keepalive ping messages when there is no active RPC call.", + "grpc-min-time-between-pings": "Minimum amount of time the server should wait between sending keepalive ping messages", + "grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.", + "grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.", + "permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls." } }, - "grid":{ - "delete-item-title":"Czy na pewno chcesz usunąć ten element?", - "delete-item-text":"Bądź ostrożny, po potwierdzeniu ten element i wszystkie powiązane dane staną się nieodwracalne.", - "delete-items-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 element} other {# elementów} }?", - "delete-items-action-title":"Usuń { count, plural, =1 {1 element} other {# elementów} }", - "delete-items-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane elementy zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", - "add-item-text":"Dodaj nowy element", - "no-items-text":"Nie znaleziono elementów", - "item-details":"Szczegóły elementu", - "delete-item":"Usuń element", - "delete-items":"Usuń elementy", - "scroll-to-top":"Przewiń do góry" - }, - "help":{ - "goto-help-page":"Przejdź do strony pomocy", - "show-help":"Pokaż pomoc" + "grid": { + "delete-item-title": "Czy na pewno chcesz usunąć ten element?", + "delete-item-text": "Bądź ostrożny, po potwierdzeniu tego elementu i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-items-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 pozycję} other {# pozycji} }?", + "delete-items-action-title": "Usuń {count, plural, =1 {1 pozycję} other {# pozycji} }", + "delete-items-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane elementy zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "add-item-text": "Dodaj nową pozycję", + "no-items-text": "Nie znaleziono żadnych elementów", + "item-details": "Szczegóły Przedmiotu", + "delete-item": "Usuń przedmiot", + "delete-items": "Usuń elementy", + "scroll-to-top": "Przewiń na górę" }, - "home":{ - "home":"Strona główna", - "profile":"Profil", - "logout":"Wyloguj się", - "menu":"Menu", - "avatar":"Awatar", - "open-user-menu":"Otwórz menu użytkownika" + "help": { + "goto-help-page": "Przejdź do strony pomocy", + "show-help": "Pokaż pomoc" }, - "file-input":{ - "browse-file":"Przeglądaj plik", - "browse-files":"Przeglądaj pliki" + "home": { + "home": "Strona główna", + "profile": "Profil", + "logout": "Wyloguj", + "menu": "Menu", + "avatar": "Awatar", + "open-user-menu": "Otwórz menu użytkownika" }, - "image":{ - "gallery":"Galeria obrazów", - "search":"Wyszukaj obraz", - "selected-images":"{ count, plural, =1 {1 obraz} other {# obrazy} } wybranych", - "created-time":"Czas utworzenia", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "resolution":"Rozdzielczość", - "size":"Rozmiar", - "system":"System", - "download-image":"Pobierz obraz", - "export-image":"Eksportuj obraz do JSON", - "import-image":"Importuj obraz z JSON", - "upload-image":"Prześlij obraz", - "edit-image":"Edytuj obraz", - "image-details":"Szczegóły obrazu", - "no-images":"Nie znaleziono obrazów", - "delete-image":"Usuń obraz", - "delete-image-title":"Czy na pewno chcesz usunąć obraz '{{imageTitle}}'?", - "delete-image-text":"Bądź ostrożny, po potwierdzeniu obraz stanie się nieodwracalnie utracony.", - "delete-images-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 obraz} other {# obrazy} }?", - "delete-images-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane obrazy zostaną usunięte, a wszelkie powiązane dane staną się nieodwracalnie utracone.", - "list-mode":"Widok listy", - "grid-mode":"Widok siatki", - "image-preview":"Podgląd obrazu", - "update-image":"Aktualizuj obraz", - "export-failed-error":"Nie można wyeksportować obrazu: {{error}}", - "image-json-file":"Plik JSON obrazu", - "invalid-image-json-file-error":"Nie można zaimportować obrazu z JSON: Nieprawidłowa struktura danych JSON obrazu.", - "image-is-in-use":"Obraz jest używany przez inne jednostki", - "images-are-in-use":"Obrazy są używane przez inne jednostki", - "image-is-in-use-text":"Obraz '{{title}}' nie został usunięty, ponieważ jest używany przez następujące jednostki:", - "images-are-in-use-text":"Nie wszystkie obrazy zostały usunięte, ponieważ są używane przez inne jednostki.
Możesz zobaczyć odwołane jednostki, klikając przycisk Odwołania w odpowiednim wierszu obrazu.
Jeśli nadal chcesz usunąć te obrazy, zaznacz je w tabeli poniżej i kliknij przycisk Usuń zaznaczone.", - "delete-image-in-use-text":"Jeśli nadal chcesz usunąć obraz, kliknij przycisk Usuń tak czy inaczej.", - "system-entities":"Jednostki systemowe:", - "entities":"jednostki:", - "references":"Odwołania", - "include-system-images":"Dołącz obrazy systemowe", - "clear-image":"Wyczyść obraz", - "no-image":"Brak obrazu", - "no-image-selected":"Nie wybrano obrazu", - "browse-from-gallery":"Przeglądaj z galerii", - "set-link":"Ustaw link", - "image-link":"Link obrazu", - "link":"Link", - "copy-image-link":"Kopiuj link obrazu", - "embed-image":"Osadź obraz", - "embed-to-html":"Osadź w HTML", - "embed-to-html-hint":"Ta funkcja umożliwia udostępnianie linku każdemu nieautoryzowanemu użytkownikowi.", - "embed-to-html-text":"Za pomocą poniższego fragmentu kodu możesz osadzić obraz w komponentach opartych na prostym HTML.
Takie komponenty obejmują widżety karty HTML, funkcje zawartości komórek itp.", - "embed-to-angular-template":"Osadź w szablonie Angular HTML", - "embed-to-angular-template-text":"Za pomocą poniższego fragmentu kodu możesz osadzić obraz w szablonie Angular HTML.
Takie komponenty obejmują widżet Markdown, sekcję HTML w edytorze widżetów, niestandardowe akcje itp." + "file-input": { + "browse-file": "Przeglądaj plik", + "browse-files": "Przeglądaj pliki" }, - "image-input":{ - "drop-images-or":"Przeciągnij i upuść obrazy lub", - "drag-and-drop":"Przeciągnij i Upuść", - "or":"lub", - "browse":"Przeglądaj", - "no-images":"Brak wybranych obrazów", - "images":"obrazy" + "image-input": { + "drop-images-or": "Przeciągnij i upuść obrazy lub", + "drag-and-drop": "Drag & Drop", + "or": "or", + "browse": "Browse", + "no-images": "Nie wybrano żadnych obrazów", + "images": "Obrazy", + "drop-image-or": "Przeciągnij i upuść obraz lub" }, - "import":{ - "no-file":"Nie wybrano pliku", - "drop-file":"Przeciągnij plik JSON lub kliknij, aby wybrać plik do przesłania.", - "drop-json-file-or":"Przeciągnij plik JSON lub", - "drop-file-csv":"Przeciągnij plik CSV lub kliknij, aby wybrać plik do przesłania.", - "drop-file-csv-or":"Przeciągnij plik CSV lub", - "column-value":"Wartość", - "column-title":"Tytuł", - "column-example":"Przykładowa wartość danych", - "column-key":"Atrybut/klucz telemetryczny", - "credentials":"Dane uwierzytelniające", - "csv-delimiter":"Separator CSV", - "csv-first-line-header":"Pierwsza linia zawiera nazwy kolumn", - "csv-update-data":"Aktualizuj atrybuty/telemetrię", - "details":"Szczegóły", - "import-csv-number-columns-error":"Plik powinien zawierać co najmniej dwie kolumny", - "import-csv-invalid-format-error":"Nieprawidłowy format pliku. Linia: '{{line}}'", - "column-type":{ - "name":"Nazwa", - "type":"Typ", - "label":"Etykieta", - "column-type":"Typ kolumny", - "client-attribute":"Atrybut klienta", - "shared-attribute":"Wspólny atrybut", - "server-attribute":"Atrybut serwera", - "timeseries":"Szereg czasowy", - "entity-field":"Pole encji", - "access-token":"Token dostępu", - "x509":"X.509", - "mqtt":{ - "client-id":"ID klienta MQTT", - "user-name":"Nazwa użytkownika MQTT", - "password":"Hasło MQTT" + "import": { + "no-file": "Nie wybrano pliku", + "drop-file": "Upuść plik JSON lub kliknij, aby wybrać plik do przesłania.", + "drop-csv-file": "Upuść plik CSV lub kliknij, aby wybrać plik do przesłania.", + "drop-json-file-or": "Przeciągnij i upuść plik JSON lub", + "drop-file-csv": "Upuść plik CSV lub kliknij, aby wybrać plik do przesłania.", + "drop-file-csv-or": "Przeciągnij i upuść plik CSV lub", + "column-value": "Wartość", + "column-title": "Tytuł", + "column-example": "Przykładowe dane wartości", + "column-key": "Klucz atrybutu/telemetrii", + "credentials": "Dane uwierzytelniające", + "csv-delimiter": "Ogranicznik CSV", + "csv-first-line-header": "Pierwsza linia zawiera nazwy kolumn", + "csv-update-data": "Zaktualizuj atrybuty/telemetrię", + "details": "Szczegóły", + "import-csv-number-columns-error": "Plik powinien zawierać co najmniej dwie kolumny", + "import-csv-invalid-format-error": "Nieprawidłowy format pliku. {{line}}'", + "column-type": { + "name": "Nazwa", + "type": "Typ", + "label": "Etykieta", + "column-type": "Typ kolumny", + "client-attribute": "Atrybut klienta", + "shared-attribute": "Wspólny atrybut", + "server-attribute": "Atrybut serwera", + "timeseries": "Szereg czasowy", + "entity-field": "Pole obiektu", + "access-token": "Token dostępu", + "x509": "X.509", + "mqtt": { + "client-id": "Identyfikator klienta MQTT", + "user-name": "Nazwa użytkownika MQTT", + "password": "Hasło MQTT" }, - "lwm2m":{ - "client-endpoint":"Nazwa klienta LwM2M endpoint", - "security-config-mode":"Tryb konfiguracji bezpieczeństwa LwM2M", - "client-identity":"Tożsamość klienta LwM2M", - "client-key":"Klucz klienta LwM2M", - "client-cert":"Certyfikat publiczny klienta LwM2M", - "bootstrap-server-security-mode":"Tryb bezpieczeństwa serwera rozruchowego LwM2M", - "bootstrap-server-secret-key":"Tajny klucz serwera rozruchowego LwM2M", - "bootstrap-server-public-key-id":"Klucz publiczny lub identyfikator serwera rozruchowego LwM2M", - "lwm2m-server-security-mode":"Tryb bezpieczeństwa serwera LwM2M", - "lwm2m-server-secret-key":"Tajny klucz serwera LwM2M", - "lwm2m-server-public-key-id":"Klucz publiczny lub identyfikator serwera LwM2M" + "lwm2m": { + "client-endpoint": "Nazwa klienta punktu końcowego LwM2M", + "security-config-mode": "Tryb konfiguracji zabezpieczeń LwM2M", + "client-identity": "Tożsamość klienta LwM2M", + "client-key": "Klucz klienta LwM2M", + "client-cert": "Klucz publiczny klienta LwM2M", + "bootstrap-server-security-mode": "Tryb bezpieczeństwa serwera startowego LwM2M", + "bootstrap-server-secret-key": "Tajny klucz serwera startowego LwM2M", + "bootstrap-server-public-key-id": "Klucz publiczny lub identyfikator serwera ładowania początkowego LwM2M", + "lwm2m-server-security-mode": "Tryb bezpieczeństwa serwera LwM2M", + "lwm2m-server-secret-key": "Tajny klucz serwera LwM2M", + "lwm2m-server-public-key-id": "Klucz publiczny lub identyfikator serwera LwM2M" }, - "snmp":{ - "host":"Host SNMP", - "port":"Port SNMP", - "version":"Wersja SNMP (v1, v2c lub v3)", - "community-string":"Ciąg społeczności SNMP" + "snmp": { + "host": "SNMP host", + "port": "SNMP port", + "version": "SNMP version (v1, v2c or v3)", + "community-string": "SNMP community string" }, - "isgateway":"Czy to jest Bramą", - "activity-time-from-gateway-device":"Czas aktywności urządzenia bramowego", - "description":"Opis", - "routing-key":"Klucz brzegowy", - "secret":"Sekret brzegowy" + "isgateway": "Czy Gateway", + "activity-time-from-gateway-device": "Czas aktywności z Gateway", + "description": "Opis", + "edge-license-key": "Klucz licencyjny", + "cloud-endpoint": "Punkt końcowy w chmurze", + "routing-key": "Klucz Edge", + "secret": "Edge secret" }, - "stepper-text":{ - "select-file":"Wybierz plik", - "configuration":"Importuj konfigurację", - "column-type":"Wybierz typy kolumn", - "creat-entities":"Tworzenie nowych jednostek" + "stepper-text": { + "select-file": "Wybierz plik", + "configuration": "Importuj konfigurację", + "column-type": "Wybierz typ kolumn", + "creat-entities": "Tworzenie nowych obiektów" }, - "message":{ - "create-entities":"{{count}} nowych jednostek zostało pomyślnie utworzonych.", - "update-entities":"{{count}} jednostek zostało pomyślnie zaktualizowanych.", - "error-entities":"Wystąpił błąd podczas tworzenia {{count}} jednostek." + "message": { + "create-entities": "{{count}} pomyślnie utworzono nowe obiekty.", + "update-entities": "{{count}} elementy zostały pomyślnie zaktualizowane.", + "error-entities": "Wystąpił błąd przy tworzeniu {{count}} obiektów." } }, - "item":{ - "selected":"Wybrane" + "integration": { + "integration": "Integracja", + "integrations": "Integracje", + "integrations-center": "Centrum Integracji", + "select-integration": "Wybierz integrację", + "no-integrations-matching": "Nie znaleziono integracji pasujących do '{{entity}}'.", + "integration-required": "Integracja jest wymagana", + "delete": "Usuń integrację", + "management": "Zarządzanie integracjami", + "add-integration-text": "Dodaj nową integrację", + "no-integrations-text": "Nie znaleziono integracji", + "data-convertor-name": "{{convertorType}} data converter for {{integrationName}}", + "integration-name": "{{integrationType}} integration", + "selected-integrations": "Wybrano {count, plural, =1 {1 integrację} other {# integracji} }", + "delete-integration-title": "Czy na pewno chcesz usunąć integrację '{{integrationName}}'?", + "delete-integration-text": "Bądź ostrożny, po potwierdzeniu integracji i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-integrations-title": "Czy na pewno chcesz usunąć {count, plural, =1 {1 integrację} other {# integracji} }?", + "delete-integrations-action-title": "Usuń {count, plural, =1 {1 integrację} other {# integracji} }", + "delete-integrations-text": "Uważaj, po potwierdzeniu wszystkie wybrane integracje zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "events": "Wydarzenia", + "enabled": "Włącz integrację", + "allow-create-devices-or-assets": "Zezwalaj na tworzenie urządzeń lub zasobów", + "all-types": "Wszystkie typy integracji", + "add": "Dodaj integrację", + "search": "Wyszukaj integracje", + "integration-details": "Szczegóły integracji", + "details": "Szczegóły", + "copyId": "Skopiuj identyfikator integracji", + "idCopiedMessage": "Identyfikator integracji został skopiowany do schowka", + "debug-mode": "Tryb debugowania", + "enable-security": "Włącz zabezpieczenia", + "enable-security-new": "Włącz zabezpieczenia automatycznych aktualizacji tokenów", + "headers-filter": "Filtr nagłówków", + "header": "nagłówek", + "no-headers-filter": "Brak filtra nagłówków", + "downlink-url": "Adres URL łącza w dół", + "downlink-url-required": "Adres URL łącza w dół jest wymagany", + "create-loriot-output": "Utwórz dane wyjściowe aplikacji Loriot", + "send-downlink": "Wyślij łącze w dół", + "allow-downlink": "Allow downlink", + "server": "serwer", + "server-required": "Serwer jest wymagany", + "domain": "Domena", + "app-id": "Identyfikator aplikacji", + "app-id-required": "Identyfikator aplikacji jest wymagany", + "app-token": "Token dostępu do aplikacji", + "app-token-required": "Token dostępu do aplikacji jest wymagany", + "email": "E-mail", + "email-required": "email jest wymagany", + "application-uri": "URI aplikacji", + "as-id": "JAKO ID", + "as-id-required": "Identyfikator AS jest wymagany.", + "as-key": "Klucz AS", + "as-key-required": "Klucz AS jest wymagany.", + "client-id-new": "Identyfikator klienta", + "client-id-new-required": "Identyfikator klienta jest wymagany (login).", + "client-secret": "Sekret Klienta", + "client-secret-required": "Sekret klienta jest wymagany (hasło).", + "max-time-diff-in-seconds": "Maksymalna różnica czasu (sekundy)", + "max-time-diff-in-seconds-required": "Maksymalna różnica czasu jest wymagana.", + "created-time": "Stworzony czas", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "description": "Opis", + "base-url": "Bazowy adres URL", + "base-url-required": "Podstawowy adres URL jest wymagany", + "security-key": "Klucz bezpieczeństwa", + "http-endpoint": "Adres URL punktu końcowego HTTP", + "replace-no-content-to-ok": "Zmień status odpowiedzi z „Brak treści” na „OK”", + "copy-http-endpoint-url": "Skopiuj adres URL punktu końcowego HTTP", + "http-endpoint-url-copied-message": "Adres URL punktu końcowego HTTP został skopiowany do schowka", + "host": "Host", + "host-required": "Host jest wymagany.", + "host-private": "Host musi znajdować się w sieci publicznej.", + "url-private": "Adres URL musi należeć do sieci publicznej.", + "host-type": "Typ hosta", + "host-type-required": "Typ hosta jest wymagany.", + "api-version": "Użyj API v3", + "custom-host": "Niestandardowy host", + "custom-host-required": "Niestandardowy host jest wymagany.", + "port": "Port", + "port-required": "Port jest wymagany.", + "port-range": "Port powinien należeć do zakresu od 1 do 65535.", + "connect-timeout": "Limit czasu połączenia (s)", + "connect-timeout-required": "Limit czasu połączenia jest wymagany.", + "connect-timeout-range": "Limit czasu połączenia powinien mieścić się w zakresie od 1 do 200.", + "client-id": "Identyfikator klienta", + "client-id-required": "Identyfikator klienta jest wymagany.", + "client-id-hint": "Wskazówka: opcjonalne. ", + "max-bytes-in-message": "Maksymalna liczba bajtów w wiadomości", + "max-bytes-in-message-range": "Maksymalna liczba bajtów w wiadomości powinna mieścić się w zakresie od 1 do 256000000.", + "device-id": "Identyfikator urzadzenia", + "device-id-required": "Identyfikator urządzenia jest wymagany.", + "device-id-range": "Identyfikator urządzenia powinien mieścić się w zakresie od 1 do 65535 znaków.", + "device-id-pattern": "Identyfikator urządzenia powinien składać się z cyfr oraz wielkich i małych liter. ", + "group-id": "Identyfikator grupy", + "group-id-required": "Identyfikator grupy jest wymagany.", + "topics": "Tematy", + "topics-required": "Temat jest wymagany.", + "routing-keys": "Klucze routingu", + "routing-keys-required": "Klucz routingu jest wymagany.", + "queues": "Kolejki", + "queues-required": "Nazwa kolejki jest wymagana.", + "durable": "Wytrzymały", + "exclusive": "Ekskluzywny", + "autoDelete": "Automatyczne usuwanie", + "exchange-name": "Nazwa wymiany", + "exchange-name-required": "Nazwa giełdy jest wymagana.", + "downlink-topic": "Temat łącza w dół", + "connection-timeout": "Przekroczono limit czasu połączenia, ms", + "connection-timeout-min": "Nieprawidłowa wartość limitu czasu połączenia.", + "handshake-timeout": "Przekroczono limit czasu uzgadniania, ms", + "handshake-timeout-min": "Nieprawidłowa wartość limitu czasu uzgadniania.", + "virtual-host": "Wirtualny gospodarz", + "rabbit-mq-poll-interval": "Interwał ankiety, ms", + "rabbit-mq-poll-interval-min": "Nieprawidłowa wartość interwału odpytywania.", + "application-server-url": "Adres URL serwera aplikacji", + "application-server-url-required": "Adres URL serwera aplikacji jest wymagany.", + "application-server-api-token": "Token API serwera aplikacji", + "application-server-api-token-required": "Token API serwera aplikacji jest wymagany.", + "use-api-four-plus": "Use API for ChirpStack 4+", + "bootstrap-servers": "Serwery startowe", + "bootstrap-servers-required": "Serwery startowe są wymagane.", + "poll-interval": "Interwał ankiety", + "poll-interval-required": "Interwał sondowania jest wymagany.", + "poll-interval-min-value": "Wartość interwału odpytywania nie może być mniejsza niż 1", + "auto-create-topics": "Automatyczne tworzenie tematów", + "clean-session": "Czysta sesja", + "enable-ssl": "Włącz protokół SSL", + "credentials": "Dane uwierzytelniające", + "credentials-type": "Typ poświadczeń", + "credentials-type-required": "Typ danych logowania jest wymagany.", + "username": "Nazwa użytkownika", + "username-required": "Nazwa użytkownika jest wymagana.", + "password": "Hasło", + "password-required": "Hasło jest wymagane.", + "azure-ca-cert": "Plik certyfikatu urzędu certyfikacji", + "ca-cert": "Plik certyfikatu urzędu certyfikacji", + "private-key": "Plik klucza prywatnego", + "private-key-password": "Hasło klucza prywatnego", + "cert": "Plik certyfikatu", + "no-file": "Nie wybrano pliku.", + "drop-file": "Upuść plik lub kliknij, aby wybrać plik do przesłania.", + "drop-file-or": "Przeciągnij i upuść plik lub", + "check-connection": "Sprawdź połączenie", + "check-success": "Połączenie nawiązane.", + "topic-filters": "Filtry tematyczne", + "remove-topic-filter": "Usuń filtr tematu", + "add-topic-filter": "Dodaj filtr tematu", + "add-topic-filter-prompt": "Proszę dodać filtr tematyczny", + "topic": "Temat", + "mqtt-qos": "Jakość usług", + "mqtt-qos-required": "QoS is required", + "mqtt-qos-range": "QoS values range is from 0 to 2", + "mqtt-qos-at-most-once": "Co najwyżej raz", + "mqtt-qos-at-least-once": "Przynajmniej raz", + "mqtt-qos-exactly-once": "Dokładnie raz", + "downlink-topic-pattern": "Wzór tematu łącza w dół", + "downlink-topic-pattern-required": "Wzór tematu łącza w dół jest wymagany.", + "retained-message": "Zachowane", + "aws-access-key-id": "Identyfikator klucza dostępu AWS", + "aws-secret-access-key": "Tajny klucz dostępu AWS", + "aws-region": "Region", + "aws-iot-endpoint": "Punkt końcowy AWS IoT", + "aws-iot-endpoint-required": "Punkt końcowy AWS IoT jest wymagany.", + "aws-iot-credentials": "Poświadczenia AWS IoT", + "aws-sqs-polling-period-in-seconds": "Okres odpytywania w sekundach", + "aws-sqs-queue-url": "Adres URL kolejki SQS", + "aws-sqs-queue-url-required": "Adres URL kolejki SQS jest wymagany", + "aws-sqs-access-key-id-required": "Identyfikator klucza dostępu jest wymagany", + "aws-sqs-secret-access-key-required": "Tajny klucz dostępu jest wymagany", + "application-credentials": "Poświadczenia aplikacji", + "api-key": "Klucz API", + "api-key-required": "Klucz API jest wymagany.", + "api-key-format": "Nieprawidłowy format klucza API.", + "auth-token": "Token uwierzytelniający", + "auth-token-required": "Token uwierzytelniający jest wymagany.", + "region": "Region", + "region-required": "Region jest wymagany.", + "application-id": "Identyfikator aplikacji", + "application-id-required": "Identyfikator aplikacji jest wymagany.", + "access-id": "Identyfikator dostępu", + "access-id-required": "Identyfikator dostępu jest wymagany.", + "access-key": "Klucz dostępu", + "access-key-required": "Klucz dostępu jest wymagany.", + "access-key-min-length": "Klucz dostępu jest za krótki.", + "access-key-max-length": "Klucz dostępu jest za długi.", + "connection-parameters": "Parametry połączenia", + "service-bus-namespace-name": "Nazwa przestrzeni nazw Service Bus", + "service-bus-namespace-name-required": "Nazwa przestrzeni nazw Service Bus jest wymagana.", + "connection-string": "Napis połączenia", + "consumer-group": "Grupa konsumencka", + "connection-string-required": "Wymagany napis połączenia!", + "event-hub-name": "Nazwa centrum zdarzeń", + "event-hub-name-required": "Nazwa centrum zdarzeń jest wymagana.", + "event-iot-hub-name-required": "Nazwa Iot Hub jest wymagana dla łącza w dół", + "sas-key-name": "Nazwa klucza SAS-owego", + "sas-key-name-required": "Nazwa klucza SAS-owego jest wymagana.", + "sas-key": "Klucz SAS-owy", + "sas-key-required": "Klucz SAS jest wymagany.", + "iot-hub-name": "Nazwa IoT Hub (wymagana dla łącza w dół)", + "hostname": "Nazwa hosta", + "hostname-required": "Nazwa hosta jest wymagana", + "integration-clazz": "Klasa integracyjna", + "integration-clazz-required": "Klasa integracyjna jest wymagana", + "integration-configuration": "Konfiguracja integracji JSON", + "metadata": "Metadane", + "type": "Typ integracji", + "select-integration-type": "Wybierz typ integracji", + "type-required": "Typ integracji jest wymagany.", + "type-not-found": "Nie znaleziono typu integracji", + "uplink-converter": "Konwerter danych łącza zwrotnego", + "uplink-converter-required": "Konwerter danych łącza zwrotnego jest wymagany.", + "downlink-converter": "Konwerter danych łącza w dół", + "type-http": "HTTP", + "type-http-description": "Protokół przesyłania plików tekstowych lub multimedialnych.", + "type-ocean-connect": "OceanConnect", + "type-ocean-connect-description": "Integracja urządzeń o różnych możliwościach z platformą IoT.", + "type-sigfox": "SigFox", + "type-sigfox-description": "Lekki protokół do obsługi małych transmisji danych.", + "type-thingpark": "ThingPark", + "type-thingpark-description": "Protokół warstwy transportowej zorientowanej na komunikaty.", + "type-loriot": "Loriota", + "type-loriot-description": "Rozproszona infrastruktura LoRaWAN.", + "type-thingpark-enterprise": "Firma ThingPark", + "type-thingpark-enterprise-description": "Platforma komunikacyjna dla sieci LPWA.", + "type-tmobile-iot-cdp": "iotcreators.com (T-Mobile – IoT CDP)", + "type-tmobile-iot-cdp-description": "Usługa łączności mobilnej IoT.", + "type-mqtt": "MQTT", + "type-mqtt-description": "Protokół sieciowy maszyna do maszyny.", + "type-aws-iot": "AWS IoT", + "type-aws-iot-description": "Usługi zarządzania, przechowywania i analizy.", + "type-aws-sqs": "AWS SQS", + "type-aws-sqs-description": "W pełni zarządzana usługa kolejkowania wiadomości.", + "type-aws-kinesis": "AWS Kinesis", + "type-aws-kinesis-description": "Usługa strumieni wideo i danych w czasie rzeczywistym.", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ibm-watson-iot-description": "W pełni zarządzana usługa hostowana w chmurze.", + "type-ttn": "The Things Stack Community", + "type-ttn-description": "Otwarta i zdecentralizowana sieć LoRaWAN.", + "type-tti": "The Things Stack Industries", + "type-tti-description": "Oprogramowanie do obsługi prywatnych sieci LoRaWAN na dużą skalę.", + "type-chirpstack": "ChirpStack", + "type-chirpstack-description": "Serwer konfiguracji sieci LoRaWAN.", + "type-particle": "Particle", + "type-particle-description": "IoT enablement platform-as-a-service.", + "type-azure-event-hub": "Centrum zdarzeń platformy Azure", + "type-azure-event-hub-description": "Platforma do strumieniowego przesyłania dużych zbiorów danych i usługa pozyskiwania zdarzeń.", + "type-azure-iot-hub": "Centrum IoT platformy Azure", + "type-azure-iot-hub-description": "Usługa przesyłania wiadomości hostowana w chmurze.", + "type-opc-ua": "OPC-UA", + "type-opc-ua-description": "Wieloplatformowa platforma do wymiany danych z czujników do aplikacji w chmurze.", + "type-custom": "custom", + "type-coap": "CoAP", + "type-coap-description": "Protokół sieciowy maszyna do maszyny dla urządzeń z ograniczeniami.", + "type-udp": "UDP", + "type-udp-description": "Protokół warstwy transportowej zorientowanej na komunikaty.", + "type-tcp": "TCP", + "type-tcp-description": "Protokół zorientowany na połączenie.", + "type-kafka": "Kafka", + "type-kafka-description": "Analityka przesyłania strumieniowego, platforma do przesyłania strumieniowego integracji danych.", + "type-rabbitmq": "RabbitMQ", + "type-rabbitmq-description": "Broker wiadomości obsługujący wiele protokołów przesyłania wiadomości.", + "type-pubsub": "Pub/sub", + "type-pubsub-description": "Usługa asynchronicznej komunikacji między aplikacjami.", + "type-apache-pulsar": "Apache Pulsar", + "type-apache-pulsar-description": "Rozproszona platforma do przesyłania wiadomości i przesyłania strumieniowego między pubami.", + "type-tuya": "Tuya", + "type-tuya-description": "Wiadomości hostowane w chmurze. ", + "type-azure-service-bus": "Magistrala usług platformy Azure", + "type-azure-service-bus-description": "W pełni zarządzany korporacyjny broker komunikatów z kolejkami komunikatów i tematami publikowania i subskrybowania.", + "opc-ua-application-name": "Nazwa aplikacji", + "opc-ua-application-uri": "Nazwa aplikacji", + "opc-ua-scan-period-in-seconds": "Okres skanowania w sekundach", + "opc-ua-scan-period-in-seconds-required": "Okres skanowania jest wymagany", + "opc-ua-timeout": "Limit czasu w milisekundach", + "opc-ua-timeout-required": "Limit czasu jest wymagany", + "opc-ua-security": "Bezpieczeństwo", + "opc-ua-security-required": "Bezpieczeństwo jest wymagane", + "opc-ua-identity": "Tożsamość", + "opc-ua-identity-required": "Tożsamość jest wymagana", + "opc-ua-keystore": "Magazyn kluczy", + "add-opc-ua-keystore-prompt": "Proszę dodać plik magazynu kluczy", + "opc-ua-keystore-required": "Magazyn kluczy jest wymagany", + "opc-ua-type": "Typ", + "opc-ua-keystore-type": "Typ", + "opc-ua-keystore-type-required": "Typ jest wymagany", + "opc-ua-keystore-location": "Lokalizacja *", + "opc-ua-keystore-password": "Hasło", + "opc-ua-keystore-password-required": "Hasło jest wymagane", + "opc-ua-keystore-alias": "Alias", + "opc-ua-keystore-alias-required": "Alias jest wymagany", + "opc-ua-keystore-key-password": "Kluczowe hasło", + "opc-ua-keystore-key-password-required": "Hasło klucza jest wymagane", + "opc-ua-mapping": "Mapowanie", + "add-opc-ua-mapping-prompt": "Proszę dodać mapowanie", + "opc-ua-mapping-type": "Typ mapowania", + "opc-ua-mapping-type-required": "Typ mapowania jest wymagany", + "opc-ua-device-node-pattern": "Wzorzec węzła urządzenia", + "opc-ua-device-node-pattern-required": "Wzorzec węzła urządzenia jest wymagany", + "opc-ua-namespace": "Przestrzeń nazw", + "opc-ua-add-map": "Dodaj element mapowania", + "kinesis-stream-name": "Nazwa strumienia", + "kinesis-stream-name-required": "Nazwa strumienia jest wymagana", + "kinesis-region": "Region", + "kinesis-region-required": "Region jest wymagany", + "kinesis-access-key-id": "Identyfikator klucza dostępu", + "kinesis-access-key-id-required": "Identyfikator klucza dostępu jest wymagany", + "kinesis-secret-access-key": "Tajny klucz dostępu", + "kinesis-secret-access-key-required": "Tajny klucz dostępu jest wymagany", + "kinesis-use-consumers-with-enhanced-fan-out": "Użyj konsumentów z ulepszonym rozgałęzianiem", + "kinesis-use-credentials-from-instance-metadata": "Użyj danych uwierzytelniających z usługi metadanych instancji Amazon EC2", + "kinesis-application-name": "Nazwa aplikacji (domyślnie jest równa nazwie strumienia)", + "kinesis-initial-position-in-stream": "Początkowa pozycja w strumieniu", + "kinesis-initial-position-in-stream-required": "Wymagana pozycja początkowa w strumieniu", + "kinesis-max-records": "Maksymalne rekordy", + "kinesis-max-records-required": "Maksymalna liczba rekordów jest wymagana", + "kinesis-max-records-length-range": "Maksymalna długość rekordów powinna mieścić się w zakresie od 1 do 10000", + "kinesis-request-timeout": "Żądaj limitu czasu w sekundach", + "kinesis-request-timeout-required": "Limit czasu żądania jest wymagany", + "other-properties": "Inne właściwości", + "subscription-tags": "Tagi subskrypcji", + "remove-subscription-tag": "Usuń tag subskrypcji", + "add-subscription-tag": "Dodaj tag subskrypcji", + "add-subscription-tag-prompt": "Dodaj tag subskrypcji", + "key": "Klucz", + "path": "Ścieżka", + "required": "Wymagany", + "integration-key": "Klucz integracyjny", + "copy-integration-key": "Skopiuj klucz integracji", + "integration-key-copied-message": "Klucz integracji został skopiowany do schowka", + "integration-secret": "Sekret integracji", + "copy-integration-secret": "Skopiuj sekret integracji", + "integration-secret-copied-message": "Sekret integracji został skopiowany do schowka", + "execute-remotely": "Wykonuj zdalnie", + "remote": "Zdalny", + "handler-configuration": "Konfiguracja modułu obsługi", + "handler-configuration-type": "Typ obsługi", + "so-broadcast": "Włącz rozgłaszanie - integracja akceptuje pakiety adresów rozgłoszeniowych", + "so-keepalive-option": "Włącz wysyłanie komunikatów podtrzymujących działanie w gniazdach zorientowanych na połączenie", + "so-reuse-addr": "Powiąż proces z portem", + "tcp-no-delay": "Wymusza wysyłanie danych przez gniazdo bez buforowania (wyłącz algorytm buforowania Nagle'a)", + "fail-fast": "Zgłoszony wyjątek, gdy dekoder zauważy, że długość ramki przekracza maksymalny rozmiar", + "strip-delimiter": "Ogranicznik pasków", + "length-field-offset": "Przesunięcie pola długości", + "length-field-offset-required": "Przesunięcie pola długości jest wymagane.", + "length-field-offset-range": "Przesunięcie pola długości powinno mieścić się w zakresie od 0 do 8.", + "length-field-length": "Długość Długość pola", + "length-field-length-required": "Długość Długość pola jest wymagana.", + "length-field-length-range": "Długość pola Długość powinna mieścić się w zakresie od 0 do 8.", + "length-adjustment": "Regulacja długości (wartość kompensacji dodana do wartości pola długości)", + "length-adjustment-required": "Regulacja długości jest wymagana.", + "length-adjustment-range": "Regulacja długości powinna mieścić się w zakresie od 0 do 8.", + "byte-order": "Kolejność bajtów w polu długości", + "initial-bytes-to-strip": "Liczba pierwszych bajtów do usunięcia z zdekodowanej ramki", + "initial-bytes-to-strip-required": "Liczba pierwszych bajtów do usunięcia z zdekodowanej ramki jest wymagana.", + "initial-bytes-to-strip-range": "Liczba pierwszych bajtów do usunięcia z zdekodowanej ramki powinna mieścić się w zakresie od 0 do 8.", + "so-backlog-option": "Maksymalna liczba oczekujących połączeń na gnieździe", + "so-backlog-option-required": "Maksymalna liczba oczekujących połączeń na gnieździe jest wymagana.", + "so-backlog-option-range": "Maksymalna liczba oczekujących połączeń w gnieździe powinna mieścić się w zakresie od 1 do 65535.", + "so-rcv-buf": "Rozmiar bufora dla gniazda przychodzącego (w KB)", + "so-rcv-buf-required": "Rozmiar bufora dla gniazda przychodzącego (w KB) jest wymagany.", + "so-rcv-buf-range": "Rozmiar bufora dla gniazda przychodzącego (w KB) powinien mieścić się w przedziale od 1 do 65535.", + "so-snd-buf": "Rozmiar bufora dla gniazda wychodzącego (w KB)", + "so-snd-buf-required": "Rozmiar bufora dla gniazda wychodzącego (w KB) jest wymagany.", + "so-snd-buf-range": "Rozmiar bufora dla gniazda wychodzącego (w KB) powinien mieścić się w przedziale od 1 do 65535.", + "charset-name": "Nazwa zestawu znaków", + "charset-name-required": "Nazwa zestawu znaków jest wymagana.", + "message-separator": "Separator wiadomości", + "message-separator-required": "Separator wiadomości jest wymagany.", + "character-sequence": "Sekwencja znaków", + "character-sequence-required": "Sekwencja znaków jest wymagana.", + "max-frame-length": "Maksymalna długość ramki (w bajtach)", + "max-frame-length-required": "Maksymalna długość ramki (w bajtach) jest wymagana.", + "max-frame-length-range": "Maksymalna długość ramki (w bajtach) powinna mieścić się w zakresie od 1 do 65535.", + "handler-type": "Typ obsługi", + "message-size": "Rozmiar wiadomości", + "message-size-required": "Rozmiar wiadomości jest wymagany.", + "service-url": "Adres URL usługi", + "service-url-required": "Adres URL usługi jest wymagany.", + "subscription-name": "Nazwa subskrypcji", + "subscription-name-required": "Nazwa subskrypcji jest wymagana.", + "max-num-messages": "Maksymalna liczba wiadomości", + "max-num-messages-required": "Maksymalna liczba wiadomości jest wymagana.", + "max-num-bytes": "Maksymalna liczba bajtów", + "max-num-bytes-required": "Wymagana maksymalna liczba bajtów.", + "timeout-in-ms": "Limit czasu w milisekundach", + "timeout-in-ms-required": "Limit czasu w milisekundach jest wymagany.", + "user-id": "Identyfikator użytkownika", + "user-id-required": "Identyfikator użytkownika jest wymagany.", + "token": "Znak", + "token-required": "Token jest wymagany.", + "project-id": "Identyfikator projektu", + "project-id-required": "Identyfikator projektu jest wymagany.", + "subscription-id": "Identyfikator subskrypcji", + "subscription-id-required": "Identyfikator subskrypcji jest wymagany.", + "service-account-key": "Plik klucza konta usługi", + "service-account-key-required": "Plik klucza konta usługi jest wymagany.", + "tcp": { + "system-line-separator": "Separator linii systemowych", + "nul-delimiter": "Separator Nul", + "byte-order-little-endian": "Little Endian", + "byte-order-big-endian": "Big Endian" + }, + "cache-size": "Rozmiar pamięci podręcznej", + "cache-time-to-live": "Czas działania pamięci podręcznej w minutach", + "min-cache-size": "Rozmiar pamięci podręcznej nie może być mniejszy niż 0", + "min-cache-time-to-live": "Czas życia pamięci podręcznej nie może być mniejszy niż 0", + "max-cache-time-to-live": "Nieprawidłowy czas aktywności pamięci podręcznej. Wybierz wartość z zakresu od 0 do 525600", + "coap-security-mode": "Tryb Bezpieczny", + "coap-security-mode-required": "Tryb zabezpieczeń CoAP jest wymagany", + "coap-security-mode-no-secure": "NIE BEZPIECZNIE", + "coap-security-mode-dtls": "DTLS", + "coap-security-mode-mixed": "MIESZANY", + "coap-endpoint": "Adres URL punktu końcowego CoAP", + "coap-endpoint-url-copied-message": "Adres URL punktu końcowego CoAP został skopiowany do schowka", + "copy-coap-endpoint-url": "Skopiuj adres URL punktu końcowego CoAP", + "copy-coap-dtls-endpoint-url": "Skopiuj adres URL punktu końcowego CoAP DTLS", + "coap-dtls-base-url": "Podstawowy adres URL DTLS", + "coap-dtls-base-url-required": "Podstawowy adres URL DTLS jest wymagany", + "coap-dtls-endpoint": "Adres URL punktu końcowego CoAP DTLS", + "coap-dtls-endpoint-url-copied-message": "Adres URL punktu końcowego CoAP DTLS został skopiowany do schowka", + "unassign-integration-title": "Czy na pewno chcesz anulować przypisanie integracji '{{integrationName}}'?", + "unassign-integration-from-edge-text": "Po potwierdzeniu integracja zostanie anulowana i nie będzie dostępna z Edge.", + "unassign-integrations-from-edge-title": "Czy na pewno chcesz usunąć przypisanie {count, plural, =1 {1 integracji} other {# integracji} }?", + "unassign-integrations-from-edge-text": "Po potwierdzeniu wszystkie wybrane integracje zostaną usunięte i nie będą dostępne na Edge.", + "unassign-integrations": "Usuń przypisanie integracji", + "edge-placeholder-hint": "Możesz użyć symbolu zastępczego ${{ATTRIBUTE_KEY}} aby zastąpić pole integracji wartością atrybutu z konkretnego obiektu Edge. Na przykład, 'Edge A' ma atrybut 'baseUrl' wynoszący 'http://localhost:9999'. Możesz ustawić {{baseUrl}} jako jedno z pól integracji i zostanie ono zastąpione przez 'http://localhost:9999” podczas przypisywania tej integracji do 'Edge A'. Dodatkowo, jeśli 'Edge A' atrybut 'baseUrl' zostanie zaktualizowany - integracja z nową zaktualizowaną wartością będzie automatycznie zapewniona 'Edge A'.", + "status": { + "status": "Status", + "active": "Aktywny", + "disabled": "Wyłączony", + "failed": "Błąd", + "pending": "Aż do" + }, + "daily-activity": "Dzienna aktywność", + "enable-debug-mode": "Włącz tryb debugowania", + "disable-debug-mode": "Wyłącz tryb debugowania", + "advanced-settings": "Zaawansowane ustawienia", + "basic-settings": "Podstawowe ustawienia", + "existing-converter": "Select existing", + "new-converter": "Create new", + "connection": "Połączenie", + "connected": "Połączony", + "not-connected": "Nie połączony", + "environment": "Środowisko", + "environment-required": "Środowisko jest wymagane.", + "region-cn": "Chiny", + "region-us": "Stany Zjednoczone", + "region-eu": "Europa", + "region-in": "Indie", + "topic-name": "Nazwa tematu", + "topic-name-required": "Nazwa tematu jest wymagana", + "sub-name": "Nazwa subskrypcji", + "sub-name-required": "Nazwa subskrypcji jest wymagana", + "downlink-connection-string": "Parametry połączenia łącza w dół", + "downlink-connection-string-required": "Parametry połączenia łącza w dół są wymagane", + "downlink-topic-name": "Nazwa tematu łącza w dół", + "downlink-topic-name-required": "Nazwa tematu łącza w dół jest wymagana", + "client-id-range": "Identyfikator klienta powinien zawierać się w zakresie od 1 do 23 znaków. ", + "client-id-pattern": "Identyfikator klienta powinien składać się z cyfr, wielkich i małych liter. ", + "existing-uplink-converter": "Wybierz istniejący konwerter danych łącza zwrotnego", + "new-uplink-converter": "Utwórz nowy konwerter danych łącza zwrotnego", + "existing-downlink-converter": "Wybierz istniejący konwerter danych łącza w dół", + "new-downlink-converter": "Utwórz nowy konwerter danych łącza w dół" + }, + "item": { + "selected": "Wybrany" }, - "js-func":{ - "no-return-error":"Funkcja musi zwrócić wartość!", - "return-type-mismatch":"Funkcja musi zwrócić wartość typu '{{type}}'!", - "tidy":"Porządek", - "mini":"Mini" + "js-func": { + "no-return-error": "Funkcja musi zwracać wartość!", + "return-type-mismatch": "Funkcja musi zwracać wartość '{{type}}' typ!", + "tidy": "Czysty", + "mini": "Mini" }, - "key-val":{ - "key":"Klucz", - "value":"Wartość", - "remove-entry":"Usuń wpis", - "add-entry":"Dodaj wpis", - "no-data":"Brak danych" + "key-val": { + "key": "Klucz", + "value": "Wartość", + "remove-entry": "Usuń wpis", + "add-entry": "Dodaj wpis", + "no-data": "Brak wpisów" }, - "layout":{ - "layout":"Układ", - "layouts":"Układy", - "manage":"Zarządzaj układami", - "settings":"Ustawienia układu", - "color":"Kolor", - "main":"Główny", - "right":"Prawo", - "left":"Lewo", - "select":"Wybierz docelowy układ", - "percentage-width":"Szerokość procentowa (%)", - "fixed-width":"Szerokość stała (px)", - "left-width":"Szerokość lewej kolumny (%)", - "right-width":"Szerokość prawej kolumny (%)", - "pick-fixed-side":"Stała strona: ", - "layout-fixed-width":"Szerokość stała (px)", - "value-min-error":"Wartość musi być większa niż {{min}}{{unit}}", - "value-max-error":"Wartość musi być mniejsza niż {{max}}{{unit}}", - "layout-fixed-width-required":"Szerokość stała jest wymagana", - "right-width-percentage-required":"Wymagany jest procent szerokości prawej strony", - "left-width-percentage-required":"Wymagany jest procent szerokości lewej strony", - "divider":"Podziałka", - "right-side":"Układ prawej strony", - "left-side":"Układ lewej strony" + "layout": { + "layout": "Układ", + "layouts": "Layouts", + "manage": "Zarządzaj układem panelu", + "settings": "Ustawienia układu", + "color": "Kolor", + "main": "Główny", + "right": "Prawy", + "left": "Lewy", + "select": "Wybierz układ docelowy", + "percentage-width": "Szerokość procentowa (%)", + "fixed-width": "Stała szerokość (px)", + "left-width": "Lewa kolumna (%)", + "right-width": "Prawa kolumna (%)", + "pick-fixed-side": "Naprawiono stronę: ", + "layout-fixed-width": "Stała szerokość (px)", + "value-min-error": "Wartość musi być zatem większa {{min}}{{unit}}", + "value-max-error": "Wartość musi być wtedy mniejsza {{max}}{{unit}}", + "layout-fixed-width-required": "Stała szerokość jest wymagana", + "right-width-percentage-required": "Odpowiedni procent jest wymagany", + "left-width-percentage-required": "Lewy procent jest wymagany", + "divider": "Rozdziel panel", + "right-side": "Układ prawej strony", + "left-side": "Układ lewej strony" }, - "legend":{ - "direction":"Kierunek", - "position":"Pozycja", - "show-values":"Pokaż wartości", - "min-option":"Min", - "max-option":"Maks.", - "average-option":"Średnia", - "total-option":"Suma", - "latest-option":"Najnowsza", - "sort-legend":"Sortuj klucze danych w legendzie", - "show-max":"Pokaż maksymalną wartość", - "show-min":"Pokaż minimalną wartość", - "show-avg":"Pokaż średnią wartość", - "show-total":"Pokaż sumę wartości", - "show-latest":"Pokaż najnowszą wartość", - "settings":"Ustawienia legendy", - "min":"min", - "max":"maks.", - "avg":"śr.", - "total":"suma", - "latest":"najnowsza", - "comparison-time-ago":{ - "previousInterval":"(poprzedni przedział)", - "customInterval":"(niestandardowy przedział)", - "days":"(dzień temu)", - "weeks":"(tydzień temu)", - "months":"(miesiąc temu)", - "years":"(rok temu)" - }, - "label":"Etykieta", - "value":"Wartość" + "legend": { + "direction": "Kierunek legendy", + "position": "Pozycja legendy", + "show-values": "Show values", + "min-option": "Min", + "max-option": "Max", + "average-option": "Average", + "total-option": "Total", + "latest-option": "Latest", + "sort-legend": "Sortuj klucze danych w legendzie", + "show-max": "Pokaż wartość maksymalną", + "show-min": "Pokaż wartość minimalną", + "show-avg": "Pokaż średnią wartość", + "show-total": "Pokaż całkowitą wartość", + "show-latest": "Pokaż najnowszą wartość", + "settings": "Ustawienia legendy", + "min": "min", + "max": "maks", + "avg": "śr", + "total": "całkowity", + "latest": "najnowszy", + "comparison-time-ago": { + "previousInterval": "(poprzedni interwał)", + "customInterval": "(interwał niestandardowy)", + "days": "(dzień temu)", + "weeks": "(tydzień temu)", + "months": "(miesiąc temu)", + "years": "(rok temu)" + } }, - "login":{ - "login":"Zaloguj się", - "request-password-reset":"Zażądaj resetu hasła", - "reset-password":"Resetuj hasło", - "create-password":"Utwórz hasło", - "two-factor-authentication":"Autentykacja dwuskładnikowa", - "passwords-mismatch-error":"Wprowadzone hasła muszą być takie same!", - "password-again":"Powtórz hasło", - "sign-in":"Proszę się zalogować", - "username":"Nazwa użytkownika (e-mail)", - "remember-me":"Zapamiętaj mnie", - "forgot-password":"Zapomniałeś hasła?", - "password-reset":"Reset hasła", - "expired-password-reset-message":"Twoje dane uwierzytelniające wygasły! Proszę utworzyć nowe hasło.", - "new-password":"Nowe hasło", - "new-password-again":"Potwierdź nowe hasło", - "password-link-sent-message":"Link do resetowania hasła został wysłany", - "email":"E-mail", - "login-with":"Zaloguj się za pomocą {{name}}", - "or":"lub", - "error":"Błąd logowania", - "verify-your-identity":"Zweryfikuj swoją tożsamość", - "select-way-to-verify":"Wybierz sposób weryfikacji", - "resend-code":"Wyślij kod ponownie", - "resend-code-wait":"Ponownie wyślij kod za { time, plural, =1 {1 sekundę} other {# sekundy} }", - "try-another-way":"Spróbuj inną metodę", - "totp-auth-description":"Proszę podać kod z aplikacji autoryzacyjnej.", - "totp-auth-placeholder":"Kod", - "sms-auth-description":"Kod zabezpieczający został wysłany na Twój telefon pod numerem {{contact}}.", - "sms-auth-placeholder":"Kod SMS", - "email-auth-description":"Kod zabezpieczający został wysłany na Twój adres e-mailowy {{contact}}.", - "email-auth-placeholder":"Kod e-mailowy", - "backup-code-auth-description":"Proszę podać jeden z kodów zapasowych.", - "backup-code-auth-placeholder":"Kod zapasowy" + "login": { + "login": "Zaloguj sie", + "request-password-reset": "Poproś o zresetowanie hasła", + "reset-password": "Zresetuj hasło", + "create-password": "Stwórz hasło", + "two-factor-authentication": "Uwierzytelnianie dwuskładnikowe", + "passwords-mismatch-error": "Wpisane hasła muszą być takie same!", + "password-again": "Hasło ponownie", + "sign-in": "proszę, zaloguj się", + "username": "Nazwa użytkownika (e-mail)", + "remember-me": "Zapamiętaj mnie", + "forgot-password": "Zapomniałeś hasła?", + "password-reset": "Resetowania hasła", + "expired-password-reset-message": "Twoje dane uwierzytelniające wygasły! ", + "new-password": "Nowe hasło", + "new-password-again": "Potwierdź nowe hasło", + "password-link-sent-message": "Link resetujący został wysłany", + "email": "E-mail", + "no-account": "Nie masz konta?", + "create-account": "Utwórz konto", + "login-with": "Zaloguj się z {{name}}", + "or": "Lub", + "error": "Błąd logowania", + "verify-your-identity": "Zweryfikować Twoją tożsamość", + "select-way-to-verify": "Wybierz sposób weryfikacji", + "resend-code": "Wyślij kod ponownie", + "resend-code-wait": "Wyślij kod ponownie za { time, plural, =1 {1 sekunda} other { # sekundy } }", + "try-another-way": "Spróbuj w inny sposób", + "totp-auth-description": "Wprowadź kod zabezpieczający z aplikacji uwierzytelniającej.", + "totp-auth-placeholder": "Kod", + "sms-auth-description": "Kod zabezpieczający został wysłany na Twój telefon pod adresem {{contact}}.", + "sms-auth-placeholder": "Kod SMS", + "email-auth-description": "Kod zabezpieczający został wysłany na Twój adres e-mail pod adresem {{contact}}.", + "email-auth-placeholder": "Kod e-mail", + "backup-code-auth-description": "Wprowadź jeden ze swoich kodów zapasowych.", + "backup-code-auth-placeholder": "Kod zapasowy" }, - "markdown":{ - "edit":"Edytuj", - "preview":"Podgląd", - "copy-code":"Kliknij, aby skopiować", - "copied":"Skopiowano!" + "signup": { + "firstname": "Imię", + "lastname": "Nazwisko", + "email": "E-mail", + "signup": "Zapisz się", + "create-password": "Stwórz hasło", + "repeat-password": "Powtórz swoje hasło", + "have-account": "Posiadasz już konto?", + "signin": "Zaloguj się", + "no-captcha-message": "Musisz potwierdzić, że nie jesteś robotem", + "password-length-message": "Twoje hasło musi mieć co najmniej 6 znaków", + "email-verification": "Weryfikacja adresu e-mail", + "email-verification-message": "Na podany adres e-mail została wysłana wiadomość e-mail zawierająca szczegóły weryfikacji.
Aby dokończyć procedurę rejestracji, postępuj zgodnie z instrukcjami zawartymi w wiadomości e-mail.
Uwaga: jeśli nie widziałeś wiadomości e-mail przez jakiś czas, sprawdź folder „spam” lub spróbuj wysłać wiadomość ponownie, klikając przycisk „Wyślij ponownie”.", + "account-activation-title": "Aktywacja konta", + "account-activated": "Konto zostało pomyślnie aktywowane!", + "account-activated-text": "Gratulacje!
Twoje konto zostało aktywowane.", + "resend": "Wyślij ponownie", + "inactive-user-exists-title": "Nieaktywny użytkownik już istnieje", + "inactive-user-exists-text": "Istnieje już zarejestrowany użytkownik z niezweryfikowanym adresem e-mail.
Kliknij przycisk „Wyślij ponownie”, jeśli chcesz ponownie wysłać wiadomość weryfikacyjną.", + "activating-account": "Aktywuję konto...", + "activating-account-text": "Twoje konto jest obecnie aktywowane. Czekaj...", + "accept-privacy-policy": "Zaakceptuj Politykę prywatności", + "accept": "Akceptuj", + "privacy-policy": "Polityka prywatności", + "accept-privacy-policy-message": "Musisz zaakceptować naszą Politykę prywatności", + "recaptcha-title": "reCAPTCHA", + "terms-of-use": "Warunki użytkowania", + "accept-terms-of-use-message": "Musisz zaakceptować nasze Warunki użytkowania" }, - "notification":{ - "action-button":"Przycisk akcji", - "action-type":"Typ akcji", - "active":"Aktywny", - "add-notification-recipients-group":"Dodaj grupę odbiorców powiadomień", - "add-notification-template":"Dodaj szablon powiadomienia", - "add-recipient":"Dodaj odbiorcę", - "add-recipients":"Dodaj odbiorców", - "add-rule":"Dodaj regułę", - "add-stage":"Dodaj etap", - "add-template":"Dodaj szablon", - "after":"Po", - "alarm-assignment-trigger-settings":"Ustawienia wyzwalacza przypisania alarmu", - "alarm-comment-trigger-settings":"Ustawienia wyzwalacza komentarza alarmu", - "alarm-trigger-settings":"Ustawienia wyzwalacza alarmu", - "all":"Wszystkie", - "api-feature-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich funkcji API", - "api-usage-trigger-settings":"Ustawienia wyzwalacza korzystania z API", - "new-platform-version-trigger-settings":"Ustawienia wyzwalacza nowej wersji platformy", - "rate-limits-trigger-settings":"Ustawienia wyzwalacza przekroczenia limitów szybkości", - "at-least-one-should-be-selected":"Przynajmniej jedna pozycja musi być zaznaczona", - "basic-settings":"Podstawowe ustawienia", - "button-text":"Tekst przycisku", - "button-text-required":"Tekst przycisku jest wymagany", - "button-text-max-length":"Tekst przycisku powinien być krótszy lub równy {{ length }} znaków", - "compose":"Komponuj", - "conversation":"Rozmowa", - "conversation-required":"Rozmowa jest wymagana", - "copy-notification-template":"Skopiuj szablon powiadomienia", - "copy-rule":"Skopiuj regułę", - "copy-template":"Skopiuj szablon", - "create-new":"Utwórz nowe", - "created":"Utworzono", - "delete-notification-text":"Uważaj, po potwierdzeniu powiadomienie stanie się nieodwracalne.", - "delete-notification-title":"Czy na pewno chcesz usunąć powiadomienie?", - "delete-notifications-text":"Uważaj, po potwierdzeniu powiadomienia staną się nieodwracalne.", - "delete-notifications-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 powiadomienie} other {# powiadomienia} }?", - "delete-recipient-text":"Uważaj, po potwierdzeniu odbiorca stanie się nieodwracalny.", - "delete-recipient-title":"Czy na pewno chcesz usunąć odbiorcę '{{recipientName}}'?", - "delete-recipients-text":"Uważaj, po potwierdzeniu odbiorcy staną się nieodwracalni.", - "delete-recipients-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 odbiorcę} other {# odbiorców} }?", - "delete-request-text":"Uważaj, po potwierdzeniu żądanie stanie się nieodwracalne.", - "delete-request-title":"Czy na pewno chcesz usunąć żądanie?", - "delete-requests-text":"Uważaj, po potwierdzeniu żądania staną się nieodwracalne.", - "delete-requests-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 żądanie} other {# żądań} }?", - "delete-rule-text":"Uważaj, po potwierdzeniu reguła stanie się nieodwracalna.", - "delete-rule-title":"Czy na pewno chcesz usunąć regułę '{{ruleName}}'?", - "delete-rules-text":"Uważaj, po potwierdzeniu reguły staną się nieodwracalne.", - "delete-rules-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 regułę} other {# reguł} }?", - "delete-template-text":"Uważaj, po potwierdzeniu szablon stanie się nieodwracalny.", - "delete-template-title":"Czy na pewno chcesz usunąć szablon '{{templateName}}'?", - "delete-templates-text":"Uważaj, po potwierdzeniu szablony staną się nieodwracalne.", - "delete-templates-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 szablon} other {# szablonów} }?", - "deleted":"Usunięto", - "delivery-method":{ - "delivery-method":"Metoda dostarczania", - "email":"Email", - "email-preview":"Podgląd powiadomienia email", - "slack":"Slack", - "slack-preview":"Podgląd powiadomienia Slack", - "microsoft-teams":"Microsoft Teams", - "microsoft-teams-preview":"Podgląd powiadomienia Microsoft Teams", - "sms":"SMS", - "sms-preview":"Podgląd powiadomienia SMS", - "web":"Web", - "web-preview":"Podgląd powiadomienia web" + "notification": { + "action-button": "Przycisk akcji", + "action-type": "Rodzaj działania", + "active": "Aktywny", + "add-notification-recipients-group": "Dodaj grupę odbiorców powiadomień", + "add-notification-template": "Dodaj szablon powiadomienia", + "add-recipient": "Dodaj odbiorcę", + "add-recipients": "Dodaj odbiorców", + "add-rule": "Dodaj regułę", + "add-stage": "Dodaj scenę", + "add-template": "Dodaj szablon", + "after": "Po", + "alarm-assignment-trigger-settings": "Ustawienia wyzwalacza przypisania alarmu", + "alarm-comment-trigger-settings": "Ustawienia wyzwalania komentarza alarmowego", + "alarm-trigger-settings": "Ustawienia wyzwalania alarmu", + "all": "Wszystko", + "api-feature-hint": "Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich funkcji interfejsu API", + "api-usage-trigger-settings": "Ustawienia wyzwalacza użycia interfejsu API", + "new-platform-version-trigger-settings": "Ustawienia wyzwalacza nowej wersji platformy", + "rate-limits-trigger-settings": "Exceeded rate limits trigger settings", + "at-least-one-should-be-selected": "Należy wybrać przynajmniej jeden", + "basic-settings": "Podstawowe ustawienia", + "button-text": "Przycisk tekstowy", + "button-text-required": "Tekst przycisku jest wymagany", + "button-text-max-length": "Długość tekst przycisku powinna być mniejsza lub równa {{ length }} znaków", + "compose": "Komponuj", + "conversation": "Rozmowa", + "conversation-required": "Rozmowa jest konieczna", + "copy-notification-template": "Skopiuj szablon powiadomienia", + "copy-rule": "Skopiuj regułę", + "copy-template": "Skopiuj szablon", + "create-new": "Stwórz nowe", + "created": "Utworzony", + "delete-notification-text": "Uważaj, po potwierdzeniu powiadomienia nie będzie można odzyskać.", + "delete-notification-title": "Czy na pewno chcesz usunąć powiadomienie?", + "delete-notifications-text": "Bądź ostrożny, po potwierdzeniu powiadomienia staną się nie do odzyskania.", + "delete-notifications-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 przypomnienie} other {# przypomnień} }?", + "delete-recipient-text": "Uważaj, po potwierdzeniu odbiorcy nie będzie można odzyskać.", + "delete-recipient-title": "Czy na pewno chcesz usunąć odbiorcę {{recipientName}}'?", + "delete-recipients-text": "Bądź ostrożny, po potwierdzeniu odbiorcy staną się niemożliwi do odzyskania.", + "delete-recipients-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 odbiorcę} other {# odbiorców} }?", + "delete-request-text": "Bądź ostrożny, po tym jak prośba o potwierdzenie stanie się niemożliwa do odzyskania.", + "delete-request-title": "Czy na pewno chcesz usunąć rządanie?", + "delete-requests-text": "Bądź ostrożny, po tym jak prośby o potwierdzenie staną się nie do odzyskania.", + "delete-requests-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 rządanie} other {# rządań} }?", + "delete-rule-text": "Bądź ostrożny, gdy reguła potwierdzenia stanie się niemożliwa do odzyskania.", + "delete-rule-title": "Czy na pewno chcesz usunąć regułę '{{ruleName}}'?", + "delete-rules-text": "Bądź ostrożny, po tym jak zasady potwierdzenia staną się nie do odzyskania.", + "delete-rules-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 regułę} other {# reguł} }?", + "delete-template-text": "Zachowaj ostrożność, ponieważ szablon potwierdzenia stanie się niemożliwy do odzyskania.", + "delete-template-title": "Czy na pewno chcesz usunąć szablon '{{templateName}}'?", + "delete-templates-text": "Bądź ostrożny, gdy szablony potwierdzeń staną się nie do odzyskania.", + "delete-templates-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 szablon} other {# szablonów} }?", + "deleted": "Usunięto", + "delivery-method": { + "delivery-method": "Sposób przesyłu", + "email": "E-mail", + "email-preview": "Podgląd powiadomienia e-mail", + "slack": "Slack", + "slack-preview": "Podgląd powiadomień Slacka", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams notification preview", + "sms": "SMS", + "sms-preview": "Podgląd powiadomień SMS", + "web": "Sieć", + "web-preview": "Podgląd powiadomień internetowych" }, - "delivery-method-not-configure-click":"Metoda dostarczania nie jest skonfigurowana. Kliknij, aby skonfigurować.", - "delivery-method-not-configure-contact":"Metoda dostarczania nie jest skonfigurowana. Skontaktuj się z administratorem systemu.", - "delivery-methods":"Metody dostarczania", - "description":"Opis", - "device-activity-trigger-settings":"Ustawienia wyzwalacza aktywności urządzenia", - "device-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich urządzeń", - "device-profiles-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich profili urządzeń", - "disabled":"Wyłączone", - "edit-notification-recipients-group":"Edytuj grupę odbiorców powiadomień", - "edit-notification-template":"Edytuj szablon powiadomień", - "edit-rule":"Edytuj regułę", - "edit-template":"Edytuj szablon", - "enabled":"Włączone", - "entities-limit-trigger-settings":"Ustawienia wyzwalacza limitu encji", - "entity-action-trigger-settings":"Ustawienia wyzwalacza akcji encji", - "entity-type":"Typ encji", - "escalation-chain":"Łańcuch eskalacji", - "failed-send":"Błędy wysyłki", - "fails":"{ count, plural, =1 {1 błąd} other {# błędów} }", - "filter":"Filtr", - "first-recipient":"Pierwszy odbiorca", - "inactive":"Nieaktywne", - "inbox":"Skrzynka odbiorcza", - "notification-inbox":"Powiadomienia / Skrzynka odbiorcza", - "input-field-support-templatization":"Pole wejściowe obsługuje szablonowanie.", - "input-fields-support-templatization":"Pola wejściowe obsługują szablonowanie.", - "link":"Odnośnik", - "link-required":"Odnośnik jest wymagany", - "link-type":{ - "dashboard":"Otwórz pulpit nawigacyjny", - "link":"Otwórz link URL" + "delivery-method-not-configure-click": "Metoda dostawy nie jest skonfigurowana. ", + "delivery-method-not-configure-contact": "Metoda dostawy nie jest skonfigurowana. ", + "delivery-methods": "metody dostarczania", + "description": "Opis", + "device-activity-trigger-settings": "Ustawienia aktywnego wyzwalacza urządzenia", + "device-list-rule-hint": "Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich urządzeń", + "device-profiles-list-rule-hint": "Jeśli pole będzie puste, wyzwalacz zostanie zastosowany do wszystkich profili urządzeń", + "disabled": "Wyłączony", + "edit-notification-recipients-group": "Edytuj grupę odbiorców powiadomień", + "edit-notification-template": "Edytuj szablon powiadomienia", + "edit-rule": "Edytuj regułę", + "edit-template": "Edytuj szablon", + "enabled": "Włączony", + "entities-limit-trigger-settings": "Ustawienia wyzwalacza ograniczania obiektów", + "entity-action-trigger-settings": "Ustawienia wyzwalacza akcji obiektu", + "entity-type": "Typ obiektu", + "escalation-chain": "Łańcuch eskalacji", + "failed-send": "Niepowodzenie wysyłania", + "fails": "{ count, plural, =1 {1 niepowodzenie} other {# niepowodzeń} }", + "filter": "Filtr", + "first-recipient": "Pierwszy odbiorca", + "inactive": "Nieaktywny", + "inbox": "Skrzynka odbiorcza", + "notification-inbox": "Powiadomienia / Skrzynka odbiorcza", + "input-field-support-templatization": "Szablonowanie obsługi pola wejściowego.", + "input-fields-support-templatization": "Pola wejściowe obsługują szablonowanie.", + "integration-action-trigger-settings": "Ustawienia wyzwalacza zdarzeń integracji", + "integration-list-rule-hint": "Jeśli pole będzie puste, wyzwalacz zostanie zastosowany do wszystkich integracji", + "link": "Link", + "link-required": "Link jest wymagany", + "link-type": { + "dashboard": "Otwórz Panel", + "link": "Otwórz łącze URL" }, - "loading-notifications":"Ładowanie powiadomień...", - "management":"Zarządzanie powiadomieniami", - "mark-all-as-read":"Oznacz wszystkie jako przeczytane", - "mark-as-read":"Oznacz jako przeczytane", - "message":"Wiadomość", - "message-required":"Wiadomość jest wymagana", - "message-max-length":"Wiadomość powinna mieć mniej niż {{ length }} znaków", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana", - "new-notification":"Nowe powiadomienie", - "no-inbox-notification":"Nie znaleziono powiadomień", - "no-notification-request":"Brak zapytania o powiadomienie", - "no-notification-templates":"Nie znaleziono szablonów powiadomień", - "no-notifications-yet":"Brak powiadomień", - "no-recipients-notification":"Powiadomienie bez odbiorców", - "no-rule":"Nie skonfigurowano reguły", - "no-rules-notification":"Brak powiadomienia o regułach", - "no-severity-found":"Nie znaleziono powagi", - "no-severity-matching":"Nie znaleziono '{{severity}}'.", - "no-template-matching":"Nie znaleziono zasobów pasujących do '{{template}}'.", - "not-found-slack-recipient":"Nie znaleziono odbiorcy Slack", - "notification":"Powiadomienie", - "notification-center":"Centrum powiadomień", - "notify":"powiadomić", - "notify-again":"Powiadom ponownie", - "notify-alarm-action":{ - "acknowledged":"Alarm potwierdzony", - "assigned":"Alarm przypisany", - "cleared":"Alarm wyczyszczony", - "created":"Alarm utworzony", - "severity-changed":"Zmieniono powagę alarmu", - "unassigned":"Alarm nieprzypisany" + "loading-notifications": "Loading notifications...", + "management": "Zarządzanie powiadomieniami", + "mark-all-as-read": "Oznacz wszystkie jako przeczytane", + "mark-as-read": "Oznacz jako przeczytane", + "message": "Wiadomość", + "message-required": "Wiadomość jest wymagana", + "message-max-length": "Komunikat powinien być mniejszy lub równy {{ length }} znaków", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana", + "new-notification": "Nowe powiadomienie", + "no-inbox-notification": "Nie znaleziono powiadomienia", + "no-notification-request": "Brak żądania powiadomienia", + "no-notification-templates": "Nie znaleziono szablonów powiadomień", + "no-notifications-yet": "Nie ma jeszcze żadnych powiadomień", + "no-recipients-notification": "Brak powiadomienia odbiorców", + "no-rule": "Nie skonfigurowano żadnej reguły", + "no-rules-notification": "Brak powiadomienia o zasadach", + "no-severity-found": "Nie znaleziono powagi", + "no-severity-matching": "'{{severity}}' nie znaleziono.", + "no-template-matching": "Brak dopasowania zasobów '{{template}}' zostały znalezione.", + "not-found-slack-recipient": "Nie znaleziono odbiorcy Slacka", + "notification": "Powiadomienie", + "notification-center": "Centrum powiadomień", + "notify": "powiadom", + "notify-again": "Powiadom ponownie", + "notify-alarm-action": { + "acknowledged": "Alarm potwierdzony", + "assigned": "Alarm przypisany", + "cleared": "Alarm usunięty", + "created": "Utworzono alarm", + "severity-changed": "Zmieniono wagę alarmu", + "unassigned": "Alarm nieprzypisany" }, - "notify-on":"Powiadamiaj o", - "notify-on-comment-update":"Powiadamiaj o aktualizacji komentarza", - "notify-on-required":"Wymagane jest powiadomienie o", - "notify-on-unassign":"Powiadamiaj o cofnięciu przypisania", - "notify-only-user-comments":"Powiadamiaj tylko o komentarzach użytkownika", - "only-rule-chain-lifecycle-failures":"Tylko niepowodzenia cyklu życia łańcucha reguł", - "only-rule-node-lifecycle-failures":"Tylko niepowodzenia cyklu życia węzła reguły", - "platform-users":"Użytkownicy platformy", - "rate-limits":"Limity częstotliwości", - "rate-limits-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich limitów częstotliwości", - "recipient":"Odbiorca", - "recipient-group":"Grupa odbiorców", - "recipient-type":{ - "affected-tenant-administrators":"Dotknięci administratorzy najemcy", - "affected-user":"Dotknięty użytkownik", - "all-users":"Wszyscy użytkownicy", - "customer-users":"Użytkownicy klientów", - "system-administrators":"Administratorzy systemu", - "tenant-administrators":"Administratorzy najemcy", - "user-filters":"Filtr użytkowników", - "user-list":"Lista użytkowników", - "users-entity-owner":"Użytkownicy właściciela jednostki" + "notify-on": "Powiadom dalej", + "notify-on-comment-update": "Powiadom o aktualizacji komentarza", + "notify-on-required": "Powiadomienie jest wymagane", + "notify-on-unassign": "Powiadom o anulowaniu przypisania", + "notify-only-integrations-errors": "Powiadamiaj tylko o błędzie", + "notify-only-user-comments": "Powiadamiaj tylko o komentarzach użytkowników", + "only-rule-chain-lifecycle-failures": "Tylko awarie cyklu życia łańcucha reguł", + "only-rule-node-lifecycle-failures": "Tylko awarie cyklu życia węzła reguły", + "platform-users": "Użytkownicy platformy", + "rate-limits": "Rate limits", + "rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits", + "recipient": "Odbiorca", + "recipient-group": "Grupa odbiorców", + "recipient-type": { + "affected-tenant-administrators": "Dotknięci administratorzy Tenanta", + "affected-user": "Dotknięty użytkownik", + "affected-user-hint": "Dotknięta wskazówka dla użytkownika", + "all-users": "Wszyscy użytkownicy", + "customer-users": "Użytkownicy klientów", + "system-administrators": "Administratorzy systemu", + "tenant-administrators": "Administratorzy tenanta", + "user-filters": "Filtr użytkownika", + "user-group-list": "Lista grup użytkowników", + "user-list": "Lista użytkowników", + "user-role": "Rola użytkownika", + "users-entity-owner": "Użytkownicy właściciela obiektu", + "users-entity-owner-hint": "Użytkownicy podpowiedzi właściciela obiektu" }, - "recipients":"Odbiorcy", - "notification-recipients":"Powiadomienia / Odbiorcy", - "recipients-count":"{ count, plural, =1 {1 odbiorca} other {# odbiorców} }", - "recipients-required":"Odbiorcy są wymagani", - "refresh-allow-delivery-method":"Odśwież metodę dostarczania", - "request-search":"Wyszukaj żądanie", - "request-status":{ - "processing":"Przetwarzanie", - "scheduled":"Zaplanowane", - "sent":"Wysłane" + "recipients": "Odbiorcy", + "notification-recipients": "Powiadomienia / Odbiorcy", + "recipients-count": "{ count, plural, =1 {1 odbiorca} other {# odbiorców} }", + "recipients-required": "Odbiorcy są potrzebni", + "refresh-allow-delivery-method": "Odśwież zezwól na metodę dostawy", + "request-search": "Poproś o wyszukiwanie", + "request-status": { + "processing": "Przetwarzanie", + "scheduled": "Zaplanowany", + "sent": "Wysłano" }, - "review":"Recenzja", - "rule":"Reguła", - "rule-chain-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich łańcuchów reguł", - "rule-engine-events-trigger-settings":"Ustawienia wyzwalaczy zdarzeń silnika reguł", - "rule-engine-filter":"Filtr silnika reguł", - "rule-name":"Nazwa reguły", - "rule-name-required":"Nazwa jest wymagana", - "rule-disable":"Wyłącz regułę powiadomienia", - "rule-enable":"Włącz regułę powiadomienia", - "rule-node-filter":"Filtr węzła reguły", - "rules":"Reguły", - "notification-rules":"Powiadomienia / Reguły", - "scheduler-later":"Zaplanuj na później", - "search-notification":"Wyszukaj powiadomienia", - "search-recipients":"Wyszukaj odbiorców", - "search-rules":"Wyszukaj reguły", - "search-templates":"Wyszukaj szablony", - "see-documentation":"Zobacz dokumentację", - "selected-notifications":"{ count, plural, =1 {1 powiadomienie} other {# powiadomień} } wybrano", - "selected-recipients":"{ count, plural, =1 {1 odbiorca} other {# odbiorców} } wybrano", - "selected-requests":"{ count, plural, =1 {1 żądanie} other {# żądań} } wybrano", - "selected-rules":"{ count, plural, =1 {1 reguła} other {# reguł} } wybrano", - "selected-template":"{ count, plural, =1 {1 szablon} other {# szablonów} } wybrano", - "send-notification":"Wyślij powiadomienie", - "sent":"Wysłane", - "notification-sent":"Powiadomienia / Wysłane", - "set-entity-from-notification":"Ustaw jednostkę z powiadomienia na stan pulpitu nawigacyjnego", - "slack-chanel-type":"Typ kanału Slack", - "slack-chanel-types":{ - "direct":"Wiadomość bezpośrednia", - "private-channel":"Prywatny kanał", - "public-channel":"Publiczny kanał" + "review": "Recenzja", + "rule": "Reguła", + "rule-chain-list-rule-hint": "Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich łańcuchów reguł", + "rule-engine-events-trigger-settings": "Ustawienia wyzwalania zdarzeń silnika reguł", + "rule-engine-filter": "Filtr silnika reguł", + "rule-name": "Nazwa reguły", + "rule-name-required": "Nazwa jest wymagana", + "rule-disable": "Disable notification rule", + "rule-enable": "Enable notification rule", + "rule-node-filter": "Filtr węzła reguły", + "rules": "Zasady", + "notification-rules": "Powiadomienia / Zasady", + "scheduler-later": "Zaplanuj na później", + "search-notification": "Wyszukaj powiadomienia", + "search-recipients": "Wyszukaj odbiorców", + "search-rules": "Zasady wyszukiwania", + "search-templates": "Wyszukaj szablony", + "see-documentation": "Zobacz dokumentację", + "selected-notifications": "Wybrano { count, plural, =1 {1 powiadomienie} other {# powiadomień} }", + "selected-recipients": "Wybrano { count, plural, =1 {1 odbiorcę} other {# odbiorców} }", + "selected-requests": "Wybrano { count, plural, =1 {1 rządanie} other {# rządań} }", + "selected-rules": "Wybrano { count, plural, =1 {1 zasadę} other {# zasad} }", + "selected-template": "Wybrano { count, plural, =1 {1 szablon} other {# szablonów} }", + "send-notification": "Wyślij powiadomienie", + "sent": "Wysłano", + "notification-sent": "Powiadomienia / Wysłane", + "set-entity-from-notification": "Ustaw obiekt z powiadomienia na stan panelu", + "slack-chanel-type": "Typ kanału slack", + "slack-chanel-types": { + "direct": "Wiadomość docelowa", + "private-channel": "Kanał prywatny", + "public-channel": "Kanał publiczny" }, - "start-from-scratch":"Rozpocznij od początku", - "status":"Status", - "stop-escalation-alarm-status-become":"Zatrzymaj eskalację, gdy status alarmu stanie się:", - "subject":"Temat", - "subject-required":"Temat jest wymagany", - "template":"Szablon", - "template-name":"Nazwa szablonu", - "template-required":"Szablon jest wymagany", - "template-type":{ - "alarm":"Alarm", - "alarm-assignment":"Przypisanie alarmu", - "alarm-comment":"Komentarz do alarmu", - "api-usage-limit":"Limit użycia API", - "device-activity":"Aktywność urządzenia", - "entities-limit":"Limit jednostek", - "entity-action":"Akcja jednostki", - "general":"Ogólny", - "rule-engine-lifecycle-event":"Zdarzenie cyklu życia silnika reguł", - "rule-node":"Węzeł reguły", - "new-platform-version":"Nowa wersja platformy", - "rate-limits":"Przekroczono limity częstotliwości" + "start-from-scratch": "Zacznij od początku", + "status": "Status", + "stop-escalation-alarm-status-become": "Zatrzymaj eskalację stanu alarmowego:", + "subject": "Temat", + "subject-required": "Temat jest wymagany", + "template": "Szablon", + "template-name": "Nazwa szablonu", + "template-required": "Szablon jest wymagany", + "template-type": { + "alarm": "Alarm", + "alarm-assignment": "Przypisanie alarmu", + "alarm-comment": "Komentarz alarmowy", + "api-usage-limit": "Limit wykorzystania API", + "device-activity": "Aktywność urządzenia", + "entities-limit": "Limit obiektów", + "entity-action": "Działanie obiektu", + "general": "Ogólny", + "integration-lifecycle-event": "Zdarzenie cyklu życia integracji", + "rule-engine-lifecycle-event": "Zdarzenie cyklu życia silnika reguł", + "rule-node": "Węzeł reguły", + "new-platform-version": "Nowa wersja platformy", + "rate-limits": "Exceeded rate limits" }, - "templates":"Szablony", - "notification-templates":"Powiadomienia / Szablony", - "tenant-profiles-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich profili najemców", - "tenants-list-rule-hint":"Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich najemców", - "threshold":"Próg", - "theme-color":"Kolor motywu", - "time":"Czas", - "track-rule-node-events":"Śledź zdarzenia węzła reguły", - "trigger":{ - "alarm":"Alarm", - "alarm-assignment":"Przypisanie alarmu", - "alarm-comment":"Komentarz do alarmu", - "api-usage-limit":"Limit użycia API", - "device-activity":"Aktywność urządzenia", - "entities-limit":"Limit jednostek", - "entity-action":"Akcja jednostki", - "rule-engine-lifecycle-event":"Zdarzenie cyklu życia silnika reguł", - "new-platform-version":"Nowa wersja platformy", - "rate-limits":"Przekroczono limity częstotliwości", - "trigger":"Wyzwalacz", - "trigger-required":"Wymagany jest wyzwalacz" + "templates": "Szablony", + "notification-templates": "Powiadomienia / Szablony", + "tenant-profiles-list-rule-hint": "Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich profili tenantów", + "tenants-list-rule-hint": "Jeśli pole jest puste, wyzwalacz zostanie zastosowany do wszystkich Tenantów", + "threshold": "Próg", + "theme-color": "Theme color", + "time": "Czas", + "track-rule-node-events": "Śledź zdarzenia węzła reguły", + "trigger": { + "alarm": "Alarm", + "alarm-assignment": "Przypisanie alarmu", + "alarm-comment": "Komentarz alarmowy", + "api-usage-limit": "Limit wykorzystania API", + "device-activity": "Aktywność urządzenia", + "entities-limit": "Limit obiektów", + "entity-action": "Działanie obiektu", + "integration-lifecycle-event": "Zdarzenie cyklu życia integracji", + "rule-engine-lifecycle-event": "Zdarzenie cyklu życia silnika reguł", + "new-platform-version": "Nowa wersja platformy", + "rate-limits": "Exceeded rate limits", + "trigger": "Wyzwalacz", + "trigger-required": "Wyzwalacz jest wymagany" }, - "type":"Typ", - "unread":"Nieprzeczytane", - "updated":"Zaktualizowane", - "use-template":"Użyj szablonu", - "view-all":"Pokaż wszystko", - "warning":"Ostrzeżenie", - "webhook-url":"URL Webhooka", - "webhook-url-required":"URL Webhooka jest wymagany", - "channel-name":"Nazwa kanału", - "channel-name-required":"Nazwa kanału jest wymagana", - "settings":{ - "notification-settings":"Ustawienia powiadomień", - "reset-all":"Zresetuj wszystkie ustawienia", - "reset-all-title":"Czy na pewno chcesz zresetować formularz?", - "reset-all-text":"Po potwierdzeniu, formularz ustawień zostanie zresetowany do domyślnej wartości i zapisany.", - "type":"Typ", - "enable-all":"Włącz wszystkie", - "disable-all":"Wyłącz wszystkie", - "delivery-not-configured":"Metoda dostarczania nie jest skonfigurowana" + "type": "Typ", + "unread": "Nieprzeczytane", + "updated": "Zaktualizowane", + "use-template": "Użyj szablonu", + "view-all": "Pokaż wszystkie", + "view-notification-recipients-group": "Wyświetl grupę odbiorców powiadomień", + "view-notification-template": "Wyświetl szablon powiadomienia", + "view-rule": "Zobacz regułę", + "warning": "Ostrzeżenie", + "webhook-url": "Webhook URL", + "webhook-url-required": "Webhook URL is required", + "channel-name": "Channel name", + "channel-name-required": "Channel name is required", + "settings": { + "notification-settings": "Notification settings", + "reset-all": "Reset all settings", + "reset-all-title": "Are you sure you want to reset form?", + "reset-all-text": "After the confirmation, the settings form will reset to the default value and save.", + "type": "Type", + "enable-all": "Enable all", + "disable-all": "Disable all", + "delivery-not-configured": "Delivery method is not configured" } }, - "ota-update":{ - "add":"Dodaj pakiet", - "assign-firmware":"Przypisane oprogramowanie", - "assign-firmware-required":"Przypisane oprogramowanie jest wymagane", - "assign-software":"Przypisane oprogramowanie", - "assign-software-required":"Przypisane oprogramowanie jest wymagane", - "auto-generate-checksum":"Automatycznie generuj sumę kontrolną", - "checksum":"Suma kontrolna", - "checksum-hint":"Jeśli suma kontrolna jest pusta, zostanie wygenerowana automatycznie", - "checksum-algorithm":"Algorytm sumy kontrolnej", - "checksum-copied-message":"Suma kontrolna pakietu została skopiowana do schowka", - "change-firmware":"Zmiana oprogramowania może spowodować aktualizację { count, plural, =1 {1 urządzenia} other {# urządzeń} }.", - "change-software":"Zmiana oprogramowania może spowodować aktualizację { count, plural, =1 {1 urządzenia} other {# urządzeń} }.", - "chose-compatible-device-profile":"Wgrany pakiet będzie dostępny tylko dla urządzeń z wybranym profilem.", - "chose-firmware-distributed-device":"Wybierz oprogramowanie, które zostanie rozprowadzone na urządzenia", - "chose-software-distributed-device":"Wybierz oprogramowanie, które zostanie rozprowadzone na urządzenia", - "content-type":"Typ zawartości", - "copy-checksum":"Kopiuj sumę kontrolną", - "copy-direct-url":"Kopiuj bezpośredni adres URL", - "copyId":"Kopiuj identyfikator pakietu", - "copied":"Skopiowane!", - "delete":"Usuń pakiet", - "delete-ota-update-text":"Bądź ostrożny, po potwierdzeniu aktualizacja OTA stanie się nieodwracalna.", - "delete-ota-update-title":"Czy na pewno chcesz usunąć aktualizację OTA „{{title}}”?", - "delete-ota-updates-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane aktualizacje OTA zostaną usunięte.", - "delete-ota-updates-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 aktualizację OTA} other {# aktualizacji OTA} }?", - "description":"Opis", - "direct-url":"Bezpośredni adres URL", - "direct-url-copied-message":"Bezpośredni adres URL pakietu został skopiowany do schowka", - "direct-url-required":"Bezpośredni adres URL jest wymagany", - "download":"Pobierz pakiet", - "drop-file":"Przeciągnij i upuść plik pakietu lub kliknij, aby wybrać plik do przesłania.", - "drop-package-file-or":"Przeciągnij i upuść plik pakietu lub", - "file-name":"Nazwa pliku", - "file-size":"Rozmiar pliku", - "file-size-bytes":"Rozmiar pliku w bajtach", - "idCopiedMessage":"Identyfikator pakietu został skopiowany do schowka", - "no-firmware-matching":"Nie znaleziono kompatybilnych pakietów aktualizacji oprogramowania układowego pasujących do „{{entity}}”.", - "no-firmware-text":"Brak kompatybilnych pakietów aktualizacji oprogramowania układowego.", - "no-packages-text":"Nie znaleziono pakietów", - "no-software-matching":"Nie znaleziono kompatybilnych pakietów aktualizacji oprogramowania pasujących do „{{entity}}”.", - "no-software-text":"Brak kompatybilnych pakietów aktualizacji oprogramowania.", - "ota-update":"Aktualizacja OTA", - "ota-update-details":"Szczegóły aktualizacji OTA", - "ota-updates":"Aktualizacje OTA", - "package-file":"Plik pakietu", - "package-type":"Typ pakietu", - "packages-repository":"Repozytorium pakietów", - "search":"Szukaj pakietów", - "selected-package":"{ count, plural, =1 {1 pakiet} other {# pakietów} } wybrano", - "title":"Tytuł", - "title-required":"Tytuł jest wymagany.", - "title-max-length":"Tytuł powinien mieć mniej niż 256", - "types":{ - "firmware":"Oprogramowanie układowe", - "software":"Oprogramowanie" + "ota-update": { + "add": "Dodaj pakiet", + "assign-firmware": "Przypisane oprogramowanie sprzętowe", + "assign-firmware-required": "Przypisane oprogramowanie sprzętowe jest wymagane", + "assign-software": "Przydzielone oprogramowanie", + "assign-software-required": "Przypisane oprogramowanie jest wymagane", + "auto-generate-checksum": "Automatycznie generuj sumę kontrolną", + "cant-applied-group-all": "Nie można zastosować do grupy Wszystkie", + "checksum": "Suma kontrolna", + "checksum-hint": "Jeśli suma kontrolna jest pusta, zostanie wygenerowana automatycznie", + "checksum-algorithm": "Algorytm sumy kontrolnej", + "checksum-copied-message": "Suma kontrolna pakietu została skopiowana do schowka", + "change-firmware": "Zmiana oprogramowania sprzętowego może spowodować aktualizację { count, plural, =1 {1 urządzenia} other {# urządzeń} }.", + "change-software": "Zmiana oprogramowania może spowodować aktualizację { count, plural, =1 {1 urządzenia} other {# urządzeń} }.", + "chose-compatible-device-profile": "Przesłany pakiet będzie dostępny tylko dla urządzeń z wybranym profilem.", + "chose-firmware-distributed-device": "Wybierz oprogramowanie sprzętowe, które będzie dystrybuowane na urządzenia", + "chose-software-distributed-device": "Wybierz oprogramowanie, które będzie dystrybuowane na urządzenia", + "content-type": "Typ zawartości", + "copy-checksum": "Skopiuj sumę kontrolną", + "copy-direct-url": "Skopiuj bezpośredni adres URL", + "copyId": "Skopiuj identyfikator pakietu", + "copied": "Skopiowano!", + "delete": "Usuń pakiet", + "delete-ota-update-text": "Uważaj, po potwierdzeniu aktualizacji OTA nie będzie można odzyskać.", + "delete-ota-update-title": "Czy na pewno chcesz usunąć aktualizację OTA {{title}}'?", + "delete-ota-updates-text": "Uważaj, po potwierdzeniu wszystkie wybrane aktualizacje OTA zostaną usunięte.", + "delete-ota-updates-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 OTA update} other {# OTA updates} }?", + "description": "Opis", + "direct-url": "Bezpośredni adres URL", + "direct-url-copied-message": "Bezpośredni adres URL pakietu został skopiowany do schowka", + "direct-url-required": "Bezpośredni adres URL jest wymagany", + "download": "Pobierz pakiet", + "drop-file": "Upuść plik pakietu lub kliknij, aby wybrać plik do przesłania.", + "drop-package-file-or": "Przeciągnij i upuść plik pakietu lub", + "file-name": "Nazwa pliku", + "file-size": "Rozmiar pliku", + "file-size-bytes": "Rozmiar pliku w bajtach", + "idCopiedMessage": "Identyfikator pakietu został skopiowany do schowka", + "no-firmware-matching": "Nie znaleziono kompatybilnych pakietów aktualizacji oprogramowania sprzętowego OTA pasujących do '{{entity}}'.", + "no-firmware-text": "Brak kompatybilnych pakietów aktualizacji oprogramowania sprzętowego OTA.", + "no-packages-text": "Nie znaleziono pakietów", + "no-software-matching": "Nie znaleziono kompatybilnych pakietów aktualizacji oprogramowania OTA pasujących do '{{entity}}'.", + "no-software-text": "Brak kompatybilnych pakietów aktualizacji oprogramowania OTA.", + "ota-update": "Aktualizacja OTA", + "ota-update-details": "Szczegóły aktualizacji OTA", + "ota-updates": "Aktualizacje OTA", + "package-type": "Typ pakietu", + "packages-repository": "Repozytorium pakietów", + "search": "Wyszukaj pakiety", + "selected-package": "Wybrano { count, plural, =1 {1 pakiet} other {# pakietów} }", + "title": "Tytuł", + "title-required": "Tytuł jest wymagany.", + "title-max-length": "Tytuł powinien być mniejszy niż 256", + "types": { + "firmware": "Oprogramowanie sprzętowe", + "software": "Oprogramowanie" }, - "upload-binary-file":"Prześlij plik binarny", - "use-external-url":"Użyj zewnętrznego adresu URL", - "version":"Wersja", - "version-required":"Wersja jest wymagana.", - "version-tag":"Tag wersji", - "version-tag-hint":"Niestandardowy tag powinien pasować do wersji pakietu zgłoszonej przez Twoje urządzenie.", - "version-max-length":"Wersja powinna mieć mniej niż 256", - "warning-after-save-no-edit":"Po przesłaniu pakietu nie będziesz mógł edytować tytułu, wersji, profilu urządzenia i typu pakietu." + "upload-binary-file": "Prześlij plik binarny", + "use-external-url": "Użyj zewnętrznego adresu URL", + "version": "Wersja", + "version-required": "Wersja jest wymagana.", + "version-tag": "Oznaczenie wersji", + "version-tag-hint": "Tag niestandardowy powinien odpowiadać wersji pakietu zgłaszanej przez Twoje urządzenie.", + "version-max-length": "Wersja powinna być mniejsza niż 256", + "warning-after-save-no-edit": "Po przesłaniu pakietu nie będzie można modyfikować tytułu, wersji, profilu urządzenia ani typu pakietu." }, - "position":{ - "top":"Góra", - "bottom":"Dół", - "left":"Lewo", - "right":"Prawo" + "position": { + "top": "Początek", + "bottom": "Koniec", + "left": "Lewy", + "right": "Prawy" }, - "profile":{ - "profile":"Profil", - "last-login-time":"Ostatnie logowanie", - "change-password":"Zmień hasło", - "current-password":"Obecne hasło", - "copy-jwt-token":"Kopiuj token JWT", - "jwt-token":"Token JWT", - "token-valid-till":"Token jest ważny do", - "tokenCopiedSuccessMessage":"Token JWT został skopiowany do schowka", - "tokenCopiedWarnMessage":"Token JWT wygasł! Proszę odświeżyć stronę." + "profile": { + "profile": "Profil", + "last-login-time": "Ostatnie logowanie", + "change-password": "Zmień hasło", + "current-password": "Aktualne hasło", + "copy-jwt-token": "Skopiuj token JWT", + "jwt-token": "token JWT", + "token-valid-till": "Token jest ważny do", + "tokenCopiedSuccessMessage": "Token JWT został skopiowany do schowka", + "tokenCopiedWarnMessage": "Token JWT wygasł! Odświerz stronę" }, - "profiles":{ - "profiles":"Profile" + "profiles": { + "profiles": "Profile" }, - "security":{ - "security":"Bezpieczeństwo", - "general-settings":"Ogólne ustawienia bezpieczeństwa", - "access-token":"Token dostępu", - "access-token-required":"Token dostępu jest wymagany", - "clientId":"ID klienta", - "clientId-required":"ID klienta jest wymagane", - "username":"Nazwa użytkownika", - "username-required":"Nazwa użytkownika jest wymagana", - "ca-cert":"Certyfikat CA", - "2fa":{ - "2fa":"Uwierzytelnianie dwuskładnikowe", - "2fa-description":"Uwierzytelnianie dwuskładnikowe chroni Twoje konto przed nieautoryzowanym dostępem. Wystarczy wprowadzić kod zabezpieczeń podczas logowania.", - "authenticate-with":"Możesz uwierzytelniać się za pomocą:", - "disable-2fa-provider-text":"Wyłączenie {{name}} spowoduje obniżenie poziomu bezpieczeństwa Twojego konta", - "disable-2fa-provider-title":"Czy na pewno chcesz wyłączyć {{name}}?", - "get-new-code":"Pobierz nowy kod", - "main-2fa-method":"Użyj jako główna metoda uwierzytelniania dwuskładnikowego", - "dialog":{ - "activation-step-description-email":"Następnym razem, gdy się zalogujesz, zostaniesz poproszony o wprowadzenie kodu zabezpieczeń, który zostanie wysłany na Twój adres e-mail.", - "activation-step-description-sms":"Następnym razem, gdy się zalogujesz, zostaniesz poproszony o wprowadzenie kodu zabezpieczeń, który zostanie wysłany na numer telefonu.", - "activation-step-description-totp":"Następnym razem, gdy się zalogujesz, będziesz musiał wprowadzić kod uwierzytelniania dwuskładnikowego.", - "activation-step-label":"Aktywacja", - "backup-code-description":"Wydrukuj kody, aby mieć je pod ręką, gdy będziesz musiał ich użyć do zalogowania się na swoje konto. Każdy kod zapasowy można użyć tylko raz.", - "backup-code-warn":"Po opuszczeniu tej strony te kody nie będą już dostępne. Przechowuj je bezpiecznie, korzystając z dostępnych opcji poniżej.", - "download-txt":"Pobierz (txt)", - "email-step-description":"Podaj adres e-mail, który będziesz używać jako autentykatora.", - "email-step-label":"E-mail", - "enable-email-title":"Włącz autentykator e-mail", - "enable-sms-title":"Włącz autentykator SMS", - "enable-totp-title":"Włącz autentykator aplikacji", - "enter-verification-code":"Wprowadź tutaj 6-cyfrowy kod", - "get-backup-code-title":"Pobierz kod zapasowy", - "next":"Dalej", - "scan-qr-code":"Zeskanuj ten kod QR za pomocą swojej aplikacji uwierzytelniającej", - "send-code":"Wyślij kod", - "sms-step-description":"Podaj numer telefonu, który będziesz używać jako autentykatora.", - "sms-step-label":"Numer telefonu", - "success":"Sukces!", - "totp-step-description-install":"Możesz zainstalować aplikacje takie jak Google Authenticator, Authy lub Duo.", - "totp-step-description-open":"Otwórz aplikację autentykatora na swoim telefonie komórkowym.", - "totp-step-label":"Pobierz aplikację", - "verification-code":"Kod weryfikacyjny 6-cyfrowy", - "verification-code-invalid":"Nieprawidłowy format kodu weryfikacyjnego", - "verification-code-incorrect":"Kod weryfikacyjny jest niepoprawny", - "verification-code-many-request":"Zbyt wiele żądań sprawdzenia kodu weryfikacyjnego", - "verification-step-description":"Wprowadź 6-cyfrowy kod, który właśnie wysłaliśmy na adres {{address}}", - "verification-step-label":"Weryfikacja" + "security": { + "security": "Bezpieczeństwo", + "general-settings": "Ogólne ustawienia zabezpieczeń", + "access-token": "Access token", + "access-token-required": "Access token is required", + "clientId": "Client ID", + "clientId-required": "Client ID is required", + "username": "Username", + "username-required": "Username is required", + "ca-cert": "CA certificate", + "2fa": { + "2fa": "Uwierzytelnianie dwuskładnikowe", + "2fa-description": "Uwierzytelnianie dwuskładnikowe chroni Twoje konto przed nieautoryzowanym dostępem. Wszystko co musisz zrobić, to wpisać kod bezpieczeństwa przy logowaniu.", + "authenticate-with": "Możesz uwierzytelnić się za pomocą:", + "disable-2fa-provider-text": "Wyłączanie {{name}} sprawi, że Twoje konto będzie mniej bezpieczne", + "disable-2fa-provider-title": "Czy na pewno chcesz wyłączyć {{name}}?", + "get-new-code": "Zdobądź nowy kod", + "main-2fa-method": "Użyj jako głównej metody uwierzytelniania dwuskładnikowego", + "dialog": { + "activation-step-description-email": "Przy następnym logowaniu zostaniesz poproszony o podanie kodu zabezpieczającego, który zostanie wysłany na Twój adres e-mail.", + "activation-step-description-sms": "Przy następnym logowaniu zostaniesz poproszony o podanie kodu zabezpieczającego, który zostanie wysłany na numer telefonu.", + "activation-step-description-totp": "Przy następnym logowaniu konieczne będzie podanie dwuskładnikowego kodu uwierzytelniającego.", + "activation-step-label": "Aktywacja", + "backup-code-description": "Wydrukuj kody, aby mieć je pod ręką, gdy będziesz musiał ich użyć do zalogowania się na swoje konto. ", + "backup-code-warn": "Po opuszczeniu tej strony kody te nie będą mogły zostać wyświetlone ponownie. ", + "download-txt": "Pobierz (txt)", + "email-step-description": "Wpisz adres e-mail, który będzie używany jako moduł uwierzytelniający.", + "email-step-label": "E-mail", + "enable-email-title": "Włącz uwierzytelnianie poczty e-mail", + "enable-sms-title": "Włącz uwierzytelnianie SMS", + "enable-totp-title": "Włącz aplikację uwierzytelniającą", + "enter-verification-code": "Wpisz tutaj 6-cyfrowy kod", + "get-backup-code-title": "Uzyskaj kod zapasowy", + "next": "Dalej", + "scan-qr-code": "Zeskanuj ten kod QR za pomocą aplikacji weryfikacyjnej", + "send-code": "Wyślij kod", + "sms-step-description": "Wprowadź numer telefonu, który będzie używany jako moduł uwierzytelniający.", + "sms-step-label": "Numer telefonu", + "success": "Sukces!", + "totp-step-description-install": "Możesz zainstalować aplikacje takie jak Google Authenticator, Authy lub Duo.", + "totp-step-description-open": "Otwórz aplikację uwierzytelniającą na swoim telefonie komórkowym.", + "totp-step-label": "Ściągnij aplikację", + "verification-code": "6-cyfrowy kod", + "verification-code-invalid": "Nieprawidłowy format kodu weryfikacyjnego", + "verification-code-incorrect": "Kod weryfikacyjny jest nieprawidłowy", + "verification-code-many-request": "Zbyt wiele próśb o sprawdzenie kodu weryfikacyjnego", + "verification-step-description": "Wpisz 6-cyfrowy kod, który właśnie wysłaliśmy na adres {{address}}", + "verification-step-label": "Weryfikacja" }, - "provider":{ - "email":"E-mail", - "email-description":"Użyj kodu zabezpieczeń wysłanego na Twój adres e-mail do uwierzytelniania.", - "email-hint":"Kody uwierzytelniania są wysyłane przez e-mail na adres {{ info }}", - "sms":"SMS", - "sms-description":"Użyj telefonu do uwierzytelniania. Wyślemy Ci kod zabezpieczeń za pomocą wiadomości SMS podczas logowania.", - "sms-hint":"Kody uwierzytelniania są wysyłane jako wiadomości tekstowe na numer telefonu {{ info }}", - "totp":"Aplikacja autentykatora", - "totp-description":"Używaj aplikacji takich jak Google Authenticator, Authy lub Duo na swoim telefonie do uwierzytelniania. Wygeneruje ona kod zabezpieczeń do logowania.", - "totp-hint":"Aplikacja autentykatora jest skonfigurowana dla Twojego konta", - "backup_code":"Kod zapasowy", - "backup-code-description":"Te jednorazowe kody do wydruku pozwalają Ci się zalogować, gdy jesteś z dala od telefonu, na przykład podczas podróży.", - "backup-code-hint":"Obecnie aktywnych jest {{ info }} jednorazowych kodów" + "provider": { + "email": "E-mail", + "email-description": "Do uwierzytelnienia użyj kodu zabezpieczającego wysłanego na Twój adres e-mail.", + "email-hint": "Kody uwierzytelniające wysyłane są e-mailem na adres {{ info }}", + "sms": "SMS-em", + "sms-description": "Użyj telefonu do uwierzytelnienia. Wyślemy ci kod bezpieczeństwa SMS-em przy logowaniu", + "sms-hint": "Kody uwierzytelniające wysyłane są SMS-em na {{ info }}", + "totp": "Aplikacja uwierzytelniająca", + "totp-description": "Do uwierzytelniania użyj aplikacji takich jak Google Authenticator, Authy lub Duo na swoim telefonie. ", + "totp-hint": "Aplikacja Authenticator jest skonfigurowana dla Twojego konta", + "backup_code": "Kod zapasowy", + "backup-code-description": "Te jednorazowe kody do wydrukowania umożliwiają logowanie się, gdy nie masz przy sobie telefonu, na przykład podczas podróży.", + "backup-code-hint": "{{ info }} kody jednorazowe są w tym momencie aktywne" } }, - "password-requirement":{ - "at-least":"Przynajmniej:", - "character":"{ count, plural, =1 {1 znak} other {# znaków} }", - "digit":"{ count, plural, =1 {1 cyfra} other {# cyfr} }", - "incorrect-password-try-again":"Nieprawidłowe hasło. Spróbuj ponownie", - "lowercase-letter":"{ count, plural, =1 {1 mała litera} other {# małe litery} }", - "new-passwords-not-match":"Nowe hasło nie pasuje", - "password-should-not-contain-spaces":"Twoje hasło nie powinno zawierać spacji", - "password-not-meet-requirements":"Hasło nie spełnia wymagań", - "password-requirements":"Wymagania dotyczące hasła", - "password-should-difference":"Nowe hasło powinno różnić się od bieżącego", - "special-character":"{ count, plural, =1 {1 znak specjalny} other {# znaków specjalnych} }", - "uppercase-letter":"{ count, plural, =1 {1 wielka litera} other {# wielkie litery} }", - "at-most":"Maksymalnie:" + "password-requirement": { + "at-least": "Co najmniej:", + "character": "{ count, plural, =1 {1 znak} other {# znaków} }", + "digit": "{ count, plural, =1 {1 cyfra} other {# cyfr} }", + "incorrect-password-try-again": "Niepoprawne hasło. Spróbuj jeszcze raz", + "lowercase-letter": "{ count, plural, =1 {1 mała litera} other {# małych liter} }", + "new-passwords-not-match": "Nowe hasło nie pasuje", + "password-should-not-contain-spaces": "Twoje hasło nie powinno zawierać spacji", + "password-not-meet-requirements": "Hasło nie spełnia wymagań", + "password-requirements": "Wymagania dotyczące hasła", + "password-should-difference": "Nowe hasło powinno różnić się od obecnego", + "special-character": "{ count, plural, =1 {1 znak specjalny} other {# znaków specjalnych} }", + "uppercase-letter": "{ count, plural, =1 {1 duża litera} other {# dużych liter} }" } }, - "relation":{ - "relations":"Relacje", - "direction":"Kierunek", - "clear-relation-type":"Wyczyść typ relacji", - "search-direction":{ - "FROM":"Od", - "TO":"Do" + "relation": { + "relations": "Relacje", + "direction": "Kierunek", + "clear-relation-type": "Clear relation type", + "search-direction": { + "FROM": "Z", + "TO": "Do" }, - "direction-type":{ - "FROM":"from", - "TO":"to" + "direction-type": { + "FROM": "z", + "TO": "do" }, - "from-relations":"Wychodzące relacje", - "to-relations":"Przychodzące relacje", - "selected-relations":"{ count, plural, =1 {1 relacja} other {# relacji} } wybrano", - "type":"Typ", - "to-entity-type":"Typ jednostki docelowej", - "to-entity-name":"Nazwa jednostki docelowej", - "from-entity-type":"Typ jednostki źródłowej", - "from-entity-name":"Nazwa jednostki źródłowej", - "to-entity":"Jednostka docelowa", - "from-entity":"Jednostka źródłowa", - "delete":"Usuń relację", - "relation-type":"Typ relacji", - "relation-type-required":"Wymagany jest typ relacji.", - "relation-type-max-length":"Typ relacji powinien mieć mniej niż 256 znaków", - "any-relation-type":"Dowolny typ", - "add":"Dodaj relację", - "edit":"Edytuj relację", - "delete-to-relation-title":"Czy na pewno chcesz usunąć relację do jednostki '{{entityName}}'?", - "delete-to-relation-text":"Bądź ostrożny, po potwierdzeniu jednostka '{{entityName}}' zostanie odłączona od bieżącej jednostki.", - "delete-to-relations-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 relacja} other {# relacje} }?", - "delete-to-relations-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane relacje zostaną usunięte, a odpowiednie jednostki zostaną odłączone od bieżącej jednostki.", - "delete-from-relation-title":"Czy na pewno chcesz usunąć relację z jednostki '{{entityName}}'?", - "delete-from-relation-text":"Bądź ostrożny, po potwierdzeniu bieżąca jednostka zostanie odłączona od jednostki '{{entityName}}'.", - "delete-from-relations-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 relacja} other {# relacje} }?", - "delete-from-relations-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane relacje zostaną usunięte, a bieżąca jednostka zostanie odłączona od odpowiednich jednostek.", - "remove-relation-filter":"Usuń filtr relacji", - "remove-filter":"Usuń filtr", - "add-relation-filter":"Dodaj filtr relacji", - "any-relation":"Dowolna relacja", - "relation-filters":"Filtry relacji", - "additional-info":"Dodatkowe informacje (JSON)", - "invalid-additional-info":"Nie można sparsować dodatkowych informacji json.", - "no-relations-text":"Nie znaleziono relacji" + "from-relations": "Relacje wychodzące", + "to-relations": "Relacje przychodzące", + "selected-relations": "Wybrano { count, plural, =1 {1 relację} other {# relacji} }", + "type": "Typ", + "to-entity-type": "Do typu obiektu", + "to-entity-name": "Do nazwy obiektu", + "from-entity-type": "Z typu obiektu", + "from-entity-name": "Od nazwy obiektu", + "to-entity": "Do obiektu", + "from-entity": "Od obiektu", + "delete": "Usuń relację", + "relation-type": "Typ relacji", + "relation-type-required": "Typ relacji jest wymagany.", + "relation-type-max-length": "Typ relacji powinien być mniejszy niż 256", + "any-relation-type": "Dowolny typ", + "add": "Dodaj relację", + "edit": "Edytuj relację", + "view": "Zobacz relację", + "delete-to-relation-title": "Czy na pewno chcesz usunąć relację z obiektem '{{entityName}}'?", + "delete-to-relation-text": "Uważaj, po potwierdzeniu obiekt '{{entityName}}' nie będzie powiązany z obecnym obiektem.", + "delete-to-relations-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 relację} other {# relacji} }?", + "delete-to-relations-text": "Uważaj, po potwierdzeniu wszystkie wybrane relacje zostaną usunięte, a odpowiadające im obiekty nie będą powiązane z bieżącym obiektem.", + "delete-from-relation-title": "Czy na pewno chcesz usunąć relację z obiektu '{{entityName}}'?", + "delete-from-relation-text": "Uważaj, po potwierdzeniu bieżący obiekt nie będzie powiązany z obiektem '{{entityName}}'.", + "delete-from-relations-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 relację} other {# relacji} }?", + "delete-from-relations-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane relacje zostaną usunięte, a bieżący obiekt nie będzie powiązana z odpowiednimi obiektami.", + "remove-relation-filter": "Usuń filtr relacji", + "remove-filter": "Remove filter", + "add-relation-filter": "Dodaj filtr relacji", + "any-relation": "Dowolny związek", + "relation-filters": "Filtry relacji", + "additional-info": "Dodatkowe informacje (JSON)", + "invalid-additional-info": "Nie można przeanalizować dodatkowych informacji json.", + "no-relations-text": "Nie znaleziono żadnych relacji" + }, + "resource": { + "add": "Dodaj bibliotekę", + "all-types": "All", + "copyId": "Skopiuj identyfikator biblioteki", + "delete": "Usuń bibliotekę", + "delete-resource-text": "Bądź ostrożny, po potwierdzeniu biblioteka stanie się niemożliwa do odzyskania.", + "delete-resource-title": "Czy na pewno chcesz usunąć bibliotekę '{{resourceTitle}}'?", + "delete-resources-action-title": "Usuń { count, plural, =1 {1 bibliotekę} other {# bibliotek} }", + "delete-resources-text": "Pamiętaj, że wybrane biblioteki, nawet jeśli są używane w profilach urządzeń, zostaną usunięte.", + "delete-resources-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 bibliotekę} other {# bibliotek} }?", + "download": "Pobierz bibliotekę", + "drop-file": "Upuść plik bibliotek lub kliknij, aby wybrać plik do przesłania.", + "drop-resource-file-or": "Przeciągnij i upuść plik bibliotek lub", + "empty": "Biblioteka jest pusta", + "file-name": "Nazwa pliku", + "idCopiedMessage": "Identyfikator biblioteki został skopiowany do schowka", + "no-resource-matching": "Nie znaleziono bibliotek pasujących do '{{widgetsBundle}}'.", + "no-resource-text": "Nie znaleziono bibliotek", + "open-widgets-bundle": "Otwórz pakiet widżetów", + "resource": "Biblioteka", + "resource-library-details": "Szczegóły bibliotek", + "resource-type": "Typ biblioteki", + "resources-library": "Biblioteka bibliotek", + "search": "Przeszukaj biblioteki", + "selected-resources": "Wybrano { count, plural, =1 {1 bibliotekę} other {# bibliotek} }", + "system": "System", + "title": "Tytuł", + "title-required": "Tytuł jest wymagany.", + "title-max-length": "Tytuł powinien być mniejszy niż 256", + "type": { + "jks": "JKS", + "js-module": "JS module", + "lwm2m-model": "LWM2M model", + "pkcs-12": "PKCS #12" + } + }, + "rulechain": { + "rulechain": "Łańcuch reguł", + "rulechain-events": "Zdarzenia łańcucha reguł", + "rulechains": "Łańcuchy reguł", + "root": "Źródło", + "delete": "Usuń łańcuch reguł", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "description": "Opis", + "add": "Dodaj łańcuch reguł", + "set-root": "Utwórz katalog główny łańcucha reguł", + "set-root-rulechain-title": "Czy na pewno chcesz utworzyć łańcuch reguł '{{ruleChainName}}' źródło?", + "set-root-rulechain-text": "Po potwierdzeniu łańcuch reguł stanie się rootem i będzie obsługiwał wszystkie przychodzące komunikaty transportowe.", + "delete-rulechain-title": "Czy na pewno chcesz usunąć łańcuch reguł '{{ruleChainName}}'?", + "delete-rulechain-text": "Bądź ostrożny, po potwierdzeniu łańcucha reguł i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-rulechains-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }?", + "delete-rulechains-action-title": "Usuń { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }", + "delete-rulechains-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane łańcuchy reguł zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "add-rulechain-text": "Dodaj nowy łańcuch reguł", + "no-rulechains-text": "Nie znaleziono łańcuchów reguł", + "rulechain-details": "Szczegóły łańcucha reguł", + "details": "Szczegóły", + "events": "Wydarzenia", + "system": "System", + "import": "Importuj łańcuch reguł", + "export": "Eksportuj łańcuch reguł", + "export-failed-error": "Nie można wyeksportować łańcucha reguł: {{error}}", + "create-new-rulechain": "Utwórz nowy łańcuch reguł", + "rulechain-file": "Plik łańcucha reguł", + "invalid-rulechain-file-error": "Nie można zaimportować łańcucha reguł: Nieprawidłowa struktura danych łańcucha reguł.", + "copyId": "Skopiuj identyfikator łańcucha reguł", + "idCopiedMessage": "Identyfikator łańcucha reguł został skopiowany do schowka", + "select-rulechain": "Wybierz łańcuch reguł", + "no-rulechains-matching": "Nie znaleziono łańcuchów regół pasujących do '{{entity}}'.", + "rulechain-required": "Łańcuch reguł jest wymagany", + "management": "Zarządzanie regułami", + "debug-mode": "Tryb debugowania", + "search": "Przeszukaj łańcuchy reguł", + "selected-rulechains": "Wybrano { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }", + "open-rulechain": "Otwórz łańcuch reguł", + "edge-template-root": "Katalog główny szablonu", + "assign-to-edge": "Przypisz do Edge", + "edge-rulechain": "Łańcuch reguł Edge", + "unassign-rulechain-from-edge-text": "Po potwierdzeniu łańcuch reguł zostanie usunięty i nie będzie dostępny na urządzeniu Edge.", + "unassign-rulechains-from-edge-title": "Czy na pewno chcesz anulować przypisanie { count, plural, =1 {1 łańcucha reguł} other {# łańcuchów reguł} }?", + "unassign-rulechains-from-edge-text": "Po potwierdzeniu wszystkie wybrane łańcuchy reguł zostaną anulowane i nie będą dostępne na urządzeniu Edge.", + "assign-rulechain-to-edge-title": "Przypisz łańcuch(y) reguł do Edge", + "assign-rulechain-to-edge-text": "Wybierz łańcuchy reguł, które chcesz przypisać do Edge", + "set-edge-template-root-rulechain": "Utwórz łańcuch reguł jako główny szablon Edge", + "set-edge-tscheduler-eventemplate-root-rulechain": "Utwórz łańcuch reguł jako główny szablon Edge", + "set-edge-template-root-rulechain-title": "Czy na pewno chcesz utworzyć łańcuch reguł '{{ruleChainName}}' Katalog główny szablonu Edge?", + "set-edge-template-root-rulechain-text": "Po potwierdzeniu łańcuch reguł stanie się głównym szablonem Edge i będzie głównym łańcuchem reguł dla nowo utworzonych Edge.", + "invalid-rulechain-type-error": "Nie można zaimportować łańcucha reguł: nieprawidłowy typ łańcucha reguł. {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Przypisz łańcuch reguł do Edge podczas tworzenia", + "set-auto-assign-to-edge-title": "Czy na pewno chcesz przypisać łańcuch reguł Edge '{{ruleChainName}}' do Edge podczas tworzenia?", + "set-auto-assign-to-edge-text": "Po potwierdzeniu łańcuch reguł Edge zostanie automatycznie przypisany do Edge podczas tworzenia.", + "unset-auto-assign-to-edge": "Nie przypisuj łańcucha reguł do Edge podczas tworzenia", + "unset-auto-assign-to-edge-title": "Czy na pewno nie chcesz przypisać łańcucha reguł Edge '{{ruleChainName}}' do Edge podczas tworzenia?", + "unset-auto-assign-to-edge-text": "Po potwierdzeniu łańcuch reguł Edge nie będzie już automatycznie przypisywany do Edge podczas tworzenia.", + "unassign-rulechain-title": "Czy na pewno chcesz usunąć przypisanie łańcucha reguł '{{ruleChainName}}'?", + "unassign-rulechains": "Usuń przypisanie łańcuchów reguł" + }, + "rulenode": { + "rule-node-events": "Zdarzenia węzła reguły", + "details": "Szczegóły", + "events": "Wydarzenia", + "search": "Wyszukaj węzły", + "open-node-library": "Otwórz bibliotekę węzłów", + "close-node-library": "Close node library", + "add": "Dodaj węzeł reguły", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "type": "Typ", + "rule-node-description": "Rule node description", + "delete": "Usuń węzeł reguły", + "select-all-objects": "Wybierz wszystkie węzły i połączenia", + "deselect-all-objects": "Odznacz wszystkie węzły i połączenia", + "delete-selected-objects": "Usuń wybrane węzły i połączenia", + "delete-selected": "Usuń wybrane", + "create-nested-rulechain": "Utwórz zagnieżdżony łańcuch reguł", + "select-all": "Zaznacz wszystko", + "copy-selected": "Skopiuj wybrane", + "deselect-all": "Odznacz wszystkie", + "rulenode-details": "Szczegóły węzła reguły", + "debug-mode": "Tryb debugowania", + "singleton-mode": "Tryb singletonowy", + "configuration": "Konfiguracja", + "link": "Połączenie", + "link-details": "Szczegóły połączenia węzła reguły", + "add-link": "Dodaj połączenie", + "link-label": "Etykieta połączenia", + "link-label-required": "Etykieta połączenia jest wymagana.", + "custom-link-label": "Niestandardowa etykieta połączenia", + "custom-link-label-required": "Niestandardowa etykieta połączenia jest wymagana.", + "link-labels": "Etykiety połaczeń", + "link-labels-required": "Etykiety połączeń są wymagane.", + "no-link-labels-found": "Nie znaleziono etykiet połączeń", + "no-link-label-matching": "Nie znaleziono '{{label}}'.", + "create-new-link-label": "Utwórz nowy!", + "type-filter": "Filtr", + "type-filter-details": "Filtruj wiadomości przychodzące według skonfigurowanych warunków", + "type-enrichment": "Wzbogacenie", + "type-enrichment-details": "Dodaj dodatkowe informacje do metadanych wiadomości", + "type-transformation": "Transformacja", + "type-transformation-details": "Zmień ładunek wiadomości i metadane", + "type-action": "Akcja", + "type-action-details": "Wykonaj akcję specjalną", + "type-analytics": "Analityka", + "type-analytics-details": "Wykonuj analizę danych przesyłanych strumieniowo lub utrwalonych", + "type-external": "Zewnętrzny", + "type-external-details": "Współpracuje z systemem zewnętrznym", + "type-rule-chain": "Łańcuch reguł", + "type-rule-chain-details": "Przekazuje przychodzące wiadomości do określonego łańcucha reguł", + "type-flow": "Przepływ", + "type-flow-details": "Organizuje przepływ wiadomości", + "type-input": "Wejście", + "type-input-details": "Logiczne wejście łańcucha reguł przekazuje przychodzące wiadomości do następnego powiązanego węzła reguł", + "type-unknown": "Nieznany", + "type-unknown-details": "Nierozwiązany węzeł reguły", + "directive-is-not-loaded": "Zdefiniowana dyrektywa konfiguracyjna '{{directiveName}}' jest niedostępne.", + "ui-resources-load-error": "Nie udało się załadować bibliotek interfejsu konfiguracyjnego.", + "invalid-target-rulechain": "Nie można rozwiązać docelowego łańcucha reguł!", + "test-script-function": "Funkcja skryptu testowego", + "script-lang-java-script": "Java Script", + "script-lang-tbel": "TBEL", + "message": "Wiadomość", + "message-type": "Typ wiadomości", + "select-message-type": "Wybierz typ wiadomości", + "message-type-required": "Typ wiadomości jest wymagany", + "metadata": "Metadane", + "metadata-required": "Wpisy metadanych nie mogą być puste.", + "output": "Wyjście", + "test": "Test", + "help": "Pomoc", + "reset-debug-mode": "Zresetuj tryb debugowania we wszystkich węzłach", + "test-with-this-message": "{{test}} with this message", + "description": "Opis" + }, + "role": { + "role": "Rola", + "roles": "Role", + "management": "Zarządzanie rolami", + "view-roles": "Wyświetl role", + "no-roles-matching": "Nie znaleziono ról pasujących do '{{entity}}'.", + "role-list": "Lista ról", + "role-list-required": "Lista ról jest wymagana", + "add": "Dodaj rolę", + "view": "Wyświetl rolę", + "search": "Szukaj ról", + "selected-roles": "Wybrano { count, plural, =1 {1 rolę} other {# ról} }", + "no-roles-text": "Nie znaleziono ról", + "role-details": "Szczegóły roli", + "add-role-text": "Dodaj nową rolę", + "delete": "Usuń rolę", + "delete-roles": "Usuń role", + "delete-role-title": "Czy na pewno chcesz usunąć rolę '{{roleName}}'?", + "delete-role-text": "Bądź ostrożny, po potwierdzeniu roli i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-roles-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 rolę} other {# ról} }?", + "delete-roles-action-title": "Usuń { count, plural, =1 {1 rolę} other {# ról} }", + "delete-roles-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane role zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "role-type": "Typ roli", + "role-type-required": "Typ roli jest wymagany.", + "select-role-type": "Wybierz typ roli", + "enter-role-type": "Wpisz typ roli", + "any-role": "Dowolna rola", + "no-role-types-matching": "Nie znaleziono typów ról pasujących do '{{entitySubtype}}'.", + "role-type-list-empty": "Nie wybrano typów ról.", + "role-types": "Typy ról", + "created-time": "Stworzony czas", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "description": "Opis", + "events": "Wydarzenia", + "details": "Szczegóły", + "copyId": "Skopiuj identyfikator roli", + "idCopiedMessage": "Identyfikator roli został skopiowany do schowka", + "permissions": "Uprawnienia", + "role-required": "Wymagana rola", + "roles-required": "Wymagane role", + "display-type": { + "GENERIC": "Ogólny", + "GROUP": "Grupa" + } + }, + "group-permission": { + "user-group-roles": "Role grupy użytkowników", + "entity-group-permissions": "Uprawnienia grupy obiektów", + "role-type": "Typ roli", + "role-name": "Nazwa roli", + "group-type": "Typ grupy", + "group-name": "Nazwa grupy", + "group-owner": "Właściciel grupy", + "user-group-name": "Nazwa grupy użytkowników", + "user-group-owner": "Właściciel grupy użytkowników", + "edit": "Edytuj uprawnienia", + "delete": "Usuń uprawnienia", + "selected-group-permissions": "Wybrano { count, plural, =1 {1 grupowe uprawnienie} other {# grupowych uprawnień} }", + "delete-group-permission-title": "Czy na pewno chcesz usunąć uprawnienia grupy {{roleName}}'?", + "delete-group-permission-text": "Bądź ostrożny, po potwierdzeniu uprawnienia grupy i wszystkie powiązane dane staną się nie do odzyskania.", + "delete-group-permission": "Usuń uprawnienia grupy", + "delete-group-permissions-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 grupowe uprawnienie} other {# grupowych uprawnień} }?", + "delete-group-permissions-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane uprawnienia grupowe zostaną usunięte, a odpowiadający im użytkownicy utracą dostęp do określonych bibliotek.", + "delete-group-permissions": "Usuń uprawnienia grupy", + "add-group-permission": "Dodaj uprawnienia grupy", + "edit-group-permission": "Edytuj uprawnienia grupy", + "entity-group": "Grupa obiektów", + "user-group": "Grupa użytkowników", + "no-owners-matching": "Nie znaleziono właściciela '{{owner}}'.", + "target-owner-required": "Właściciel grupy obiektów jest wymagany.", + "target-user-group-owner-required": "Właściciel grupy użytkowników jest wymagany.", + "no-group-permissions-text": "Nie znaleziono uprawnień grupy" }, - "resource":{ - "add":"Dodaj zasób", - "all-types":"Wszystkie", - "copyId":"Skopiuj identyfikator zasobu", - "delete":"Usuń zasób", - "delete-resource-text":"Bądź ostrożny, po potwierdzeniu zasób zostanie bezpowrotnie usunięty.", - "delete-resource-title":"Czy na pewno chcesz usunąć zasób '{{resourceTitle}}'?", - "delete-resources-action-title":"Usuń { count, plural, =1 {1 zasób} other {# zasobów} }", - "delete-resources-text":"Proszę zauważyć, że wybrane zasoby, nawet jeśli są używane w profilach urządzeń, zostaną usunięte.", - "delete-resources-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 zasób} other {# zasobów} }?", - "download":"Pobierz zasób", - "drop-file":"Upuść plik zasobu lub kliknij, aby wybrać plik do przesłania.", - "drop-resource-file-or":"Przeciągnij i upuść plik zasobu lub", - "empty":"Zasób jest pusty", - "file-name":"Nazwa pliku", - "idCopiedMessage":"Identyfikator zasobu został skopiowany do schowka", - "no-resource-matching":"Nie znaleziono zasobów pasujących do '{{widgetsBundle}}'.", - "no-resource-text":"Nie znaleziono zasobów", - "open-widgets-bundle":"Otwórz paczkę widżetów", - "resource":"Zasób", - "resource-file":"Plik zasobu", - "resource-files":"Pliki zasobów", - "resource-library-details":"Szczegóły zasobu", - "resource-type":"Typ zasobu", - "resources-library":"Biblioteka zasobów", - "search":"Szukaj zasobów", - "selected-resources":"{ count, plural, =1 {1 zasób} other {# zasobów} } wybrano", - "system":"System", - "title":"Tytuł", - "title-required":"Tytuł jest wymagany.", - "title-max-length":"Tytuł powinien mieć mniej niż 256 znaków", - "type":{ - "jks":"JKS", - "js-module":"Moduł JS", - "lwm2m-model":"Model LWM2M", - "pkcs-12":"PKCS #12" + "permission": { + "permissions-required": "Należy określić co najmniej jeden wpis uprawnień.", + "remove-permission": "Usuń wpis uprawnień", + "add-permission": "Dodaj wpis uprawnień", + "other": "Inny", + "resource": { + "resource": "Biblioteka", + "select-resource": "Wybierz bibliotekę", + "resource-required": "Biblioteka jest wymagana", + "no-resources-matching": "Nie znaleziono biblioteki '{{resource}}'.", + "display-type": { + "ALL": "Wszystko", + "PROFILE": "Profil", + "ADMIN_SETTINGS": "Ustawienia administratora", + "ALARM": "Alarm", + "DEVICE": "Urządzenie", + "DEVICE_PROFILE": "Profil urządzenia", + "ASSET": "Zasób", + "CUSTOMER": "Klient", + "DASHBOARD": "Panel", + "ENTITY_VIEW": "Widok obiektów", + "EDGE": "Edge", + "TENANT": "Tenant", + "TENANT_PROFILE": "Profil tenanta", + "RULE_CHAIN": "Łańcuch reguł", + "USER": "Użytkownik", + "WIDGETS_BUNDLE": "Pakiet widżetów", + "WIDGET_TYPE": "Typ widżetu", + "CONVERTER": "Przetwornik", + "INTEGRATION": "Integracja", + "SCHEDULER_EVENT": "Zdarzenie planisty", + "BLOB_ENTITY": "Obiekt typu Blob", + "CUSTOMER_GROUP": "Grupa klientów", + "DEVICE_GROUP": "Grupa urządzeń", + "ASSET_GROUP": "Grupa zasobów", + "USER_GROUP": "Grupa użytkowników", + "ENTITY_VIEW_GROUP": "Grupa widoku obiektów", + "DASHBOARD_GROUP": "Grupa Panelu", + "ROLE": "Rola", + "GROUP_PERMISSION": "Zezwolenie grupowe", + "WHITE_LABELING": "White Labeling", + "AUDIT_LOG": "Dziennik kontroli", + "API_USAGE_STATE": "Stan użycia interfejsu API", + "TB_RESOURCE": "Biblioteka", + "EDGE_GROUP": "Grupa Edge", + "OTA_PACKAGE": "Pakiet Ota", + "QUEUE": "Kolejka", + "VERSION_CONTROL": "Kontrola wersji", + "ASSET_PROFILE": "Profil zasobu", + "NOTIFICATION": "Powiadomienie" + } + }, + "operation": { + "operation": "Operacja", + "operations": "Operacje", + "operations-required": "Należy określić co najmniej jedną operację.", + "enter-operation": "Wprowadź operację", + "no-operations-matching": "Nie znaleziono operacji '{{operation}}'.", + "display-type": { + "ALL": "Wszystko", + "CREATE": "Stwórz", + "READ": "Czytaj", + "WRITE": "Zapisz", + "DELETE": "Usuń", + "ASSIGN_TO_CUSTOMER": "Przypisz do Klienta", + "UNASSIGN_FROM_CUSTOMER": "Usuń przypisanie od klienta", + "RPC_CALL": "Zadzwoń do RPC", + "READ_CREDENTIALS": "Przeczytaj dane uwierzytelniające", + "WRITE_CREDENTIALS": "Zapisz dane uwierzytelniające", + "READ_ATTRIBUTES": "Przeczytaj Atrybuty", + "WRITE_ATTRIBUTES": "Zapisz atrybuty", + "READ_TELEMETRY": "Przeczytaj telemetrię", + "WRITE_TELEMETRY": "Zapisz telemetrię", + "CLAIM_DEVICES": "Zgłoś urządzenia", + "IMPERSONATE": "Podszywać się", + "CHANGE_OWNER": "Zmiana właściciela", + "ADD_TO_GROUP": "Dodaj do grupy", + "REMOVE_FROM_GROUP": "Usuń z grupy", + "SHARE_GROUP": "Udostępnij grupę", + "ASSIGN_TO_TENANT": "Przypisz do tenanta" + } } }, - "rulechain":{ - "rulechain":"Łańcuch reguł", - "rulechain-events":"Zdarzenia łańcucha reguł", - "rulechains":"Łańcuchy reguł", - "root":"Korzeń", - "delete":"Usuń łańcuch reguł", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", - "description":"Opis", - "add":"Dodaj łańcuch reguł", - "set-root":"Ustaw łańcuch reguł jako korzeń", - "set-root-rulechain-title":"Jesteś pewien, że chcesz ustawić łańcuch reguł '{{ruleChainName}}' jako korzeń?", - "set-root-rulechain-text":"Po potwierdzeniu łańcuch reguł stanie się korzeniem i będzie obsługiwał wszystkie przychodzące wiadomości transportowe.", - "delete-rulechain-title":"Jesteś pewien, że chcesz usunąć łańcuch reguł '{{ruleChainName}}'?", - "delete-rulechain-text":"Bądź ostrożny, po potwierdzeniu łańcuch reguł i wszystkie powiązane dane staną się nieodwracalne.", - "delete-rulechains-title":"Jesteś pewien, że chcesz usunąć { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }?", - "delete-rulechains-action-title":"Usuń { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }", - "delete-rulechains-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane łańcuchy reguł zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", - "add-rulechain-text":"Dodaj nowy łańcuch reguł", - "no-rulechains-text":"Nie znaleziono łańcuchów reguł", - "rulechain-details":"Szczegóły łańcucha reguł", - "details":"Szczegóły", - "events":"Zdarzenia", - "system":"System", - "import":"Importuj łańcuch reguł", - "export":"Eksportuj łańcuch reguł", - "export-failed-error":"Nie można wyeksportować łańcucha reguł: {{error}}", - "create-new-rulechain":"Utwórz nowy łańcuch reguł", - "rulechain-file":"Plik łańcucha reguł", - "invalid-rulechain-file-error":"Nie można zaimportować łańcucha reguł: Nieprawidłowa struktura danych łańcucha reguł.", - "copyId":"Skopiuj identyfikator łańcucha reguł", - "idCopiedMessage":"Identyfikator łańcucha reguł został skopiowany do schowka", - "select-rulechain":"Wybierz łańcuch reguł", - "no-rulechains-matching":"Nie znaleziono łańcuchów reguł pasujących do '{{entity}}'.", - "rulechain-required":"Łańcuch reguł jest wymagany", - "management":"Zarządzanie regułami", - "debug-mode":"Tryb debugowania", - "search":"Wyszukaj łańcuchy reguł", - "selected-rulechains":"{ count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} } wybranych", - "open-rulechain":"Otwórz łańcuch reguł", - "edge-template-root":"Korzeń szablonu", - "assign-to-edge":"Przypisz do krawędzi", - "edge-rulechain":"Łańcuch reguł krawędzi", - "unassign-rulechain-from-edge-text":"Po potwierdzeniu łańcuch reguł zostanie odłączony i nie będzie dostępny dla krawędzi.", - "unassign-rulechains-from-edge-title":"Jesteś pewien, że chcesz odłączyć { count, plural, =1 {1 łańcuch reguł} other {# łańcuchów reguł} }?", - "unassign-rulechains-from-edge-text":"Po potwierdzeniu wszystkie wybrane łańcuchy reguł zostaną odłączone i nie będą dostępne dla krawędzi.", - "assign-rulechain-to-edge-title":"Przypisz łańcuch(y) reguł do krawędzi", - "assign-rulechain-to-edge-text":"Wybierz łańcuchy reguł do przypisania do krawędzi", - "set-edge-template-root-rulechain":"Ustaw łańcuch reguł jako korzeń szablonu krawędzi", - "set-edge-template-root-rulechain-title":"Jesteś pewien, że chcesz ustawić łańcuch reguł '{{ruleChainName}}' jako korzeń szablonu krawędzi?", - "set-edge-template-root-rulechain-text":"Po potwierdzeniu łańcuch reguł stanie się korzeniem szablonu krawędzi i będzie korzeniem łańcucha reguł dla nowo tworzonych krawędzi.", - "invalid-rulechain-type-error":"Nie można zaimportować łańcucha reguł: Nieprawidłowy typ łańcucha reguł. Oczekiwany typ to {{expectedRuleChainType}}.", - "set-auto-assign-to-edge":"Przypisz łańcuch reguł do krawędzi podczas tworzenia", - "set-auto-assign-to-edge-title":"Jesteś pewien, że chcesz przypisać łańcuch reguł krawędzi '{{ruleChainName}}' do krawędzi podczas tworzenia?", - "set-auto-assign-to-edge-text":"Po potwierdzeniu łańcuch reguł krawędzi zostanie automatycznie przypisany do krawędzi podczas tworzenia.", - "unset-auto-assign-to-edge":"Nie przypisuj łańcucha reguł do krawędzi podczas tworzenia", - "unset-auto-assign-to-edge-title":"Jesteś pewien, że nie chcesz przypisywać łańcucha reguł krawędzi '{{ruleChainName}}' do krawędzi podczas tworzenia?", - "unset-auto-assign-to-edge-text":"Po potwierdzeniu łańcuch reguł krawędzi nie będzie automatycznie przypisywany do krawędzi podczas tworzenia.", - "unassign-rulechain-title":"Jesteś pewien, że chcesz odłączyć łańcuch reguł '{{ruleChainName}}'?", - "unassign-rulechains":"Odłącz łańcuchy reguł" + "scheduler": { + "scheduler": "Planista", + "scheduler-event": "zdarzenie planisty", + "select-scheduler-event": "Wybierz zdarzenie planisty", + "no-scheduler-events-matching": "Nie znaleziono zdarzeń planisty pasujących do '{{entity}}'.", + "scheduler-event-required": "Zdarzenie planisty jest wymagane", + "management": "Zarządzanie harmonogramem", + "scheduler-events": "Harmonogram wydarzeń", + "add-scheduler-event": "Dodaj zdarzenie planisty", + "search-scheduler-events": "Wyszukaj zdarzenia planisty", + "created-time": "Stworzony czas", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "type": "Typ", + "created_customer": "Stworzony przez Klienta", + "edit-scheduler-event": "Edytuj zdarzenie planisty", + "view-scheduler-event": "Wyświetl zdarzenie planisty", + "delete-scheduler-event": "Usuń zdarzenie planisty", + "no-scheduler-events": "Nie znaleziono żadnych wydarzeń planisty", + "selected-scheduler-events": "Wybrano { count, plural, =1 {1 zdarzenie planisty} other {# zdarzeń(ia) planisty} }", + "delete-scheduler-event-title": "Czy na pewno chcesz usunąć zdarzenie planisty '{{schedulerEventName}}'?", + "delete-scheduler-event-text": "Bądź ostrożny, po zdarzeniu planisty potwierdzenia i wszystkich powiązanych danych stanie się niemożliwy do odzyskania.", + "delete-scheduler-events-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 zdarzenie planisty} other {# zdarzeń(ia) planisty} }?", + "delete-scheduler-events-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane zdarzenia planisty zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "create": "Utwórz zdarzenie planisty", + "edit": "Edytuj zdarzenie planisty", + "view": "Wyświetl zdarzenie planisty", + "configuration": "Konfiguracja", + "schedule": "Harmonogram", + "start-time": "Czas rozpoczęcia", + "repeat": "Powtarzaj", + "repeats": "Powtórzenia", + "daily": "Codziennie", + "every-n-days": "Co N dni", + "every-n-days-text": "Co { days, plural, =1 {dzień} other {# dni} }", + "weekly": "Co tydzień", + "every-n-weeks": "Co N tygodni", + "every-n-weeks-text": "Co { weeks, plural, =1 {tydzień} other {# tygodni(e)} }", + "monthly": "Miesięczny", + "yearly": "Rocznie", + "timer": "Oparte na timerze", + "repeats-required": "Powtórzenia są wymagane.", + "repeat-on": "Powtórz w", + "repeat-every": "Powtarzaj każde", + "repeat-every-n-days": "Powtarzaj co N dni", + "repeat-days-required": "Liczba dni do powtórzenia jest wymagana.", + "invalid-repeat-days-value": "Liczba dni powtarzania musi być dodatnią liczbą całkowitą.", + "repeat-every-n-weeks": "Powtarzaj co N tygodni", + "repeat-weeks-required": "Liczba tygodni do powtórzenia jest wymagana.", + "invalid-repeat-weeks-value": "Liczba tygodni do powtórzenia musi być dodatnią liczbą całkowitą.", + "ends-on": "Kończy się", + "sunday-label": "N", + "monday-label": "P", + "tuesday-label": "W", + "wednesday-label": "Ś", + "thursday-label": "C", + "friday-label": "Pt", + "saturday-label": "S", + "repeat-on-sunday": "Powtórz w niedzielę", + "repeat-on-monday": "Powtórz w poniedziałek", + "repeat-on-tuesday": "Powtórz we wtorek", + "repeat-on-wednesday": "Powtórz w środę", + "repeat-on-thursday": "Powtórz w czwartek", + "repeat-on-friday": "Powtórz w piątek", + "repeat-on-saturday": "Powtórz w sobotę", + "event-type": "Typ wydarzenia", + "select-event-type": "Wybierz typ wydarzenia", + "event-type-required": "Typ wydarzenia jest wymagany.", + "event-type-max-length": "Typ wydarzenia powinien być mniejszy niż 256.", + "list-mode": "Widok listy", + "calendar-mode": "Widok kalendarza", + "calendar-view-type": "Typ widoku kalendarza", + "month": "Miesiąc", + "week": "Tydzień", + "day": "Dzień", + "agenda-week": "Tydzień porządku dziennego", + "agenda-day": "Dzień Agendy", + "list-year": "Wypisz rok", + "list-month": "Wypisz miesiąc", + "list-week": "Wypisz Tydzień", + "list-day": "Wypisz Dzień", + "today": "Dzisiaj", + "navigate-before": "Nawiguj przed", + "navigate-next": "Przejdź dalej", + "starting-from": "Zaczynając od", + "until": "dopóki", + "on": "w", + "sunday": "Niedziela", + "monday": "Poniedziałek", + "tuesday": "Wtorek", + "wednesday": "Środa", + "thursday": "Czwartek", + "friday": "Piątek", + "saturday": "Sobota", + "originator": "Inicjator", + "single-entity": "Pojedyńczy obiekt", + "group-of-entities": "Grupa obiektów", + "entities-group-owner": "Właściciel grupy obiektów", + "single-device": "Pojedyncze urządzenie", + "group-of-devices": "Grupa urządzeń", + "devices-group-owner": "Właściciel grupy urządzeń", + "message-body": "Treść wiadomości", + "target": "Cel", + "rpc-method": "metoda", + "rpc-method-required": "Metoda jest wymagana", + "rpc-method-white-space": "Znak biały jest niedozwolony.", + "rpc-params": "Parametry", + "select-dashboard-state": "Wybierz stan panelu", + "hours": "Godziny", + "minutes": "Minuty", + "seconds": "Sekundy", + "time-interval-required": "Odstęp czasu jest wymagany", + "time-unit-required": "Jednostka czasu jest wymagana", + "every-hour": "co { count, plural, =1 {godzinę} other {# godzin(y)} }", + "every-minute": "co { count, plural, =1 {minutę} other {# minut(y)} }", + "every-second": "co { count, plural, =1 {sekundę} other {# sekund(y)} }", + "invalid-time": "Nieprawidłowy czas" + }, + "report": { + "report-config": "Konfiguracja raportu", + "email-config": "Konfiguracja poczty", + "dashboard-state-param": "Wartość parametru stanu panelu", + "base-url": "Bazowy adres URL", + "base-url-required": "Podstawowy adres URL jest wymagany.", + "use-dashboard-timewindow": "Użyj okna czasowego panelu", + "timewindow": "Okno czasowe", + "name-pattern": "Wzór nazwy raportu", + "name-pattern-required": "Wzorzec nazwy raportu jest wymagany", + "type": "Typ raportu", + "use-current-user-credentials": "Użyj aktualnych poświadczeń użytkownika", + "customer-user-credentials": "Poświadczenia użytkownika klienta", + "customer-user-credentials-required": "Poświadczenia użytkownika klienta są wymagane", + "generate-test-report": "Wygeneruj raport z testu", + "send-email": "Wysłać email", + "from": "Od", + "from-required": "Nadawca jest wymagany.", + "to": "Do", + "to-required": "Odbiorca jest wymagany.", + "cc": "Dw", + "bcc": "UDW", + "subject": "Temat", + "subject-required": "Temat jest wymagany.", + "body": "Treść", + "body-required": "Treść jest wymagana." }, - "rulenode":{ - "rule-node-events":"Zdarzenia węzła reguły", - "details":"Szczegóły", - "events":"Zdarzenia", - "search":"Wyszukaj węzły", - "open-node-library":"Otwórz bibliotekę węzłów", - "close-node-library":"Zamknij bibliotekę węzłów", - "add":"Dodaj węzeł reguły", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "name-max-length":"Nazwa powinna mieć mniej niż 256 znaków", - "type":"Typ", - "rule-node-description":"Opis węzła reguły", - "delete":"Usuń węzeł reguły", - "select-all-objects":"Zaznacz wszystkie węzły i połączenia", - "deselect-all-objects":"Odznacz wszystkie węzły i połączenia", - "delete-selected-objects":"Usuń wybrane węzły i połączenia", - "delete-selected":"Usuń zaznaczone", - "create-nested-rulechain":"Utwórz zagnieżdżony łańcuch reguł", - "select-all":"Zaznacz wszystkie", - "copy-selected":"Skopiuj zaznaczone", - "deselect-all":"Odznacz wszystkie", - "rulenode-details":"Szczegóły węzła reguły", - "debug-mode":"Tryb debugowania", - "singleton-mode":"Tryb pojedynczej instancji", - "configuration":"Konfiguracja", - "link":"Połączenie", - "link-details":"Szczegóły połączenia węzła reguły", - "add-link":"Dodaj połączenie", - "link-label":"Etykieta połączenia", - "link-label-required":"Etykieta połączenia jest wymagana.", - "custom-link-label":"Niestandardowa etykieta połączenia", - "custom-link-label-required":"Niestandardowa etykieta połączenia jest wymagana.", - "link-labels":"Etykiety połączeń", - "link-labels-required":"Wymagane są etykiety połączeń.", - "no-link-labels-found":"Nie znaleziono etykiet połączeń", - "no-link-label-matching":"'{{label}}' nie zostało znalezione.", - "create-new-link-label":"Utwórz nową!", - "type-filter":"Filtr", - "type-filter-details":"Filtruj przychodzące wiadomości z określonymi warunkami", - "type-enrichment":"Wzbogacenie", - "type-enrichment-details":"Dodaj dodatkowe informacje do metadanych wiadomości", - "type-transformation":"Transformacja", - "type-transformation-details":"Zmień ładunek wiadomości i metadane", - "type-action":"Akcja", - "type-action-details":"Wykonaj specjalną akcję", - "type-external":"Zewnętrzne", - "type-external-details":"Komunikuje się z zewnętrznym systemem", - "type-rule-chain":"Łańcuch reguł", - "type-rule-chain-details":"Przekazuje przychodzące wiadomości do określonego łańcucha reguł", - "type-flow":"Przepływ", - "type-flow-details":"Organizuje przepływ wiadomości", - "type-input":"Wejście", - "type-input-details":"Logiczne wejście łańcucha reguł, przekazuje przychodzące wiadomości do następnego powiązanego węzła reguły", - "type-unknown":"Nieznany", - "type-unknown-details":"Nierozwiązany węzeł reguły", - "directive-is-not-loaded":"Zdefiniowana dyrektywa konfiguracji '{{directiveName}}' nie jest dostępna.", - "ui-resources-load-error":"Nie udało się załadować zasobów interfejsu użytkownika konfiguracji.", - "invalid-target-rulechain":"Nie można rozpoznać docelowego łańcucha reguł!", - "test-script-function":"Funkcja skryptu testowego", - "script-lang-java-script":"JavaScript", - "script-lang-tbel":"TBEL", - "message":"Wiadomość", - "message-type":"Typ wiadomości", - "select-message-type":"Wybierz typ wiadomości", - "message-type-required":"Wymagany jest typ wiadomości", - "metadata":"Metadane", - "metadata-required":"Wpisy metadanych nie mogą być puste.", - "output":"Wynik", - "test":"Test", - "help":"Pomoc", - "reset-debug-mode":"Zresetuj tryb debugowania we wszystkich węzłach", - "test-with-this-message":"{{test}} z tą wiadomością", - "queue-hint":"Wybierz kolejkę do przekazywania wiadomości do innej kolejki. Domyślnie używana jest kolejka „Main”.", - "queue-singleton-hint":"Wybierz kolejkę do przekazywania wiadomości w środowiskach wielu instancji. Domyślnie używana jest kolejka „Main”." + "blob-entity": { + "blob-entity": "obiekt typu blob", + "select-blob-entity": "Wybierz obiekt BLOB", + "no-blob-entities-matching": "Nie znaleziono obiektów BLOB pasujących do '{{entity}}'", + "blob-entity-required": "Obiekt typu BLOB jest wymagany", + "files": "Pliki", + "search": "Wyszukaj pliki", + "clear-search": "Wyczyść wyszukiwanie", + "no-blob-entities-prompt": "Nie znaleziono plików", + "report": "Raport", + "created-time": "Stworzony czas", + "name": "Nazwa", + "type": "Typ", + "created_customer": "Stworzony przez Klienta", + "selected-blob-entities": "{ count, plural, =1 {1 file} other {# files} } wybrane", + "download-blob-entity": "Pobierz plik", + "delete-blob-entity": "Usuń plik", + "delete-blob-entity-title": "Czy na pewno chcesz usunąć plik '{{blobEntityName}}'?", + "delete-blob-entity-text": "Bądź ostrożny, po przesłaniu pliku potwierdzenia dane staną się nie do odzyskania.", + "delete-blob-entities-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 plik} other {# plików} }", + "delete-blob-entities-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane pliki zostaną usunięte, a wszystkie powiązane dane staną się niemożliwe do odzyskania." }, - "timezone":{ - "timezone":"Strefa czasowa", - "select-timezone":"Wybierz strefę czasową", - "no-timezones-matching":"Nie znaleziono stref czasowych pasujących do '{{timezone}}'.", - "timezone-required":"Strefa czasowa jest wymagana.", - "browser-time":"Czas przeglądarki" + "timezone": { + "timezone": "Strefa czasowa", + "select-timezone": "Wybierz strefę czasową", + "no-timezones-matching": "Nie znaleziono stref czasowych pasujących do '{{timezone}}'.", + "timezone-required": "Strefa czasowa jest wymagana.", + "browser-time": "Czas przeglądarki" }, - "queue":{ - "queue-name":"Kolejka", - "no-queues-found":"Nie znaleziono kolejek.", - "no-queues-matching":"Nie znaleziono kolejek pasujących do '{{queue}}'.", - "select-name":"Wybierz nazwę kolejki", - "name":"Nazwa", - "name-required":"Nazwa kolejki jest wymagana!", - "name-unique":"Nazwa kolejki nie jest unikalna!", - "name-pattern":"Nazwa kolejki zawiera znak inny niż alfanumeryczny ASCII, '.', '_', i '-'!", - "queue-required":"Kolejka jest wymagana!", - "topic-required":"Temat kolejki jest wymagany!", - "poll-interval-required":"Interwał odpytywania jest wymagany!", - "poll-interval-min-value":"Wartość interwału odpytywania nie może być mniejsza niż 1", - "partitions-required":"Partycje są wymagane!", - "partitions-min-value":"Wartość partycji nie może być mniejsza niż 1", - "pack-processing-timeout-required":"Limit czasu przetwarzania jest wymagany", - "pack-processing-timeout-min-value":"Wartość limitu czasu przetwarzania nie może być mniejsza niż 1", - "batch-size-required":"Rozmiar partii jest wymagany!", - "batch-size-min-value":"Wartość rozmiaru partii nie może być mniejsza niż 1", - "retries-required":"Liczba prób jest wymagana!", - "retries-min-value":"Wartość liczby prób nie może być ujemna", - "failure-percentage-required":"Procent awaryjnych wiadomości jest wymagany!", - "failure-percentage-min-value":"Wartość procenta awaryjnych wiadomości nie może być mniejsza niż 0", - "failure-percentage-max-value":"Wartość procenta awaryjnych wiadomości nie może być większa niż 100", - "pause-between-retries-required":"Odstęp między próbami jest wymagany!", - "pause-between-retries-min-value":"Wartość odstępu między próbami nie może być mniejsza niż 1", - "max-pause-between-retries-required":"Maksymalny odstęp między próbami jest wymagany!", - "max-pause-between-retries-min-value":"Wartość maksymalnego odstępu między próbami nie może być mniejsza niż 1", - "submit-strategy-type-required":"Typ strategii przesyłania jest wymagany!", - "processing-strategy-type-required":"Typ strategii przetwarzania jest wymagany!", - "queues":"Kolejki", - "selected-queues":"{ count, plural, =1 {1 kolejka} other {# kolejek} } wybranych", - "delete-queue-title":"Czy na pewno chcesz usunąć kolejkę '{{queueName}}'?", - "delete-queues-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 kolejkę} other {# kolejki} }?", - "delete-queue-text":"Bądź ostrożny, po potwierdzeniu kolejka i wszystkie związane z nią dane zostaną bezpowrotnie usunięte.", - "delete-queues-text":"Po potwierdzeniu wszystkie wybrane kolejki zostaną usunięte i nie będą dostępne.", - "search":"Wyszukaj kolejkę", - "add":"Dodaj kolejkę", - "details":"Szczegóły kolejki", - "topic":"Temat", - "submit-settings":"Ustawienia przesyłania", - "submit-strategy":"Typ strategii *", - "grouping-parameter":"Parametr grupowania", - "processing-settings":"Ustawienia przetwarzania", - "processing-strategy":"Typ przetwarzania *", - "retries-settings":"Ustawienia prób", - "polling-settings":"Ustawienia odpytywania", - "batch-processing":"Przetwarzanie partiami", - "poll-interval":"Interwał odpytywania", - "partitions":"Partycje", - "immediate-processing":"Natychmiastowe przetwarzanie", - "consumer-per-partition":"Wysyłaj odpytywanie dla każdego konsumenta", - "consumer-per-partition-hint":"Włącz oddzielnych konsumentów dla każdej partycji", - "processing-timeout":"Przetwarzaj w ciągu, ms", - "batch-size":"Rozmiar partii", - "retries":"Liczba prób (0 - nieograniczona)", - "failure-percentage":"Procent awaryjnych wiadomości do pominięcia prób", - "pause-between-retries":"Odstęp między próbami, s", - "max-pause-between-retries":"Maksymalny odstęp między próbami, s", - "delete":"Usuń kolejkę", - "copyId":"Skopiuj Id kolejki", - "idCopiedMessage":"Id kolejki zostało skopiowane do schowka", - "description":"Opis", - "description-hint":"Ten tekst będzie wyświetlany w opisie kolejki zamiast wybranej strategii", - "alt-description":"Strategia przesyłania: {{submitStrategy}}, Strategia przetwarzania: {{processingStrategy}}", - "custom-properties":"Własne właściwości", - "custom-properties-hint":"Własne właściwości tworzenia kolejki (tematu), np. 'retention.ms:604800000;retention.bytes:1048576000'", - "strategies":{ - "sequential-by-originator-label":"Sekwencyjne według nadawcy", - "sequential-by-originator-hint":"Nowa wiadomość, na przykład dla urządzenia A, nie jest przesyłana, dopóki poprzednia wiadomość dla urządzenia A nie zostanie potwierdzona", - "sequential-by-tenant-label":"Sekwencyjne według najemcy", - "sequential-by-tenant-hint":"Nowa wiadomość, na przykład dla najemcy A, nie jest przesyłana, dopóki poprzednia wiadomość dla najemcy A nie zostanie potwierdzona", - "sequential-label":"Sekwencyjne", - "sequential-hint":"Nowa wiadomość nie jest przesyłana, dopóki poprzednia wiadomość nie zostanie potwierdzona", - "burst-label":"Burst", - "burst-hint":"Wszystkie wiadomości są przesyłane do łańcuchów reguł w kolejności ich przyjścia", - "batch-label":"Partia", - "batch-hint":"Nowa partia nie jest przesyłana, dopóki poprzednia partia nie zostanie potwierdzona", - "skip-all-failures-label":"Pomiń wszystkie awarie", - "skip-all-failures-hint":"Ignoruj wszystkie awarie", - "skip-all-failures-and-timeouts-label":"Pomiń wszystkie awarie i przerwy czasowe", - "skip-all-failures-and-timeouts-hint":"Ignoruj wszystkie awarie i przerwy czasowe", - "retry-all-label":"Ponów wszystkie", - "retry-all-hint":"Ponów wszystkie wiadomości z pakietu przetwarzania", - "retry-failed-label":"Ponów nieudane", - "retry-failed-hint":"Ponów wszystkie nieudane wiadomości z pakietu przetwarzania", - "retry-timeout-label":"Ponów przerwę czasową", - "retry-timeout-hint":"Ponów wszystkie wiadomości przekroczone czasem z pakietu przetwarzania", - "retry-failed-and-timeout-label":"Ponów nieudane i przerwę czasową", - "retry-failed-and-timeout-hint":"Ponów wszystkie nieudane i przekroczone czasem wiadomości z pakietu przetwarzania" + "queue": { + "queue-name": "Kolejka", + "no-queues-found": "Nie znaleziono kolejek.", + "no-queues-matching": "Brak kolejek pasujących do '{{queue}}'", + "select-name": "Wybierz nazwę kolejki", + "name": "Nazwa", + "name-required": "Nazwa kolejki jest wymagana!", + "name-unique": "Nazwa kolejki nie jest unikalna!", + "name-pattern": "Nazwa kolejki zawiera znak inny niż alfanumeryczny ASCII, „.”, „_” i „-”!", + "queue-required": "Kolejka jest wymagana!", + "topic-required": "Temat kolejki jest wymagany!", + "poll-interval-required": "Interwał odpytywania jest wymagany!", + "poll-interval-min-value": "Wartość interwału odpytywania nie może być mniejsza niż 1", + "partitions-required": "Partycje są wymagane!", + "partitions-min-value": "Wartość partycji nie może być mniejsza niż 1", + "pack-processing-timeout-required": "Limit czasu przetwarzania jest wymagany", + "pack-processing-timeout-min-value": "Wartość limitu czasu przetwarzania nie może być mniejsza niż 1", + "batch-size-required": "Wielkość partii jest wymagana!", + "batch-size-min-value": "Wartość wielkości partii nie może być mniejsza niż 1", + "retries-required": "Ilość ponownych prób jest wymagana!", + "retries-min-value": "Wartość ponownych prób nie może być ujemna", + "failure-percentage-required": "Procent niepowodzeń jest wymagany!", + "failure-percentage-min-value": "Wartość procentowa niepowodzeń nie może być mniejsza niż 0", + "failure-percentage-max-value": "Wartość procentowa niepowodzeń nie może być większa niż 100", + "pause-between-retries-required": "Przerwa między ponownymi próbami jest wymagana!", + "pause-between-retries-min-value": "Wartość przerwy między ponownymi próbami nie może być mniejsza niż 1", + "max-pause-between-retries-required": "Maksymalna przerwa między ponownymi próbami jest wymagana!", + "max-pause-between-retries-min-value": "Maksymalna wartość przerwy między ponownymi próbami nie może być mniejsza niż 1", + "submit-strategy-type-required": "Prześlij typ strategii jest wymagany!", + "processing-strategy-type-required": "Typ strategii przetwarzania jest wymagany!", + "queues": "Kolejki", + "selected-queues": "Wybrano { count, plural, =1 {1 kolejkę} other {# kolejek} }", + "delete-queue-title": "Czy na pewno chcesz usunąć kolejkę {{queueName}}'?", + "delete-queues-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 kolejkę} other {# kolejek} }?", + "delete-queue-text": "Bądź ostrożny, po potwierdzeniu kolejki i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-queues-text": "Po potwierdzeniu wszystkie wybrane kolejki zostaną usunięte i nie będą dostępne.", + "search": "Kolejka wyszukiwania", + "add": "Dodaj kolejkę", + "details": "Szczegóły kolejki", + "topic": "Temat", + "submit-settings": "Prześlij ustawienia", + "submit-strategy": "Typ strategii *", + "grouping-parameter": "Parametr grupowania", + "processing-settings": "Ponawia próbę przetwarzania ustawień", + "processing-strategy": "Rodzaj przetwarzania *", + "retries-settings": "Ponawia ustawienia", + "polling-settings": "Ustawienia odpytywania", + "batch-processing": "Przetwarzanie wsadowe", + "poll-interval": "Interwał odpytywania", + "partitions": "Partycje", + "immediate-processing": "Natychmiastowe przetwarzanie", + "consumer-per-partition": "Wyślij ankietę do każdego konsumenta", + "consumer-per-partition-hint": "Włącz osobnych konsumentów dla każdej partycji", + "processing-timeout": "Przetwarzanie w ciągu, ms", + "batch-size": "Rozmiar partii", + "retries": "Liczba ponownych prób (0 – nieograniczona)", + "failure-percentage": "Procent komunikatów o błędach związanych z pominięciem ponownych prób", + "pause-between-retries": "Spróbuj ponownie w ciągu, sek", + "max-pause-between-retries": "Dodatkowa ponowna próba w ciągu, sek", + "delete": "Usuń kolejkę", + "copyId": "Skopiuj identyfikator kolejki", + "idCopiedMessage": "Identyfikator kolejki został skopiowany do schowka", + "description": "Opis", + "description-hint": "Tekst ten będzie wyświetlany w opisie kolejki zamiast wybranej strategii", + "alt-description": "Prześlij strategię: {{submitStrategy}}, Strategia przetwarzania: {{processingStrategy}}", + "custom-properties": "Custom properties", + "custom-properties-hint": "Custom queue (topic) creation properties, e.g. 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "Sekwencja według inicjatora", + "sequential-by-originator-hint": "Nowa wiadomość dla m.in. ", + "sequential-by-tenant-label": "Kolejność według tenanta", + "sequential-by-tenant-hint": "Nowa wiadomość dla np. tenanta A nie zostanie wysłana do czasu potwierdzenia poprzedniej wiadomości dla tenanta A", + "sequential-label": "Sekwencyjny", + "sequential-hint": "Nowa wiadomość nie zostanie przesłana, dopóki poprzednia wiadomość nie zostanie potwierdzona", + "burst-label": "Wybuch", + "burst-hint": "Wszystkie wiadomości są przesyłane do łańcuchów reguł w kolejności ich nadejścia", + "batch-label": "Seria", + "batch-hint": "Nowa partia nie zostanie przesłana do czasu potwierdzenia poprzedniej partii", + "skip-all-failures-label": "Pomiń wszystkie niepowodzenia", + "skip-all-failures-hint": "Ignoruj ​​wszystkie niepowodzenia", + "skip-all-failures-and-timeouts-label": "Pomiń wszystkie awarie i przekroczenia limitu czasu", + "skip-all-failures-and-timeouts-hint": "Ignoruj ​​wszystkie awarie i przekroczenia limitu czasu", + "retry-all-label": "Spróbuj ponownie", + "retry-all-hint": "Ponów próbę wszystkich wiadomości z pakietu przetwarzania", + "retry-failed-label": "Ponowna próba nie powiodła się", + "retry-failed-hint": "Ponów próbę dla wszystkich nieudanych wiadomości z pakietu przetwarzania", + "retry-timeout-label": "Przekroczono limit czasu ponownej próby", + "retry-timeout-hint": "Ponów próbę dla wszystkich wiadomości, których przekroczono limit czasu, z pakietu przetwarzania", + "retry-failed-and-timeout-label": "Ponowna próba nie powiodła się i przekroczono limit czasu", + "retry-failed-and-timeout-hint": "Ponów próbę dla wszystkich wiadomości z pakietu przetwarzania, które nie powiodły się i przekroczyły limit czasu" } }, - "server-error":{ - "general":"Ogólny błąd serwera", - "authentication":"Błąd uwierzytelniania", - "jwt-token-expired":"Token JWT wygasł", - "tenant-trial-expired":"Próba najemcy wygasła", - "credentials-expired":"Wygasły poświadczenia", - "permission-denied":"Brak uprawnień", - "invalid-arguments":"Nieprawidłowe argumenty", - "bad-request-params":"Nieprawidłowe parametry żądania", - "item-not-found":"Nie znaleziono elementu", - "too-many-requests":"Zbyt wiele żądań", - "too-many-updates":"Zbyt wiele aktualizacji" + "server-error": { + "general": "Ogólny błąd serwera", + "authentication": "Błąd autoryzacji", + "jwt-token-expired": "Token JWT wygasł", + "tenant-trial-expired": "Okres próbny tenanta dobiegł końca", + "credentials-expired": "Poświadczenia wygasły", + "permission-denied": "Odmowa pozwolenia", + "invalid-arguments": "Nieprawidłowe argumenty", + "bad-request-params": "Złe parametry żądania", + "item-not-found": "Nie znaleziono obiektów", + "too-many-requests": "zbyt dużo próśb", + "too-many-updates": "Za dużo aktualizacji (⊙ _ ⊙ )" }, - "tenant":{ - "tenant":"Najemca", - "tenants":"Najemcy", - "management":"Zarządzanie najemcami", - "add":"Dodaj najemcę", - "admins":"Administratorzy", - "manage-tenant-admins":"Zarządzaj administratorami najemcy", - "delete":"Usuń najemcę", - "add-tenant-text":"Dodaj nowego najemcę", - "no-tenants-text":"Nie znaleziono najemców", - "tenant-details":"Szczegóły najemcy", - "title-max-length":"Tytuł powinien zawierać mniej niż 256 znaków", - "delete-tenant-title":"Czy na pewno chcesz usunąć najemcę '{{tenantTitle}}'?", - "delete-tenant-text":"Bądź ostrożny, po potwierdzeniu najemca i wszystkie związane z nim dane zostaną bezpowrotnie usunięte.", - "delete-tenants-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 najemcę} other {# najemców} }?", - "delete-tenants-action-title":"Usuń { count, plural, =1 {1 najemcę} other {# najemców} }", - "delete-tenants-text":"Bądź ostrożny, po potwierdzeniu wszyscy wybrani najemcy zostaną usunięci, a wszystkie związane z nimi dane zostaną bezpowrotnie usunięte.", - "title":"Tytuł", - "title-required":"Tytuł jest wymagany.", - "description":"Opis", - "details":"Szczegóły", - "events":"Zdarzenia", - "copyId":"Kopiuj Id najemcy", - "idCopiedMessage":"Id najemcy zostało skopiowane do schowka", - "select-tenant":"Wybierz najemcę", - "no-tenants-matching":"Nie znaleziono najemców pasujących do '{{entity}}'", - "tenant-required":"Najemca jest wymagany", - "search":"Szukaj najemców", - "selected-tenants":"{ count, plural, =1 {1 najemca} other {# najemcy} } wybrano", - "isolated-tb-rule-engine":"Użyj izolowanych kolejek ThingsBoard Rule Engine", - "isolated-tb-rule-engine-details":"Każdy najemca będzie miał dedykowane kolejki Rule Engine" + "tenant": { + "tenant": "Tenant", + "tenants": "Tenanci", + "management": "Zarządzanie Tenantami", + "add": "Dodaj tenanta", + "admins": "Administratorzy", + "manage-tenant-admins": "Zarządzaj administratorami tenantów", + "delete": "Usuń tenanta", + "add-tenant-text": "Dodaj nowego tenanta", + "no-tenants-text": "Nie znaleziono tenantów", + "tenant-details": "Dane tenanta", + "title-max-length": "Tytuł powinien być mniejszy niż 256", + "delete-tenant-title": "Czy na pewno chcesz usunąć tenanta '{{tenantTitle}}'?", + "delete-tenant-text": "Bądź ostrożny, po potwierdzeniu tenanta i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-tenants-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 tenanta} other {# tenantów} }?", + "delete-tenants-action-title": "Usuń { count, plural, =1 {1 tenanta} other {# tenantów} }", + "delete-tenants-text": "Bądź ostrożny, po potwierdzeniu wszyscy wybrani tenanci zostaną usunięci, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "title": "Tytuł", + "title-required": "Tytuł jest wymagany.", + "description": "Opis", + "details": "Szczegóły", + "events": "Wydarzenia", + "copyId": "Skopiuj identyfikator tenanta", + "idCopiedMessage": "Identyfikator został skopiowany do schowka", + "select-tenant": "Wybierz tenanta", + "no-tenants-matching": "Nie znaleziono tenanta '{{entity}}'.", + "tenant-required": "Tenant jest wymagany", + "allow-white-labeling": "Zezwalaj na White Labeling", + "allow-customer-white-labeling": "Zezwalaj na White Labeling klienta", + "search": "Wyszukaj tenantów", + "selected-tenants": "Wybrano { count, plural, =1 {1 tenanta} other {# tenantów} }", + "isolated-tb-rule-engine": "Przetwarzanie w izolowanym kontenerze mechanizmu reguł ThingsBoard", + "isolated-tb-rule-engine-details": "Wymaga oddzielnych mikrousług dla każdego izolowanego Tenanta" }, - "tenant-profile":{ - "tenant-profile":"Profil najemcy", - "tenant-profiles":"Profile najemców", - "add":"Dodaj profil najemcy", - "add-profile":"Dodaj profil", - "edit":"Edytuj profil najemcy", - "tenant-profile-details":"Szczegóły profilu najemcy", - "no-tenant-profiles-text":"Nie znaleziono profili najemców", - "name-max-length":"Nazwa powinna zawierać mniej niż 256 znaków", - "search":"Szukaj profili najemców", - "selected-tenant-profiles":"{ count, plural, =1 {1 profil najemcy} other {# profile najemców} } wybrano", - "no-tenant-profiles-matching":"Nie znaleziono profili najemców pasujących do '{{entity}}'", - "tenant-profile-required":"Profil najemcy jest wymagany", - "idCopiedMessage":"Id profilu najemcy zostało skopiowane do schowka", - "set-default":"Ustaw profil najemcy jako domyślny", - "delete":"Usuń profil najemcy", - "copyId":"Kopiuj Id profilu najemcy", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "data":"Dane profilu", - "profile-configuration":"Konfiguracja profilu", - "description":"Opis", - "default":"Domyślny", - "delete-tenant-profile-title":"Czy na pewno chcesz usunąć profil najemcy '{{tenantProfileName}}'?", - "delete-tenant-profile-text":"Bądź ostrożny, po potwierdzeniu profil najemcy i wszystkie związane z nim dane zostaną bezpowrotnie usunięte.", - "delete-tenant-profiles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 profil najemcy} other {# profile najemców} }?", - "delete-tenant-profiles-text":"Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile najemców zostaną usunięte, a wszystkie związane z nimi dane zostaną bezpowrotnie usunięte.", - "set-default-tenant-profile-title":"Czy na pewno chcesz ustawić profil najemcy '{{tenantProfileName}}' jako domyślny?", - "set-default-tenant-profile-text":"Po potwierdzeniu profil najemcy zostanie oznaczony jako domyślny i będzie używany dla nowych najemców bez określonego profilu.", - "no-tenant-profiles-found":"Nie znaleziono profili najemców.", - "create-new-tenant-profile":"Utwórz nowy!", - "create-tenant-profile":"Utwórz nowy profil najemcy", - "import":"Importuj profil najemcy", - "export":"Eksportuj profil najemcy", - "export-failed-error":"Nie można wyeksportować profilu najemcy: {{error}}", - "tenant-profile-file":"Plik profilu najemcy", - "invalid-tenant-profile-file-error":"Nie można zaimportować profilu najemcy: Nieprawidłowa struktura danych profilu najemcy.", - "advanced-settings":"Ustawienia zaawansowane", - "entities":"Encje", - "rule-engine":"Silnik reguł", - "time-to-live":"Czas życia", - "alarms-and-notifications":"Alarmy i powiadomienia", - "ota-files-in-bytes":"Pliki", - "ws-title":"WS", - "unlimited":"(0 - nielimitowany)", - "maximum-devices":"Maksymalna liczba urządzeń", - "maximum-devices-required":"Maksymalna liczba urządzeń jest wymagana.", - "maximum-devices-range":"Maksymalna liczba urządzeń nie może być ujemna", - "maximum-assets":"Maksymalna liczba zasobów", - "maximum-assets-required":"Maksymalna liczba zasobów jest wymagana.", - "maximum-assets-range":"Maksymalna liczba zasobów nie może być ujemna", - "maximum-customers":"Maksymalna liczba klientów", - "maximum-customers-required":"Maksymalna liczba klientów jest wymagana.", - "maximum-customers-range":"Maksymalna liczba klientów nie może być ujemna", - "maximum-users":"Maksymalna liczba użytkowników", - "maximum-users-required":"Maksymalna liczba użytkowników jest wymagana.", - "maximum-users-range":"Maksymalna liczba użytkowników nie może być ujemna", - "maximum-dashboards":"Maksymalna liczba desek rozdzielczych", - "maximum-dashboards-required":"Maksymalna liczba desek rozdzielczych jest wymagana.", - "maximum-dashboards-range":"Maksymalna liczba desek rozdzielczych nie może być ujemna", - "maximum-rule-chains":"Maksymalna liczba łańcuchów reguł", - "maximum-rule-chains-required":"Maksymalna liczba łańcuchów reguł jest wymagana.", - "maximum-rule-chains-range":"Maksymalna liczba łańcuchów reguł nie może być ujemna", - "maximum-resources-sum-data-size":"Maksymalny łączny rozmiar plików zasobów (bajty)", - "maximum-resources-sum-data-size-required":"Maksymalny łączny rozmiar plików zasobów jest wymagany.", - "maximum-resources-sum-data-size-range":"Maksymalny łączny rozmiar plików zasobów nie może być ujemny", - "maximum-resource-size":"Maksymalny rozmiar pliku zasobu (bajty)", - "maximum-resource-size-required":"Maksymalny rozmiar pliku zasobu jest wymagany", - "maximum-resource-size-range":"Maksymalny rozmiar pliku zasobu nie może być ujemny", - "maximum-ota-packages-sum-data-size":"Maksymalny łączny rozmiar plików pakietów OTA (bajty)", - "maximum-ota-package-sum-data-size-required":"Maksymalny łączny rozmiar plików pakietów OTA jest wymagany.", - "maximum-ota-package-sum-data-size-range":"Maksymalny łączny rozmiar plików pakietów OTA nie może być ujemny", - "rest-requests-for-tenant":"Zapytania REST dla najemcy", - "transport-tenant-telemetry-msg-rate-limit":"Limit wiadomości telemetrii najemcy", - "transport-tenant-telemetry-data-points-rate-limit":"Limit punktów danych telemetrii najemcy", - "transport-device-msg-rate-limit":"Limit wiadomości urządzenia", - "transport-device-telemetry-msg-rate-limit":"Limit wiadomości telemetrii urządzenia", - "transport-device-telemetry-data-points-rate-limit":"Limit punktów danych telemetrii urządzenia", - "tenant-entity-export-rate-limit":"Limit eksportu wersji encji", - "tenant-entity-import-rate-limit":"Limit ładowania wersji encji", - "tenant-notification-request-rate-limit":"Limit żądań powiadomień", - "tenant-notification-requests-per-rule-rate-limit":"Limit żądań powiadomień na regułę powiadomień", - "max-transport-messages":"Maksymalna liczba wiadomości transportowych", - "max-transport-messages-required":"Maksymalna liczba wiadomości transportowych jest wymagana.", - "max-transport-messages-range":"Maksymalna liczba wiadomości transportowych nie może być ujemna", - "max-transport-data-points":"Maksymalna liczba punktów danych transportowych", - "max-transport-data-points-required":"Maksymalna liczba punktów danych transportowych jest wymagana.", - "max-transport-data-points-range":"Maksymalna liczba punktów danych transportowych nie może być ujemna", - "max-r-e-executions":"Maksymalna liczba wykonanych operacji Silnika Reguł", - "max-r-e-executions-required":"Maksymalna liczba wykonanych operacji Silnika Reguł jest wymagana.", - "max-r-e-executions-range":"Maksymalna liczba wykonanych operacji Silnika Reguł nie może być ujemna", - "max-j-s-executions":"Maksymalna liczba wykonanych operacji JavaScript", - "max-j-s-executions-required":"Maksymalna liczba wykonanych operacji JavaScript jest wymagana.", - "max-j-s-executions-range":"Maksymalna liczba wykonanych operacji JavaScript nie może być ujemna", - "max-tbel-executions":"Maksymalna liczba wykonanych operacji TBEL", - "max-tbel-executions-required":"Maksymalna liczba wykonanych operacji TBEL jest wymagana.", - "max-tbel-executions-range":"Maksymalna liczba wykonanych operacji TBEL nie może być ujemna", - "max-d-p-storage-days":"Maksymalna liczba dni przechowywania punktów danych", - "max-d-p-storage-days-required":"Maksymalna liczba dni przechowywania punktów danych jest wymagana.", - "max-d-p-storage-days-range":"Maksymalna liczba dni przechowywania punktów danych nie może być ujemna", - "default-storage-ttl-days":"Domyślny czas życia pamięci masowej w dniach", - "default-storage-ttl-days-required":"Domyślny czas życia pamięci masowej w dniach jest wymagany.", - "default-storage-ttl-days-range":"Domyślny czas życia pamięci masowej w dniach nie może być ujemny", - "alarms-ttl-days":"Czas życia alarmów w dniach", - "alarms-ttl-days-required":"Czas życia alarmów jest wymagany", - "alarms-ttl-days-days-range":"Okres przechowywania alarmów (TTL) nie może być ujemny", - "rpc-ttl-days":"RPC Okres przechowywania (TTL) dni", - "rpc-ttl-days-required":"Wymagany okres przechowywania (TTL) dni dla RPC", - "rpc-ttl-days-days-range":"Okres przechowywania (TTL) dni dla RPC nie może być ujemny", - "queue-stats-ttl-days":"Okres przechowywania (TTL) dni statystyk kolejki", - "queue-stats-ttl-days-required":"Wymagany okres przechowywania (TTL) dni dla statystyk kolejki", - "queue-stats-ttl-days-range":"Okres przechowywania (TTL) dni dla statystyk kolejki nie może być ujemny", - "rule-engine-exceptions-ttl-days":"Okres przechowywania (TTL) dni wyjątków silnika reguł", - "rule-engine-exceptions-ttl-days-required":"Wymagany okres przechowywania (TTL) dni dla wyjątków silnika reguł", - "rule-engine-exceptions-ttl-days-range":"Okres przechowywania (TTL) dni dla wyjątków silnika reguł nie może być ujemny", - "max-rule-node-executions-per-message":"Maksymalna liczba wykonanych węzłów reguł na wiadomość", - "max-rule-node-executions-per-message-required":"Wymagana maksymalna liczba wykonanych węzłów reguł na wiadomość", - "max-rule-node-executions-per-message-range":"Maksymalna liczba wykonanych węzłów reguł na wiadomość nie może być ujemna", - "max-emails":"Maksymalna liczba wysłanych wiadomości e-mail", - "max-emails-required":"Wymagana maksymalna liczba wysłanych wiadomości e-mail", - "max-emails-range":"Maksymalna liczba wysłanych wiadomości e-mail nie może być ujemna", - "sms-enabled":"SMS włączony", - "max-sms":"Maksymalna liczba wysłanych wiadomości SMS", - "max-sms-required":"Wymagana maksymalna liczba wysłanych wiadomości SMS", - "max-sms-range":"Maksymalna liczba wysłanych wiadomości SMS nie może być ujemna", - "max-created-alarms":"Maksymalna liczba utworzonych alarmów", - "max-created-alarms-required":"Wymagana maksymalna liczba utworzonych alarmów", - "max-created-alarms-range":"Maksymalna liczba utworzonych alarmów nie może być ujemna", - "no-queue":"Brak skonfigurowanej kolejki", - "add-queue":"Dodaj kolejkę", - "queues-with-count":"Kolejki ({{count}})", - "tenant-rest-limits":"Żądania REST dla najemcy", - "customer-rest-limits":"Żądania REST dla klienta", - "incorrect-pattern-for-rate-limits":"Nieprawidłowy format dla limitów szybkości. Pary pojemność:okres (w sekundach) oddzielone przecinkiem, np. 100:1,2000:60", - "too-small-value-zero":"Wartość musi być większa niż 0", - "too-small-value-one":"Wartość musi być większa niż 1", - "queue-size-is-limited-by-system-configuration":"Rozmiar kolejki jest również ograniczony konfiguracją systemu.", - "cassandra-tenant-limits-configuration":"Zapytanie do Cassandry dla najemcy", - "ws-limit-max-sessions-per-tenant":"Maksymalna liczba sesji na najemcę", - "ws-limit-max-sessions-per-customer":"Maksymalna liczba sesji na klienta", - "ws-limit-max-sessions-per-regular-user":"Maksymalna liczba sesji na zwykłego użytkownika", - "ws-limit-max-sessions-per-public-user":"Maksymalna liczba sesji na publicznego użytkownika", - "ws-limit-queue-per-session":"Maksymalny rozmiar kolejki wiadomości na sesję", - "ws-limit-max-subscriptions-per-tenant":"Maksymalna liczba subskrypcji na najemcę", - "ws-limit-max-subscriptions-per-customer":"Maksymalna liczba subskrypcji na klienta", - "ws-limit-max-subscriptions-per-regular-user":"Maksymalna liczba subskrypcji na zwykłego użytkownika", - "ws-limit-max-subscriptions-per-public-user":"Maksymalna liczba subskrypcji na publicznego użytkownika", - "ws-limit-updates-per-session":"Aktualizacje WS na sesję", - "rate-limits":{ - "add-limit":"Dodaj limit", - "advanced-settings":"Zaawansowane ustawienia", - "edit-limit":"Edytuj limit", - "but-less-than":"ale mniejsze niż", - "edit-transport-tenant-msg-title":"Edytuj limity szybkości przesyłania komunikatów najemcy", - "edit-transport-tenant-telemetry-msg-title":"Edytuj limity szybkości przesyłania telemetrii komunikatów najemcy", - "edit-transport-tenant-telemetry-data-points-title":"Edytuj limity szybkości przesyłania punktów danych telemetrii najemcy", - "edit-transport-device-msg-title":"Edytuj limity szybkości przesyłania komunikatów urządzenia", - "edit-transport-device-telemetry-msg-title":"Edytuj limity szybkości przesyłania telemetrii komunikatów urządzenia", - "edit-transport-device-telemetry-data-points-title":"Edytuj limity szybkości przesyłania punktów danych telemetrii urządzenia", - "edit-tenant-rest-limits-title":"Edytuj limity szybkości żądań REST dla najemcy", - "edit-customer-rest-limits-title":"Edytuj limity szybkości żądań REST dla klienta", - "edit-ws-limit-updates-per-session-title":"Edytuj limity szybkości aktualizacji WS na sesję", - "edit-cassandra-tenant-limits-configuration-title":"Edytuj limity szybkości zapytań do Cassandry dla najemcy", - "edit-tenant-entity-export-rate-limit-title":"Edytuj limity szybkości tworzenia wersji jednostki dla najemcy", - "edit-tenant-entity-import-rate-limit-title":"Edytuj limity szybkości wczytywania wersji jednostki dla najemcy", - "edit-tenant-notification-request-rate-limit-title":"Edytuj limity szybkości żądań powiadomień dla najemcy", - "edit-tenant-notification-requests-per-rule-rate-limit-title":"Edytuj limity szybkości żądań powiadomień na regułę powiadomień dla najemcy", - "messages-per":"komunikatów na", - "not-set":"Nie ustawiono", - "number-of-messages":"Liczba komunikatów", - "number-of-messages-required":"Wymagana liczba komunikatów.", - "number-of-messages-min":"Minimalna wartość to 1.", - "preview":"Podgląd", - "per-seconds":"Na sekundę", - "per-seconds-required":"Wymagana jest szybkość czasowa.", - "per-seconds-min":"Minimalna wartość to 1.", - "rate-limits":"Limity szybkości", - "remove-limit":"Usuń limit", - "transport-tenant-msg":"Przesyłanie komunikatów najemcy", - "transport-tenant-telemetry-msg":"Przesyłanie telemetrii komunikatów najemcy", - "transport-tenant-telemetry-data-points":"Przesyłanie punktów danych telemetrii najemcy", - "transport-device-msg":"Przesyłanie komunikatów urządzenia", - "transport-device-telemetry-msg":"Przesyłanie telemetrii komunikatów urządzenia", - "transport-device-telemetry-data-points":"Przesyłanie punktów danych telemetrii urządzenia", - "sec":"sek" + "tenant-profile": { + "tenant-profile": "Profil tenanta", + "tenant-profiles": "Profile Tenantów", + "add": "Dodaj profil tenanta", + "add-profile": "Dodaj profil", + "edit": "Edytuj profil tenanta", + "tenant-profile-details": "Dane profilu tenanta", + "no-tenant-profiles-text": "Nie znaleziono profili Tenantów", + "name-max-length": "Nazwa powinna być mniejsza niż 256", + "search": "Przeszukaj profile Tenantów", + "selected-tenant-profiles": "Wybrano { count, plural, =1 {1 profil tenanta} other {# profili tenantów} }", + "no-tenant-profiles-matching": "Nie znaleziono profilu tenanta '{{entity}}'.", + "tenant-profile-required": "Profil tenanta jest wymagany", + "idCopiedMessage": "Identyfikator profilu tenanta został skopiowany do schowka", + "set-default": "Ustaw profil tenanta jako domyślny", + "delete": "Usuń profil tenanta", + "copyId": "Skopiuj identyfikator profilu tenanta", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "data": "Dane profilu", + "profile-configuration": "Konfiguracja profilu", + "description": "Opis", + "default": "Domyślny", + "delete-tenant-profile-title": "Czy na pewno chcesz usunąć profil tenanta '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Bądź ostrożny, po potwierdzeniu profilu tenanta i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-tenant-profiles-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 profil tenanta} other {# profili tenantów} }", + "delete-tenant-profiles-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane profile Tenantów zostaną usunięte, a wszystkich powiązanych danych nie będzie można odzyskać.", + "set-default-tenant-profile-title": "Czy na pewno chcesz utworzyć profil tenanta '{{tenantProfileName}}' domyślny?", + "set-default-tenant-profile-text": "Po potwierdzeniu profil tenanta zostanie oznaczony jako domyślny i będzie używany dla nowych Tenantów bez określonego profilu.", + "no-tenant-profiles-found": "Nie znaleziono profili Tenantów.", + "create-new-tenant-profile": "Utwórz nowy!", + "create-tenant-profile": "Utwórz nowy profil tenanta", + "import": "Importuj profil tenanta", + "export": "Eksportuj profil tenanta", + "export-failed-error": "Nie można wyeksportować profilu tenanta: {{error}}", + "tenant-profile-file": "Plik profilu tenanta", + "invalid-tenant-profile-file-error": "Nie można zaimportować profilu tenanta: nieprawidłowa struktura danych profilu tenanta.", + "advanced-settings": "Zaawansowane ustawienia", + "entities": "obiekty", + "rule-engine": "Silnik reguł", + "time-to-live": "Czas żyć", + "alarms-and-notifications": "Alarmy i powiadomienia", + "ota-files-in-bytes": "Pliki OTA w bajtach", + "ws-title": "WS", + "unlimited": "(0 - nieograniczony)", + "maximum-devices": "Maksymalna liczba urządzeń", + "maximum-devices-required": "Maksymalna liczba urządzeń jest wymagana.", + "maximum-devices-range": "Maksymalna liczba urządzeń nie może być ujemna", + "maximum-assets": "Maksymalna liczba zasobów", + "maximum-assets-required": "Maksymalna liczba zasobów jest wymagana.", + "maximum-assets-range": "Maksymalna liczba zasobów nie może być ujemna", + "maximum-customers": "Maksymalna liczba klientów", + "maximum-customers-required": "Maksymalna liczba klientów jest wymagana.", + "maximum-customers-range": "Maksymalna liczba klientów nie może być ujemna", + "maximum-users": "Maksymalna liczba użytkowników", + "maximum-users-required": "Maksymalna liczba użytkowników jest wymagana.", + "maximum-users-range": "Maksymalna liczba użytkowników nie może być ujemna", + "maximum-dashboards": "Maksymalna liczba paneli", + "maximum-dashboards-required": "Maksymalna liczba paneli jest wymagana.", + "maximum-dashboards-range": "Maksymalna liczba paneli nie może być ujemna", + "maximum-rule-chains": "Maksymalna liczba łańcuchów reguł", + "maximum-rule-chains-required": "Maksymalna liczba łańcuchów reguł jest wymagana.", + "maximum-rule-chains-range": "Maksymalna liczba łańcuchów reguł nie może być ujemna", + "maximum-integrations": "Maksymalna liczba integracji", + "maximum-integrations-required": "Maksymalna liczba integracji jest wymagana.", + "maximum-integrations-range": "Maksymalna liczba integracji nie może być ujemna", + "maximum-converters": "Maksymalna liczba konwerterów", + "maximum-converters-required": "Maksymalna liczba konwerterów jest wymagana.", + "maximum-converters-range": "Maksymalna liczba konwerterów nie może być ujemna", + "maximum-scheduler-events": "Maksymalna liczba zdarzeń planisty", + "maximum-scheduler-events-required": "Maksymalna liczba zdarzeń planisty jest wymagana.", + "maximum-scheduler-events-range": "Maksymalna liczba zdarzeń planisty nie może być ujemna", + "maximum-resources-sum-data-size": "Sumaryczny rozmiar plików bibliotek", + "maximum-resources-sum-data-size-required": "Sumaryczny rozmiar plików bibliotek jest wymagany.", + "maximum-resources-sum-data-size-range": "Suma rozmiarów plików bibliotek nie może być ujemna", + "maximum-ota-packages-sum-data-size": "Sumaryczny rozmiar plików pakietu OTA", + "maximum-ota-package-sum-data-size-required": "Sumaryczny rozmiar plików pakietu OTA jest wymagany.", + "maximum-ota-package-sum-data-size-range": "Suma plików pakietu OTA nie może być ujemna", + "rest-requests-for-tenant": "Żądania REST dla Tenanta", + "transport-tenant-telemetry-msg-rate-limit": "Komunikaty telemetryczne Tenanta transportu", + "transport-tenant-telemetry-data-points-rate-limit": "Punkty danych telemetrycznych tenanta transportu", + "transport-device-msg-rate-limit": "Wiadomości z urządzenia transportowego", + "transport-device-telemetry-msg-rate-limit": "Komunikaty telemetryczne urządzenia transportowego", + "transport-device-telemetry-data-points-rate-limit": "Punkty danych telemetrycznych urządzenia transportowego", + "tenant-entity-export-rate-limit": "Tworzenie wersji obiektów", + "tenant-entity-import-rate-limit": "Ładowanie wersji obiektów", + "tenant-notification-request-rate-limit": "Żądania powiadomień", + "tenant-notification-requests-per-rule-rate-limit": "Żądania powiadomień według reguły powiadomień", + "integration-msgs-per-tenant-rate-limit": "Tenant integration messages", + "integration-msgs-per-device-rate-limit": "Device integration messages", + "max-transport-messages": "Maksymalna liczba komunikatów transportowych", + "max-transport-messages-required": "Maksymalna liczba komunikatów transportowych jest wymagana.", + "max-transport-messages-range": "Maksymalna liczba komunikatów transportowych nie może być ujemna", + "max-transport-data-points": "Maksymalna liczba punktów danych dotyczących transportu ", + "max-transport-data-points-required": "Maksymalna liczba punktów danych dotyczących transportu jest wymagana.", + "max-transport-data-points-range": "Maksymalna liczba punktów danych dotyczących transportu nie może być ujemna", + "max-r-e-executions": "Maksymalna liczba wykonań silnika reguły", + "max-r-e-executions-required": "Maksymalna liczba wykonań mechanizmu reguły jest wymagana.", + "max-r-e-executions-range": "Maksymalna liczba wykonań mechanizmu reguł nie może być ujemna", + "max-j-s-executions": "Maksymalna liczba wykonań JavaScript ", + "max-j-s-executions-required": "Maksymalna liczba wykonań JavaScript jest wymagana.", + "max-j-s-executions-range": "Maksymalna liczba wykonań JavaScript nie może być ujemna", + "max-d-p-storage-days": "Maksymalna liczba dni przechowywania punktów danych", + "max-d-p-storage-days-required": "Maksymalna liczba dni przechowywania punktów danych jest wymagana.", + "max-d-p-storage-days-range": "Maksymalna liczba dni przechowywania punktów danych nie może być ujemna", + "default-storage-ttl-days": "Domyślnie dni TTL przechowywania", + "default-storage-ttl-days-required": "Domyślnie wymagane są dni TTL przechowywania.", + "default-storage-ttl-days-range": "Domyślnie dni TTL magazynu nie mogą być ujemne", + "alarms-ttl-days": "Alarmy TTL dni", + "alarms-ttl-days-required": "Wymagane dni TTL alarmów", + "alarms-ttl-days-days-range": "Dni alarmów TTL nie mogą być ujemne", + "rpc-ttl-days": "Dni RPC TTL", + "rpc-ttl-days-required": "Wymagane dni RPC TTL", + "rpc-ttl-days-days-range": "Dni RPC TTL nie mogą być ujemne", + "max-rule-node-executions-per-message": "Maksymalna liczba wykonań węzła reguły na wiadomość", + "max-rule-node-executions-per-message-required": "Maksymalna liczba wykonań węzła MRule na komunikat jest wymagana.", + "max-rule-node-executions-per-message-range": "Maksymalna liczba wykonań węzła reguły na komunikat nie może być ujemna", + "max-emails": "Maksymalna liczba wysłanych e-maili", + "max-emails-required": "Maksymalna liczba wysłanych e-maili jest wymagana.", + "max-emails-range": "Maksymalna liczba wysłanych e-maili nie może być ujemna", + "sms-enabled": "SMS enabled", + "max-sms": "Maksymalna liczba wysłanych SMS-ów", + "max-sms-required": "Maksymalna liczba wysłanych SMS-ów jest wymagana.", + "max-sms-range": "Maksymalna liczba wysłanych SMS-ów nie może być ujemna", + "max-created-alarms": "Alarmy utworzyły maksymalną liczbę", + "max-created-alarms-required": "Maksymalna liczba utworzonych alarmów jest wymagana.", + "max-created-alarms-range": "Maksymalna liczba utworzonych alarmów będzie ujemna", + "no-queue": "Nie skonfigurowano kolejki", + "add-queue": "Dodaj kolejkę", + "queues-with-count": "Kolejki ({{count}})", + "tenant-rest-limits": "Żądania REST dla Tenanta", + "customer-rest-limits": "Żądania REST dla klienta", + "incorrect-pattern-for-rate-limits": "Format to oddzielane przecinkami pary pojemności i kropki (w sekundach) z dwukropkiem pomiędzy np. ", + "too-small-value-zero": "Wartość musi być większa niż 0", + "too-small-value-one": "Wartość musi być większa niż 1", + "queue-size-is-limited-by-system-configuration": "Rozmiar kolejki jest również ograniczony konfiguracją systemu.", + "cassandra-tenant-limits-configuration": "Zapytanie Cassandry o Tenanta", + "ws-limit-max-sessions-per-tenant": "Maksymalna liczba sesji na Tenanta", + "ws-limit-max-sessions-per-customer": "Maksymalna liczba sesji na klienta", + "ws-limit-max-sessions-per-regular-user": "Maksymalna liczba sesji na zwykłego użytkownika", + "ws-limit-max-sessions-per-public-user": "Maksymalna liczba sesji na użytkownika publicznego", + "ws-limit-queue-per-session": "Maksymalny rozmiar kolejki komunikatów na sesję", + "ws-limit-max-subscriptions-per-tenant": "Maksymalna liczba subskrypcji na Tenanta", + "ws-limit-max-subscriptions-per-customer": "Maksymalna liczba subskrypcji na klienta", + "ws-limit-max-subscriptions-per-regular-user": "Maksymalna liczba subskrypcji na zwykłego użytkownika", + "ws-limit-max-subscriptions-per-public-user": "Maksymalna liczba subskrypcji na użytkownika publicznego", + "ws-limit-updates-per-session": "Aktualizacje WS na sesję", + "rate-limits": { + "add-limit": "Dodaj limit", + "advanced-settings": "Zaawansowane ustawienia", + "edit-limit": "Edytuj limit", + "but-less-than": "ale mniej niż", + "edit-transport-tenant-msg-title": "Edytuj limity szybkości wiadomości tenanta transportu", + "edit-transport-tenant-telemetry-msg-title": "Edytuj limity szybkości komunikatów telemetrycznych Tenanta transportu", + "edit-transport-tenant-telemetry-data-points-title": "Edytuj limity szybkości punktów danych telemetrycznych Tenanta transportu", + "edit-transport-device-msg-title": "Edytuj limity szybkości komunikatów urządzeń transportowych", + "edit-transport-device-telemetry-msg-title": "Edytuj limity szybkości komunikatów telemetrycznych urządzeń transportowych", + "edit-transport-device-telemetry-data-points-title": "Edytuj limity szybkości punktów danych telemetrycznych urządzeń transportowych", + "edit-tenant-rest-limits-title": "Edytuj żądania REST dotyczące limitów stawek Tenanta", + "edit-customer-rest-limits-title": "Edytuj żądania REST dotyczące limitów stawek klienta", + "edit-ws-limit-updates-per-session-title": "Edytuj limity szybkości aktualizacji WS na sesję", + "edit-cassandra-tenant-limits-configuration-title": "Edytuj zapytanie Cassandry dotyczące limitów stawek tenanta", + "edit-tenant-entity-export-rate-limit-title": "Edytuj limity szybkości tworzenia wersji obiektu", + "edit-tenant-entity-import-rate-limit-title": "Edytuj limity szybkości ładowania wersji obiektu", + "edit-tenant-notification-request-rate-limit-title": "Edytuj limity żądań powiadomień", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Edytuj żądania powiadomień według limitów częstotliwości reguł powiadomień", + "edit-integration-msgs-per-tenant-rate-limit-title": "Edit integration messages per tenant rate limits", + "edit-integration-msgs-per-device-rate-limit-title": "Edit integration messages per device rate limits", + "messages-per": "wiadomości na", + "not-set": "Nie ustawiony", + "number-of-messages": "Liczba wiadomości", + "number-of-messages-required": "Liczba wiadomości jest wymagana.", + "number-of-messages-min": "Minimalna wartość to 1.", + "preview": "Zapowiedź", + "per-seconds": "Na sekundę", + "per-seconds-required": "Tempo jest wymagane.", + "per-seconds-min": "Minimalna wartość to 1.", + "rate-limits": "Limity", + "remove-limit": "Usuń limit", + "transport-tenant-msg": "Wiadomości tenanta transportu", + "transport-tenant-telemetry-msg": "Komunikaty telemetryczne Tenanta transportu", + "transport-tenant-telemetry-data-points": "Punkty danych telemetrycznych tenanta transportu", + "transport-device-msg": "Wiadomości z urządzenia transportowego", + "transport-device-telemetry-msg": "Komunikaty telemetryczne urządzenia transportowego", + "transport-device-telemetry-data-points": "Punkty danych telemetrycznych urządzenia transportowego", + "sec": "sek" } }, - "timeinterval":{ - "seconds-interval":"{ seconds, plural, =1 {1 sekunda} other {# sekundy} }", - "minutes-interval":"{ minutes, plural, =1 {1 minuta} other {# minuty} }", - "hours-interval":"{ hours, plural, =1 {1 godzina} other {# godziny} }", - "days-interval":"{ days, plural, =1 {1 dzień} other {# dni} }", - "days":"Dni", - "hours":"Godziny", - "minutes":"Minuty", - "seconds":"Sekundy", - "advanced":"Zaawansowane", - "predefined":{ - "yesterday":"Wczoraj", - "day-before-yesterday":"Przedwczoraj", - "this-day-last-week":"Ten dzień w zeszłym tygodniu", - "previous-week":"Poprzedni tydzień (Ndz - Sob)", - "previous-week-iso":"Poprzedni tydzień (Pon - Ndz)", - "previous-month":"Poprzedni miesiąc", - "previous-quarter":"Poprzedni kwartał", - "previous-half-year":"Poprzednie półrocze", - "previous-year":"Poprzedni rok", - "current-hour":"Bieżąca godzina", - "current-day":"Bieżący dzień", - "current-day-so-far":"Bieżący dzień do tej pory", - "current-week":"Bieżący tydzień (Ndz - Sob)", - "current-week-iso":"Bieżący tydzień (Pon - Ndz)", - "current-week-so-far":"Bieżący tydzień do tej pory (Ndz - Sob)", - "current-week-iso-so-far":"Bieżący tydzień do tej pory (Pon - Ndz)", - "current-month":"Bieżący miesiąc", - "current-month-so-far":"Bieżący miesiąc do tej pory", - "current-quarter":"Bieżący kwartał", - "current-quarter-so-far":"Bieżący kwartał do tej pory", - "current-half-year":"Bieżące półrocze", - "current-half-year-so-far":"Bieżące półrocze do tej pory", - "current-year":"Bieżący rok", - "current-year-so-far":"Bieżący rok do tej pory" - }, - "type":{ - "week":"Tydzień (Ndz - Sob)", - "week-iso":"Tydzień (Pon - Ndz)", - "month":"Miesiąc", - "quarter":"Kwartał" + "timeinterval": { + "seconds-interval": "{ seconds, plural, =1 {1 sekunda} other {# sekund(y)} }", + "minutes-interval": "{ minutes, plural, =1 {1 minuta} other {# minut(y)} }", + "hours-interval": "{ hours, plural, =1 {1 godzina} other {# godzin(y)} }", + "days-interval": "{ days, plural, =1 {1 dzień} other {# dni} }", + "days": "Dni", + "hours": "Godziny", + "minutes": "Minuty", + "seconds": "Sekundy", + "advanced": "Zaawansowane", + "predefined": { + "yesterday": "Wczoraj", + "day-before-yesterday": "Przedwczoraj", + "this-day-last-week": "Ten dzień w zeszłym tygodniu", + "previous-week": "Poprzedni tydzień (niedziela - sobota)", + "previous-week-iso": "Poprzedni tydzień (poniedziałek - niedziela)", + "previous-month": "Poprzedni miesiac", + "previous-quarter": "Poprzedni kwartał", + "previous-half-year": "Poprzednie półrocze", + "previous-year": "Poprzedni rok", + "current-hour": "Aktualna godzina", + "current-day": "Bieżący dzień", + "current-day-so-far": "Do tej pory bieżący dzień", + "current-week": "Bieżący tydzień (niedziela - sobota)", + "current-week-iso": "Bieżący tydzień (poniedziałek - niedziela)", + "current-week-so-far": "Dotychczasowy tydzień (niedziela - sobota)", + "current-week-iso-so-far": "Dotychczasowy tydzień (poniedziałek - niedziela)", + "current-month": "Obecny miesiąc", + "current-month-so-far": "Do tej pory bieżący miesiąc", + "current-quarter": "Bieżący kwartał", + "current-quarter-so-far": "Do tej pory bieżący kwartał", + "current-half-year": "Bieżące półrocze", + "current-half-year-so-far": "Dotychczasowe półrocze", + "current-year": "Rok bieżący", + "current-year-so-far": "Do tej pory bieżący rok" } }, - "timeunit":{ - "milliseconds":"Milisekundy", - "seconds":"Sekundy", - "minutes":"Minuty", - "hours":"Godziny", - "days":"Dni" + "timeunit": { + "milliseconds": "Milisekundy", + "seconds": "Sekundy", + "minutes": "Minuty", + "hours": "Godziny", + "days": "Dni" }, - "timewindow":{ - "timewindow":"Okno czasowe", - "years":"{ years, plural, =1 { rok } other {# lata } }", - "years-short":"{{ years }}r", - "months":"{ months, plural, =1 { miesiąc } other {# miesiące } }", - "months-short":"{{ months }}m", - "weeks":"{ weeks, plural, =1 { tydzień } other {# tygodnie } }", - "weeks-short":"{{ weeks }}t", - "days":"{ days, plural, =1 { dzień } other {# dni } }", - "days-short":"{{ days }}d", - "hours":"{ hours, plural, =0 { godzina } =1 {1 godzina } other {# godziny } }", - "hr":"{{ hr }} godz", - "hr-short":"{{ hr }}h", - "minutes":"{ minutes, plural, =0 { minuta } =1 {1 minuta } other {# minuty } }", - "min":"{{ min }} min", - "min-short":"{{ min }}m", - "seconds":"{ seconds, plural, =0 { sekunda } =1 {1 sekunda } other {# sekundy } }", - "sec":"{{ sec }} sek", - "sec-short":"{{ sec }}s", - "short":{ - "days":"{ days, plural, =1 {1 dzień } other {# dni } }", - "hours":"{ hours, plural, =1 {1 godzina } other {# godziny } }", - "minutes":"{{minutes}} min ", - "seconds":"{{seconds}} sek " + "timewindow": { + "timewindow": "Timewindow", + "years": "{ years, plural, =1 { rok } other {# lat(a) } }", + "years-short": "{{ years }}y", + "months": "{ months, plural, =1 { miesiąc } other {# miesięcy } }", + "months-short": "{{ months }}M", + "weeks": "{ weeks, plural, =1 { tydzień } other {# tygodni(e) } }", + "weeks-short": "{{ weeks }}w", + "days": "{ days, plural, =1 { dzień } other {# dni } }", + "days-short": "{{ days }}d", + "hours": "{ hours, plural, =0 { 0 godzin } =1 {1 godzina } other {# godzin(y) } }", + "hr": "{{ hr }} godz", + "hr-short": "{{ hr }}h", + "minutes": "{ minutes, plural, =0 { 0 minut } =1 {1 minuta } other {# minut(y) } }", + "min": "{{ min }} min", + "min-short": "{{ min }}m", + "seconds": "{ seconds, plural, =0 { 0 sekund } =1 {1 sekunda } other {# sekund(y) } }", + "sec": "{{ sec }} sek", + "sec-short": "{{ sec }}s", + "short": { + "days": "{ days, plural, =1 { dzień } other {# dni } }", + "hours": "{ hours, plural, =1 {1 godzina } other {# godzin(y) } }", + "minutes": "{{minutes}} min ", + "seconds": "{{seconds}} sek " }, - "realtime":"Na żywo", - "history":"Historia", - "last-prefix":"ostatni", - "period":"od {{ startTime }} do {{ endTime }}", - "edit":"Edytuj okno czasowe", - "date-range":"Zakres dat", - "for-all-time":"Dla wszystkich dostępnych danych", - "last":"Ostatnie", - "time-period":"Okres czasu", - "hide":"Ukryj", - "interval":"Interwał", - "just-now":"Teraz", - "just-now-lower":"teraz", - "ago":"temu", - "style":"Styl okna czasowego", - "icon":"Ikona", - "icon-position":"Pozycja ikony", - "icon-position-left":"Lewo", - "icon-position-right":"Prawo", - "font":"Czcionka", - "color":"Kolor", - "displayTypePrefix":"Wyświetlaj przedrostek Na żywo/Historia", - "preview":"Podgląd" - }, - "tooltip":{ - "value":"Wartość", - "date":"Data", - "background-color":"Kolor tła", - "background-blur":"Rozmycie tła" + "realtime": "Czas rzeczywisty", + "history": "Historia", + "last-prefix": "ostatni", + "period": "Od {{ startTime }} Do {{ endTime }}", + "edit": "Edytuj okno czasowe", + "date-range": "Zakres dat", + "for-all-time": "Cały dotychczasowy okres", + "last": "Ostatni(e)", + "time-period": "Okres czasu", + "hide": "Ukrywać", + "interval": "Interwał", + "just-now": "W tej chwili", + "just-now-lower": "just now", + "ago": "temu", + "style": "Timewindow style", + "icon": "Icon", + "icon-position": "Icon position", + "icon-position-left": "Left", + "icon-position-right": "Right", + "font": "Font", + "color": "Color", + "displayTypePrefix": "Display Realtime/History prefix", + "preview": "Preview" }, - "unit":{ - "millimeter":"Milimetr", - "centimeter":"Centymetr", - "angstrom":"Angstrom", - "nanometer":"Nanometr", - "micrometer":"Mikrometr", - "meter":"Metr", - "kilometer":"Kilometr", - "inch":"Cal", - "foot":"Stopa", - "yard":"Jard", - "mile":"Mila", - "nautical-mile":"Mila morska", - "astronomical-unit":"Jednostka astronomiczna", - "reciprocal-metre":"Odwrócony metr", - "meter-per-meter":"Metr na metr", - "steradian":"Steradian", - "thou":"Thou", - "barleycorn":"Jęczmień", - "hand":"Dłoń", - "chain":"Ciąg", - "furlong":"Furlong", - "league":"Liga", - "fathom":"Fathom", - "cable":"Kabel", - "link":"Link", - "rod":"Ród", - "nanogram":"Nanogram", - "microgram":"Mikrogram", - "milligram":"Miligram", - "gram":"Gram", - "kilogram":"Kilogram", - "tonne":"Tonna", - "ounce":"Uncja", - "pound":"Funt", - "stone":"Stoun", - "hundredweight-count":"Cental", - "short-tons":"Krótka tona", - "dalton":"Dalton", - "grain":"Gran", - "drachm":"Drachma", - "quarter":"Quarter", - "slug":"Slug", - "carat":"Karad", - "cubic-millimeter":"Milimetr sześcienny", - "cubic-centimeter":"Centymetr sześcienny", - "cubic-meter":"Metr sześcienny", - "cubic-kilometer":"Kilometr sześcienny", - "microliter":"Mikrolitr", - "milliliter":"Mililitr", - "liter":"Litr", - "hectoliter":"Hektolitr", - "cubic-inch":"Cal sześcienny", - "cubic-foot":"Stopa sześcienna", - "cubic-yard":"Jard sześcienny", - "fluid-ounce":"Uncja płynu", - "pint":"Pinta", - "quart":"Kwarta", - "gallon":"Galona", - "oil-barrels":"Baryłki naftowe", - "cubic-meter-per-kilogram":"Metr sześcienny na kilogram", - "gill":"Gill", - "hogshead":"Hogshead", - "teaspoon":"Łyżeczka", - "tablespoon":"Łyżka stołowa", - "cup":"Kubek", - "celsius":"Stopnie Celsiusza", - "kelvin":"Kelwin", - "rankine":"Rankine", - "fahrenheit":"Stopnie Fahrenheita", - "percent":"Procent", - "meter-per-second":"Metr na sekundę", - "kilometer-per-hour":"Kilometr na godzinę", - "foot-per-second":"Stopa na sekundę", - "mile-per-hour":"Mila na godzinę", - "knot":"Węzeł", - "millimeters-per-minute":"Milimetrów na minutę", - "kilometer-per-hour-squared":"Kilometr na godzinę do kwadratu", - "foot-per-second-squared":"Stopa na sekundę do kwadratu", - "pascal":"Pascal", - "kilopascal":"Kilopascal", - "megapascal":"Megapascal", - "gigapascal":"Gigapascal", - "millibar":"Milibar", - "bar":"Bar", - "kilobar":"Kilobar", - "newton":"Newton", - "newton-meter":"Newton-metr", - "foot-pounds":"Stopy funtów", - "inch-pounds":"Calowe funty", - "newton-per-meter":"Newton na metr", - "atmospheres":"Atmosfery", - "pounds-per-square-inch":"Funtów na cal kwadratowy", - "torr":"Torr", - "inches-of-mercury":"Cale rtęci", - "pascal-per-square-meter":"Pascal na metr kwadratowy", - "pound-per-square-inch":"Funt na cal kwadratowy", - "newton-per-square-meter":"Newton na metr kwadratowy", - "kilogram-force-per-square-meter":"Kilogram-siła na metr kwadratowy", - "pascal-per-square-centimeter":"Pascal na centymetr kwadratowy", - "ton-force-per-square-inch":"Ton-siła na cal kwadratowy", - "kilonewton-per-square-meter":"Kilonewton na metr kwadratowy", - "newton-per-square-millimeter":"Newton na milimetr kwadratowy", - "microjoule":"Mikrodżul", - "millijoule":"Milidżul", - "joule":"Dżul", - "kilojoule":"Kilodżul", - "megajoule":"Megadżul", - "gigajoule":"Gigadżul", - "watt-hour":"Watogodzina", - "kilowatt-hour":"Kilowatogodzina", - "electron-volts":"Elektronowolt", - "joules-per-coulomb":"Dżule na kulomb", - "british-thermal-unit":"Brytyjska jednostka ciepła", - "foot-pound":"Stopa-funt", - "calorie":"Kaloria", - "small-calorie":"Mała kaloria", - "kilocalorie":"Kilokaloria", - "joule-per-kelvin":"Dżul na kelwin", - "joule-per-kilogram-kelvin":"Dżul na kilogram-kelwin", - "joule-per-kilogram":"Dżul na kilogram", - "watt-per-meter-kelvin":"Wat na metr-kelwin", - "joule-per-cubic-meter":"Dżul na metr sześcienny", - "therm":"Term", - "electric-dipole-moment":"Moment dipolowy elektryczny", - "magnetic-dipole-moment":"Moment dipolowy magnetyczny", - "debye":"Debye", - "coulomb-per-square-meter-per-volt":"Kulomb na metr kwadratowy na wolt", - "milliwatt":"Miliwat", - "microwatt":"Mikrowat", - "watt":"Wat", - "kilowatt":"Kilowat", - "megawatt":"Megawat", - "gigawatt":"Gigawat", - "metric-horsepower":"Koń mechaniczny metryczny", - "milliwatt-per-square-centimeter":"Miliwaty na centymetr kwadratowy", - "watt-per-square-centimeter":"Waty na centymetr kwadratowy", - "kilowatt-per-square-centimeter":"Kilowat na centymetr kwadratowy", - "milliwatt-per-square-meter":"Miliwaty na metr kwadratowy", - "watt-per-square-meter":"Waty na metr kwadratowy", - "kilowatt-per-square-meter":"Kilowaty na metr kwadratowy", - "watt-per-square-inch":"Waty na cal kwadratowy", - "kilowatt-per-square-inch":"Kilowaty na cal kwadratowy", - "horsepower":"Koń mechaniczny", - "btu-per-hour":"Brytyjskie jednostki ciepła na godzinę", - "coulomb":"Kulomb", - "millicoulomb":"Milikulomb", - "microcoulomb":"Mikrokulomb", - "picocoulomb":"Pikokulomb", - "coulomb-per-meter":"Kulomb na metr", - "coulomb-per-cubic-meter":"Kulomb na metr sześcienny", - "coulomb-per-square-meter":"Kulomb na metr kwadratowy", - "square-millimeter":"Metr kwadratowy", - "square-centimeter":"Centymetr kwadratowy", - "square-meter":"Metr kwadratowy", - "hectare":"Hektar", - "square-kilometer":"Kilometr kwadratowy", - "square-inch":"Cal kwadratowy", - "square-foot":"Stopa kwadratowa", - "square-yard":"Jard kwadratowy", - "acre":"Akr", - "square-mile":"Mila kwadratowa", - "are":"Ar", - "barn":"Barn", - "circular-inch":"Cal okrągły", - "milliampere-hour":"Miliamperogodzina", - "milliampere-hour-tags":"prąd elektryczny, przepływ prądu, ładunek elektryczny, pojemność prądu, przepływ elektryczności, przepływ elektryczny, godzina miliamperowa, miliamperogodziny, mAh", - "ampere-hours":"Amperogodziny", - "ampere-hours-tags":"prąd elektryczny, przepływ prądu, ładunek elektryczny, pojemność prądu, przepływ elektryczności, przepływ elektryczny, amper, amperogodziny, Ah", - "kiloampere-hours":"Kiloamperogodziny", - "kiloampere-hours-tags":"prąd elektryczny, przepływ prądu, ładunek elektryczny, pojemność prądu, przepływ elektryczności, przepływ elektryczny, kiloamperogodziny, kiloamperogodzina, kAh", - "nanoampere":"Nanoamper", - "nanoampere-tags":"prąd, ampery, nanoamper, nA", - "picoampere":"Pikoamper", - "picoampere-tags":"prąd, ampery, pikoamper, pA", - "microampere":"Mikroamper", - "microampere-tags":"prąd elektryczny, mikroamper, mikroampery, μA", - "milliampere":"Miliamper", - "milliampere-tags":"prąd elektryczny, miliamper, miliampery, mA", - "ampere":"Amper", - "ampere-tags":"prąd elektryczny, przepływ prądu, przepływ elektryczności, przepływ elektryczny, amper, ampery, amperaż, A", - "kiloamperes":"Kiloampery", - "kiloamperes-tags":"prąd elektryczny, przepływ prądu, kiloampery, kA", - "microampere-per-square-centimeter":"Mikroamper na centymetr kwadratowy", - "microampere-per-square-centimeter-tags":"Gęstość prądu, mikroamper na centymetr kwadratowy, µA/cm²", - "ampere-per-square-meter":"Amper na metr kwadratowy", - "ampere-per-square-meter-tags":"gęstość prądu, prąd na jednostkę powierzchni, amper na metr kwadratowy, A/m²", - "ampere-per-meter":"Amper na metr", - "ampere-per-meter-tags":"siła pola magnetycznego, intensywność pola magnetycznego, amper na metr, A/m", - "oersted":"Oersted", - "oersted-tags":"pole magnetyczne, oersted, Oe", - "bohr-magneton":"Magnetony Bohra", - "bohr-magneton-tags":"fizyka atomowa, moment magnetyczny, magneton Bohra, μB", - "ampere-meter-squared":"Amper-Metr Kwadratowy", - "ampere-meter-squared-tags":"moment magnetyczny, moment dipolowy, amper-metr kwadratowy, A·m²", - "ampere-meter":"Amper-Metr", - "ampere-meter-tags":"pole magnetyczne, pętla prądowa, amper-metr, A·m", - "nanovolt":"Nanowolt", - "picovolt":"Pikowolt", - "millivolts":"Milivolty", - "microvolts":"Mikrowolty", - "volt":"Wolt", - "kilovolts":"Kilowolty", - "dbmV":"dBmV", - "dbm":"dBm", - "volt-meter":"Woltomierz", - "kilovolt-meter":"Kilowoltomierz", - "megavolt-meter":"Megawoltomierz", - "microvolt-meter":"Mikrowoltomierz", - "millivolt-meter":"Milivoltomierz", - "nanovolt-meter":"Nanowoltomierz", - "ohm":"Om", - "microohm":"Mikroom", - "milliohm":"Miloom", - "kilohm":"Kiloom", - "megohm":"Megaom", - "gigohm":"Gigaom", - "hertz":"Herz", - "kilohertz":"Kilohertz", - "megahertz":"Megahertz", - "gigahertz":"Gigahertz", - "rpm":"Obroty na minutę", - "candela-per-square-meter":"Kandela na metr kwadratowy", - "candela":"Kandela", - "lumen":"Lumen", - "lux":"Luks", - "foot-candle":"Stopa-świeca", - "lumen-per-square-meter":"Lumen na metr kwadratowy", - "lux-second":"Luks-sekunda", - "lumen-second":"Lumen-sekunda", - "lumens-per-watt":"Lumeny na wat", - "absorbance":"Absorpcja", - "mole":"Mol", - "nanomole":"Nanomol", - "micromole":"Mikromol", - "millimole":"Milimol", - "kilomole":"Kilomol", - "mole-per-cubic-meter":"Mol na metr sześcienny", - "rssi":"RSSI", - "ppm":"Części na milion", - "ppb":"Części na miliard", - "micrograms-per-cubic-meter":"Mikrogramy na metr sześcienny", - "aqi":"AQI", - "gram-per-cubic-meter":"Gram na metr sześcienny", - "gram-per-kilogram":"Wilgotność właściwa", - "millimeters-per-second":"Milimetry na sekundę", - "neper":"Neper", - "bel":"Bel", - "decibel":"Decybel", - "meters-per-second-squared":"Metry na sekundę kwadratową", - "becquerel":"Bekerel", - "curie":"Kiri", - "gray":"Graj", - "sievert":"Sivert", - "roentgen":"Rentgen", - "cps":"Zliczenia na sekundę", - "rad":"Rad", - "rem":"Rem", - "dps":"Rozpady na sekundę", - "rutherford":"Rutherford", - "coulombs-per-kilogram":"Kulomby na kilogram", - "becquerels-per-cubic-meter":"Bekerel na metr sześcienny", - "curies-per-liter":"Kiri na litr", - "becquerels-per-second":"Bekerel na sekundę", - "curies-per-second":"Kiri na sekundę", - "gy-per-second":"Graj na sekundę", - "watt-per-steradian":"Wat na steradian", - "watt-per-square-metre-steradian":"Wat na metr kwadratowy-steradian", - "ph-level":"Poziom pH", - "turbidity":"Mętność", - "mg-per-liter":"Miligramy na litr", - "microsiemens-per-centimeter":"Mikrosiemeny na centymetr", - "millisiemens-per-meter":"Milisiemeny na metr", - "siemens-per-meter":"Siemeny na metr", - "kilogram-per-cubic-meter":"Kilogram na metr sześcienny", - "gram-per-cubic-centimeter":"Gram na centymetr sześcienny", - "kilogram-per-square-meter":"Kilogram na metr kwadratowy", - "milligram-per-milliliter":"Miligram na mililitr", - "milligram-per-cubic-meter":"Miligram na metr sześcienny", - "pound-per-cubic-foot":"Funt na stopę sześcienną", - "ounces-per-cubic-inch":"Uncje na cal sześcienny", - "tons-per-cubic-yard":"Tony na jard sześcienny", - "particle-density":"Gęstość cząstek", - "kilometers-per-liter":"Kilometry na litr", - "miles-per-gallon":"Mile na galon", - "liters-per-100-km":"Litrów na 100 kilometrów", - "gallons-per-mile":"Galony na milę", - "liters-per-hour":"Litrów na godzinę", - "gallons-per-hour":"Galony na godzinę", - "beats-per-minute":"Uderzenia na minutę", - "millimeters-of-mercury":"Milimetry słupa rtęci", - "milligrams-per-deciliter":"Miligramy na decylitr", - "g-force":"Siła G", - "kilonewton":"Kiloniuton", - "kilogram-force":"Kilogram-siła", - "pound-force":"Funt-siła", - "kilopound-force":"Kilofunt-siła", - "dyne":"Dyn", - "poundal":"Poundal", - "kip":"Kip", - "gal":"Gal", - "gravity":"Przyspieszenie grawitacyjne", - "hectopascal":"Hektopaskal", - "atmosphere":"Atmosfera", - "millibars":"Milibary", - "inch-of-mercury":"Cal słupa rtęci", - "richter-scale":"Skala Richtera", - "second":"Sekunda", - "minute":"Minuta", - "hour":"Godzina", - "day":"Dzień", - "week":"Tydzień", - "month":"Miesiąc", - "year":"Rok", - "cubic-foot-per-minute":"Stopy sześcienne na minutę", - "cubic-meters-per-hour":"Metry sześcienne na godzinę", - "cubic-meters-per-second":"Metry sześcienne na sekundę", - "liter-per-second":"Litr na sekundę", - "liter-per-minute":"Litr na minutę", - "gallons-per-minute":"Galony na minutę", - "cubic-foot-per-second":"Stopa sześcienna na sekundę", - "milliliters-per-minute":"Mililitrów na minutę", - "bit":"Bit", - "byte":"Bajt", - "kilobyte":"Kilobajt", - "megabyte":"Megabajt", - "gigabyte":"Gigabajt", - "terabyte":"Terabajt", - "petabyte":"Petabajt", - "exabyte":"Eksabajt", - "zettabyte":"Zettabajt", - "yottabyte":"Jottabajt", - "bit-per-second":"Bit na sekundę", - "kilobit-per-second":"Kilobit na sekundę", - "megabit-per-second":"Megabit na sekundę", - "gigabit-per-second":"Gigabit na sekundę", - "terabit-per-second":"Terabit na sekundę", - "byte-per-second":"Bajt na sekundę", - "kilobyte-per-second":"Kilobajt na sekundę", - "megabyte-per-second":"Megabajt na sekundę", - "gigabyte-per-second":"Gigabajt na sekundę", - "degree":"Stopień", - "radian":"Radian", - "gradian":"Grad", - "mil":"Mil", - "revolution":"Obrotów", - "siemens":"Siemens", - "millisiemens":"Milisiemens", - "microsiemens":"Mikrosiemens", - "kilosiemens":"Kilosiemens", - "megasiemens":"Megasiemens", - "gigasiemens":"Gigasiemens", - "farad":"Farad", - "millifarad":"Milifarad", - "microfarad":"Mikrofarad", - "nanofarad":"Nanofarad", - "picofarad":"Pikofarad", - "kilofarad":"Kilofarad", - "megafarad":"Megafarad", - "gigafarad":"Gigafarad", - "terfarad":"Terfarad", - "farad-per-meter":"Farad na metr", - "tesla":"Tesla", - "gauss":"Gaus", - "kilogauss":"Kilogaus", - "millitesla":"Millitesla", - "microtesla":"Mikrotesla", - "nanotesla":"Nanotesla", - "kilotesla":"Kilotesla", - "megatesla":"Megatesla", - "millitesla-square-meters":"Millitesla na metry kwadratowe", - "gamma":"Gamma", - "lambda":"Lambda", - "square-meter-per-second":"Metr kwadratowy na sekundę", - "square-centimeter-per-second":"Centymetr kwadratowy na sekundę", - "stoke":"Stoke", - "centistokes":"Centistoke", - "square-foot-per-second":"Stopa kwadratowa na sekundę", - "square-inch-per-second":"Cal kwadratowy na sekundę", - "pascal-second":"Paskal-sekunda", - "centipoise":"Centipoise", - "poise":"Poise", - "reynolds":"Reynolds", - "pound-per-foot-hour":"Funt na stopę kwadratową na godzinę", - "newton-second-per-square-meter":"Newton-sekunda na metr kwadratowy", - "dyne-second-per-square-centimeter":"Dyn-sekunda na centymetr kwadratowy", - "kilogram-per-meter-second":"Kilogram na metr-sekundę", - "tesla-square-meters":"Tesla na metry kwadratowe", - "maxwell":"Maxwell", - "tesla-per-meter":"Tesla na metr", - "gauss-per-centimeter":"Gaus na centymetr", - "weber":"Weber", - "microweber":"Mikroweber", - "milliweber":"Milliweber", - "gauss-square-centimeter":"Gaus na centymetr kwadratowy", - "kilogauss-square-centimeter":"Kilogaus na centymetr kwadratowy", - "henry":"Henry", - "millihenry":"Millihenry", - "microhenry":"Mikrohenry", - "nanohenry":"Nanohenry", - "henry-per-meter":"Henry na metr", - "tesla-meter-per-ampere":"Tesla na metr na amper", - "gauss-per-oersted":"Gaus na Oersted", - "kilogram-per-mole":"Kilogram na mol", - "gram-per-mole":"Gram na mol", - "milligram-per-mole":"Miligram na mol", - "joule-per-mole":"Dżul na mol", - "joule-per-mole-kelvin":"Dżul na mol-kelwin", - "millivolts-per-meter":"Milivolty na metr", - "volts-per-meter":"Volty na metr", - "kilovolts-per-meter":"Kilovolty na metr", - "radian-per-second":"Radian na sekundę", - "radian-per-second-squared":"Radian na sekundę kwadratową", - "revolutions-per-minute-per-second":"Przyspieszenie kątowe", - "revolutions-per-minute-per-second-squared":"Przyspieszenie kątowe", - "deg-per-second":"Stopnie na sekundę", - "degrees-brix":"Stopnie Brix", - "katal":"Katal", - "katal-per-cubic-metre":"Katal na metr sześcienny" + "unit": { + "millimeter": "Millimeter", + "centimeter": "Centimeter", + "angstrom": "Angstrom", + "nanometer": "Nanometer", + "micrometer": "Micrometer", + "meter": "Meter", + "kilometer": "Kilometer", + "inch": "Inch", + "foot": "Foot", + "yard": "Yard", + "mile": "Mile", + "nautical-mile": "Nautical Mile", + "astronomical-unit": "Astronomical Unit", + "reciprocal-metre": "Reciprocal Metre", + "meter-per-meter": "Meter per meter", + "steradian": "Steradian", + "thou": "Thou", + "barleycorn": "Barleycorn", + "hand": "Hand", + "chain": "Chain", + "furlong": "Furlong", + "league": "League", + "fathom": "Fathom", + "cable": "Cable", + "link": "Link", + "rod": "Rod", + "nanogram": "Nanogram", + "microgram": "Microgram", + "milligram": "Milligram", + "gram": "Gram", + "kilogram": "Kilogram", + "tonne": "Tonne", + "ounce": "Ounce", + "pound": "Pound", + "stone": "Stone", + "hundredweight-count": "Hundredweight count", + "short-tons": "Short tons", + "dalton": "Dalton", + "grain": "Grain", + "drachm": "Drachm", + "quarter": "Quarter", + "slug": "Slug", + "carat": "Carat", + "cubic-millimeter": "Cubic Millimeter", + "cubic-centimeter": "Cubic Centimeter", + "cubic-meter": "Cubic Meter/s", + "cubic-kilometer": "Cubic Kilometers", + "microliter": "Microliter", + "milliliter": "Milliliter", + "liter": "Liter", + "hectoliter": "Hectolitre", + "cubic-inch": "Cubic Inch", + "cubic-foot": "Cubic Foot", + "cubic-yard": "Cubic Yards", + "fluid-ounce": "Fluid Ounce", + "pint": "Pint", + "quart": "Quart", + "gallon": "Gallon", + "oil-barrels": "Oil Barrels", + "cubic-meter-per-kilogram": "Cubic Meter per Kilogram", + "gill": "Gill", + "hogshead": "Hogshead", + "teaspoon": "Teaspoon", + "tablespoon": "Tablespoon", + "cup": "Cup", + "celsius": "Celsius", + "kelvin": "Kelvin", + "rankine": "Rankine", + "fahrenheit": "Fahrenheit", + "percent": "Percent", + "meter-per-second": "Meter per Second", + "kilometer-per-hour": "Kilometer per Hour", + "foot-per-second": "Foot per Second", + "mile-per-hour": "Mile per Hour", + "knot": "Knot", + "millimeters-per-minute": "Millimeters per minute", + "kilometer-per-hour-squared": "Kilometer per hour squared", + "foot-per-second-squared": "Foot per second squared", + "pascal": "Pascal", + "kilopascal": "Kilopascal", + "megapascal": "Megapascal", + "gigapascal": "Gigapascal", + "millibar": "Millibar", + "bar": "Bar", + "kilobar": "Kilobar", + "newton": "Newton", + "newton-meter": "Newton meter", + "foot-pounds": "Foot-pounds", + "inch-pounds": "Inch-pounds", + "newton-per-meter": "Newton per meter", + "atmospheres": "Atmospheres", + "pounds-per-square-inch": "Pounds per Square Inch", + "torr": "Torr", + "inches-of-mercury": "Inches of Mercury", + "pascal-per-square-meter": "Pascal per Square Meter", + "pound-per-square-inch": "Pound per Square Inch", + "newton-per-square-meter": "Newton per Square Meter", + "kilogram-force-per-square-meter": "Kilogram-force per Square Meter", + "pascal-per-square-centimeter": "Pascal per Square Centimeter", + "ton-force-per-square-inch": "Ton-force per Square Inch", + "kilonewton-per-square-meter": "Kilonewton per Square Meter", + "newton-per-square-millimeter": "Newton per Square Millimeter", + "microjoule": "Microjoule", + "millijoule": "Millijoule", + "joule": "Joule", + "kilojoule": "Kilojoule", + "megajoule": "Megajoule", + "gigajoule": "Gigajoule", + "watt-hour": "Watt-hour", + "kilowatt-hour": "Kilowatt-hour", + "electron-volts": "Electron volts", + "joules-per-coulomb": "Joules per Coulomb", + "british-thermal-unit": "British Thermal Units", + "foot-pound": "Foot-pound", + "calorie": "Calorie", + "small-calorie": "Small Calorie", + "kilocalorie": "Kilocalorie", + "joule-per-kelvin": "Joule per Kelvin", + "joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin", + "joule-per-kilogram": "Joule per Kilogram", + "watt-per-meter-kelvin": "Watt per Meter-Kelvin", + "joule-per-cubic-meter": "Joule per Cubic Meter", + "therm": "Therm", + "electric-dipole-moment": "Electric Dipole Moment", + "magnetic-dipole-moment": "Magnetic Dipole Moment", + "debye": "Debye", + "coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt", + "milliwatt": "Milliwatt", + "microwatt": "Microwatt", + "watt": "Watt", + "kilowatt": "Kilowatt", + "megawatt": "Megawatt", + "gigawatt": "Gigawatt", + "metric-horsepower": "Metric Horsepower", + "milliwatt-per-square-centimeter": "Milliwatts per square centimeter", + "watt-per-square-centimeter": "Watts per square centimeter", + "kilowatt-per-square-centimeter": "Kilowatts per square centimeter", + "milliwatt-per-square-meter": "Milliwatts per square meter", + "watt-per-square-meter": "Watts per square meter", + "kilowatt-per-square-meter": "Kilowatts per square meter", + "watt-per-square-inch": "Watts per square inch", + "kilowatt-per-square-inch": "Kilowatts per square inch", + "horsepower": "Horsepower", + "btu-per-hour": "British thermal units/hour", + "coulomb": "Coulomb", + "millicoulomb": "Millicoulombs", + "microcoulomb": "Microcoulomb", + "picocoulomb": "Picocoulomb", + "coulomb-per-meter": "Coulomb per meter", + "coulomb-per-cubic-meter": "Coulomb per Cubic Meter", + "coulomb-per-square-meter": "Coulomb per Square Meter", + "square-millimeter": "Square Millimeter", + "square-centimeter": "Square Centimeter", + "square-meter": "Square Meter", + "hectare": "Hectare", + "square-kilometer": "Square Kilometer", + "square-inch": "Square Inch", + "square-foot": "Square Foot", + "square-yard": "Square Yard", + "acre": "Acre", + "square-mile": "Square Mile", + "are": "Are", + "barn": "Barn", + "circular-inch": "Circular Inch", + "milliampere-hour": "Milliampere-hour", + "milliampere-hour-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, milliampere-hour, milliampere-hours, mAh", + "ampere-hours": "Ampere-hours", + "ampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, ampere, ampere-hours, Ah", + "kiloampere-hours": "Kiloampere-hours", + "kiloampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, kiloampere-hours, kiloampere-hour, kAh", + "nanoampere": "Nanoampere", + "nanoampere-tags": "current, amperes, nanoampere, nA", + "picoampere": "Picoampere", + "picoampere-tags": "current, amperes, picoampere, pA", + "microampere": "Microampere", + "microampere-tags": "electric current, microampere, microamperes, μA", + "milliampere": "Milliampere", + "milliampere-tags": "electric current, milliampere, milliamperes, mA", + "ampere": "Ampere", + "ampere-tags": "electric current, current flow, flow of electricity, electrical flow, ampere, amperes, amperage, A", + "kiloamperes": "Kiloamperes", + "kiloamperes-tags": "electric current, current flow, kiloamperes, kA", + "microampere-per-square-centimeter": "Microampere per square centimeter", + "microampere-per-square-centimeter-tags": "Current density, microampere per square centimeter, µA/cm²", + "ampere-per-square-meter": "Ampere per Square Meter", + "ampere-per-square-meter-tags": "current density, current per unit area, ampere per square meter, A/m²", + "ampere-per-meter": "Ampere per Meter", + "ampere-per-meter-tags": "magnetic field strength, magnetic field intensity, ampere per meter, A/m", + "oersted": "Oersted", + "oersted-tags": "magnetic field, oersted, Oe", + "bohr-magneton": "Bohr Magneton", + "bohr-magneton-tags": "atomic physics, magnetic moment, bohr magneton, μB", + "ampere-meter-squared": "Ampere-Meter Squared", + "ampere-meter-squared-tags": "magnetic moment, dipole moment, ampere-meter squared, A·m²", + "ampere-meter": "Ampere-Meter", + "ampere-meter-tags": "magnetic field, current loop, ampere-meter, A·m", + "nanovolt": "Nanovolt", + "picovolt": "Picovolt", + "millivolts": "Millivolts", + "microvolts": "Microvolts", + "volt": "Volt", + "kilovolts": "Kilovolts", + "dbmV": "dBmV", + "volt-meter": "Volt-Meter", + "kilovolt-meter": "Kilovolt-Meter", + "megavolt-meter": "Megavolt-Meter", + "microvolt-meter": "Microvolt-Meter", + "millivolt-meter": "Millivolt-Meter", + "nanovolt-meter": "Nanovolt-Meter", + "ohm": "Ohm", + "microohm": "Microohm", + "milliohm": "Milliohm", + "kilohm": "Kilohm", + "megohm": "Megohm", + "gigohm": "Gigohm", + "hertz": "Hertz", + "kilohertz": "Kilohertz", + "megahertz": "Megahertz", + "gigahertz": "Gigahertz", + "rpm": "Revolutions Per Minute", + "candela-per-square-meter": "Candela per square meter", + "candela": "Candela", + "lumen": "Lumen", + "lux": "Lux", + "foot-candle": "Foot-candle", + "lumen-per-square-meter": "Lumen per square meter", + "lux-second": "Lux second", + "lumen-second": "Lumen second", + "lumens-per-watt": "Lumens per watt", + "absorbance": "Absorbance", + "mole": "Mole", + "nanomole": "Nanomole", + "micromole": "MicroMole", + "millimole": "Millimole", + "kilomole": "Kilomole", + "mole-per-cubic-meter": "Mole per Cubic Meter", + "rssi": "RSSI", + "ppm": "Parts Per Million", + "ppb": "Parts Per Billion", + "micrograms-per-cubic-meter": "Micrograms per Cubic Meter", + "aqi": "AQI", + "gram-per-cubic-meter": "Gram per cubic meter", + "gram-per-kilogram": "Specific Humidity", + "millimeters-per-second": "Millimeters per second", + "neper": "Neper", + "bel": "Bel", + "decibel": "Decibel", + "meters-per-second-squared": "Meters per second squared", + "becquerel": "Becquerel", + "curie": "Curie", + "gray": "Gray", + "sievert": "Sievert", + "roentgen": "Roentgen", + "cps": "Counts per Second", + "rad": "Rad", + "rem": "Rem", + "dps": "Disintegrations per second", + "rutherford": "Rutherford", + "coulombs-per-kilogram": "Coulombs per kilogram", + "becquerels-per-cubic-meter": "Becquerels per cubic meter", + "curies-per-liter": "Curies per liter", + "becquerels-per-second": "Becquerels per second", + "curies-per-second": "Curies per second", + "gy-per-second": "Gray per Second", + "watt-per-steradian": "Watt per Steradian", + "watt-per-square-metre-steradian": "Watt per Square Metre-Steradian", + "ph-level": "pH Level", + "turbidity": "Turbidity", + "mg-per-liter": "Milligrams per liter", + "microsiemens-per-centimeter": "Microsiemens per centimeter", + "millisiemens-per-meter": "Millisiemens per meter", + "siemens-per-meter": "Siemens per meter", + "kilogram-per-cubic-meter": "Kilogram per cubic meter", + "gram-per-cubic-centimeter": "Gram per cubic centimeter", + "kilogram-per-square-meter": "Kilogram per square metre", + "milligram-per-milliliter": "Milligram per milliliter", + "pound-per-cubic-foot": "Pound per cubic foot", + "ounces-per-cubic-inch": "Ounces per cubic inch", + "tons-per-cubic-yard": "Tons per cubic yard", + "particle-density": "Particle density", + "kilometers-per-liter": "Kilometers per liter", + "miles-per-gallon": "Miles per gallon", + "liters-per-100-km": "Liters per 100 km", + "gallons-per-mile": "Gallons per mile", + "liters-per-hour": "Liters per hour", + "gallons-per-hour": "Gallons per hour", + "beats-per-minute": "Beats per minute", + "millimeters-of-mercury": "Millimeters of mercury", + "milligrams-per-deciliter": "Milligrams per deciliter", + "g-force": "G-force", + "kilonewton": "Kilonewton", + "kilogram-force": "Kilogram-Force", + "pound-force": "Pound-Force", + "kilopound-force": "Kilopound-Force", + "dyne": "Dyne", + "poundal": "Poundal", + "kip": "Kip", + "gal": "Gal", + "gravity": "Gravity", + "hectopascal": "Hectopascal", + "atmosphere": "Atmosphere", + "millibars": "Millibars", + "inch-of-mercury": "One inch of mercury", + "richter-scale": "Richter Scale", + "second": "Second", + "minute": "Minute", + "hour": "Hour", + "day": "Day", + "week": "Week", + "month": "Month", + "year": "Year", + "cubic-foot-per-minute": "Cubic Foot Per Minute", + "cubic-meters-per-hour": "Cubic Meters Per Hour", + "cubic-meters-per-second": "Cubic Meters Per Second", + "liter-per-second": "Liter Per Second", + "liter-per-minute": "Liter Per Minute", + "gallons-per-minute": "Gallons Per Minute", + "cubic-foot-per-second": "Cubic foot per second", + "milliliters-per-minute": "Milliliters per minute", + "bit": "Bit", + "byte": "Byte", + "kilobyte": "Kilobyte", + "megabyte": "Megabyte", + "gigabyte": "Gigabyte", + "terabyte": "Terabyte", + "petabyte": "Petabyte", + "exabyte": "Exabyte", + "zettabyte": "Zettabyte", + "yottabyte": "Yottabyte", + "bit-per-second": "Bit per second", + "kilobit-per-second": "Kilobit per second", + "megabit-per-second": "Megabit per second", + "gigabit-per-second": "Gigabit per second", + "terabit-per-second": "Terabit per second", + "byte-per-second": "Byte per second", + "kilobyte-per-second": "Kilobyte per second", + "megabyte-per-second": "Megabyte per second", + "gigabyte-per-second": "Gigabyte per second", + "degree": "Degree", + "radian": "Radian", + "gradian": "Gradian", + "mil": "Mil", + "revolution": "Revolution", + "siemens": "Siemens", + "millisiemens": "Millisiemens", + "microsiemens": "Microsiemens", + "kilosiemens": "Kilosiemens", + "megasiemens": "Megasiemens", + "gigasiemens": "Gigasiemens", + "farad": "Farad", + "millifarad": "Millifarad", + "microfarad": "Microfarad", + "nanofarad": "Nanofarad", + "picofarad": "Picofarad", + "kilofarad": "Kilofarad", + "megafarad": "Megafarad", + "gigafarad": "Gigafarad", + "terfarad": "Terfarad", + "farad-per-meter": "Farad per Meter", + "tesla": "Tesla", + "gauss": "Gauss", + "kilogauss": "Kilogauss", + "millitesla": "Millitesla", + "microtesla": "Microtesla", + "nanotesla": "Nanotesla", + "kilotesla": "Kilotesla", + "megatesla": "Megatesla", + "millitesla-square-meters": "millitesla square meters", + "gamma": "Gamma", + "lambda": "Lambda", + "square-meter-per-second": "Square meter per second", + "square-centimeter-per-second": "Square centimeter per second", + "stoke": "Stoke", + "centistokes": "Centistokes", + "square-foot-per-second": "Square foot per second", + "square-inch-per-second": "Square inch per second", + "pascal-second": "Pascal-second", + "centipoise": "Centipoise", + "poise": "Poise", + "reynolds": "Reynolds", + "pound-per-foot-hour": "Pound per foot-hour", + "newton-second-per-square-meter": "Newton second per square meter", + "dyne-second-per-square-centimeter": "Dyne second per square centimeter", + "kilogram-per-meter-second": "Kilogram per meter-second", + "tesla-square-meters": "Tesla square meters", + "maxwell": "Maxwell", + "tesla-per-meter": "Tesla per Meter", + "gauss-per-centimeter": "Gauss per Centimeter", + "weber": "Weber", + "microweber": "Microweber", + "milliweber": "Milliweber", + "gauss-square-centimeter": "Gauss-Square Centimeter", + "kilogauss-square-centimeter": "Kilogauss-Square Centimeter", + "henry": "Henry", + "millihenry": "Millihenry", + "microhenry": "Microhenry", + "nanohenry": "Nanohenry", + "henry-per-meter": "Henry per Meter", + "tesla-meter-per-ampere": "Tesla Meter per Ampere", + "gauss-per-oersted": "Gauss per Oersted", + "kilogram-per-mole": "Kilogram per mole", + "gram-per-mole": "Gram per mole", + "milligram-per-mole": "Milligram per mole", + "joule-per-mole": "Joule per Mole", + "joule-per-mole-kelvin": "Joule per Mole-Kelvin", + "millivolts-per-meter": "Millivolts per meter", + "volts-per-meter": "Volts per meter", + "kilovolts-per-meter": "Kilovolts per meter", + "radian-per-second": "Radian per second", + "radian-per-second-squared": "Radian per second squared", + "revolutions-per-minute-per-second": "Angular acceleration", + "revolutions-per-minute-per-second-squared": "Angular Acceleration", + "deg-per-second": "deg/s", + "degrees-brix": "Degrees Brix", + "katal": "Katal", + "katal-per-cubic-metre": "Katal per Cubic Metre" }, - "user":{ - "user":"Użytkownik", - "users":"Użytkownicy", - "customer-users":"Użytkownicy klienta", - "tenant-admins":"Administratorzy najemcy", - "sys-admin":"Administrator systemu", - "tenant-admin":"Administrator najemcy", - "customer":"Klient", - "anonymous":"Anonimowy", - "add":"Dodaj użytkownika", - "delete":"Usuń użytkownika", - "add-user-text":"Dodaj nowego użytkownika", - "no-users-text":"Nie znaleziono użytkowników", - "user-details":"Szczegóły użytkownika", - "delete-user-title":"Czy na pewno chcesz usunąć użytkownika '{{userEmail}}'?", - "delete-user-text":"Uważaj, po potwierdzeniu użytkownik i wszystkie powiązane dane staną się nieodwracalne.", - "delete-users-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 użytkownika} other {# użytkowników} }?", - "delete-users-action-title":"Usuń { count, plural, =1 {1 użytkownika} other {# użytkowników} }", - "delete-users-text":"Uważaj, po potwierdzeniu wszyscy wybrani użytkownicy zostaną usunięci, a wszystkie powiązane dane staną się nieodwracalne.", - "activation-email-sent-message":"E-mail aktywacyjny został wysłany pomyślnie!", - "resend-activation":"Wyślij ponownie aktywację", - "email":"Email", - "email-required":"Email jest wymagany.", - "invalid-email-format":"Nieprawidłowy format email.", - "first-name":"Imię", - "last-name":"Nazwisko", - "description":"Opis", - "default-dashboard":"Domyślny pulpit", - "always-fullscreen":"Zawsze na pełnym ekranie", - "select-user":"Wybierz użytkownika", - "no-users-matching":"Nie znaleziono użytkowników pasujących do '{{entity}}'.", - "user-required":"Użytkownik jest wymagany", - "activation-method":"Metoda aktywacji", - "display-activation-link":"Wyświetl link aktywacyjny", - "send-activation-mail":"Wyślij mail aktywacyjny", - "activation-link":"Link aktywacyjny użytkownika", - "activation-link-text":"Aby aktywować użytkownika, użyj następującego linku aktywacyjnego:", - "copy-activation-link":"Kopiuj link aktywacyjny", - "activation-link-copied-message":"Link aktywacyjny użytkownika został skopiowany do schowka", - "details":"Szczegóły", - "login-as-tenant-admin":"Zaloguj jako Administrator Najemcy", - "login-as-customer-user":"Zaloguj jako Użytkownik Klienta", - "search":"Szukaj użytkowników", - "selected-users":"{ count, plural, =1 {1 użytkownik} other {# użytkowników} } wybrano", - "disable-account":"Wyłącz konto użytkownika", - "enable-account":"Włącz konto użytkownika", - "enable-account-message":"Konto użytkownika zostało pomyślnie włączone!", - "disable-account-message":"Konto użytkownika zostało pomyślnie wyłączone!", - "copyId":"Kopiuj Id użytkownika", - "idCopiedMessage":"Id użytkownika zostało skopiowane do schowka", - "user-list":"Lista użytkowników", - "user-list-required":"Lista użytkowników jest wymagana" + "user": { + "all": "Wszystko", + "all-users": "Wszyscy użytkownicy", + "groups": "Grupy", + "user": "Użytkownik", + "users": "Użytkownicy", + "management": "Zarządzanie użytkownikami", + "customer-users": "Użytkownicy Klienta", + "tenant-admins": "Administratorzy Tenantów", + "sys-admin": "Administrator systemu", + "tenant-admin": "Administrator Tenantów", + "customer": "Klient", + "anonymous": "Anonimowy", + "add": "Dodaj użytkownika", + "delete": "Usuń użytkownika", + "add-user-text": "Dodaj nowego użytkownika", + "no-users-text": "Nie znaleziono użytkowników", + "user-details": "Dane użytkownika", + "delete-users": "Usuń użytkowników", + "delete-user-title": "Czy na pewno chcesz usunąć użytkownika '{{userEmail}}'?", + "delete-user-text": "Bądź ostrożny, po potwierdzeniu użytkownika i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-users-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 użytkownika} other {# użytkowników} }?", + "delete-users-action-title": "Usuń { count, plural, =1 {1 użytkownika} other {# użytkowników} }?", + "delete-users-text": "Bądź ostrożny, po potwierdzeniu wszyscy wybrani użytkownicy zostaną usunięci, a wszystkie powiązane dane staną się niemożliwe do odzyskania.", + "activation-email-sent-message": "E-mail aktywacyjny został pomyślnie wysłany!", + "resend-activation": "Wyślij ponownie aktywację", + "email": "E-mail", + "email-required": "Email jest wymagany.", + "invalid-email-format": "Niewłaściwy format wiadomości email.", + "first-name": "Imię", + "last-name": "Nazwisko", + "description": "Opis", + "default-dashboard": "Domyślny panel", + "always-fullscreen": "Zawsze na pełnym ekranie", + "select-user": "Wybierz użytkownika", + "no-users-matching": "Brak użytkowników pasujących do '{{entity}}' zostały znalezione.", + "user-required": "Użytkownik jest wymagany", + "activation-method": "Metoda aktywacji", + "display-activation-link": "Wyświetl link aktywacyjny", + "send-activation-mail": "Wyślij e-mail aktywacyjny", + "activation-link": "Link aktywacyjny użytkownika", + "activation-link-text": "Aby aktywować użytkownika, użyj poniższych opcji link aktywacyjny :", + "copy-activation-link": "Skopiuj link aktywacyjny", + "activation-link-copied-message": "Link aktywacyjny użytkownika został skopiowany do schowka", + "details": "Szczegóły", + "login-as-tenant-admin": "Zaloguj się jako administrator tenanta", + "login-as-customer-user": "Zaloguj się jako użytkownik-klient", + "select-group-to-add": "Wybierz grupę docelową, aby dodać wybranych użytkowników", + "select-group-to-move": "Wybierz grupę docelową, aby przenieść wybranych użytkowników", + "remove-users-from-group": "Czy na pewno chcesz usunąć { count, plural, =1 {1 użytkownika} other {# użytkowników} } z grupy {{entityGroup}}'?", + "group": "Grupa użytkowników", + "list-of-groups": "{ count, plural, =1 {Lista zawierająca jednego użytkownika} other {Lista # użytkowników} }", + "group-name-starts-with": "Grupy użytkowników, których nazwy zaczynają się od '{{prefix}}'", + "search": "Wyszukaj użytkowników", + "selected-users": "Wybrano { count, plural, =1 {1 użytkownika} other {# użytkowników} }", + "disable-account": "Wyłącz konto użytkownika", + "enable-account": "Włącz konto użytkownika", + "enable-account-message": "Konto użytkownika zostało pomyślnie włączone!", + "disable-account-message": "Konto użytkownika zostało pomyślnie wyłączone!", + "copyId": "Skopiuj identyfikator użytkownika", + "idCopiedMessage": "Identyfikator użytkownika został skopiowany do schowka", + "user-list": "Lista użytkowników", + "user-list-required": "Lista użytkowników jest wymagana" }, - "value":{ - "type":"Typ wartości", - "string":"Ciąg znaków", - "string-value":"Wartość ciągu znaków", - "string-value-required":"Wartość ciągu znaków jest wymagana", - "integer":"Liczba całkowita", - "integer-value":"Wartość liczby całkowitej", - "integer-value-required":"Wartość liczby całkowitej jest wymagana", - "invalid-integer-value":"Nieprawidłowa wartość liczby całkowitej", - "double":"Podwójna", - "double-value":"Wartość podwójna", - "double-value-required":"Wartość podwójna jest wymagana", - "boolean":"Boolean", - "boolean-value":"Wartość Boolean", - "false":"Fałsz", - "true":"Prawda", - "long":"Długa", - "json":"JSON", - "json-value":"Wartość JSON", - "json-value-invalid":"Wartość JSON ma nieprawidłowy format", - "json-value-required":"Wartość JSON jest wymagana." + "value": { + "type": "Typ wartości", + "string": "Łańcuch znaków", + "string-value": "Łańcuch znaków", + "string-value-required": "Łańcuch znaków jest wymagany", + "integer": "Liczba całkowita", + "integer-value": "Wartość całkowita", + "integer-value-required": "Wartość całkowita jest wymagana", + "invalid-integer-value": "Nieprawidłowa wartość całkowita", + "double": "Liczba zmiennoprzecinkowa podwójnej precyzji", + "double-value": "Wartość zmiennoprzecinkowa podwójnej precyzji", + "double-value-required": "Wartość zmiennoprzecinkowa podwójnej precyzji jest wymagana", + "boolean": "Wartość logiczna", + "boolean-value": "Wartość logiczna", + "false": "Fałsz", + "true": "Prawda", + "long": "Duża liczba", + "json": "JSON", + "json-value": "Wartość JSON", + "json-value-invalid": "Wartość JSON ma nieprawidłowy format", + "json-value-required": "Wartość JSON jest wymagana." }, - "version-control":{ - "version-control":"Kontrola wersji", - "management":"Zarządzanie kontrolą wersji", - "search":"Szukaj wersji", - "branch":"Gałąź", - "default":"Domyślna", - "select-branch":"Wybierz gałąź", - "branch-required":"Gałąź jest wymagana", - "create-entity-version":"Utwórz wersję encji", - "version-name":"Nazwa wersji", - "version-name-required":"Nazwa wersji jest wymagana", - "author":"Autor", - "export-relations":"Eksportuj relacje", - "export-attributes":"Eksportuj atrybuty", - "export-credentials":"Eksportuj poświadczenia", - "entity-versions":"Wersje encji", - "versions":"Wersje", - "created-time":"Czas utworzenia", - "version-id":"ID wersji", - "no-entity-versions-text":"Nie znaleziono wersji encji", - "no-versions-text":"Nie znaleziono wersji", - "copy-full-version-id":"Kopiuj pełne ID wersji", - "create-version":"Utwórz wersję", - "creating-version":"Tworzenie wersji... Proszę czekać", - "nothing-to-commit":"Brak zmian do zatwierdzenia", - "restore-version":"Przywróć wersję", - "restore-entity-from-version":"Przywróć encję z wersji '{{versionName}}'", - "restoring-entity-version":"Przywracanie wersji encji... Proszę czekać", - "load-relations":"Załaduj relacje", - "load-attributes":"Załaduj atrybuty", - "load-credentials":"Załaduj poświadczenia", - "compare-with-current":"Porównaj z obecną", - "diff-entity-with-version":"Różnice z wersją encji '{{versionName}}'", - "previous-difference":"Poprzednia różnica", - "next-difference":"Następna różnica", - "current":"Obecna", - "differences":"{ count, plural, =1 {1 różnica} other {# różnice} }", - "create-entities-version":"Utwórz wersję encji", - "default-sync-strategy":"Domyślna strategia synchronizacji", - "sync-strategy-merge":"Scal", - "sync-strategy-overwrite":"Nadpisz", - "entities-to-export":"Encje do eksportu", - "entities-to-restore":"Encje do przywrócenia", - "sync-strategy":"Strategia synchronizacji", - "all-entities":"Wszystkie encje", - "no-entities-to-export-prompt":"Proszę określić encje do eksportu", - "no-entities-to-restore-prompt":"Proszę określić encje do przywrócenia", - "add-entity-type":"Dodaj typ encji", - "remove-all":"Usuń wszystko", - "version-create-result":"{ added, plural, =0 {Nie dodano encji} =1 {Dodano 1 encję} other {Dodano # encji} }.
{ modified, plural, =0 {Nie zmodyfikowano encji} =1 {Zmodyfikowano 1 encję} other {Zmodyfikowano # encji} }.
{ removed, plural, =0 {Nie usunięto encji} =1 {Usunięto 1 encję} other {Usunięto # encji} }.", - "remove-other-entities":"Usuń inne encje", - "find-existing-entity-by-name":"Znajdź istniejącą encję po nazwie", - "restore-entities-from-version":"Przywróć encje z wersji '{{versionName}}'", - "restoring-entities-from-version":"Przywracanie encji... Proszę czekać", - "no-entities-restored":"Nie przywrócono żadnych encji", - "created":"{{created}} utworzonych", - "updated":"{{updated}} zaktualizowanych", - "deleted":"{{deleted}} usuniętych", - "remove-other-entities-confirm-text":"Uważaj! To trwale usunie wszystkie bieżące encje
nieobecne w wersji, którą chcesz przywrócić.

Proszę wpisać \"usuń inne encje\", aby potwierdzić.", - "auto-commit-to-branch":"automatyczne zatwierdzenie do gałęzi {{ branch }}", - "default-create-entity-version-name":"Aktualizacja {{entityName}}", - "sync-strategy-merge-hint":"Tworzy lub aktualizuje wybrane encje w repozytorium. Wszystkie inne encje w repozytorium są niezmodyfikowane.", - "sync-strategy-overwrite-hint":"Tworzy lub aktualizuje wybrane encje w repozytorium. Wszystkie inne encje w repozytorium są usunięte.", - "device-credentials-conflict":"Nie udało się załadować urządzenia z zewnętrznym id {{entityId}}
ponieważ te same poświadczenia są już obecne w bazie danych dla innego urządzenia.
Rozważ wyłączenie opcji załaduj poświadczenia w formularzu przywracania.", - "missing-referenced-entity":"Nie udało się załadować {{sourceEntityTypeName}} z zewnętrznym id {{sourceEntityId}}
ponieważ odnosi się do brakującej {{targetEntityTypeName}} z id {{targetEntityId}}.", - "runtime-failed":"Failed: {{message}}", - "auto-commit-settings-read-only-hint":"Funkcja automatycznego zatwierdzania nie działa z włączoną opcją tylko do odczytu w ustawieniach repozytorium." + "version-control": { + "version-control": "Kontrola wersji", + "management": "Zarządzanie kontrolą wersji", + "search": "Wyszukaj wersje", + "branch": "Gałąź", + "default": "Domyślny", + "select-branch": "Wybierz Gałąź", + "branch-required": "Gałąź jest wymagana", + "create-entity-version": "Utwórz wersję obiektu", + "version-name": "Nazwa wersji", + "version-name-required": "Nazwa wersji jest wymagana", + "author": "Autor", + "export-relations": "Relacje eksportowe", + "export-attributes": "Eksportuj atrybuty", + "export-credentials": "Eksportuj dane uwierzytelniające", + "export-group-entities": "Eksportuj obiekty grupy", + "export-roles": "Eksportuj role", + "entity-versions": "Wersje obiektu", + "versions": "Wersje", + "created-time": "Stworzony czas", + "version-id": "Identyfikator wersji", + "no-entity-versions-text": "Nie znaleziono wersji obiektu", + "no-versions-text": "Nie znaleziono wersji", + "copy-full-version-id": "Skopiuj identyfikator pełnej wersji", + "create-version": "Utwórz wersję", + "creating-version": "Tworzenie wersji... Proszę czekać", + "nothing-to-commit": "Brak zmian do zatwierdzenia", + "restore-version": "Przywróć wersję", + "restore-entity-from-version": "Przywróć obiekt z wersji '{{versionName}}'", + "restoring-entity-version": "Przywracanie wersji obiektu... Proszę czekać", + "load-relations": "Załaduj relacje", + "load-attributes": "Załaduj atrybuty", + "load-credentials": "Załaduj dane uwierzytelniające", + "load-group-entities": "Załaduj obiekty grupy", + "load-roles": "Załaduj role", + "compare-with-current": "Porównaj z aktualnym", + "diff-entity-with-version": "Porównaj z wersją obiektu '{{versionName}}'", + "previous-difference": "Poprzednia różnica", + "next-difference": "Następna różnica", + "current": "Aktualny", + "differences": "{ count, plural, =1 {1 różnica} other {# różnic} }", + "create-entities-version": "Utwórz wersję obiektu", + "default-sync-strategy": "Domyślna strategia synchronizacji", + "sync-strategy-merge": "Połącz", + "sync-strategy-overwrite": "Nadpisz", + "entities-to-export": "Obiekty do eksportu", + "entities-to-restore": "Obiekty do przywrócenia", + "sync-strategy": "Strategia synchronizacji", + "all-entities": "Wszystkie obiekty", + "no-entities-to-export-prompt": "Proszę określić obiekty do wyeksportowania", + "no-entities-to-restore-prompt": "Proszę określić obiekty do przywrócenia", + "add-entity-type": "Dodaj typ obiektu", + "remove-all": "Usuń wszystko", + "version-create-result": "Dodano { added, plural, =0 {0 obiektów} =1 {1 obiekt} other {# obiektów} }.
Zmodyfikowano { modified, plural, =0 {0 obiektów} =1 {1 obiekt} other {# obiektów} }.
Usunięto{ removed, plural, =0 {0 obiektów} =1 {1 obiekt} other {# obiektów} }.", + "remove-other-entities": "Usuń inne obiekty", + "find-existing-entity-by-name": "Znajdź istniejący obiekt według nazwy", + "restore-entities-from-version": "Przywróć obiekty z wersji '{{versionName}}'", + "restoring-entities-from-version": "Przywracanie obiektów... Proszę czekać", + "no-entities-restored": "Nie przywrócono żadnych obiektów", + "created": "{{created}} utworzone", + "updated": "{{updated}} zaktualizowane", + "deleted": "{{deleted}} usunięte", + "groups-created": "Stworzono { created, plural, =1 {1 grupę} other {# grup(y)} }", + "groups-updated": "Zaktualizowano { updated, plural, =1 {1 grupę} other {# grup(y)} }", + "groups-deleted": "Usunięto { deleted, plural, =1 {1 grupę} other {# grup(y)} }", + "remove-other-entities-confirm-text": "Bądź ostrożny! To permanentnie usunie wszystkie obecne obiekty
nie występujące w wersji, którą chcesz przywrócić.

Proszę wpisać remove other entities aby potwierdzić.", + "auto-commit-to-branch": "automatyczne zatwierdzenie do gałęzi {{ branch }}", + "default-create-entity-version-name": "{{entityName}} aktualizacja", + "sync-strategy-merge-hint": "Tworzy lub aktualizuje wybrane obiekty w repozytorium. niezmodyfikowany.", + "sync-strategy-overwrite-hint": "Tworzy lub aktualizuje wybrane obiekty w repozytorium. usunięte.", + "device-credentials-conflict": "Nie udało się załadować urządzenia z zewnętrznym identyfikatorem {{entityId}}
ze względu na te same dane uwierzytelniające, które znajdują się już w bazie danych dla innego urządzenia.
Proszę rozważyć wyłączenie załaduj dane uwierzytelniające ustawienie w formularzu przywracania.", + "missing-referenced-entity": "Nie udało się załadować {{sourceEntityTypeName}} z identyfikatorem zewnętrznym {{sourceEntityId}}
ponieważ brakuje odniesień {{targetEntityTypeName}} z identyfikatorem {{targetEntityId}}.", + "add-entity-groups": "Dodaj grupy obiektów", + "entity-groups": "Grupy obiektów", + "integration-routing-key-conflict": "Nie udało się załadować integracji z zewnętrznym identyfikatorem {{entityId}}
ponieważ ten sam klucz integracji jest już obecny w bazie danych dla innej integracji.
Proszę rozważyć włączenie opcji automatycznie wygeneruj klucz integracyjny w formularzu przywracania.", + "auto-generate-integration-key": "Automatycznie generuj klucz integracyjny", + "runtime-failed": "Nieudana: {{message}}", + "auto-commit-settings-read-only-hint": "Funkcja automatycznego zatwierdzania nie działa z włączoną opcją tylko do odczytu w ustawieniach repozytorium." }, - "widget":{ - "widget-library":"Biblioteka widgetów", - "widget-bundle":"Pakiet widgetów", - "all-bundles":"Wszystkie pakiety", - "select-widgets-bundle":"Wybierz pakiet widgetów", - "widgets":"Widgety", - "all-widgets":"Wszystkie widgety", - "widget":"Widget", - "select-widget":"Wybierz widget", - "no-widgets-matching":"Nie znaleziono widgetów pasujących do '{{entity}}'.", - "no-widgets":"Brak widgetów", - "no-widgets-text":"Nie znaleziono widgetów", - "management":"Zarządzanie widgetami", - "editor":"Edytor widgetów", - "confirm-to-exit-editor-html":"Masz niezapisane ustawienia widgetu.
Czy na pewno chcesz opuścić tę stronę?", - "widget-type-not-found":"Problem z ładowaniem konfiguracji widgetu.
Prawdopodobnie powiązany typ widgetu został usunięty.", - "widget-type-load-error":"Widget nie został załadowany z powodu następujących błędów:", - "remove":"Usuń widget", - "delete":"Usuń widget", - "edit":"Edytuj widget", - "remove-widget-title":"Czy na pewno chcesz usunąć widget '{{widgetTitle}}'?", - "remove-widget-text":"Po potwierdzeniu widget i wszystkie powiązane dane staną się nieodwracalne.", - "timeseries":"Serie czasowe", - "search-data":"Szukaj danych", - "no-data-found":"Nie znaleziono danych", - "latest":"Najnowsze wartości", - "rpc":"Widget sterowania", - "alarm":"Widget alarmów", - "static":"Widget statyczny", - "timeseries-short":"serie", - "latest-short":"najnowsze", - "rpc-short":"sterowanie", - "alarm-short":"alarm", - "static-short":"statyczny", - "select-widget-type":"Wybierz typ widgetu", - "missing-widget-title-error":"Tytuł widgetu musi być określony!", - "widget-saved":"Widget zapisany", - "unable-to-save-widget-error":"Nie można zapisać widgetu! Widget zawiera błędy!", - "save":"Zapisz widget", - "saveAs":"Zapisz widget jako", - "move":"Przenieś widget", - "save-widget-as":"Zapisz widget jako", - "save-widget-as-text":"Proszę wprowadzić nowy tytuł widgetu", - "toggle-fullscreen":"Przełącz pełny ekran", - "run":"Uruchom widget", - "widget-title":"Tytuł widgetu", - "title":"Tytuł", - "title-required":"Tytuł widgetu jest wymagany.", - "title-max-length":"Tytuł powinien mieć mniej niż 256 znaków", - "system":"System", - "type":"Typ widgetu", - "resources":"Zasoby", - "resource-url":"URL JavaScript/CSS", - "resource-is-module":"Jest modułem", - "remove-resource":"Usuń zasób", - "add-resource":"Dodaj zasób", - "html":"HTML", - "tidy":"Uporządkuj", - "css":"CSS", - "settings-schema":"Schemat ustawień", - "datakey-settings-schema":"Schemat ustawień klucza danych", - "latest-datakey-settings-schema":"Schemat ustawień najnowszego klucza danych", - "widget-settings":"Ustawienia widgetu", - "description":"Opis", - "tags":"Tagi", - "image-preview":"Podgląd obrazu", - "settings-form-selector":"Selektor formularza ustawień", - "data-key-settings-form-selector":"Selektor formularza ustawień klucza danych", - "latest-data-key-settings-form-selector":"Selektor formularza ustawień najnowszego klucza danych", - "all":"Wszystkie", - "actual":"Aktualne", - "deprecated":"Przestarzałe", - "has-basic-mode":"Ma tryb podstawowy", - "basic-mode-form-selector":"Selektor formularza trybu podstawowego", - "basic-mode":"Podstawowy", - "advanced-mode":"Zaawansowany", - "javascript":"Javascript", - "js":"JS", - "delete-widget-title":"Czy na pewno chcesz usunąć widget '{{widgetName}}'?", - "delete-widget-text":"Po potwierdzeniu widget i wszystkie powiązane dane staną się nieodwracalne.", - "delete-widgets-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 widget} other {# widgetów} }?", - "delete-widgets-text":"Uważaj, po potwierdzeniu wszystkie wybrane widgety zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", - "delete-widget":"Usuń widget", - "widget-template-load-failed-error":"Nie udało się załadować szablonu widgetu!", - "details":"Szczegóły", - "widget-details":"Szczegóły widgetu", - "add":"Dodaj widget", - "add-existing-widget":"Dodaj istniejący widget", - "add-new-widget":"Dodaj nowy widget", - "search-widgets":"Szukaj widgetów", - "selected-widgets":"{ count, plural, =1 {1 widget} other {# widgetów} } wybrano", - "undo":"Cofnij zmiany widgetu", - "export":"Eksportuj widget", - "export-widgets":"Eksportuj widgety", - "import":"Importuj widget", - "no-data":"Brak danych do wyświetlenia na widżecie", - "data-overflow":"Widget wyświetla {{count}} z {{total}} encji", - "alarm-data-overflow":"Widget wyświetla alarmy dla {{allowedEntities}} (maksymalna dozwolona) encji z {{totalEntities}} encji", - "search":"Szukaj widgetu", - "filter":"Typ filtru widgetu", - "loading-widgets":"Ładowanie widgetów...", - "widget-template-error":"Nieprawidłowy szablon HTML widgetu." + "widget": { + "widget-library": "Biblioteka widżetów", + "widget-bundle": "Pakiet widżetów", + "all-bundles": "Wszystkie pakiety", + "select-widgets-bundle": "Wybierz pakiet widżetów", + "widgets": "Widgets", + "all-widgets": "All widgets", + "widget": "Widget", + "select-widget": "Select widget", + "no-widgets-matching": "No widgets matching '{{entity}}' were found.", + "no-widgets": "No widgets yet", + "no-widgets-text": "No widgets found", + "management": "Zarządzanie widgetami", + "editor": "Edytor widżetów", + "widget-type-not-found": "Problem z ładowaniem konfiguracji widgetu.
Prawdopodobnie powiązany\n typ widżetu został usunięty.", + "widget-type-load-error": "Widżet nie został załadowany z powodu następujących błędów:", + "remove": "Usuń widżet", + "delete": "Delete widget", + "edit": "Edytuj widżet", + "edit-widget-type": "Edit widget type", + "remove-widget-title": "Czy na pewno chcesz usunąć widżet '{{widgetTitle}}'?", + "remove-widget-text": "Po potwierdzeniu widżetu i wszystkich powiązanych danych nie będzie można odzyskać.", + "timeseries": "Szereg czasowy", + "search-data": "Wyszukaj dane", + "no-data-found": "Nie znaleziono danych", + "latest": "Najnowsze wartości", + "rpc": "Widżet sterujący", + "alarm": "Widżet alarmowy", + "static": "Widget statyczny", + "select-widget-type": "Wybierz typ widżetu", + "missing-widget-title-error": "Tytuł widgetu musi zostać określony!", + "widget-saved": "Widżet został zapisany", + "unable-to-save-widget-error": "Nie można zapisać widżetu! ", + "save": "Zapisz widżet", + "saveAs": "Zapisz widżet jako", + "move": "Move widget", + "save-widget-type-as": "Zapisz typ widżetu jako", + "save-widget-type-as-text": "Proszę podać nowy tytuł widżetu i/lub wybrać docelowy pakiet widżetów", + "toggle-fullscreen": "Przełącz tryb pełnoekranowy", + "run": "Uruchom widget", + "widget-title": "Widget title", + "title": "Tytul widzeta", + "title-required": "Tytuł widgetu jest wymagany.", + "title-max-length": "Title should be less than 256", + "system": "System", + "type": "Typ widgetu", + "resources": "Biblioteki", + "resource-url": "Adres URL JavaScript/CSS", + "resource-is-module": "Jest modułem", + "remove-resource": "Usuń zasób", + "add-resource": "Dodaj zasób", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Schemat ustawień", + "datakey-settings-schema": "Schemat ustawień klucza danych", + "latest-datakey-settings-schema": "Najnowszy schemat ustawień klucza danych", + "widget-settings": "Ustawienia widgetu", + "description": "Opis", + "image-preview": "Podgląd obrazu", + "settings-form-selector": "Ustawienia selektora formularza", + "data-key-settings-form-selector": "Selektor formularza ustawień klucza danych", + "latest-data-key-settings-form-selector": "Selektor formularza najnowszych ustawień klucza danych", + "all": "All", + "actual": "Actual", + "deprecated": "Deprecated", + "has-basic-mode": "Has basic mode", + "basic-mode-form-selector": "Basic mode form selector", + "basic-mode": "Basic", + "advanced-mode": "Advanced", + "javascript": "JavaScript", + "js": "JS", + "remove-widget-type-title": "Czy na pewno chcesz usunąć typ widżetu '{{widgetName}}'?", + "remove-widget-type-text": "Po potwierdzeniu typu widżetu i wszystkich powiązanych danych nie będzie można odzyskać.", + "remove-widget-type": "Usuń typ widżetu", + "widget-types": "Widget Types", + "delete-widget-type-title": "Are you sure you want to delete the widget type '{{widgetTypeName}}'?", + "delete-widget-type-text": "After the confirmation the widget and all related data will become unrecoverable.", + "delete-widget-types-title": "Are you sure you want to delete { count, plural, =1 {1 widget type} other {# widget types} }?", + "delete-widget-types-text": "Be careful, after the confirmation all selected widget types will be removed and all related data will become unrecoverable.", + "delete-widget-type": "Delete widget type", + "add-widget-type": "Dodaj nowy typ widżetu", + "widget-type-load-failed-error": "Nie udało się załadować typu widżetu!", + "widget-template-load-failed-error": "Nie udało się załadować szablonu widżetu!", + "details": "Details", + "widget-type-details": "Widget type details", + "add": "Dodaj widżet", + "no-widget-types-text": "No widget types found", + "search-widget-types": "Search widget types", + "selected-widget-types": "{ count, plural, =1 {1 widget type} other {# widget types} } selected", + "undo": "Cofnij zmiany w widżecie", + "export": "Eksportuj widżet", + "export-data": "Eksportuj dane widżetu", + "export-to-csv": "Eksportuj dane do pliku CSV...", + "export-to-excel": "Eksportuj dane do formatu XLS...", + "export-to-excel-xlsx": "Eksportuj dane do XLSX...", + "no-data": "Brak danych do wyświetlenia w widżecie", + "data-overflow": "Widżet wyświetla {{count}} z {{total}} obiektów", + "alarm-data-overflow": "Widżet wyświetla alarmy dla {{allowedEntities}} (maksymalnie dozwolonych) obiektów z {{totalEntities}} obiektów", + "search": "Wyszukaj widżet", + "filter": "Typ filtra widgetu", + "loading-widgets": "Ładowanie widżetów...", + "widget-template-error": "Nieprawidłowy szablon HTML widgetu." }, - "widget-action":{ - "header-button":"Przycisk nagłówka widgetu", - "open-dashboard-state":"Przejdź do nowego stanu pulpitów", - "update-dashboard-state":"Zaktualizuj bieżący stan pulpitów", - "open-dashboard":"Przejdź do innego pulpitu", - "custom":"Akcja niestandardowa", - "custom-pretty":"Akcja niestandardowa (z szablonem HTML)", - "custom-pretty-error-title":"Błąd niestandardowego okna dialogowego", - "custom-pretty-template-error":"Nieprawidłowy szablon niestandardowego okna dialogowego.", - "custom-pretty-controller-error":"Wystąpił błąd podczas oceny funkcji niestandardowego okna dialogowego.", - "mobile-action":"Akcja mobilna", - "target-dashboard-state":"Docelowy stan pulpitu", - "target-dashboard-state-required":"Docelowy stan pulpitu jest wymagany", - "set-entity-from-widget":"Ustaw encję z widgetu", - "target-dashboard":"Docelowy pulpit", - "open-right-layout":"Otwórz prawy układ pulpitu (widok mobilny)", - "state-display-type":"Opcja wyświetlania stanu pulpitu", - "open-normal":"Normalnie", - "open-in-separate-dialog":"Otwórz w osobnym oknie dialogowym", - "open-in-popover":"Otwórz w dymku", - "dialog-title":"Tytuł okna dialogowego", - "dialog-hide-dashboard-toolbar":"Ukryj pasek narzędzi pulpitu w oknie dialogowym", - "dialog-width":"Szerokość okna dialogowego w procentach względem szerokości viewportu", - "dialog-height":"Wysokość okna dialogowego w procentach względem wysokości viewportu", - "dialog-size-range-error":"Wartość procentowa rozmiaru okna dialogowego powinna być w zakresie od 1 do 100.", - "popover-preferred-placement":"Preferowane umiejscowienie dymka", - "popover-placement-top":"Góra", - "popover-placement-topLeft":"Góra lewo", - "popover-placement-topRight":"Góra prawo", - "popover-placement-right":"Prawo", - "popover-placement-rightTop":"Prawo góra", - "popover-placement-rightBottom":"Prawo dół", - "popover-placement-bottom":"Dół", - "popover-placement-bottomLeft":"Dół lewo", - "popover-placement-bottomRight":"Dół prawo", - "popover-placement-left":"Lewo", - "popover-placement-leftTop":"Lewo góra", - "popover-placement-leftBottom":"Lewo dół", - "popover-hide-on-click-outside":"Ukryj dymek po kliknięciu na zewnątrz", - "popover-hide-dashboard-toolbar":"Ukryj pasek narzędzi pulpitu w dymku", - "popover-width":"Szerokość dymka w jednostkach przeglądarki (np. 100px, 25vw)", - "popover-height":"Wysokość dymka w jednostkach przeglądarki (np. 100px, 25vh)", - "popover-style":"Styl dymka", - "open-new-browser-tab":"Otwórz w nowej karcie przeglądarki", - "mobile":{ - "action-type":"Typ akcji mobilnej", - "action-type-required":"Typ akcji mobilnej jest wymagany", - "take-picture-from-gallery":"Wybierz zdjęcie z galerii", - "take-photo":"Zrób zdjęcie", - "map-direction":"Otwórz wskazówki dojazdu na mapie", - "map-location":"Otwórz lokalizację na mapie", - "scan-qr-code":"Skanuj kod QR", - "make-phone-call":"Wykonaj połączenie telefoniczne", - "get-location":"Pobierz lokalizację telefonu", - "take-screenshot":"Zrób zrzut ekranu" + "widget-action": { + "header-button": "Przycisk nagłówka widgetu", + "open-dashboard-state": "Przejdź do nowego stanu panelu", + "update-dashboard-state": "Zaktualizuj bieżący stan panelu", + "open-dashboard": "Przejdź do innego panelu", + "custom": "Akcja niestandardowa", + "custom-pretty": "Akcja niestandardowa (z szablonem HTML)", + "custom-pretty-error-title": "Niestandardowy błąd okna dialogowego", + "custom-pretty-template-error": "Nieprawidłowy niestandardowy szablon okna dialogowego.", + "custom-pretty-controller-error": "Wystąpił błąd podczas oceniania niestandardowej funkcji okna dialogowego.", + "mobile-action": "Akcja mobilna", + "target-dashboard-state": "Docelowy stan panelu", + "target-dashboard-state-required": "Docelowy stan panelu jest wymagany", + "set-entity-from-widget": "Ustaw obiekt z widżetu", + "target-dashboard": "Docelowy panel", + "open-right-layout": "Otwórz prawy układ panelu (widok mobilny)", + "state-display-type": "Opcja wyświetlania stanu panelu", + "open-normal": "Normalna", + "open-in-separate-dialog": "Otwórz w osobnym oknie dialogowym", + "open-in-popover": "Otwórz w wyskakującym okienku", + "dialog-title": "Tytuł okna dialogowego", + "dialog-hide-dashboard-toolbar": "Ukryj pasek narzędzi panelu w oknie dialogowym", + "dialog-width": "Szerokość okna dialogowego w procentach w stosunku do szerokości rzutni", + "dialog-height": "Wysokość okna dialogowego w procentach w stosunku do wysokości rzutni", + "dialog-size-range-error": "Wartość procentowa rozmiaru okna dialogowego powinna mieścić się w przedziale od 1 do 100.", + "popover-preferred-placement": "Preferowane miejsce wyskakujące", + "popover-placement-top": "Na górze", + "popover-placement-topLeft": "Na górze po lewej", + "popover-placement-topRight": "Na górze po lewej", + "popover-placement-right": "Po prawej", + "popover-placement-rightTop": "Po prawej na górze", + "popover-placement-rightBottom": "Po prawej na dole", + "popover-placement-bottom": "Na dole", + "popover-placement-bottomLeft": "Na dole po lewej", + "popover-placement-bottomRight": "Na dole po prawej", + "popover-placement-left": "Po lewej", + "popover-placement-leftTop": "Po lewej na górze", + "popover-placement-leftBottom": "Po lewej na dole", + "popover-hide-on-click-outside": "Ukryj wyskakujące okienko po kliknięciu zewnętrznym", + "popover-hide-dashboard-toolbar": "Ukryj pasek narzędzi panelu w wyskakującym okienku", + "popover-width": "Szerokość wyskakującego okienka w jednostkach przeglądarki (np. 100px, 25vw)", + "popover-height": "Wysokość wyskakującego okienka w jednostkach przeglądarki (np. 100px, 25vh)", + "popover-style": "Styl wyskakującego okienka", + "open-new-browser-tab": "Otwórz w nowej karcie przeglądarki", + "mobile": { + "action-type": "Typ akcji mobilnej", + "action-type-required": "Typ akcji mobilnej jest wymagany", + "take-picture-from-gallery": "Zrób zdjęcie z galerii", + "take-photo": "Zrób zdjęcie", + "map-direction": "Otwórz wskazówki na mapie", + "map-location": "Otwórz lokalizację na mapie", + "scan-qr-code": "Skanuj kod QR", + "make-phone-call": "Wykonaj telefon", + "get-location": "Uzyskaj lokalizację telefonu", + "take-screenshot": "Zrób zrzut ekranu" } }, - "widgets-bundle":{ - "current":"Aktualny zestaw", - "widgets-bundles":"Zestawy widżetów", - "widgets-bundle-widgets":"Widżety zestawu widżetów", - "add":"Dodaj zestaw widżetów", - "delete":"Usuń zestaw widżetów", - "title":"Tytuł", - "title-required":"Tytuł jest wymagany.", - "title-max-length":"Tytuł powinien być krótszy niż 256", - "description":"Opis", - "image-preview":"Podgląd obrazu", - "order":"Kolejność", - "add-widgets-bundle-text":"Dodaj nowy zestaw widżetów", - "no-widgets-bundles-text":"Nie znaleziono zestawów widżetów", - "empty":"Zestaw widżetów jest pusty", - "details":"Szczegóły", - "widgets-bundle-details":"Szczegóły zestawu widżetów", - "delete-widgets-bundle-title":"Czy na pewno chcesz usunąć zestaw widżetów '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text":"Uważaj, po potwierdzeniu zestaw widżetów oraz wszystkie powiązane dane staną się nieodwracalne.", - "delete-widgets-bundles-title":"Czy na pewno chcesz usunąć { count, plural, =1 {1 zestaw widżetów} other {# zestawy widżetów} }?", - "delete-widgets-bundles-action-title":"Usuń { count, plural, =1 {1 zestaw widżetów} other {# zestawy widżetów} }", - "delete-widgets-bundles-text":"Uważaj, po potwierdzeniu wszystkie wybrane zestawy widżetów zostaną usunięte, a wszystkie powiązane dane staną się nieodwracalne.", - "no-widgets-bundles-matching":"Nie znaleziono zestawów widżetów pasujących do '{{widgetsBundle}}'.", - "widgets-bundle-required":"Zestaw widżetów jest wymagany.", - "system":"System", - "import":"Importuj zestaw widżetów", - "export":"Eksportuj zestaw widżetów", - "export-widgets-bundle-widgets-prompt":"Dołącz widżety zestawu w eksportowanych danych (w przeciwnym razie zostaną wyeksportowane tylko odniesienia do pełnych kwalifikacji nazw widżetów)", - "export-failed-error":"Nie można wyeksportować zestawu widżetów: {{error}}", - "create-new-widgets-bundle":"Utwórz nowy zestaw widżetów", - "widgets-bundle-file":"Plik zestawu widżetów", - "invalid-widgets-bundle-file-error":"Nie można zaimportować zestawu widżetów: Nieprawidłowa struktura danych zestawu widżetów.", - "search":"Szukaj zestawów widżetów", - "selected-widgets-bundles":"{ count, plural, =1 {1 wybrany zestaw widżetów} other {# wybrane zestawy widżetów} }", - "open-widgets-bundle":"Otwórz zestaw widżetów", - "loading-widgets-bundles":"Ładowanie zestawów widżetów..." + "widgets-bundle": { + "current": "Aktualny pakiet", + "widgets-bundles": "Pakiety widżetów", + "widgets-bundle-widgets": "Widgets Bundle Widgets", + "add": "Dodaj pakiet widżetów", + "delete": "Usuń pakiet widżetów", + "title": "Tytuł", + "title-required": "Tytuł jest wymagany.", + "title-max-length": "Tytuł powinien być mniejszy niż 256", + "description": "Opis", + "image-preview": "Podgląd obrazu", + "add-widgets-bundle-text": "Dodaj nowy pakiet widżetów", + "no-widgets-bundles-text": "Nie znaleziono pakietów widżetów", + "empty": "Pakiet widżetów jest pusty", + "details": "Szczegóły", + "widgets-bundle-details": "Szczegóły pakietu widżetów", + "delete-widgets-bundle-title": "Czy na pewno chcesz usunąć pakiet widżetów '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Bądź ostrożny, po potwierdzeniu pakietu widżetów i wszystkich powiązanych danych nie będzie można odzyskać.", + "delete-widgets-bundles-title": "Czy na pewno chcesz usunąć { count, plural, =1 {1 pakiet} other {# pakietów} } widżetów?", + "delete-widgets-bundles-action-title": "Usuń { count, plural, =1 {1 pakiet} other {# pakietów} } widżetów", + "delete-widgets-bundles-text": "Bądź ostrożny, po potwierdzeniu wszystkie wybrane pakiety widżetów zostaną usunięte, a wszystkich powiązanych danych nie będzie można odzyskać.", + "no-widgets-bundles-matching": "Nie znaleziono pakietów widżetów pasujących do '{{widgetsBundle}}'.", + "widgets-bundle-required": "Pakiet widżetów jest wymagany.", + "system": "System", + "import": "Importuj pakiet widżetów", + "export": "Eksportuj pakiet widżetów", + "export-widgets-bundle-widgets-prompt": "Include bundle widget types in exported data (otherwise only referenced widget type FQNs will be exported)", + "export-failed-error": "Nie można wyeksportować pakietu widżetów: {{error}}", + "create-new-widgets-bundle": "Utwórz nowy pakiet widżetów", + "widgets-bundle-file": "Plik pakietu widżetów", + "invalid-widgets-bundle-file-error": "Nie można zaimportować pakietu widżetów: Nieprawidłowa struktura danych pakietu widżetów.", + "search": "Wyszukaj pakiety widżetów", + "selected-widgets-bundles": "Wybrano { count, plural, =1 {1 pakiet} other {# pakietów} } widżetów", + "open-widgets-bundle": "Otwórz pakiet widżetów", + "loading-widgets-bundles": "Ładowanie pakietów widżetów..." + }, + "widget-config": { + "data": "Dane", + "settings": "Ustawienia", + "advanced": "Zaawansowane", + "appearance": "Wygląd", + "widget-card": "Widget card", + "mobile": "Mobile", + "title": "Tytuł", + "title-tooltip": "Podpowiedź tytułu", + "general-settings": "Ustawienia główne", + "display-title": "Wyświetl tytuł widżetu", + "card-title": "Card title", + "drop-shadow": "Rzuć cień", + "enable-fullscreen": "Włącz pełny ekran", + "enable-data-export": "Włącz eksport danych", + "data-export": "Data export", + "background-color": "Kolor tła", + "text-color": "Kolor tekstu", + "border-radius": "Border radius", + "padding": "Wyściółka", + "margin": "Margines", + "widget-style": "Styl widgetu", + "widget-css": "Widżet CSS", + "title-style": "Styl tytułu", + "mobile-mode-settings": "Tryb mobilny", + "order": "Kolejność", + "height": "Wysokość", + "mobile-hide": "Ukryj widżet w trybie mobilnym", + "desktop-hide": "Ukryj widżet w trybie panelu", + "units": "Specjalny symbol wyświetlany obok wartości", + "units-by-default": "Units by default", + "decimals": "Liczba cyfr po zmiennoprzecinkowym", + "decimals-by-default": "Decimals by default", + "default-data-key-parameter-hint": "This parameter applies to all widget values unless overridden by data key configuration", + "units-short": "Units", + "decimals-short": "Decimals", + "decimals-suffix": "decimals", + "timewindow": "Okno czasowe", + "use-dashboard-timewindow": "Użyj okna czasowego panelu", + "use-widget-timewindow": "Use widget timewindow", + "display-timewindow": "Wyświetl okno czasowe", + "legend": "Legenda", + "display-legend": "Wyświetl legendę", + "datasources": "Źródła danych", + "datasource": "Datasource", + "maximum-datasources": "Maksymalnie { count, plural, =1 {1 źródło danych jest dozwolone} other {# źródeł danych jest dozwolonych} }", + "timeseries-key-error": "Należy określić co najmniej jeden klucz danych szeregu czasowego", + "datasource-type": "Typ", + "datasource-parameters": "Parametry", + "remove-datasource": "Usuń źródło danych", + "add-datasource": "Dodaj źródło danych", + "target-device": "Urządzenie docelowe", + "alarm-source": "Źródło alarmu", + "actions": "Działania", + "action": "Działanie", + "add-action": "Dodaj akcję", + "search-actions": "Szukaj działań", + "no-actions-text": "Nie znaleziono żadnych działań", + "action-source": "Źródło akcji", + "action-source-required": "Źródło działania jest wymagane.", + "action-name": "Nazwa", + "action-name-required": "Nazwa działania jest wymagana.", + "action-name-not-unique": "Istnieje już inna akcja o tej samej nazwie.
Nazwa akcji powinna być unikalna w obrębie tego samego źródła akcji.", + "action-icon": "Ikona", + "show-hide-action-using-function": "Pokaż/ukryj akcję za pomocą funkcji", + "action-type": "Typ", + "action-type-required": "Typ działania jest wymagany.", + "edit-action": "Edytuj akcję", + "delete-action": "Usuń akcję", + "delete-action-title": "Usuń akcję widgetu", + "delete-action-text": "Czy na pewno chcesz usunąć akcję widgetu o nazwie '{{actionName}}'?", + "title-icon": "Ikona tytułu", + "display-icon": "Wyświetl ikonę tytułu", + "card-icon": "Card icon", + "icon-color": "Kolor ikony", + "icon-size": "Rozmiar ikony", + "advanced-settings": "Zaawansowane ustawienia", + "data-settings": "Ustawienia danych", + "limits": "Limits", + "no-data-display-message": "Alternatywny komunikat \"Brak danych do wyświetlenia\"", + "data-page-size": "Maksymalna liczba obiektów na źródło danych", + "settings-component-not-found": "Nie znaleziono komponentu formularza ustawień dla selektora '{{selector}}'", + "preview": "Preview", + "set": "Set", + "set-message": "Set message", + "advanced-title-style": "Advanced title style", + "card-style": "Card style", + "text": "Text", + "background": "Background", + "advanced-widget-style": "Advanced widget style", + "card-buttons": "Card buttons", + "show-card-buttons": "Show card buttons", + "card-border-radius": "Card border radius", + "card-appearance": "Card appearance", + "color": "Color" + }, + "widget-type": { + "import": "Importuj typ widżetu", + "export": "Eksportuj typ widżetu", + "export-widget-types": "Export widget types", + "export-failed-error": "Nie można wyeksportować typu widżetu: {{error}}", + "create-new-widget-type": "Utwórz nowy typ widżetu", + "widget-type-file": "Plik typu widżetu", + "invalid-widget-type-file-error": "Nie można zaimportować typu widżetu: Nieprawidłowa struktura danych typu widżetu." + }, + "self-registration": { + "self-registration": "Rejestracja własna", + "self-registration-url": "Adres URL samodzielnej rejestracji", + "captcha-version": "wersja reCAPTCHA", + "captcha-action": "nazwa akcji wpisu reCAPTCHA", + "captcha-site-key": "Klucz witryny reCAPTCHA", + "captcha-secret-key": "tajny klucz reCAPTCHA", + "notification-email": "E-mail z powiadomieniem", + "notification-email-invalid": "Invalid email format.", + "notification-email-required": "Notification email is required.", + "privacy-policy-text": "Tekst polityki prywatności", + "terms-of-use-text": "Tekst Regulaminu", + "text-message-page": "Wiadomość tekstowa do strony rejestracji", + "enable-mobile-self-registration": "Włącz samodzielną rejestrację z poziomu aplikacji mobilnej", + "mobile-self-registration-title": "Ustawienia samodzielnej rejestracji aplikacji mobilnej", + "mobile-package": "Pakiet aplikacji", + "mobile-package-placeholder": "Np.: moja.przykładowa.aplikacja", + "mobile-package-hint": "Dla Androida: Twój własny, unikalny identyfikator aplikacji. Dla iOS: Identyfikator pakietu produktów.", + "mobile-package-required": "Pakiet aplikacji jest wymagany", + "mobile-app-secret": "Sekret aplikacji", + "invalid-mobile-app-secret": "Klucz tajny aplikacji może zawierać wyłącznie znaki alfanumeryczne i mieć długość od 16 do 2048 znaków.", + "copy-mobile-app-secret": "Skopiuj sekret aplikacji", + "mobile-app-scheme": "Schemat adresu URL aplikacji", + "mobile-app-scheme-placeholder": "Np.: przykladowyschemat", + "mobile-app-scheme-required": "Schemat adresu URL aplikacji jest wymagany", + "mobile-app-host": "Nazwa hosta adresu URL aplikacji", + "mobile-app-host-placeholder": "Np.: moja.aplikacja.host", + "mobile-app-host-required": "Nazwa hosta adresu URL aplikacji jest wymagana", + "show-privacy-policy": "Pokaż Politykę Prywatności", + "show-terms-of-use": "Pokaż Warunki użytkowania", + "domain-settings": "Ustawienia domeny", + "general-settings": "Ustawienia główne" + }, + "solution-template": { + "solution-template": "Szablon rozwiązania", + "solution-templates": "Szablony rozwiązań", + "management": "Zarządzaj szablonami rozwiązań", + "details": "Szczegóły", + "install": "Instaluj", + "level": "Poziom", + "install-title": "Pomyślnie zainstalowano szablon rozwiązania", + "install-failed-title": "Instalacja szablonu rozwiązania nie powiodła się", + "instructions": "Instrukcje", + "goto-main-dashboard": "Przejdź do głównego panelu", + "delete": "Usuń", + "delete-solution-title": "Czy na pewno chcesz usunąć rozwiązanie '{{solutionTitle}}'?", + "delete-solution-text": "Bądź ostrożny, po potwierdzeniu rozwiązania i wszystkich powiązanych danych nie będzie można odzyskać.", + "installing": "Instaluję szablon rozwiązania..." }, - "widget-config":{ - "data":"Dane", - "settings":"Ustawienia", - "advanced":"Zaawansowane", - "appearance":"Wygląd", - "widget-card":"Karta widżetu", - "mobile":"Mobilny", - "title":"Tytuł", - "title-tooltip":"Podpowiedź tytułu", - "general-settings":"Ustawienia ogólne", - "display-title":"Wyświetl tytuł widżetu", - "card-title":"Tytuł karty", - "drop-shadow":"Cień", - "enable-fullscreen":"Włącz pełny ekran", - "background-color":"Kolor tła", - "text-color":"Kolor tekstu", - "border-radius":"Promień obramowania", - "padding":"Odstęp", - "margin":"Margines", - "widget-style":"Styl widżetu", - "widget-css":"CSS widżetu", - "title-style":"Styl tytułu", - "mobile-mode-settings":"Ustawienia trybu mobilnego", - "order":"Kolejność", - "height":"Wysokość", - "mobile-hide":"Ukryj widżet w trybie mobilnym", - "desktop-hide":"Ukryj widżet w trybie desktopowym", - "units":"Specjalny symbol obok wartości", - "units-by-default":"Domyślne jednostki", - "decimals":"Liczba miejsc po przecinku", - "decimals-by-default":"Domyślna liczba miejsc po przecinku", - "default-data-key-parameter-hint":"Ten parametr dotyczy wszystkich wartości widżetu, chyba że zostanie nadpisany przez konfigurację klucza danych", - "units-short":"Jednostki", - "decimals-short":"Miejsca po przecinku", - "decimals-suffix":"miejsca po przecinku", - "timewindow":"Okno czasowe", - "use-dashboard-timewindow":"Użyj okna czasowego dashboardu", - "use-widget-timewindow":"Użyj okna czasowego widżetu", - "display-timewindow":"Wyświetl okno czasowe", - "legend":"Legenda", - "display-legend":"Wyświetl legendę", - "datasources":"Źródła danych", - "datasource":"Źródło danych", - "maximum-datasources":"Maksymalnie { count, plural, =1 {1 źródło danych jest dozwolone.} other {# źródeł danych jest dozwolonych} }", - "timeseries-key-error":"Należy określić co najmniej jeden klucz danych szeregów czasowych", - "datasource-type":"Typ", - "datasource-parameters":"Parametry", - "remove-datasource":"Usuń źródło danych", - "add-datasource":"Dodaj źródło danych", - "target-device":"Docelowe urządzenie", - "alarm-source":"Źródło alarmu", - "actions":"Akcje", - "action":"Akcja", - "add-action":"Dodaj akcję", - "search-actions":"Szukaj akcji", - "no-actions-text":"Nie znaleziono akcji", - "action-source":"Źródło akcji", - "action-source-required":"Źródło akcji jest wymagane.", - "action-name":"Nazwa", - "action-name-required":"Nazwa akcji jest wymagana.", - "action-name-not-unique":"Inna akcja o tej samej nazwie już istnieje.
Nazwa akcji powin.", - "action-icon":"Ikona", - "show-hide-action-using-function":"Pokaż/ukryj akcję za pomocą funkcji", - "action-type":"Typ", - "action-type-required":"Typ akcji jest wymagany.", - "edit-action":"Edytuj akcję", - "delete-action":"Usuń akcję", - "delete-action-title":"Usuń akcję widżetu", - "delete-action-text":"Czy na pewno chcesz usunąć akcję widżetu o nazwie '{{actionName}}'?", - "title-icon":"Ikona tytułu", - "display-icon":"Wyświetl ikonę tytułu", - "card-icon":"Ikona karty", - "icon":"Ikona", - "icon-color":"Kolor ikony", - "icon-size":"Rozmiar ikony", - "advanced-settings":"Zaawansowane ustawienia", - "data-settings":"Ustawienia danych", - "limits":"Limity", - "no-data-display-message":"Alternatywny komunikat \"Brak danych do wyświetlenia\"", - "data-page-size":"Maksymalna liczba encji na źródło danych", - "settings-component-not-found":"Nie znaleziono komponentu formularza ustawień dla selektora '{{selector}}'", - "preview":"Podgląd", - "set":"Ustaw", - "set-message":"Ustaw komunikat", - "advanced-title-style":"Zaawansowany styl tytułu", - "card-style":"Styl karty", - "text":"Tekst", - "background":"Tło", - "advanced-widget-style":"Zaawansowany styl widżetu", - "card-buttons":"Przyciski karty", - "show-card-buttons":"Pokaż przyciski karty", - "card-border-radius":"Promień obramowania karty", - "card-appearance":"Wygląd karty", - "color":"Kolor", - "tooltip":"Podpowiedź", - "units-required":"Jednostka jest wymagana." + "markdown": { + "edit": "Edytuj", + "preview": "Podgląd", + "copy-code": "Kliknij, aby skopiować", + "copied": "Skopiowano!" }, - "widget-type":{ - "import":"Importuj typ widżetu", - "export":"Eksportuj typ widżetu", - "export-failed-error":"Nie można wyeksportować widżetu: {{error}}", - "widget-file":"Plik widżetu", - "invalid-widget-file-error":"Nie można zaimportować widżetu: Nieprawidłowa struktura danych widżetu." + "white-labeling": { + "white-labeling": "Dostosuj wygląd", + "white-labeling-general": "Dostosuj ogólny wygląd", + "login-white-labeling": "Zaloguj się do dodtosowywania wyglądu", + "general": "Ogólny", + "login": "Zaloguj sie", + "preview": "Podgląd", + "app-title": "Tytuł aplikacji", + "favicon": "Ikona strony internetowej", + "favicon-description": "Obraz *.ico, *.gif lub *.png o maksymalnym rozmiarze {{kbSize}} KB.", + "favicon-size-error": "Obraz witryny jest za duży. {{kbSize}} KB.", + "favicon-type-error": "Nieprawidłowy format pliku obrazu witryny internetowej. ", + "drop-favicon-image": "Upuść obraz ikony witryny internetowej lub kliknij, aby wybrać plik do przesłania.", + "drop-favicon-image-or": "Przeciągnij i upuść obraz ikony witryny internetowej lub", + "no-favicon-image": "Nie wybrano żadnej ikony", + "logo": "Logo", + "logo-description": "Dowolny obraz o maksymalnym rozmiarze {{kbSize}} KB.", + "logo-size-error": "Obraz logo jest za duży. {{kbSize}} KB.", + "logo-type-error": "Nieprawidłowy format pliku logo. ", + "drop-logo-image": "Upuść obraz logo lub kliknij, aby wybrać plik do przesłania.", + "drop-logo-image-or": "Przeciągnij i upuść obraz logo lub", + "no-logo-image": "Nie wybrano logo", + "logo-height": "Wysokość logo, px", + "primary-palette": "Podstawowa paleta", + "accent-palette": "Paleta akcentów", + "customize-palette": "Dostosuj", + "advanced-css": "Zaawansowany CSS", + "edit-palette": "Edytuj paletę", + "save-palette": "Zapisz paletę", + "primary-background": "Podstawowe tło", + "secondary-background": "Drugie tło", + "hue1": "ODCIEŃ 1", + "hue2": "ODCIEŃ 2", + "hue3": "ODCIEŃ 3", + "page-background-color": "Kolor tła strony", + "dark-foreground": "Ciemny pierwszy plan", + "domain-name": "Nazwa domeny", + "base-url": "Bazowy adres URL", + "prohibit-different-url": "Zabroń używania nazwy hosta z nagłówków żądań klienta", + "prohibit-different-url-hint": "To ustawienie powinno być włączone w środowiskach produkcyjnych. Może powodować problemy bezpiczeństwa w przypadku wyłączenia", + "help-link-base-url": "Bazowy adres URL linków pomocy", + "ui-help-base-url": "Bazowy adres URL pomocy interfejsu użytkownika", + "enable-help-links": "Włącz linki pomocy", + "error-verification-url": "Nazwa domeny nie powinna zawierać symboli „/” i „:”. Przykład: thingsboard.io", + "show-platform-name-version": "Pokaż nazwę i wersję platformy", + "platform-name": "Nazwa platformy", + "platform-version": "Wersja platformowa", + "version-mask": "{{name}} v.{{version}}", + "position": { + "label": "Nazwa platformy i pozycja wersji", + "under-logo": "Pod logo", + "bottom": "Na dole formularza logowania" + }, + "base-url-required": "Podstawowy adres URL jest wymagany." }, "widgets":{ "background":{ @@ -5178,736 +6534,686 @@ "value-on-bar":"Wartość na słupku", "bar-chart-style":"Styl wykresu słupkowego" }, - "battery-level":{ - "layout":"Układ", - "layout-vertical-solid":"Pionowy. Stały", - "layout-horizontal-solid":"Poziomy. Stały", - "layout-vertical-divided":"Pionowy. Podzielony", - "layout-horizontal-divided":"Poziomy. Podzielony", - "icon":"Ikona", - "value":"Wartość", - "auto-scale":"Automatyczne skalowanie", - "battery-level-color":"Kolor poziomu baterii", - "battery-shape-color":"Kolor kształtu baterii", - "battery-level-card-style":"Styl karty poziomu baterii", - "sections-count":"Liczba sekcji" - }, - "signal-strength":{ - "value":"Wartość", - "last-update":"Ostatnia aktualizacja", - "no-signal":"Brak sygnału", - "layout":"Układ", - "layout-wifi":"Wi-Fi", - "layout-cellular-bar":"Słupkowy komórkowy", - "icon":"Ikona", - "date":"Data", - "active-bars-color":"Kolor aktywnych pasków sygnału", - "inactive-bars-color":"Kolor nieaktywnych pasków sygnału", - "signal-strength-card-style":"Styl karty siły sygnału" + "battery-level": { + "layout": "Layout", + "layout-vertical-solid": "Vertical. Solid", + "layout-horizontal-solid": "Horizontal. Solid", + "layout-vertical-divided": "Vertical. Divided", + "layout-horizontal-divided": "Horizontal. Divided", + "icon": "Icon", + "value": "Value", + "auto-scale": "Auto scale", + "battery-level-color": "Battery level color", + "battery-shape-color": "Battery shape color", + "battery-level-card-style": "Battery level card style" }, - "chart":{ - "common-settings":"Wspólne ustawienia", - "enable-stacking-mode":"Włącz tryb układania", - "selection":"Wybór zakresu czasowego", - "enable-selection-mode":"Włącz tryb wyboru", - "line-shadow-size":"Rozmiar cienia linii", - "display-smooth-lines":"Wyświetlaj gładkie (krzywe) linie", - "default-bar-width":"Domyślna szerokość słupka dla danych niezagregowanych (milisekundy)", - "bar-alignment":"Wyrównanie słupka", - "bar-alignment-left":"Lewo", - "bar-alignment-right":"Prawo", - "bar-alignment-center":"Środek", - "default-font":"Domyślna czcionka", - "default-font-size":"Domyślny rozmiar czcionki", - "default-font-color":"Domyślny kolor czcionki", - "thresholds-line-width":"Domyślna szerokość linii dla wszystkich progów", - "tooltip-settings":"Ustawienia podpowiedzi", - "tooltip":"Podpowiedź", - "show-tooltip":"Pokaż podpowiedź", - "hover-individual-points":"Najedź na pojedyncze punkty", - "show-cumulative-values":"Pokaż wartości kumulatywne w trybie układania", - "hide-zero-false-values":"Ukryj zera/fałszywe wartości z podpowiedzi", - "tooltip-value-format-function":"Funkcja formatująca wartość podpowiedzi", - "grid-settings":"Ustawienia siatki", - "show-vertical-lines":"Pokaż linie pionowe", - "show-horizontal-lines":"Pokaż linie poziome", - "grid-outline-border-width":"Szerokość krawędzi/ramki siatki (px)", - "primary-color":"Kolor podstawowy", - "background-color":"Kolor tła", - "ticks-color":"Kolor znaczników", - "xaxis-settings":"Ustawienia osi X", - "axis-title":"Tytuł osi", - "xaxis-tick-labels-settings":"Ustawienia etykiet osi X", - "show-tick-labels":"Pokaż etykiety osi", - "yaxis-settings":"Ustawienia osi Y", - "min-scale-value":"Minimalna wartość na skali", - "max-scale-value":"Maksymalna wartość na skali", - "yaxis-tick-labels-settings":"Ustawienia etykiet osi Y", - "tick-step-size":"Rozmiar kroku między znacznikami", - "number-of-decimals":"Liczba miejsc po przecinku do wyświetlenia", - "ticks-formatter-function":"Funkcja formatująca znaczniki", - "comparison-settings":"Ustawienia porównywania", - "enable-comparison":"Włącz porównywanie", - "time-for-comparison":"Okres porównywania", - "time-for-comparison-previous-interval":"Poprzedni interwał (domyślnie)", - "time-for-comparison-days":"Dzień temu", - "time-for-comparison-weeks":"Tydzień temu", - "time-for-comparison-months":"Miesiąc temu", - "time-for-comparison-years":"Rok temu", - "time-for-comparison-custom-interval":"Niestandardowy interwał", - "custom-interval-value":"Wartość niestandardowego interwału (ms)", - "comparison-x-axis-settings":"Ustawienia osi X porównywania", - "axis-position":"Pozycja osi", - "axis-position-top":"Góra (domyślnie)", - "axis-position-bottom":"Dół", - "custom-legend-settings":"Niestandardowe ustawienia legendy", - "enable-custom-legend":"Włącz niestandardową legendę (pozwoli to na korzystanie z atrybutów/czasu rzeczywistego w etykietach kluczy)", - "key-name":"Nazwa klucza", - "key-name-required":"Nazwa klucza jest wymagana", - "key-type":"Typ klucza", - "key-type-attribute":"Atrybut", - "key-type-timeseries":"Czas rzeczywisty", - "label-keys-list":"Lista kluczy do użycia w etykietach", - "no-label-keys":"Brak skonfigurowanych kluczy", - "add-label-key":"Dodaj nowy klucz", - "line-width":"Szerokość linii", - "color":"Kolor", - "data-is-hidden-by-default":"Dane są domyślnie ukryte", - "disable-data-hiding":"Wyłącz ukrywanie danych", - "remove-from-legend":"Usuń klucz danych z legendy", - "exclude-from-stacking":"Wyłącz z układania (dostępne w trybie „Układanie”)", - "line-settings":"Ustawienia linii", - "show-line":"Pokaż linię", - "fill-line":"Wypełnij linię", - "fill-line-opacity":"Przezroczystość wypełnienia linii", - "points-settings":"Ustawienia punktów", - "show-points":"Pokaż punkty", - "points-line-width":"Szerokość linii punktów", - "points-radius":"Promień punktów", - "point-shape":"Kształt punktu", - "point-shape-circle":"Okrąg", - "point-shape-cross":"Krzyż", - "point-shape-diamond":"Diament", - "point-shape-square":"Kwadrat", - "point-shape-triangle":"Trójkąt", - "point-shape-custom":"Niestandardowa funkcja", - "point-shape-draw-function":"Funkcja rysowania kształtu punktu", - "show-separate-axis":"Pokaż oddzielną oś", - "axis-position-left":"Lewo", - "axis-position-right":"Prawo", - "thresholds":"Progi", - "no-thresholds":"Brak skonfigurowanych progów", - "add-threshold":"Dodaj próg", - "show-values-for-comparison":"Pokaż historyczne wartości do porównania", - "comparison-values-label":"Etykieta historycznych wartości", - "comparison-line-color":"Kolor linii porównawczej", - "threshold-settings":"Ustawienia progu", - "use-as-threshold":"Użyj wartości klucza jako progu", - "threshold-line-width":"Szerokość linii progu", - "threshold-color":"Kolor progu", - "common-pie-settings":"Wspólne ustawienia wykresu kołowego", - "radius":"Promień", - "inner-radius":"Wewnętrzny promień", - "tilt":"Nachylenie", - "common-pie-settings-range-error":"Wartość powinna być w zakresie od 0 do 1", - "stroke-settings":"Ustawienia obrysu", - "width-pixels":"Szerokość (piksele)", - "show-labels":"Pokaż etykiety", - "animation-settings":"Ustawienia animacji", - "animated-pie":"Włącz animację kołową (eksperymentalne)", - "border-settings":"Ustawienia obramowania", - "border-width":"Szerokość obramowania", - "border-color":"Kolor obramowania", - "legend-settings":"Ustawienia legendy", - "display-legend":"Wyświetl legendę", - "labels-font-color":"Kolor czcionki etykiet", - "series":"Seria", - "add-series":"Dodaj serię", - "series-settings":"Ustawienia serii", - "remove-series":"Usuń serię", - "no-series":"Brak skonfigurowanych serii", - "no-series-error":"Należy określić co najmniej jedną serię", - "chart-appearance":"Wygląd wykresu", - "vertical-grid-lines":"Linie siatki pionowej", - "horizontal-grid-lines":"Linie siatki poziomej", - "chart-background":"Tło wykresu", - "grid-lines-color":"Kolor linii siatki", - "border":"Obramowanie", - "axis":"Oś", - "vertical-axis":"Oś pionowa", - "ticks":"Znaczniki", - "horizontal-axis":"Oś pozioma" + "chart": { + "common-settings": "Wspólne ustawienia", + "enable-stacking-mode": "Włącz tryb układania", + "selection": "Time range selection", + "enable-selection-mode": "Enable selection mode", + "line-shadow-size": "Rozmiar cienia linii", + "display-smooth-lines": "Wyświetl gładkie (zakrzywione) linie", + "default-bar-width": "Domyślna szerokość paska dla danych niezagregowanych (milisekundy)", + "bar-alignment": "Wyrównanie paska", + "bar-alignment-left": "Do lewej", + "bar-alignment-right": "Do prawej", + "bar-alignment-center": "Do środka", + "default-font": "Default font", + "default-font-size": "Domyślny rozmiar czcionki", + "default-font-color": "Domyślny kolor czcionki", + "thresholds-line-width": "Domyślna szerokość linii dla wszystkich progów", + "tooltip-settings": "Ustawienia podpowiedzi", + "tooltip": "Tooltip", + "show-tooltip": "Pokaż podpowiedź", + "hover-individual-points": "Najedź kursorem na poszczególne punkty", + "show-cumulative-values": "Pokaż wartości skumulowane w trybie układania", + "hide-zero-false-values": "Ukryj wartości zerowe/fałszywe w podpowiedzi", + "tooltip-value-format-function": "Funkcja formatu wartości podpowiedzi", + "grid-settings": "Ustawienia siatki", + "show-vertical-lines": "Pokaż pionowe linie", + "show-horizontal-lines": "Pokaż poziome linie", + "grid-outline-border-width": "Szerokość obrysu/obramowania siatki (px)", + "primary-color": "Kolor podstawowy", + "background-color": "Kolor tła", + "ticks-color": "Kolor znaczników", + "xaxis-settings": "Ustawienia osi X", + "axis-title": "Tytuł osi", + "xaxis-tick-labels-settings": "Ustawienia etykiet znaczników osi X", + "show-tick-labels": "Pokaż etykiety znaczników osi", + "yaxis-settings": "Ustawienia osi Y", + "min-scale-value": "Minimalna wartość na skali", + "max-scale-value": "Maksymalna wartość na skali", + "yaxis-tick-labels-settings": "Ustawienia etykiet znaczników osi Y", + "tick-step-size": "Rozmiar kroku między znacznikami", + "number-of-decimals": "Liczba miejsc dziesiętnych do wyświetlenia", + "ticks-formatter-function": "Funkcja formatowania znaczników", + "comparison-settings": "Ustawienia porównania", + "enable-comparison": "Włącz porównanie", + "time-for-comparison": "Okres porównawczy", + "time-for-comparison-previous-interval": "Poprzedni interwał (domyślny)", + "time-for-comparison-days": "Dzień temu", + "time-for-comparison-weeks": "Tydzień temu", + "time-for-comparison-months": "Miesiąc temu", + "time-for-comparison-years": "Rok temu", + "time-for-comparison-custom-interval": "Niestandardowy interwał", + "custom-interval-value": "Niestandardowa wartość interwału (ms)", + "comparison-x-axis-settings": "Porównanie ustawień osi X", + "axis-position": "Pozycja osi", + "axis-position-top": "Góra (domyślnie)", + "axis-position-bottom": "Spód", + "custom-legend-settings": "Niestandardowe ustawienia legendy", + "enable-custom-legend": "Włącz niestandardową legendę (pozwoli to na użycie wartości atrybutów/szeregów czasowych w kluczowych etykietach)", + "key-name": "Nazwa klucza", + "key-name-required": "Nazwa klucza jest wymagana", + "key-type": "Typ klucza", + "key-type-attribute": "Atrybut", + "key-type-timeseries": "Szereg czasowy", + "label-keys-list": "Lista kluczy do użycia w etykietach", + "no-label-keys": "Nie skonfigurowano żadnych kluczy", + "add-label-key": "Dodaj nowy klucz", + "line-width": "Szerokość linii", + "color": "Kolor", + "data-is-hidden-by-default": "Dane są domyślnie ukryte", + "disable-data-hiding": "Wyłącz ukrywanie danych", + "remove-from-legend": "Usuń klucz danych z legendy", + "exclude-from-stacking": "Wyklucz z układania (dostępne w trybie \"Układanie\")", + "line-settings": "Ustawienia linii", + "show-line": "Pokaż linię", + "fill-line": "Wypełnij linię", + "fill-line-opacity": "Wypełnij krycie", + "points-settings": "Ustawienia punktów", + "show-points": "Pokaż punkty", + "points-line-width": "Szerokość linii punktów", + "points-radius": "Promień punktów", + "point-shape": "Kształt punktowy", + "point-shape-circle": "Koło", + "point-shape-cross": "Krzyż", + "point-shape-diamond": "Diament", + "point-shape-square": "Kwadrat", + "point-shape-triangle": "Trójkąt", + "point-shape-custom": "Funkcja niestandardowa", + "point-shape-draw-function": "Funkcja rysowania kształtu punktu", + "show-separate-axis": "Pokaż osobną oś", + "axis-position-left": "Lewy", + "axis-position-right": "Prawy", + "thresholds": "Progi", + "no-thresholds": "Nie skonfigurowano żadnych progów", + "add-threshold": "Dodaj nowy próg", + "show-values-for-comparison": "Pokaż wartości historyczne dla porównania", + "comparison-values-label": "Etykieta wartości historycznych", + "comparison-line-color": "Comparison line color", + "threshold-settings": "Ustawienia progów", + "use-as-threshold": "Użyj wartości klucza jako progu", + "threshold-line-width": "Szerokość linii progu", + "threshold-color": "Kolor progu", + "common-pie-settings": "Typowe ustawienia ciast", + "radius": "Promień", + "inner-radius": "Promień wewnętrzny", + "tilt": "Nachylenie", + "common-pie-settings-range-error": "Value should be in range from 0 to 1", + "stroke-settings": "Ustawienia soku", + "width-pixels": "Szerokość (piksele)", + "show-labels": "Pokaż etykiety", + "animation-settings": "Ustawienia animacji", + "animated-pie": "Włącz animację kołową (eksperymentalnie)", + "border-settings": "Ustawienia obramowania", + "border-width": "Szerokość granicy", + "border-color": "Kolor ramki", + "legend-settings": "Ustawienia legendy", + "display-legend": "Wyświetl legendę", + "labels-font-color": "Kolor czcionki etykiet", + "series": "Series", + "add-series": "Add series", + "series-settings": "Series settings", + "remove-series": "Remove series", + "no-series": "No series configured", + "no-series-error": "At least one series should be specified", + "chart-appearance": "Chart appearance", + "vertical-grid-lines": "Vertical grid lines", + "horizontal-grid-lines": "Horizontal grid lines", + "chart-background": "Chart background", + "grid-lines-color": "Grid lines color", + "border": "Border", + "axis": "Axis", + "vertical-axis": "Vertical axis", + "ticks": "Ticks", + "horizontal-axis": "Horizontal axis" }, - "color":{ - "color-settings":"Ustawienia koloru", - "color-type-constant":"Stały", - "color-type-range":"Zakres", - "color-type-function":"Funkcja", - "color":"Kolor", - "value-range":"Zakres wartości", - "from":"Od", - "to":"Do", - "color-function":"Funkcja koloru", - "copy-color-settings-from":"Skopiuj ustawienia koloru z" + "color": { + "color-settings": "Color settings", + "color-type-constant": "Constant", + "color-type-range": "Range", + "color-type-function": "Function", + "color": "Color", + "value-range": "Value range", + "from": "From", + "to": "To", + "color-function": "Color function" }, - "dashboard-state":{ - "dashboard-state-settings":"Ustawienia stanu pulpitu", - "dashboard-state":"Identyfikator stanu pulpitu", - "autofill-state-layout":"Domyślne wypełnianie wysokości układu stanu automatycznie", - "default-margin":"Domyślny margines widżetów", - "default-background-color":"Domyślny kolor tła", - "sync-parent-state-params":"Synchronizuj parametry stanu z nadrzędnym pulpitem" + "dashboard-state": { + "dashboard-state-settings": "Ustawienia stanu panelu", + "dashboard-state": "Identyfikator stanu panelu", + "autofill-state-layout": "Dopełnienie do wysokości strony - domyślnie", + "default-margin": "Domyślny margines widżetów", + "default-background-color": "Domyślny kolor tła", + "sync-parent-state-params": "Synchronizuj parametry stanu z nadrzędnym panelem" }, - "date-range-picker-settings":{ - "date-range-picker-settings":"Ustawienia wybieraka zakresu dat", - "hide-date-range-picker":"Ukryj wybierak zakresu dat", - "picker-one-panel":"Wybierak zakresu dat jednopanelowy", - "picker-auto-confirm":"Automatyczne potwierdzanie wybieraka zakresu dat", - "picker-show-template":"Pokazuj szablon wybieraka zakresu dat", - "first-day-of-week":"Pierwszy dzień tygodnia", - "interval-settings":"Ustawienia interwału", - "hide-interval":"Ukryj interwał", - "initial-interval":"Początkowy interwał", - "interval-hour":"Godzina", - "interval-day":"Dzień", - "interval-week":"Tydzień", - "interval-two-weeks":"2 tygodnie", - "interval-month":"Miesiąc", - "interval-three-months":"3 miesiące", - "interval-six-months":"6 miesięcy", - "step-settings":"Ustawienia kroku", - "hide-step-size":"Ukryj rozmiar kroku", - "initial-step-size":"Początkowy rozmiar kroku", - "hide-labels":"Ukryj etykiety", - "use-session-storage":"Użyj przechowywania sesji", - "localizationMap":{ - "Sun":"Niedz.", - "Mon":"Pon.", - "Tue":"Wt.", - "Wed":"Śr.", - "Thu":"Czw.", - "Fri":"Piąt.", - "Sat":"Sob.", - "Jan":"Sty", - "Feb":"Lut", - "Mar":"Mar", - "Apr":"Kwi", - "May":"Maj", - "Jun":"Cze", - "Jul":"Lip", - "Aug":"Sie", - "Sep":"Wrz", - "Oct":"Paź", - "Nov":"Lis", - "Dec":"Gru", - "January":"Styczeń", - "February":"Luty", - "March":"Marzec", - "April":"Kwiecień", - "June":"Czerwiec", - "July":"Lipiec", - "August":"Sierpień", - "September":"Wrzesień", - "October":"Październik", - "November":"Listopad", - "December":"Grudzień", - "Custom Date Range":"Niestandardowy zakres dat", - "Date Range Template":"Szablon zakresu dat", - "Today":"Dziś", - "Yesterday":"Wczoraj", - "This Week":"Bieżący tydzień", - "Last Week":"Ostatni tydzień", - "This Month":"Bieżący miesiąc", - "Last Month":"Ostatni miesiąc", - "Year":"Rok", - "This Year":"Bieżący rok", - "Last Year":"Ostatni rok", - "Date picker":"Wybierak dat", - "Hour":"Godzina", - "Day":"Dzień", - "Week":"Tydzień", - "2 weeks":"2 Tygodnie", - "Month":"Miesiąc", - "3 months":"3 Miesiące", - "6 months":"6 Miesięcy", - "Custom interval":"Niestandardowy interwał", - "Interval":"Interwał", - "Step size":"Rozmiar kroku", - "Ok":"Ok" + "date-range-navigator": { + "date-range-picker-settings": "Ustawienia selektora zakresu dat", + "hide-date-range-picker": "Ukryj selektor zakresu dat", + "picker-one-panel": "Selektor zakresu dat w jednym panelu", + "picker-auto-confirm": "Automatyczne potwierdzenie wyboru zakresu dat", + "picker-show-template": "Szablon pokaż selektor zakresu dat", + "first-day-of-week": "Pierwszy dzień tygodnia", + "interval-settings": "Ustawienia interwałów", + "hide-interval": "Ukryj interwał", + "initial-interval": "Początkowy odstęp", + "interval-hour": "Godzina", + "interval-day": "Dzień", + "interval-week": "Tydzień", + "interval-two-weeks": "2 tygodnie", + "interval-month": "Miesiąc", + "interval-three-months": "3 miesiące", + "interval-six-months": "6 miesięcy", + "step-settings": "Ustawienia kroku", + "hide-step-size": "Ukryj rozmiar kroku", + "initial-step-size": "Początkowy rozmiar kroku", + "hide-labels": "Ukryj etykiety", + "use-session-storage": "Użyj magazynu sesji", + "localizationMap": { + "Sun": "Ndz", + "Mon": "Pon", + "Tue": "Wt", + "Wed": "Śr", + "Thu": "Czw", + "Fri": "Pt", + "Sat": "Sob", + "Jan": "Sty", + "Feb": "Lut", + "Mar": "Mar", + "Apr": "Kwi", + "May": "Maj", + "Jun": "Cze", + "Jul": "Lip", + "Aug": "Sie", + "Sep": "Wrz", + "Oct": "Paź", + "Nov": "Lis", + "Dec": "Gru", + "January": "Styczeń", + "February": "Luty", + "March": "Marzec", + "April": "Kwiecień", + "June": "Czerwiec", + "July": "Lipiec", + "August": "Sierpień", + "September": "Wrzesień", + "October": "Październik", + "November": "Listopad", + "December": "Grudzień", + "Custom Date Range": "Niestandardowy zakres dat", + "Date Range Template": "Szablon zakresu dat", + "Today": "Dzisiaj", + "Yesterday": "Wczoraj", + "This Week": "W tym tygodniu", + "Last Week": "Zeszły tydzień", + "This Month": "Ten miesiąc", + "Last Month": "W zeszłym miesiącu", + "Year": "Rok", + "This Year": "W tym roku", + "Last Year": "Ostatni rok", + "Date picker": "Selektor dat", + "Hour": "Godzina", + "Day": "Dzień", + "Week": "Tydzień", + "2 weeks": "2 tygodnie", + "Month": "Miesiąc", + "3 months": "3 miesiące", + "6 months": "6 miesięcy", + "Custom interval": "Niestandardowy interwał", + "Interval": "Interwał", + "Step size": "Rozmiar kroku", + "Ok": "OK" } }, - "doughnut":{ - "total":"Suma", - "layout":"Układ", - "layout-default":"Domyślny", - "layout-with-total":"Z sumą", - "auto-scale":"Automatyczne skalowanie", - "clockwise-layout":"Układ zgodny z ruchem wskazówek zegara", - "sort-series":"Sortuj serie według etykiety", - "central-total-value":"Centralna wartość sumy", - "tooltip-value-type-absolute":"Bezwzględna", - "tooltip-value-type-percentage":"Procentowa", - "doughnut-card-style":"Styl karty pączka" - }, - "entities-hierarchy":{ - "hierarchy-data-settings":"Ustawienia danych hierarchii", - "relations-query-function":"Funkcja zapytania o relacje węzła", - "has-children-function":"Funkcja sprawdzająca, czy węzeł ma dzieci", - "node-state-settings":"Ustawienia stanu węzła", - "node-opened-function":"Domyślna funkcja otwierania węzła", - "node-disabled-function":"Funkcja dezaktywacji węzła", - "display-settings":"Ustawienia wyświetlania", - "node-icon-function":"Funkcja ikony węzła", - "node-text-function":"Funkcja tekstu węzła", - "sort-settings":"Ustawienia sortowania", - "nodes-sort-function":"Funkcja sortowania węzłów" + "entities-hierarchy": { + "hierarchy-data-settings": "Ustawienia danych hierarchii", + "relations-query-function": "Funkcja zapytania o relacje węzłów", + "has-children-function": "Węzeł ma funkcję potomną", + "node-state-settings": "Ustawienia stanu węzła", + "node-opened-function": "Domyślna funkcja otwierania węzła", + "node-disabled-function": "Funkcja węzła wyłączona", + "display-settings": "Ustawienia wyświetlania", + "node-icon-function": "Funkcja ikony węzła", + "node-text-function": "Funkcja tekstowa węzła", + "sort-settings": "Ustawienia sortowania", + "nodes-sort-function": "Funkcja sortowania węzłów" }, - "edge":{ - "display-default-title":"Wyświetl domyślny tytuł" + "edge": { + "display-default-title": "Wyświetl tytuł domyślny" }, - "gateway":{ - "general-settings":"Ustawienia ogólne", - "widget-title":"Tytuł widżetu", - "default-archive-file-name":"Domyślna nazwa pliku archiwum", - "device-type-for-new-gateway":"Typ urządzenia dla nowej bramy", - "messages-settings":"Ustawienia komunikatów", - "save-config-success-message":"Komunikat tekstowy o pomyślnie zapisanej konfiguracji bramy", - "device-name-exists-message":"Komunikat tekstowy, gdy urządzenie o podanej nazwie już istnieje", - "gateway-title":"Formularz bramy", - "read-only":"Tylko do odczytu", - "events-title":"Tytuł formularza zdarzeń bramy", - "events-filter":"Filtr zdarzeń", - "event-key-contains":"Klucz zdarzenia zawiera...", - "show-connector":"Pokaż dla konektora", - "connector-state-param-key":"Klucz parametru stanu konektora", - "status":"Status", - "message":"Wiadomość", - "created-time":"Czas utworzenia" + "gateway": { + "general-settings": "Ustawienia główne", + "widget-title": "Tytul widzeta", + "default-archive-file-name": "Domyślna nazwa pliku archiwum", + "device-type-for-new-gateway": "Typ urządzenia dla nowej bramy", + "messages-settings": "Ustawienia wiadomości", + "save-config-success-message": "Wiadomość SMS o pomyślnym zapisaniu konfiguracji bramy", + "device-name-exists-message": "Wiadomość SMS, gdy urządzenie o podanej nazwie już istnieje", + "gateway-title": "Formularz bramy", + "read-only": "Tylko do odczytu", + "events-title": "Tytuł formularza zdarzenia bramy", + "events-filter": "Filtr zdarzeń", + "event-key-contains": "Klucz zdarzenia zawiera...", + "show-connector": "Show for the connector", + "connector-state-param-key": "Connector state parameter key", + "status": "Status", + "message": "Message", + "created-time": "Created time" }, - "gauge":{ - "default-color":"Domyślny kolor", - "radial-gauge-settings":"Ustawienia miernika radialnego", - "ticks-settings":"Ustawienia podziałek", - "min-value":"Wartość minimalna", - "max-value":"Wartość maksymalna", - "min-value-short":"min", - "max-value-short":"max", - "start-ticks-angle":"Kąt początkowy podziałek", - "ticks-angle":"Kąt podziałek", - "major-ticks":"Podziałki główne", - "major-ticks-count":"Liczba podziałek głównych", - "major-ticks-color":"Kolor podziałek głównych", - "minor-ticks":"Podziałki dodatkowe", - "minor-ticks-count":"Liczba podziałek dodatkowych", - "minor-ticks-color":"Kolor podziałek dodatkowych", - "tick-numbers-font":"Czcionka numerów podziałek", - "unit-title-settings":"Ustawienia jednostki", - "show-unit-title":"Pokaż nazwę jednostki", - "unit-title":"Nazwa jednostki", - "title-font":"Czcionka tytułu", - "units-settings":"Ustawienia jednostek", - "units-font":"Czcionka jednostek", - "value-box-settings":"Ustawienia pola wartości", - "show-value-box":"Pokaż pole wartości", - "value-box":"Pole wartości", - "value-int":"Liczba cyfr dla części całkowitej wartości", - "value-text":"Tekst wartości", - "value-text-shadow":"Cień tekstu wartości", - "value-font":"Czcionka tekstu wartości", - "rect-stroke-color-start":"Kolor obwódki prostokąta - początek gradientu", - "rect-stroke-color-end":"Kolor obwódki prostokąta - koniec gradientu", - "background-color":"Kolor tła", - "shadow-color":"Kolor cienia", - "value-box-rect-stroke-color":"Kolor obwódki prostokąta pola wartości", - "value-box-rect-stroke-color-end":"Kolor obwódki prostokąta pola wartości - koniec gradientu", - "value-box-background-color":"Kolor tła pola wartości", - "value-box-shadow-color":"Kolor cienia pola wartości", - "plate-settings":"Ustawienia tarczy", - "show-plate-border":"Pokaż krawędź tarczy", - "plate-color":"Kolor tarczy", - "needle-settings":"Ustawienia wskazówki", - "needle-circle-size":"Rozmiar koła wskazówki", - "needle-color":"Kolor wskazówki", - "needle-color-start":"Kolor wskazówki - początek gradientu", - "needle-color-end":"Kolor wskazówki - koniec gradientu", - "needle-color-shadow-up":"Kolor górnej części cienia wskazówki", - "needle-color-shadow-down":"Cień", - "highlights-settings":"Ustawienia punktów kulminacyjnych", - "highlights-width":"Szerokość punktów kulminacyjnych", - "highlights":"Punkty kulminacyjne", - "highlight-from":"Od", - "highlight-to":"Do", - "highlight-color":"Kolor", - "no-highlights":"Brak skonfigurowanych punktów kulminacyjnych", - "add-highlight":"Dodaj punkt kulminacyjny", - "animation-settings":"Ustawienia animacji", - "enable-animation":"Animacja", - "animation-duration-rule":"Czas trwania i reguła animacji", - "animation-duration":"Czas trwania animacji", - "animation-rule":"Reguła animacji", - "animation-linear":"Liniowa", - "animation-quad":"Kwadratowa", - "animation-quint":"Kwintowa", - "animation-cycle":"Cykliczna", - "animation-bounce":"Odbijająca się", - "animation-elastic":"Elastyczna", - "animation-dequad":"Odwrócona kwadratowa", - "animation-dequint":"Odwrócona kwintowa", - "animation-decycle":"Odwrócona cykliczna", - "animation-debounce":"Odwrócona odbijająca się", - "animation-delastic":"Odwrócona elastyczna", - "linear-gauge-settings":"Ustawienia miernika liniowego", - "bar-stroke":"Obwódka słupka", - "bar-stroke-width":"Szerokość obwódki słupka", - "bar-stroke-color":"Kolor obwódki słupka", - "bar-background-color":"Kolor tła słupka - początek gradientu", - "bar-background-color-end":"Kolor tła słupka - koniec gradientu", - "progress-bar-color":"Kolor paska postępu", - "progress-bar":"Pasek postępu", - "progress-bar-color-start":"Kolor paska postępu - początek gradientu", - "progress-bar-color-end":"Kolor paska postępu - koniec gradientu", - "major-ticks-names":"Nazwy podziałek głównych", - "show-stroke-ticks":"Pokaż obwódki podziałek", - "major-ticks-font":"Czcionka podziałek głównych", - "border-color":"Kolor obramowania", - "border-width":"Szerokość obramowania", - "needle-circle":"Koło wskazówki", - "needle-circle-color":"Kolor koła wskazówki", - "animation-target":"Cel animacji", - "animation-target-needle":"Wskazówka", - "animation-target-plate":"Tarcza", - "common-settings":"Wspólne ustawienia miernika", - "gauge-type":"Typ miernika", - "gauge-type-arc":"Łuk", - "gauge-type-donut":"Pączek", - "gauge-type-horizontal-bar":"Słupkowy poziomy", - "gauge-type-vertical-bar":"Słupkowy pionowy", - "donut-start-angle":"Kąt początkowy pączka", - "bar-settings":"Ustawienia słupka", - "relative-bar-width":"Względna szerokość słupka", - "neon-glow-brightness":"Jasność efektu świetlnego, (0-100), 0 - wyłączony efekt", - "stripes-thickness":"Grubość pasków, 0 - brak pasków", - "rounded-line-cap":"Użyj zaokrąglonego zakończenia linii", - "bar-color-settings":"Ustawienia koloru słupka", - "use-precise-level-color-values":"Użyj precyzyjnych poziomów kolorów", - "bar-colors":"Kolory słupków, od dolnego do górnego", - "color":"Kolor", - "no-bar-colors":"Brak skonfigurowanych kolorów słupków", - "add-bar-color":"Dodaj kolor słupka", - "from":"Od", - "to":"Do", - "fixed-level-colors":"Kolory słupków z użyciem wartości granicznych", - "gauge-title-settings":"Ustawienia tytułu miernika", - "show-gauge-title":"Pokaż tytuł miernika", - "gauge-title":"Tytuł miernika", - "gauge-title-font":"Czcionka tytułu miernika", - "unit-title-and-timestamp-settings":"Ustawienia jednostki i znacznika czasowego", - "show-timestamp":"Pokaż znacznik czasu wartości", - "timestamp-format":"Format znacznika czasu", - "label-font":"Czcionka etykiety pod wartością", - "value-settings":"Ustawienia wartości", - "show-value":"Pokaż tekst wartości", - "min-max-settings":"Ustawienia etykiet minimalnej/maksymalnej", - "show-min-max":"Pokaż wartości minimalną i maksymalną", - "min-max-font":"Czcionka etykiet minimalnej i maksymalnej", - "show-ticks":"Pokaż podziały", - "tick-width":"Szerokość podziałek", - "tick-color":"Kolor podziałek", - "tick-values":"Wartości podziałek", - "no-tick-values":"Brak skonfigurowanych wartości podziałek", - "add-tick-value":"Dodaj wartość podziałki", - "gauge-appearance":"Wygląd miernika", - "units-title":"Tytuł jednostek", - "value":"Wartość", - "ticks":"Podziały", - "arrow-and-scale-color":"Kolor strzałki i skali domyślnej", - "scale-settings":"Ustawienia skali", - "scale":"Skala", - "scale-color":"Kolory skali", - "compass-appearance":"Wygląd kompasu", - "labels":"Etykiety", - "label-style":"Styl etykiety" + "gauge": { + "default-color": "Domyślny kolor", + "radial-gauge-settings": "Ustawienia miernika promieniowego", + "ticks-settings": "Ustawienia znaczników", + "min-value": "Minimalna wartość", + "max-value": "Maksymalna wartość", + "start-ticks-angle": "Startowy kąt znaczników", + "ticks-angle": "Kąt znaczników", + "major-ticks-count": "Liczba głównych znaczników", + "major-ticks-color": "Kolor głównych znaczników", + "minor-ticks-count": "Liczba pobocznych znaczników", + "minor-ticks-color": "Kolor pobocznych znaczników", + "tick-numbers-font": "Czcionka liczb znaczników", + "unit-title-settings": "Ustawienia tytułu jednostki", + "show-unit-title": "Pokaż tytuł jednostki", + "unit-title": "Tytuł jednostki", + "title-font": "Czcionka tekstu tytułowego", + "units-settings": "Ustawienia jednostki", + "units-font": "Czcionka tekstu jednostki", + "value-box-settings": "Ustawienia pola wartości", + "show-value-box": "Pokaż pole wartości", + "value-int": "Liczba cyfr całkowitej części wartości", + "value-font": "Wartość czcionki tekstu", + "value-box-rect-stroke-color": "Kolor obrysu prostokąta pola wartości", + "value-box-rect-stroke-color-end": "Kolor obrysu prostokąta pola wartości — koniec gradientu", + "value-box-background-color": "Kolor tła pola wartości", + "value-box-shadow-color": "Kolor cienia pola wartości", + "plate-settings": "Ustawienia tarczy", + "show-plate-border": "Pokaż obramowanie tarczy", + "plate-color": "Kolor tarczy", + "needle-settings": "Ustawienia igły", + "needle-circle-size": "Rozmiar okręgu igły", + "needle-color": "Kolor igły", + "needle-color-end": "Kolor igły - gradient końcowy", + "needle-color-shadow-up": "Górna połowa koloru cienia igły", + "needle-color-shadow-down": "Kolor igły w cieniu", + "highlights-settings": "Ustawienia wyróżnień", + "highlights-width": "Szerokość wyróżnień", + "highlights": "Wyróżnienia", + "highlight-from": "Od", + "highlight-to": "Do", + "highlight-color": "Kolor", + "no-highlights": "Nie skonfigurowano żadnych wyróżnień", + "add-highlight": "Dodaj wyróżnienie", + "animation-settings": "Ustawienia animacji", + "enable-animation": "Włącz animację", + "animation-duration": "Czas trwania animacji", + "animation-rule": "Zasada animacji", + "animation-linear": "Liniowa", + "animation-quad": "Kwadrat", + "animation-quint": "Kwinta", + "animation-cycle": "Cykl", + "animation-bounce": "Odbicie", + "animation-elastic": "Elastyczny", + "animation-dequad": "Dekwadrat", + "animation-dequint": "Dekwinta", + "animation-decycle": "Decykl", + "animation-debounce": "Deodbicie", + "animation-delastic": "Delastyczny", + "linear-gauge-settings": "Ustawienia miernika liniowego", + "bar-stroke-width": "Szerokość obrysu paska", + "bar-stroke-color": "Kolor obrysu paska", + "bar-background-color": "Kolor tła paska wskaźników", + "bar-background-color-end": "Kolor tła paska - koniec gradientu", + "progress-bar-color": "Kolor paska postępu", + "progress-bar-color-end": "Kolor paska postępu - koniec gradientu", + "major-ticks-names": "Nazwy głównych znaczników", + "show-stroke-ticks": "Pokaż obrys znaczników", + "major-ticks-font": "Czcionka głównych znaczników", + "border-color": "Kolor ramki", + "border-width": "Szerokość ramki", + "needle-circle-color": "Kolor okręgu igły", + "animation-target": "Cel animacji", + "animation-target-needle": "Igła", + "animation-target-plate": "Tarcza", + "common-settings": "Zwykłe ustawienia miernika", + "gauge-type": "Typ miernika", + "gauge-type-arc": "Łuk", + "gauge-type-donut": "Pączek", + "gauge-type-horizontal-bar": "Poziomy pasek", + "gauge-type-vertical-bar": "Pionowy pasek", + "donut-start-angle": "Kąt rozpoczęcia", + "bar-settings": "Ustawienia paska miernika", + "relative-bar-width": "Względna szerokość paska", + "neon-glow-brightness": "Jasność efektu blasku neonu, (0-100), 0 - wyłączenie efektu", + "stripes-thickness": "Grubość pasków, 0 - brak pasków", + "rounded-line-cap": "Wyświetl zaokrąglony koniec linii", + "bar-color-settings": "Ustawienia koloru paska", + "use-precise-level-color-values": "Używaj precyzyjnych poziomów kolorów", + "bar-colors": "Kolory pasków, od dołu do góry", + "color": "Kolor", + "no-bar-colors": "Nie skonfigurowano kolorów pasków", + "add-bar-color": "Dodaj kolor paska", + "from": "Od", + "to": "Do", + "fixed-level-colors": "Kolory pasków przy użyciu wartości Edge", + "gauge-title-settings": "Ustawienia tytułu miernika", + "show-gauge-title": "Pokaż tytuł miernika", + "gauge-title": "Tytuł miernika", + "gauge-title-font": "Czcionka tytułu miernika", + "unit-title-and-timestamp-settings": "Ustawienia tytułu jednostki i znacznika czasu", + "show-timestamp": "Pokaż znacznik czasu wartości", + "timestamp-format": "Format znacznika czasu", + "label-font": "Czcionka etykiety wyświetlana pod wartością", + "value-settings": "Ustawienia wartości", + "show-value": "Pokaż tekst wartości", + "min-max-settings": "Minimalne/maksymalne ustawienia etykiet", + "show-min-max": "Pokaż wartości minimalne i maksymalne", + "min-max-font": "Czcionka etykiet minimalnych i maksymalnych", + "show-ticks": "Pokaż znaczniki", + "tick-width": "Szerokość znacznika", + "tick-color": "Kolor znacznika", + "tick-values": "Wartości znacznika", + "no-tick-values": "Nie skonfigurowano żadnych wartości znaczników", + "add-tick-value": "Dodaj wartość znacznika" }, - "gpio":{ - "pin":"Pin", - "label":"Label", - "row":"Row", - "column":"Column", - "color":"Color", - "panel-settings":"Panel settings", - "background-color":"Background color", - "gpio-switches":"GPIO switches", - "no-gpio-switches":"No GPIO switches configured", - "add-gpio-switch":"Add GPIO switch", - "gpio-status-request":"GPIO status request", - "method-name":"Method name", - "method-body":"Method body", - "gpio-status-change-request":"GPIO status change request", - "parse-gpio-status-function":"Parse gpio status function", - "gpio-leds":"GPIO leds", - "no-gpio-leds":"No GPIO leds configured", - "add-gpio-led":"Add GPIO led" + "gpio": { + "pin": "Pin", + "label": "Etykieta", + "row": "Wiersz", + "column": "Kolumna", + "color": "Kolor", + "panel-settings": "Ustawienia panelu", + "background-color": "Kolor tła", + "gpio-switches": "Przełączniki GPIO", + "no-gpio-switches": "Nie skonfigurowano żadnych przełączników GPIO", + "add-gpio-switch": "Dodaj przełącznik GPIO", + "gpio-status-request": "Żądanie statusu GPIO", + "method-name": "Nazwa metody", + "method-body": "Ciało metody", + "gpio-status-change-request": "Żądanie zmiany statusu GPIO", + "parse-gpio-status-function": "Przeanalizuj funkcję statusu gpio", + "gpio-leds": "Diody GPIO", + "no-gpio-leds": "Nie skonfigurowano żadnych diod GPIO", + "add-gpio-led": "Dodaj diodę GPIO" }, - "html-card":{ - "html":"HTML", - "css":"CSS" + "html-card": { + "html": "HTML", + "css": "CSS" }, - "input-widgets":{ - "attribute-not-allowed":"Parametr atrybutu nie może być używany w tym widżecie", - "blocked-location":"Geolokalizacja jest zablokowana w twojej przeglądarce", - "claim-device":"Zgłoś urządzenie", - "claim-failed":"Nie udało się zgłosić urządzenia!", - "claim-not-found":"Nie znaleziono urządzenia!", - "claim-successful":"Urządzenie zostało pomyślnie zgłoszone!", - "date":"Data", - "device-name":"Nazwa urządzenia", - "device-name-required":"Wymagana jest nazwa urządzenia", - "discard-changes":"Odrzuć zmiany", - "entity-attribute-required":"Wymagany jest atrybut encji", - "entity-coordinate-required":"Wymagane są obie pola, szerokość geograficzna i długość geograficzna", - "entity-timeseries-required":"Wymagany jest szereg czasowy encji", - "get-location":"Pobierz bieżącą lokalizację", - "invalid-date":"Nieprawidłowa data", - "latitude":"Szerokość geograficzna", - "longitude":"Długość geograficzna", - "min-value-error":"Wartość minimalna to {{value}}", - "max-value-error":"Wartość maksymalna to {{value}}", - "not-allowed-entity":"Wybrana encja nie może mieć współdzielonych atrybutów", - "no-attribute-selected":"Nie wybrano atrybutu", - "no-datakey-selected":"Nie wybrano klucza danych", - "no-coordinate-specified":"Nie określono klucza danych dla szerokości/długości geograficznej", - "no-entity-selected":"Nie wybrano encji", - "no-image":"Brak obrazu", - "no-support-geolocation":"Twoja przeglądarka nie obsługuje geolokalizacji", - "no-support-web-camera":"Twoja przeglądarka nie obsługuje kamer", - "enable-https-use-widget":"Proszę włączyć protokół HTTPS, aby korzystać z tego widżetu", - "no-found-your-camera":"Nie można znaleźć twojej kamery", - "no-permission-camera":"Użytkownik odmówił zgody / Ta witryna nie ma uprawnień do korzystania z kamery", - "no-timeseries-selected":"Nie wybrano szeregu czasowego", - "secret-key":"Tajny klucz", - "secret-key-required":"Wymagany jest tajny klucz", - "switch-attribute-value":"Przełącz wartość atrybutu encji", - "switch-camera":"Przełącz kamerę", - "switch-timeseries-value":"Przełącz wartość szeregu czasowego encji", - "take-photo":"Zrób zdjęcie", - "time":"Czas", - "timeseries-not-allowed":"Parametr szeregu czasowego nie może być używany w tym widżecie", - "update-failed":"Aktualizacja nieudana", - "update-successful":"Aktualizacja udana", - "update-attribute":"Zaktualizuj atrybut", - "update-timeseries":"Zaktualizuj szereg czasowy", - "value":"Wartość", - "general-settings":"Ustawienia ogólne", - "widget-title":"Tytuł widżetu", - "claim-button-label":"Etykieta przycisku zgłaszania", - "show-secret-key-field":"Pokaż pole wprowadzania „Tajny klucz”", - "labels-settings":"Ustawienia etykiet", - "show-labels":"Pokaż etykiety", - "device-name-label":"Etykieta pola wprowadzania nazwy urządzenia", - "secret-key-label":"Etykieta pola wprowadzania tajnego klucza", - "messages-settings":"Ustawienia wiadomości", - "claim-device-success-message":"Komunikat o udanym zgłoszeniu urządzenia", - "claim-device-not-found-message":"Komunikat, gdy urządzenie nie zostało znalezione", - "claim-device-failed-message":"Komunikat o nieudanym zgłoszeniu urządzenia", - "claim-device-name-required-message":"Komunikat błędu „Wymagana nazwa urządzenia”", - "claim-device-secret-key-required-message":"Komunikat błędu „Wymagany tajny klucz”", - "show-label":"Pokaż etykietę", - "label":"Etykieta", - "required":"Wymagane", - "required-error-message":"Komunikat błędu „Wymagane”", - "show-result-message":"Pokaż komunikat z wynikiem", - "integer-field-settings":"Ustawienia pola całkowitego", - "min-value":"Wartość minimalna", - "max-value":"Wartość maksymalna", - "double-field-settings":"Ustawienia pola liczbowego", - "text-field-settings":"Ustawienia pola tekstowego", - "min-length":"Minimalna długość", - "max-length":"Maksymalna długość", - "checkbox-settings":"Ustawienia pola wyboru", - "true-label":"Etykieta zaznaczenia", - "false-label":"Etykieta odznaczenia", - "image-input-settings":"Ustawienia wejścia obrazu", - "display-preview":"Wyświetl podgląd", - "display-clear-button":"Wyświetl przycisk Wyczyść", - "display-apply-button":"Wyświetl przycisk Zastosuj", - "display-discard-button":"Wyświetl przycisk Odrzuć", - "datetime-field-settings":"Ustawienia pola daty/czasu", - "display-time-input":"Wyświetl pole czasowe", - "latitude-key-name":"Nazwa klucza szerokości geograficznej", - "longitude-key-name":"Nazwa klucza długości geograficznej", - "show-get-location-button":"Pokaż przycisk „Pobierz bieżącą lokalizację”", - "use-high-accuracy":"Użyj wysokiej dokładności", - "location-fields-settings":"Ustawienia pól lokalizacji", - "latitude-label":"Etykieta dla szerokości geograficznej", - "longitude-label":"Etykieta dla długości geograficznej", - "input-fields-alignment":"Wyrównanie pól wprowadzania", - "input-fields-alignment-column":"Kolumna (domyślnie)", - "input-fields-alignment-row":"Rząd", - "layout":"Układ", - "row-gap":"Odstęp między rzędami w pikselach", - "column-gap":"Odstęp między kolumnami w pikselach", - "latitude-field-required":"Pole szerokości geograficznej jest wymagane", - "longitude-field-required":"Pole długości geograficznej jest wymagane", - "attribute-settings":"Ustawienia atrybutu", - "widget-mode":"Tryb widżetu", - "widget-mode-update-attribute":"Zaktualizuj atrybut", - "widget-mode-update-timeseries":"Zaktualizuj szereg czasowy", - "attribute-scope":"Zakres atrybutu", - "attribute-scope-server":"Atrybut serwera (domyślnie)", - "attribute-scope-shared":"Współdzielony atrybut", - "value-required":"Wartość wymagana", - "image-settings":"Ustawienia obrazu", - "image-format":"Format obrazu", - "image-format-jpeg":"JPEG", - "image-format-png":"PNG", - "image-format-webp":"WEBP", - "image-quality":"Jakość obrazu stosująca kompresję stratną, np. jpeg i webp", - "max-image-width":"Maksymalna szerokość obrazu", - "max-image-height":"Maksymalna wysokość obrazu", - "action-buttons":"Przyciski akcji", - "show-action-buttons":"Pokaż przyciski akcji", - "update-all-values":"Zaktualizuj wszystkie wartości, nie tylko zmodyfikowane", - "save-button-label":"Etykieta przycisku „ZAPISZ”", - "reset-button-label":"Etykieta przycisku „COFNIJ”", - "group-settings":"Ustawienia grupy", - "show-group-title":"Pokaż tytuł grupy pól związanych z różnymi encjami", - "group-title":"Tytuł grupy", - "fields-alignment":"Wyrównanie pól", - "fields-alignment-row":"Rząd (domyślnie)", - "fields-alignment-column":"Kolumna", - "fields-in-row":"Liczba pól w rzędzie", - "option-value":"Wartość (wpisz „null”, aby utworzyć pustą opcję)", - "option-label":"Etykieta", - "hide-input-field":"Ukryj pole wprowadzania", - "datakey-type":"Typ klucza danych", - "datakey-type-server":"Atrybut serwera (domyślnie)", - "datakey-type-shared":"Współdzielony atrybut", - "datakey-type-timeseries":"Szereg czasowy", - "datakey-value-type":"Typ wartości klucza danych", - "datakey-value-type-string":"Ciąg znaków", - "datakey-value-type-double":"Liczba zmiennoprzecinkowa", - "datakey-value-type-integer":"Liczba całkowita", - "datakey-value-type-json":"JSON", - "datakey-value-type-boolean-checkbox":"Boolean (Pole wyboru)", - "datakey-value-type-boolean-switch":"Boolean (Przełącznik)", - "datakey-value-type-date-time":"Data i godzina", - "datakey-value-type-date":"Data", - "datakey-value-type-time":"Czas", - "datakey-value-type-select":"Wybierz", - "datakey-value-type-color":"Kolor", - "value-is-required":"Wartość jest wymagana", - "ability-to-edit-attribute":"Zdolność do edycji atrybutu", - "ability-to-edit-attribute-editable":"Edytowalny (domyślnie)", - "ability-to-edit-attribute-disabled":"Wyłączony", - "ability-to-edit-attribute-readonly":"Tylko do odczytu", - "disable-on-datakey-name":"Wyłącz na fałszywą wartość innego klucza danych (podaj nazwę klucza danych)", - "field-appearance":"Wygląd pola", - "appearance-fill":"Wypełnienie", - "appearance-outline":"Obrys", - "subscript-sizing":"Rozmiar indeksu dolnego", - "subscript-sizing-fixed":"Stały", - "subscript-sizing-dynamic":"Dynamiczny", - "slide-toggle-settings":"Ustawienia przełącznika", - "slide-toggle-label-position":"Pozycja etykiety przełącznika", - "slide-toggle-label-position-after":"Po", - "slide-toggle-label-position-before":"Przed", - "select-options":"Opcje wyboru", - "no-select-options":"Brak skonfigurowanych opcji wyboru", - "add-select-option":"Dodaj opcję wyboru", - "numeric-field-settings":"Ustawienia pola numerycznego", - "step-interval":"Interwał kroku pomiędzy wartościami", - "error-messages":"Komunikaty o błędach", - "min-value-error-message":"Komunikat o błędzie 'Wartość minimalna'", - "max-value-error-message":"Komunikat o błędzie 'Wartość maksymalna'", - "invalid-date-error-message":"Komunikat o błędzie 'Nieprawidłowa data'", - "invalid-JSON-error-message":"Komunikat o błędzie 'Nieprawidłowy JSON'", - "icon-settings":"Ustawienia ikony", - "dialog-editor-settings":"Ustawienia edytora dialogu", - "use-custom-icon":"Użyj niestandardowej ikony", - "input-cell-icon":"Ikona do wyświetlenia przed komórką wejściową", - "value-conversion-settings":"Ustawienia konwersji wartości", - "get-value-settings":"Ustawienia pobierania wartości", - "use-get-value-function":"Użyj funkcji getValue", - "get-value-function":"Funkcja getValue", - "set-value-settings":"Ustawienia ustawiania wartości", - "use-set-value-function":"Użyj funkcji setValue", - "set-value-function":"Funkcja setValue", - "json-invalid":"Wartość JSON ma nieprawidłowy format", - "title":"Tytuł", - "cancel-button-label":"Etykieta przycisku 'Anuluj'" + "input-widgets": { + "attribute-not-allowed": "W tym widżecie nie można używać parametru atrybutu", + "blocked-location": "Geolokalizacja jest zablokowana w Twojej przeglądarce", + "claim-device": "Przypisz urządzenie", + "claim-failed": "Nie udało się przypisać urządzenia!", + "claim-not-found": "Urządzenie nie zostało znalezione!", + "claim-successful": "Urządzenie zostało pomyślnie przypisane!", + "date": "Data", + "device-name": "Nazwa urządzenia", + "device-name-required": "Nazwa urządzenia jest wymagana", + "discard-changes": "Odrzuć zmiany", + "entity-attribute-required": "Atrybut obiektu jest wymagany", + "entity-coordinate-required": "Obydwa pola, szerokość i długość geograficzna, są wymagane", + "entity-timeseries-required": "Szeregi czasowe obiektu są wymagane", + "get-location": "Uzyskaj aktualną lokalizację", + "invalid-date": "Nieprawidłowa data", + "latitude": "Szerokość geograficzna", + "longitude": "Długość geograficzna", + "min-value-error": "Wartość minimalna to {{value}}", + "max-value-error": "Maksymalna wartość to {{value}}", + "not-allowed-entity": "Wybrany obiekt nie może mieć wspólnych atrybutów", + "no-attribute-selected": "Nie wybrano żadnego atrybutu", + "no-datakey-selected": "Nie wybrano klucza danych", + "no-coordinate-specified": "Nie określono klucza danych dla szerokości/długości geograficznej", + "no-entity-selected": "Nie wybrano żadnego obiektu", + "no-image": "Brak obrazka", + "no-support-geolocation": "Twoja przeglądarka nie obsługuje geolokalizacji", + "no-support-web-camera": "Twoja przeglądarka nie obsługuje kamer", + "enable-https-use-widget": "Aby korzystać z tego widżetu, włącz protokół HTTPS", + "no-found-your-camera": "Nie można znaleźć aparatu", + "no-permission-camera": "Użytkownik odmówił pozwolenia / Ta witryna nie ma uprawnień do korzystania z kamery", + "no-timeseries-selected": "Nie wybrano szeregów czasowych", + "secret-key": "Tajny klucz", + "secret-key-required": "Tajny klucz jest wymagany", + "switch-attribute-value": "Zmień wartość atrybutu obiektu", + "switch-camera": "Przełącz aparat", + "switch-timeseries-value": "Zmień wartość szeregu czasowego obiektu", + "take-photo": "Zrób zdjęcie", + "time": "Czas", + "timeseries-not-allowed": "W tym widżecie nie można używać parametru Timeseries", + "update-failed": "Aktualizacja nie powiodła się", + "update-successful": "Aktualizacja powiodła się", + "update-attribute": "Aktualizuj atrybut", + "update-timeseries": "Zaktualizuj szeregi czasowe", + "value": "Wartość", + "general-settings": "Ustawienia główne", + "widget-title": "Tytul widżetu", + "claim-button-label": "Przypisywnie etykiety przycisku", + "show-secret-key-field": "Pokaż pole wejściowe 'Tajny klucz'", + "labels-settings": "Ustawienia etykiet", + "show-labels": "Pokaż etykiety", + "device-name-label": "Etykieta pola wprowadzania nazwy urządzenia", + "secret-key-label": "Etykieta pola wprowadzania tajnego klucza", + "messages-settings": "Ustawienia wiadomości", + "claim-device-success-message": "Wiadomość tekstowa o pomyślnym przypisaniu urządzenia", + "claim-device-not-found-message": "Wiadomość tekstowa w przypadku nie znalezienia urządzenia", + "claim-device-failed-message": "Wiadomość tekstowa z informacją o nieudanym przypisaniu urządzenia", + "claim-device-name-required-message": "Komunikat o błędzie „Wymagana nazwa urządzenia”", + "claim-device-secret-key-required-message": "Komunikat o błędzie „Wymagany tajny klucz”", + "relations-settings": "Ustawienia relacji", + "relate-device-to-state-entity": "Powiąż urządzenie z obiektem bieżącego stanu", + "relate-direction": "Kierunek odniesienia", + "relate-direction-from": "Od", + "relate-direction-to": "Do", + "relation-type": "Typ relacji", + "show-label": "Pokaż etykietę", + "label": "Etykieta", + "required": "Wymagany", + "required-error-message": "Komunikat o błędzie „Wymagany”", + "show-result-message": "Pokaż komunikat o wynikach", + "integer-field-settings": "Ustawienia pola całkowitego", + "min-value": "Wartość minimalna", + "max-value": "Wartość maksymalna", + "double-field-settings": "Ustawienia podwójnego pola", + "text-field-settings": "Ustawienia pola tekstowego", + "min-length": "Minimalna długość", + "max-length": "Maksymalna długość", + "checkbox-settings": "Ustawienia pola wyboru", + "true-label": "Sprawdzona etykieta", + "false-label": "Niesprawdzona etykieta", + "image-input-settings": "Ustawienia wejścia obrazu", + "display-preview": "Wyświetl podgląd", + "display-clear-button": "Przycisk czyszczenia wyświetlacza", + "display-apply-button": "Wyświetl przycisk zastosowania", + "display-discard-button": "Wyświetl przycisk odrzucania", + "datetime-field-settings": "Ustawienia pola daty/godziny", + "display-time-input": "Wyświetl wprowadzony czas", + "latitude-key-name": "Nazwa klucza szerokości geograficznej", + "longitude-key-name": "Nazwa klucza długości geograficznej", + "show-get-location-button": "Pokaż przycisk „Pobierz aktualną lokalizację”", + "use-high-accuracy": "Użyj wysokiej dokładności", + "location-fields-settings": "Ustawienia pól lokalizacji", + "latitude-label": "Etykieta szerokości geograficznej", + "longitude-label": "Etykieta długości geograficznej", + "input-fields-alignment": "Wyrównanie pól wejściowych", + "input-fields-alignment-column": "Kolumna (domyślna)", + "input-fields-alignment-row": "Wiersz", + "layout": "Layout", + "row-gap": "Gap between rows in pixels", + "column-gap": "Gap between columns in pixels", + "latitude-field-required": "Wymagane pole szerokości geograficznej", + "longitude-field-required": "Wymagane pole długości geograficznej", + "attribute-settings": "Ustawienia atrybutów", + "widget-mode": "Tryb widgetu", + "widget-mode-update-attribute": "Aktualizuj atrybut", + "widget-mode-update-timeseries": "Zaktualizuj szeregi czasowe", + "attribute-scope": "Zakres atrybutu", + "attribute-scope-server": "Atrybut serwera", + "attribute-scope-shared": "Wspólny atrybut", + "value-required": "Wymagana wartość", + "image-settings": "Ustawienia obrazu", + "image-format": "Format obrazu", + "image-format-jpeg": "JPG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "Jakość obrazu korzystająca z kompresji stratnej, np. JPEG i WebP", + "max-image-width": "Maksymalna szerokość obrazu", + "max-image-height": "Maksymalna wysokość obrazu", + "action-buttons": "Przyciski akcji", + "show-action-buttons": "Pokaż przyciski akcji", + "update-all-values": "Zaktualizuj wszystkie wartości, a nie tylko zmodyfikowane", + "save-button-label": "Etykieta przycisku „ZAPISZ”", + "reset-button-label": "Etykieta przycisku „COFNIJ”", + "group-settings": "Ustawienia grupy", + "show-group-title": "Pokaż tytuł grupy pól powiązanych z różnymi obiektami", + "group-title": "Tytuł grupy", + "fields-alignment": "Wyrównanie pól", + "fields-alignment-row": "Wiersz (domyślny)", + "fields-alignment-column": "Kolumna", + "fields-in-row": "Liczba pól w wierszu", + "option-value": "Wartość (wpisz „null”, aby utworzyć pustą opcję)", + "option-label": "Etykieta", + "hide-input-field": "Ukryj pole wprowadzania", + "datakey-type": "Typ klucza danych", + "datakey-type-server": "Atrybut serwera (domyślny)", + "datakey-type-shared": "Wspólny atrybut", + "datakey-type-timeseries": "Szereg czasowy", + "datakey-value-type": "Typ wartości klucza danych", + "datakey-value-type-string": "Łańcuch znaków", + "datakey-value-type-double": "Liczba zmiennoprzecinkowa podwójnej precyzji", + "datakey-value-type-integer": "Liczba całkowita", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "Wartość logiczna (pole wyboru)", + "datakey-value-type-boolean-switch": "Wartość logiczna (przełącznik)", + "datakey-value-type-date-time": "Data i czas", + "datakey-value-type-date": "Data", + "datakey-value-type-time": "Czas", + "datakey-value-type-select": "Wybierz", + "datakey-value-type-color": "Color", + "value-is-required": "Wartość jest wymagana", + "ability-to-edit-attribute": "Możliwość edycji atrybutu", + "ability-to-edit-attribute-editable": "Edytowalne (domyślne)", + "ability-to-edit-attribute-disabled": "Wyłączone", + "ability-to-edit-attribute-readonly": "Tylko do odczytu", + "disable-on-datakey-name": "Wyłącz w przypadku fałszywej wartości innego klucza danych (podaj nazwę klucza danych)", + "field-appearance": "Field appearance", + "appearance-fill": "Fill", + "appearance-outline": "Outline", + "subscript-sizing": "Subscript sizing", + "subscript-sizing-fixed": "Fixed", + "subscript-sizing-dynamic": "Dynamic", + "slide-toggle-settings": "Ustawienia przełączania suwaka", + "slide-toggle-label-position": "Przesuń przełącznik pozycji etykiety", + "slide-toggle-label-position-after": "Po", + "slide-toggle-label-position-before": "Przed", + "select-options": "Wybierz opcje", + "no-select-options": "Nie skonfigurowano opcji wyboru", + "add-select-option": "Dodaj opcję wyboru", + "numeric-field-settings": "Ustawienia pola numerycznego", + "step-interval": "Odstęp kroku między wartościami", + "error-messages": "Komunikaty o błędach", + "min-value-error-message": "Komunikat o błędzie „Wartość minimalna”", + "max-value-error-message": "Komunikat o błędzie „Wartość maksymalna”", + "invalid-date-error-message": "Komunikat o błędzie „Nieprawidłowa data”", + "invalid-JSON-error-message": "Komunikat o błędzie „Nieprawidłowy JSON”", + "icon-settings": "Ustawienia ikon", + "dialog-editor-settings": "Ustawienia edytora okien dialogowych", + "use-custom-icon": "Użyj niestandardowej ikony", + "input-cell-icon": "Ikona wyświetlana przed komórką wejściową", + "value-conversion-settings": "Ustawienia konwersji wartości", + "get-value-settings": "Uzyskaj ustawienia wartości", + "use-get-value-function": "Użyj funkcji getValue", + "get-value-function": "funkcja getValue", + "set-value-settings": "Ustaw ustawienia wartości", + "use-set-value-function": "Użyj funkcji setValue", + "set-value-function": "funkcja setValue", + "json-invalid": "Wartość JSON ma nieprawidłowy format", + "title": "Tytuł", + "cancel-button-label": "Etykieta przycisku „Anuluj”" }, - "invalid-qr-code-text":"Nieprawidłowy tekst wejściowy dla kodu QR. Wejście powinno być typu string.", - "qr-code":{ - "use-qr-code-text-function":"Użyj funkcji tekstowej kodu QR", - "qr-code-text-pattern":"Wzór tekstu kodu QR (np. '${entityName} | ${keyName} - jakiś tekst.')", - "qr-code-text-pattern-hint":"Wzór tekstu kodu QR używa wartości pierwszego znalezionego klucza w encjach w aliasie encji.", - "qr-code-text-pattern-required":"Wzór tekstu kodu QR jest wymagany.", - "qr-code-text-function":"Funkcja tekstu kodu QR" + "invalid-qr-code-text": "Nieprawidłowy tekst wejściowy dla kodu QR. Wartość wpisana powinna być łańcuchem znaków", + "qr-code": { + "use-qr-code-text-function": "Użyj funkcji tekstowej kodu QR", + "qr-code-text-pattern": "Wzór tekstu kodu QR (np. '${entityName} | ${keyName} – jakiś tekst.')", + "qr-code-text-pattern-hint": "Wzorzec tekstu kodu QR wykorzystuje wartość pierwszego znalezionego klucza w obiektach w aliasie obiektu", + "qr-code-text-pattern-required": "Wymagany jest wzór tekstu kodu QR", + "qr-code-text-function": "Funkcja tekstu kodu QR" }, - "label-widget":{ - "label-pattern":"Wzór", - "label-pattern-hint":"Wskazówka: na przykład 'Tekst ${keyName} jednostki.' lub ${#<index klucza>} jednostki'", - "label-pattern-required":"Wzór jest wymagany", - "label-position":"Położenie (Procentowo w stosunku do tła)", - "x-pos":"X", - "y-pos":"Y", - "background-color":"Kolor tła", - "font-settings":"Ustawienia czcionki", - "background-image":"Obraz tła", - "labels":"Etykiety", - "no-labels":"Brak skonfigurowanych etykiet", - "add-label":"Dodaj etykietę" + "label-widget": { + "label-pattern": "Wzór", + "label-pattern-hint": "Podpowiedź: np. 'Text ${keyName} units.' or ${#<key index>} units'", + "label-pattern-required": "Wzór jest wymagany", + "label-position": "Pozycja (procent w stosunku do tła)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "Kolor tła", + "font-settings": "Ustawienia czcionki", + "background-image": "Zdjęcie w tle", + "labels": "Etykiety", + "no-labels": "Nie skonfigurowano żadnych etykiet", + "add-label": "Dodaj etykietę" }, - "navigation":{ - "title":"Tytuł", - "navigation-path":"Ścieżka nawigacji", - "filter-type":"Typ filtru", - "filter-type-all":"Wszystkie elementy", - "filter-type-include":"Zawiera elementy", - "filter-type-exclude":"Wyklucza elementy", - "items":"Elementy", - "enter-urls-to-filter":"Wprowadź adresy URL do filtrowania..." + "navigation": { + "title": "Tytuł", + "navigation-path": "Ścieżka nawigacji", + "filter-type": "Typ filtra", + "filter-type-all": "Wszystkie przedmioty", + "filter-type-include": "Uwzględnij elementy", + "filter-type-exclude": "Wyklucz elementy", + "items": "Rzeczy", + "enter-urls-to-filter": "Wprowadź adresy URL, aby filtrować..." }, - "persistent-table":{ - "rpc-id":"ID RPC", - "message-type":"Typ wiadomości", - "method":"Metoda", - "params":"Parametry", - "created-time":"Czas utworzenia", - "expiration-time":"Czas wygaśnięcia", - "retries":"Próby", - "status":"Status", - "filter":"Filtr", - "refresh":"Odśwież", - "add":"Dodaj żądanie RPC", - "details":"Szczegóły", - "delete":"Usuń", - "delete-request-title":"Usuń trwałe żądanie RPC", - "delete-request-text":"Czy na pewno chcesz usunąć to żądanie?", - "details-title":"Szczegóły ID RPC: ", - "additional-info":"Dodatkowe informacje", - "response":"Odpowiedź", - "any-status":"Dowolny status", - "rpc-status-list":"Lista statusów RPC", - "no-request-prompt":"Brak żądań do wyświetlenia", - "send-request":"Wyślij żądanie", - "add-title":"Utwórz trwałe żądanie RPC", - "method-error":"Wymagana jest metoda.", - "timeout-error":"Minimalna wartość czasu oczekiwania to 5000 (5 sekund).", - "white-space-error":"Biała przestrzeń nie jest dozwolona.", - "rpc-status":{ - "QUEUED":"W KOLEJCE", - "SENT":"WYSŁANE", - "DELIVERED":"DOSTARCZONE", - "SUCCESSFUL":"UDANE", - "TIMEOUT":"PRZEKROCZONY LIMIT CZASU", - "EXPIRED":"WYGASŁE", - "FAILED":"NIEUDANE" + "persistent-table": { + "rpc-id": "Identyfikator RPC", + "message-type": "Typ wiadomości", + "method": "Metoda", + "params": "Parametry", + "created-time": "Stworzony czas", + "expiration-time": "Data ważności", + "retries": "Ponowne próby", + "status": "Status", + "filter": "Filtr", + "refresh": "Odśwież", + "add": "Dodaj żądanie RPC", + "details": "Szczegóły", + "delete": "Usuń", + "delete-request-title": "Usuń trwałe żądanie RPC", + "delete-request-text": "Czy na pewno chcesz usunąć prośbę?", + "details-title": "Szczegóły Identyfikator RPC: ", + "additional-info": "dodatkowe informacje", + "response": "Odpowiedź", + "any-status": "Dowolny status", + "rpc-status-list": "Lista stanu RPC", + "no-request-prompt": "Brak żądania wyświetlenia", + "send-request": "Wyślij prośbę", + "add-title": "Utwórz trwałe żądanie RPC", + "method-error": "Metoda jest wymagana.", + "timeout-error": "Minimalna wartość limitu czasu wynosi 5000 (5 sekund).", + "white-space-error": "Znak biały jest niedozwolony.", + "rpc-status": { + "QUEUED": "W KOLEJCE", + "SENT": "WYSŁANO", + "DELIVERED": "DOSTARCZONY", + "SUCCESSFUL": "SUKCES", + "TIMEOUT": "KONIEC CZASU", + "EXPIRED": "WYGASŁY", + "FAILED": "NIEUDANY" }, - "rpc-search-status-all":"WSZYSTKO", - "message-types":{ - "false":"Dwukierunkowe", - "true":"Jednokierunkowe" + "rpc-search-status-all": "WSZYSTKO", + "message-types": { + "false": "Dwukierunkowe", + "true": "Jednokierunkowa" }, "general-settings":"Ustawienia ogólne", "enable-filter":"Włącz filtr", @@ -5930,778 +7236,752 @@ "fill-area":"Wypełnij obszar", "range-chart-style":"Styl wykresu zakresu" }, - "rpc":{ - "value-settings":"Ustawienia wartości", - "initial-value":"Początkowa wartość", - "retrieve-value-settings":"Ustawienia pobierania włączonej/wyłączonej wartości", - "retrieve-value-method":"Pobierz wartość za pomocą metody", - "retrieve-value-method-none":"Nie pobieraj", - "retrieve-value-method-rpc":"Wywołaj metodę pobierania wartości RPC", - "retrieve-value-method-attribute":"Subskrybuj atrybut", - "retrieve-value-method-timeseries":"Subskrybuj szereg czasowy", - "attribute-value-key":"Klucz atrybutu", - "timeseries-value-key":"Klucz szeregu czasowego", - "get-value-method":"Metoda RPC pobierz wartość", - "parse-value-function":"Funkcja analizy wartości", - "update-value-settings":"Ustawienia aktualizacji wartości", - "set-value-method":"Metoda RPC ustaw wartość", - "convert-value-function":"Funkcja konwersji wartości", - "rpc-settings":"Ustawienia RPC", - "request-timeout":"Limit czasu żądania RPC (ms)", - "persistent-rpc-settings":"Ustawienia trwałego RPC", - "request-persistent":"Trwałe żądanie RPC", - "persistent-polling-interval":"Interwał odpytywania (ms) w celu uzyskania odpowiedzi na trwałe polecenie RPC", - "common-settings":"Wspólne ustawienia", - "switch-title":"Tytuł przełącznika", - "show-on-off-labels":"Pokaż etykiety włącz/wyłącz", - "slide-toggle-label":"Etykieta przycisku przełącznika", - "label-position":"Pozycja etykiety", - "label-position-before":"Przed", - "label-position-after":"Po", - "slider-color":"Kolor suwaka", - "slider-color-primary":"Podstawowy", - "slider-color-accent":"Akcent", - "slider-color-warn":"Ostrzeżenie", - "button-style":"Styl przycisku", - "button-raised":"Podniesiony przycisk", - "button-primary":"Podstawowy kolor", - "button-background-color":"Kolor tła przycisku", - "button-text-color":"Kolor tekstu przycisku", - "widget-title":"Tytuł widżetu", - "button-label":"Etykieta przycisku", - "device-attribute-scope":"Zakres atrybutu urządzenia", - "server-attribute":"Atrybut serwera", - "shared-attribute":"Atrybut wspólny", - "device-attribute-parameters":"Parametry atrybutu urządzenia", - "is-one-way-command":"Jest to polecenie jednokierunkowe", - "rpc-method":"Metoda RPC", - "rpc-method-params":"Parametry metody RPC", - "show-rpc-error":"Pokaż błąd wykonania polecenia RPC", - "led-title":"Tytuł diody LED", - "led-color":"Kolor diody LED", - "check-status-settings":"Ustawienia sprawdzania statusu", - "perform-rpc-status-check":"Wykonaj sprawdzanie statusu urządzenia RPC", - "retrieve-led-status-value-method":"Pobierz stan diody LED za pomocą metody", - "led-status-value-attribute":"Atrybut urządzenia zawierający stan diody LED", - "led-status-value-timeseries":"Szereg czasowy urządzenia zawierający stan diody LED", - "check-status-method":"Metoda sprawdzania statusu urządzenia RPC", - "parse-led-status-value-function":"Funkcja analizy stanu diody LED", - "knob-title":"Tytuł pokrętła", - "min-value":"Minimalna wartość", - "max-value":"Maksymalna wartość" + "rpc": { + "value-settings": "Ustawienia wartości", + "initial-value": "Wartość początkowa", + "retrieve-value-settings": "Pobierz ustawienia wartości włączenia/wyłączenia", + "retrieve-value-method": "Pobierz wartość za pomocą metody", + "retrieve-value-method-none": "Nie odzyskuj", + "retrieve-value-method-rpc": "Wywołaj metodę pobierania wartości RPC", + "retrieve-value-method-attribute": "Subskrybuj atrybut", + "retrieve-value-method-timeseries": "Subskrybuj serie czasowe", + "attribute-value-key": "Klucz atrybutu", + "timeseries-value-key": "Klucz do serii czasowych", + "get-value-method": "Metoda pobierania wartości RPC", + "parse-value-function": "Przeanalizuj funkcję wartości", + "update-value-settings": "Zaktualizuj ustawienia wartości", + "set-value-method": "Metoda wartości zadanej RPC", + "convert-value-function": "Konwertuj funkcję wartości", + "rpc-settings": "Ustawienia RPC", + "request-timeout": "Limit czasu żądania RPC (ms)", + "persistent-rpc-settings": "Trwałe ustawienia RPC", + "request-persistent": "Żądanie RPC trwałe", + "persistent-polling-interval": "Interwał odpytywania (ms) w celu uzyskania trwałej odpowiedzi na polecenie RPC", + "common-settings": "Podstawowe ustawienia", + "switch-title": "Zmień tytuł", + "show-on-off-labels": "Pokaż etykiety włączania/wyłączania", + "slide-toggle-label": "Przesuń etykietę przełączania", + "label-position": "Pozycja etykiety", + "label-position-before": "Przed", + "label-position-after": "Po", + "slider-color": "Kolor suwaka", + "slider-color-primary": "Podstawowy", + "slider-color-accent": "Akcent", + "slider-color-warn": "Ostrzegaj", + "button-style": "Styl guzika", + "button-raised": "Podniesiony przycisk", + "button-primary": "Kolor podstawowy", + "button-background-color": "Kolor tła przycisku", + "button-text-color": "Kolor tekstu przycisku", + "widget-title": "Tytuł widżetu", + "button-label": "Etykieta przycisku", + "device-attribute-scope": "Zakres atrybutów urządzenia", + "server-attribute": "Atrybut serwera", + "shared-attribute": "Wspólny atrybut", + "device-attribute-parameters": "Parametry atrybutów urządzenia", + "is-one-way-command": "Jest poleceniem jednokierunkowym", + "rpc-method": "Metoda RPC", + "rpc-method-params": "Parametry metody RPC", + "show-rpc-error": "Pokaż błąd wykonania polecenia RPC", + "led-title": "Tytuł diody LED", + "led-color": "Kolor diody LED", + "check-status-settings": "Sprawdź ustawienia stanu", + "perform-rpc-status-check": "Wykonaj sprawdzenie stanu urządzenia RPC", + "retrieve-led-status-value-method": "Pobierz wartość stanu diody LED za pomocą metody", + "led-status-value-attribute": "Atrybut urządzenia zawierający wartość stanu diody LED", + "led-status-value-timeseries": "Szereg czasowy urządzenia zawierający wartość stanu diody LED", + "check-status-method": "Metoda sprawdzania stanu urządzenia przez RPC", + "parse-led-status-value-function": "Przeanalizuj funkcję wartości stanu diody LED", + "knob-title": "Tytuł pokrętła", + "min-value": "Minimalna wartość", + "max-value": "Maksymalna wartość" }, - "maps":{ - "select-entity":"Wybierz jednostkę", - "select-entity-hint":"Wskazówka: po wybraniu kliknij na mapie, aby ustawić pozycję", - "tooltips":{ - "placeMarker":"Kliknij, aby umieścić jednostkę '{{entityName}}'", - "firstVertex":"Wielokąt dla '{{entityName}}': kliknij, aby umieścić pierwszy punkt", - "firstVertex-cut":"Kliknij, aby umieścić pierwszy punkt", - "continueLine":"Wielokąt dla '{{entityName}}': kliknij, aby kontynuować rysowanie", - "continueLine-cut":"Kliknij, aby kontynuować rysowanie", - "finishLine":"Kliknij na istniejący znacznik, aby zakończyć", - "finishPoly":"Wielokąt dla '{{entityName}}': kliknij na pierwszy znacznik, aby zakończyć i zapisać", - "finishPoly-cut":"Kliknij na pierwszy znacznik, aby zakończyć i zapisać", - "finishRect":"Wielokąt dla '{{entityName}}': kliknij, aby zakończyć i zapisać", - "startCircle":"Okrąg dla '{{entityName}}': kliknij, aby umieścić środek okręgu", - "finishCircle":"Okrąg dla '{{entityName}}': kliknij, aby zakończyć okrąg", - "placeCircleMarker":"Kliknij, aby umieścić znacznik okręgu" + "maps": { + "select-entity": "Wybierz obiekt", + "select-entity-hint": "Wskazówka: po dokonaniu wyboru kliknij na mapę, aby ustawić pozycję", + "tooltips": { + "placeMarker": "Kliknij, aby umieścić obiekt '{{entityName}}'", + "firstVertex": "Poligon dla '{{entityName}}': kliknij, aby umieścić pierwszy punkt", + "firstVertex-cut": "Kliknij, aby umieścić pierwszy punkt", + "continueLine": "Poligon dla '{{entityName}}': kliknij, aby kontynuować rysowanie", + "continueLine-cut": "Kliknij, aby kontynuować rysowanie", + "finishLine": "Aby zakończyć, kliknij dowolny istniejący znacznik", + "finishPoly": "Poligon dla '{{entityName}}': kliknij pierwszy znacznik, aby zakończyć i zapisać", + "finishPoly-cut": "Kliknij pierwszy znacznik, aby zakończyć i zapisać", + "finishRect": "Poligon dla '{{entityName}}': kliknij, aby zakończyć i zapisać", + "startCircle": "Okrąg dla '{{entityName}}': kliknij, aby umieścić środek okręgu", + "finishCircle": "Okrąg dla '{{entityName}}': kliknij, aby zakończyć okrąg", + "placeCircleMarker": "Kliknij, aby umieścić znacznik okręgu" }, - "actions":{ - "finish":"Zakończ", - "cancel":"Anuluj", - "removeLastVertex":"Usuń ostatni punkt" + "actions": { + "finish": "Zakończ", + "cancel": "Anuluj", + "removeLastVertex": "Usuń ostatni punkt" }, - "buttonTitles":{ - "drawMarkerButton":"Umieść obiekt", - "drawPolyButton":"Utwórz wielokąt", - "drawLineButton":"Utwórz linię", - "drawCircleButton":"Utwórz okrąg", - "drawRectButton":"Utwórz prostokąt", - "editButton":"Tryb edycji", - "dragButton":"Tryb przeciągania", - "cutButton":"Odcinanie obszaru wielokąta", - "deleteButton":"Usuń", - "drawCircleMarkerButton":"Utwórz znacznik okręgu", - "rotateButton":"Obróć wielokąt" + "buttonTitles": { + "drawMarkerButton": "Umieść obiekt", + "drawPolyButton": "Utwórz poligon", + "drawLineButton": "Utwórz polilinię", + "drawCircleButton": "Utwórz okrąg", + "drawRectButton": "Utwórz prostokąt", + "editButton": "Tryb edycji", + "dragButton": "Tryb przeciągnij i upuść", + "cutButton": "Wytnij obszar poligonu", + "deleteButton": "Usuń", + "drawCircleMarkerButton": "Utwórz znacznik okręgu", + "rotateButton": "Obróć poligon" }, - "map-provider-settings":"Ustawienia dostawcy map", - "map-provider":"Dostawca map", - "map-provider-google":"Google Maps", - "map-provider-openstreet":"OpenStreet Maps", - "map-provider-here":"HERE Maps", - "map-provider-image":"Mapa obrazu", - "map-provider-tencent":"Tencent Maps", - "openstreet-provider":"Dostawca mapy OpenStreet", - "openstreet-provider-mapnik":"OpenStreetMap.Mapnik (Domyślny)", - "openstreet-provider-hot":"OpenStreetMap.HOT", - "openstreet-provider-esri-street":"Esri.WorldStreetMap", - "openstreet-provider-esri-topo":"Esri.WorldTopoMap", - "openstreet-provider-esri-imagery":"Esri.WorldImagery", - "openstreet-provider-cartodb-positron":"CartoDB.Positron", - "openstreet-provider-cartodb-dark-matter":"CartoDB.DarkMatter", - "use-custom-provider":"Użyj dostawcy niestandardowego", - "custom-provider-tile-url":"Niestandardowy adres URL kafelka dostawcy", - "google-maps-api-key":"Klucz API Google Maps", - "default-map-type":"Domyślny rodzaj mapy", - "google-map-type-roadmap":"Mapa drogowa", - "google-map-type-satelite":"Satelita", - "google-map-type-hybrid":"Hybrydowa", - "google-map-type-terrain":"Tereno", - "map-layer":"Warstwa mapy", - "here-map-normal-day":"HERE.normalDay (Domyślna)", - "here-map-normal-night":"HERE.normalNight", - "here-map-hybrid-day":"HERE.hybridDay", - "here-map-terrain-day":"HERE.terrainDay", - "credentials":"Poświadczenia", - "here-app-id":"ID aplikacji HERE", - "here-app-code":"Kod aplikacji HERE", - "here-api-key":"Klucz API HERE", - "here-use-new-version-api-3":"Użyj wersji API 3", - "tencent-maps-api-key":"Klucz API Tencent Maps", - "tencent-map-type-roadmap":"Mapa drogowa", - "tencent-map-type-satelite":"Satelita", - "tencent-map-type-hybrid":"Hybrydowa", - "image-map-background":"Tło mapy obrazu", - "image-map-background-from-entity-attribute":"Użyj tła mapy obrazu z atrybutu encji", - "image-url-source-entity-alias":"Źródło URL obrazu z aliasu encji", - "image-url-source-entity-attribute":"Źródło URL obrazu z atrybutu encji", - "common-map-settings":"Wspólne ustawienia mapy", - "x-pos-key-name":"Nazwa klucza X", - "y-pos-key-name":"Nazwa klucza Y", - "latitude-key-name":"Nazwa klucza szerokości geograficznej", - "longitude-key-name":"Nazwa klucza długości geograficznej", - "default-map-zoom-level":"Domyślny poziom przybliżenia mapy (0 - 20)", - "default-map-center-position":"Domyślna pozycja centralna mapy (0,0)", - "disable-scroll-zooming":"Wyłącz przybliżanie za pomocą przewijania", - "disable-double-click-zooming":"Wyłącz przybliżanie podwójnym kliknięciem", - "disable-zoom-control-buttons":"Wyłącz przyciski sterowania przybliżeniem", - "fit-map-bounds":"Dopasuj granice mapy, aby pokryć wszystkie markery", - "use-default-map-center-position":"Użyj domyślnej pozycji centralnej mapy", - "entities-limit":"Limit encji do wczytania", - "markers-settings":"Ustawienia znaczników", - "marker-offset-x":"Przesunięcie X znacznika względem pozycji pomnożone przez szerokość znacznika", - "marker-offset-y":"Przesunięcie Y znacznika względem pozycji pomnożone przez wysokość znacznika", - "position-function":"Funkcja konwersji pozycji, powinna zwracać współrzędne x, y jako liczby z zakresu od 0 do 1", - "draggable-marker":"Znacznik do przeciągania", - "label":"Etykieta", - "show-label":"Pokaż etykietę", - "use-label-function":"Użyj funkcji etykiety", - "label-pattern":"Etykieta (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", - "label-function":"Funkcja etykiety", - "tooltip":"Podpowiedź", - "show-tooltip":"Pokaż podpowiedź", - "show-tooltip-action":"Działanie wyświetlania podpowiedzi", - "show-tooltip-action-click":"Pokaż podpowiedź po kliknięciu (Domyślnie)", - "show-tooltip-action-hover":"Pokaż podpowiedź po najechaniu", - "auto-close-tooltips":"Automatyczne zamykanie podpowiedzi", - "use-tooltip-function":"Użyj funkcji podpowiedzi", - "tooltip-pattern":"Podpowiedź (np. 'Tekst ${keyName} jednostki.' lub Tekst linku')", - "tooltip-function":"Funkcja podpowiedzi", - "tooltip-offset-x":"Przesunięcie X podpowiedzi względem kotwicy znacznika pomnożone przez szerokość znacznika", - "tooltip-offset-y":"Przesunięcie Y podpowiedzi względem kotwicy znacznika pomnożone przez wysokość znacznika", - "color":"Kolor", - "use-color-function":"Użyj funkcji koloru", - "color-function":"Funkcja koloru", - "marker-image":"Obraz znacznika", - "use-marker-image-function":"Użyj funkcji obrazu znacznika", - "custom-marker-image":"Niestandardowy obraz znacznika", - "custom-marker-image-size":"Niestandardowy rozmiar obrazu znacznika (px)", - "marker-image-function":"Funkcja obrazu znacznika", - "marker-images":"Obrazy znaczników", - "polygon-settings":"Ustawienia wielokąta", - "show-polygon":"Pokaż wielokąt", - "polygon-key-name":"Nazwa klucza wielokąta", - "enable-polygon-edit":"Włącz edycję wielokąta", - "polygon-label":"Etykieta wielokąta", - "show-polygon-label":"Pokaż etykietę wielokąta", - "use-polygon-label-function":"Użyj funkcji etykiety wielokąta", - "polygon-label-pattern":"Etykieta wielokąta (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", - "polygon-label-function":"Funkcja etykiety wielokąta", - "polygon-tooltip":"Podpowiedź wielokąta", - "show-polygon-tooltip":"Pokaż podpowiedź wielokąta", - "auto-close-polygon-tooltips":"Automatyczne zamykanie podpowiedzi wielokąta", - "use-polygon-tooltip-function":"Użyj funkcji podpowiedzi wielokąta", - "polygon-tooltip-pattern":"Podpowiedź (np. 'Tekst ${keyName} jednostki.' lub Tekst linku')", - "polygon-tooltip-function":"Funkcja podpowiedzi wielokąta", - "polygon-color":"Kolor wielokąta", - "polygon-opacity":"Przezroczystość wielokąta", - "use-polygon-color-function":"Użyj funkcji koloru wielokąta", - "polygon-color-function":"Funkcja koloru wielokąta", - "polygon-stroke":"Obrys wielokąta", - "stroke-color":"Kolor obrysu", - "stroke-opacity":"Przezroczystość obrysu", - "stroke-weight":"Grubość obrysu", - "use-polygon-stroke-color-function":"Użyj funkcji koloru obrysu wielokąta", - "polygon-stroke-color-function":"Funkcja koloru obrysu wielokąta", - "circle-settings":"Ustawienia okręgu", - "show-circle":"Pokaż okrąg", - "circle-key-name":"Nazwa klucza okręgu", - "enable-circle-edit":"Włącz edycję okręgu", - "circle-label":"Etykieta okręgu", - "show-circle-label":"Pokaż etykietę okręgu", - "use-circle-label-function":"Użyj funkcji etykiety okręgu", - "circle-label-pattern":"Etykieta okręgu (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", - "circle-label-function":"Funkcja etykiety okręgu", - "circle-tooltip":"Podpowiedź okręgu", - "show-circle-tooltip":"Pokaż podpowiedź okręgu", - "auto-close-circle-tooltips":"Automatyczne zamykanie podpowiedzi okręgu", - "use-circle-tooltip-function":"Użyj funkcji podpowiedzi okręgu", - "circle-tooltip-pattern":"Podpowiedź (np. 'Tekst ${keyName} jednostki.' lub Tekst linku')", - "circle-tooltip-function":"Funkcja podpowiedzi okręgu", - "circle-fill-color":"Kolor wypełnienia okręgu", - "circle-fill-color-opacity":"Przezroczystość koloru wypełnienia okręgu", - "use-circle-fill-color-function":"Użyj funkcji koloru wypełnienia okręgu", - "circle-fill-color-function":"Funkcja koloru wypełnienia okręgu", - "circle-stroke":"Obrys okręgu", - "use-circle-stroke-color-function":"Użyj funkcji koloru obrysu okręgu", - "circle-stroke-color-function":"Funkcja koloru obrysu okręgu", - "markers-clustering-settings":"Ustawienia grupowania znaczników", - "use-map-markers-clustering":"Użyj grupowania znaczników na mapie", - "zoom-on-cluster-click":"Powiększ po kliknięciu na klastrze", - "max-cluster-zoom":"Maksymalny poziom powiększenia, przy którym znacznik może być częścią klastra (0 - 18)", - "max-cluster-radius-pixels":"Maksymalny promień, który może obejmować klaster w pikselach", - "cluster-zoom-animation":"Pokaż animację znaczników podczas przybliżania", - "show-markers-bounds-on-cluster-mouse-over":"Pokaż granice znaczników po najechaniu myszą na klaster", - "spiderfy-max-zoom-level":"Rozpoznaj na najwyższym poziomie powiększenia (aby zobaczyć wszystkie znaczniki klastra)", - "load-optimization":"Optymalizacja ładowania", - "cluster-chunked-loading":"Użyj fragmentów do dodawania znaczników, aby strona się nie zawiesiła", - "cluster-markers-lazy-load":"Użyj ładowania opóźnionego do dodawania znaczników", - "editor-settings":"Ustawienia edytora", - "enable-snapping":"Włącz przyciąganie do innych wierzchołków dla precyzyjnego rysowania", - "init-draggable-mode":"Inicjalizuj mapę w trybie przeciągania", - "hide-all-edit-buttons":"Ukryj wszystkie przyciski edycji", - "hide-draw-buttons":"Ukryj przyciski rysowania", - "hide-edit-buttons":"Ukryj przyciski edycji", - "hide-remove-button":"Ukryj przycisk usuwania", - "route-map-settings":"Ustawienia mapy trasy", - "trip-animation-settings":"Ustawienia animacji podróży", - "normalization-step":"Krok normalizacji danych (ms)", - "tooltip-background-color":"Kolor tła podpowiedzi", - "tooltip-font-color":"Kolor czcionki podpowiedzi", - "tooltip-opacity":"Przezroczystość podpowiedzi (0-1)", - "auto-close-tooltip":"Automatyczne zamykanie podpowiedzi", - "rotation-angle":"Ustaw dodatkowy kąt obrotu dla znacznika (stopnie)", - "path-settings":"Ustawienia ścieżki", - "path-color":"Kolor ścieżki", - "use-path-color-function":"Użyj funkcji koloru ścieżki", - "path-color-function":"Funkcja koloru ścieżki", - "path-decorator":"Dekorator ścieżki", - "use-path-decorator":"Użyj dekoratora ścieżki", - "decorator-symbol":"Symbol dekoratora", - "decorator-symbol-arrow-head":"Strzała", - "decorator-symbol-dash":"Kreska", - "decorator-symbol-size":"Rozmiar symbolu dekoratora (px)", - "use-path-decorator-custom-color":"Użyj niestandardowego koloru dekoratora ścieżki", - "decorator-custom-color":"Niestandardowy kolor dekoratora", - "decorator-offset":"Przesunięcie dekoratora", - "end-decorator-offset":"Przesunięcie końcowe dekoratora", - "decorator-repeat":"Powtórzenie dekoratora", - "points-settings":"Ustawienia punktów", - "show-points":"Pokaż punkty", - "point-color":"Kolor punktu", - "point-size":"Rozmiar punktu (px)", - "use-point-color-function":"Użyj funkcji koloru punktu", - "point-color-function":"Funkcja koloru punktu", - "use-point-as-anchor":"Użyj punktu jako kotwicy", - "point-as-anchor-function":"Funkcja punktu jako kotwicy", - "independent-point-tooltip":"Niezależna podpowiedź punktu", - "clustering-markers":"Klastrowanie znaczników", - "use-icon-create-function":"Użyj funkcji koloru znaczników", - "marker-color-function":"Funkcja koloru znacznika" - }, - "markdown":{ - "use-markdown-text-function":"Użyj funkcji wartości markdown/HTML", - "markdown-text-function":"Funkcja wartości markdown/HTML", - "markdown-text-pattern":"Wzorzec markdown/HTML (markdown lub HTML z zmiennymi, np. '${entityName} lub ${keyName} - jakiś tekst.')", - "apply-default-markdown-style":"Zastosuj domyślny styl markdown", - "markdown-css":"CSS markdown/HTML" + "map-provider-settings": "Ustawienia dostawcy map", + "map-provider": "Dostawca map", + "map-provider-google": "Google maps", + "map-provider-openstreet": "OpenStreet maps", + "map-provider-here": "HERE maps", + "map-provider-image": "Image map", + "map-provider-tencent": "Tencent maps", + "openstreet-provider": "OpenStreet map provider", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Domyślny)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "Użyj niestandardowego dostawcy", + "custom-provider-tile-url": "Niestandardowy kafelek URL dostawcy", + "google-maps-api-key": "Klucz API Google Maps", + "default-map-type": "Domyślny typ mapy", + "google-map-type-roadmap": "Mapa drogowa", + "google-map-type-satelite": "Satelita", + "google-map-type-hybrid": "Hybrydowy", + "google-map-type-terrain": "Teren", + "map-layer": "Warstwa mapy", + "here-map-normal-day": "HERE.normalDay (Domyślne)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "dane uwierzytelniające", + "here-app-id": "TUTAJ identyfikator aplikacji", + "here-app-code": "TUTAJ kod aplikacji", + "here-api-key": "TUTAJ klucz API", + "here-use-new-version-api-3": "Użyj wersji API 3", + "tencent-maps-api-key": "Klucz API Tencent Maps", + "tencent-map-type-roadmap": "Mapa drogowa", + "tencent-map-type-satelite": "Satelita", + "tencent-map-type-hybrid": "Hybrydowy", + "image-map-background": "Tło mapy obrazu", + "image-map-background-from-entity-attribute": "Pobierz tło mapy obrazu z atrybutu obiektu", + "image-url-source-entity-alias": "Alias ​​obiektu źródłowej adresu URL obrazu", + "image-url-source-entity-attribute": "Atrybut obiektu źródłowej adresu URL obrazu", + "common-map-settings": "Typowe ustawienia mapy", + "x-pos-key-name": "Nazwa klawisza pozycji X", + "y-pos-key-name": "Nazwa klawisza pozycji Y", + "latitude-key-name": "Nazwa klucza szerokości geograficznej", + "longitude-key-name": "Nazwa klucza długości geograficznej", + "default-map-zoom-level": "Domyślny poziom powiększenia mapy (0 - 20)", + "default-map-center-position": "Domyślna pozycja środkowa mapy (0,0)", + "disable-scroll-zooming": "Wyłącz powiększanie podczas przewijania", + "disable-double-click-zooming": "Wyłącz powiększanie za pomocą podwójnego kliknięcia", + "disable-zoom-control-buttons": "Wyłącz przyciski sterowania zoomem", + "fit-map-bounds": "Dopasuj granice mapy, aby pokryć wszystkie znaczniki", + "use-default-map-center-position": "Użyj domyślnej pozycji środkowej mapy", + "entities-limit": "Limit obiektów do załadowania", + "markers-settings": "Ustawienia znaczników", + "marker-offset-x": "Przesunięcie znacznika X względem pozycji pomnożonej przez szerokość znacznika", + "marker-offset-y": "Przesunięcie znacznika Y względem pozycji pomnożonej przez wysokość znacznika", + "position-function": "Funkcja konwersji pozycji powinna zwracać współrzędne x, y w postaci liczby zmiennoprzecinkowej podwójnej precyji od 0 do 1 każda", + "draggable-marker": "Przeciągany znacznik", + "label": "Etykieta", + "show-label": "Pokaż etykietę", + "use-label-function": "Użyj funkcji etykiety", + "label-pattern": "Etykieta (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", + "label-function": "Funkcja etykiety", + "tooltip": "Podpowiedź", + "show-tooltip": "Pokaż podpowiedź", + "show-tooltip-action": "Akcja służąca do wyświetlania podpowiedzi", + "show-tooltip-action-click": "Pokaż podpowiedź po kliknięciu (domyślne)", + "show-tooltip-action-hover": "Pokaż podpowiedź po najechaniu myszką", + "auto-close-tooltips": "Automatyczne zamykanie podpowiedzi", + "use-tooltip-function": "Użyj funkcji podpowiedzi", + "tooltip-pattern": "Podpowiedź (np. „Tekst ${keyName} jednostki.” lub tekst linku')", + "tooltip-function": "Funkcja podpowiedzi", + "tooltip-offset-x": "Przesunięcie X podpowiedzi względem kotwicy znacznika pomnożonej przez szerokość znacznika", + "tooltip-offset-y": "Przesunięcie Y podpowiedzi w stosunku do kotwicy znacznika pomnożonej przez wysokość znacznika", + "color": "Kolor", + "use-color-function": "Użyj funkcji koloru", + "color-function": "Funkcja koloru", + "marker-image": "Obraz znacznika", + "use-marker-image-function": "Użyj funkcji obrazu znacznika", + "custom-marker-image": "Niestandardowy obraz znacznika", + "custom-marker-image-size": "Niestandardowy rozmiar obrazu znacznika (px)", + "marker-image-function": "Funkcja obrazu znacznika", + "marker-images": "Obrazy znaczników", + "polygon-settings": "Ustawienia poligonu", + "show-polygon": "Pokaż poligon", + "polygon-key-name": "Nazwa klucza poligonu", + "enable-polygon-edit": "Włącz edycję poligonu", + "polygon-label": "Etykieta poligonu", + "show-polygon-label": "Pokaż etykietę poligonu", + "use-polygon-label-function": "Użyj funkcji etykiety poligonu", + "polygon-label-pattern": "Etykieta poligonu (przykłady wzorców: '${entityName}', '${entityName}: (Tekst ${keyName} jednostki.)' )", + "polygon-label-function": "Funkcja etykiety poligonu", + "polygon-tooltip": "Etykietka poligonu", + "show-polygon-tooltip": "Pokaż podpowiedź poligonu", + "auto-close-polygon-tooltips": "Automatyczne zamykanie etykiet poligonu", + "use-polygon-tooltip-function": "Użyj funkcji podpowiedzi poligonu", + "polygon-tooltip-pattern": "Etykietka narzędzia (np. „Tekst ${keyName} jednostki.” lub tekst linku')", + "polygon-tooltip-function": "Funkcja podpowiedzi poligonu", + "polygon-color": "Kolor poligonu", + "polygon-opacity": "Nieprzezroczystość poligonu", + "use-polygon-color-function": "Użyj funkcji koloru poligonu", + "polygon-color-function": "Funkcja koloru poligonu", + "polygon-stroke": "Obrys poligonu", + "stroke-color": "Kolor obrysu", + "stroke-opacity": "Nieprzezroczystość obrysu", + "stroke-weight": "Waga obrysu", + "use-polygon-stroke-color-function": "Użyj funkcji koloru obrysu poligonu", + "polygon-stroke-color-function": "Funkcja koloru obrysu poligonu", + "circle-settings": "Ustawienia kręgu", + "show-circle": "Pokaż okrąg", + "circle-key-name": "Nazwa klucza okręgu", + "enable-circle-edit": "Włącz edycję okręgu", + "circle-label": "Etykieta okręgu", + "show-circle-label": "Pokaż etykietę okręgu", + "use-circle-label-function": "Użyj funkcji etykiety okręgu", + "circle-label-pattern": "Etykieta okrągła (przykłady wzorów: '${entityName}', '${entityName}: (Tekst ${keyName} obiektu.)' )", + "circle-label-function": "Funkcja etykiety okręgu", + "circle-tooltip": "Okrągła podpowiedź", + "show-circle-tooltip": "Pokaż podpowiedź okręgu", + "auto-close-circle-tooltips": "Automatyczne zamykanie podpowiedzi w okręgu", + "use-circle-tooltip-function": "Użyj funkcji podpowiedzi okręgu", + "circle-tooltip-pattern": "Etykietka narzędzia (np. „Tekst ${keyName} obiektu.” lub tekst linku')", + "circle-tooltip-function": "Funkcja podpowiedzi okręgu", + "circle-fill-color": "Kolor wypełnienia okręgu", + "circle-fill-color-opacity": "Nieprzezroczystość koloru wypełnienia okręgu", + "use-circle-fill-color-function": "Użyj funkcji koloru wypełnienia okręgu", + "circle-fill-color-function": "Funkcja koloru wypełnienia okręgu", + "circle-stroke": "Obrys okręgu", + "use-circle-stroke-color-function": "Użyj funkcji koloru obrysu okręgu", + "circle-stroke-color-function": "Funkcja koloru obrysu okręgu", + "markers-clustering-settings": "Ustawienia grupowania znaczników", + "use-map-markers-clustering": "Użyj grupowania znaczników mapy", + "zoom-on-cluster-click": "Powiększ po kliknięciu klastra", + "max-cluster-zoom": "Maksymalny poziom powiększenia, gdy znacznik może być częścią klastra (0–18)", + "max-cluster-radius-pixels": "Maksymalny promień, jaki będzie obejmował klaster w pikselach", + "cluster-zoom-animation": "Pokaż animację na znacznikach podczas powiększania", + "show-markers-bounds-on-cluster-mouse-over": "Pokaż granice znaczników, gdy najedziesz myszką na klaster", + "spiderfy-max-zoom-level": "Rozrzedź na maksymalnym poziomie powiększenia (aby zobaczyć wszystkie znaczniki klastrów)", + "load-optimization": "Optymalizacja obciążenia", + "cluster-chunked-loading": "Do dodawania znaczników używaj fragmentów, aby strona się nie zawieszała", + "cluster-markers-lazy-load": "Do dodawania znaczników używaj leniwego ładowania", + "editor-settings": "Ustawienia edytora", + "enable-snapping": "Włącz przyciąganie do innych wierzchołków w celu precyzyjnego rysowania", + "init-draggable-mode": "Zainicjuj mapę w trybie przeciągania", + "hide-all-edit-buttons": "Ukryj wszystkie przyciski sterujące edycją", + "hide-draw-buttons": "Ukryj przyciski rysowania", + "hide-edit-buttons": "Ukryj przyciski edycji", + "hide-remove-button": "Ukryj przycisk usuwania", + "route-map-settings": "Ustawienia mapy drogowej", + "trip-animation-settings": "Ustawienia animacji podróży", + "normalization-step": "Krok normalizacji danych (ms)", + "tooltip-background-color": "Kolor tła podpowiedzi", + "tooltip-font-color": "Kolor czcionki podpowiedzi", + "tooltip-opacity": "Nieprzezroczystość podpowiedzi (0-1)", + "auto-close-tooltip": "Automatyczne zamykanie podpowiedzi", + "rotation-angle": "Ustaw dodatkowy kąt obrotu znacznika (stopnie)", + "path-settings": "Ustawienia ścieżki", + "path-color": "Kolor ścieżki", + "use-path-color-function": "Użyj funkcji koloru ścieżki", + "path-color-function": "Funkcja koloru ścieżki", + "path-decorator": "Dekorator ścieżek", + "use-path-decorator": "Użyj dekoratora ścieżek", + "decorator-symbol": "Symbol dekoratora", + "decorator-symbol-arrow-head": "Strzałka", + "decorator-symbol-dash": "Kropla", + "decorator-symbol-size": "Rozmiar symbolu dekoratora (px)", + "use-path-decorator-custom-color": "Użyj niestandardowego koloru dekoratora ścieżek", + "decorator-custom-color": "Kolor niestandardowy dekoratora", + "decorator-offset": "Przesunięcie dekoratora", + "end-decorator-offset": "Przesunięcie dekoratora końcowego", + "decorator-repeat": "Powtórzenie dekoratora", + "points-settings": "Ustawienia punktów", + "show-points": "Pokaż punkty", + "point-color": "Kolor punktowy", + "point-size": "Rozmiar punktu (px)", + "use-point-color-function": "Użyj funkcji koloru punktu", + "point-color-function": "Funkcja koloru punktu", + "use-point-as-anchor": "Użyj punktu jako kotwicy", + "point-as-anchor-function": "Punkt jako funkcja kotwicy", + "independent-point-tooltip": "Niezależna podpowiedź dotycząca punktu", + "clustering-markers": "Markery grupujące", + "use-icon-create-function": "Użyj funkcji koloru znaczników", + "marker-color-function": "Funkcja koloru znacznika" }, - "simple-card":{ - "label":"Etykieta", - "label-position":"Pozycja etykiety", - "label-position-left":"Po lewej", - "label-position-top":"Na górze" + "markdown": { + "use-markdown-text-function": "Użyj funkcji markdown/wartości HTML", + "markdown-text-function": "Funkcja Markdown/wartości HTML", + "markdown-text-pattern": "Wzorzec Markdown/HTML (markdown lub HTML ze zmiennymi, np. „${entityName} lub ${keyName} - jakiś tekst.”)", + "apply-default-markdown-style": "Zastosuj domyślny styl przeceny", + "markdown-css": "Markdown/HTML CSS" }, - "value-card":{ - "layout":"Układ", - "layout-square":"Kwadratowy", - "layout-vertical":"Pionowy", - "layout-centered":"Wyśrodkowany", - "layout-simplified":"Uproszczony", - "layout-horizontal":"Poziomy", - "layout-horizontal-reversed":"Odwrócony poziomy", - "label":"Etykieta", - "icon":"Ikona", - "value":"Wartość", - "date":"Data", - "value-card-style":"Styl karty wartości", - "auto-scale":"Automatyczne dostosowanie skali" + "simple-card": { + "label": "Label", + "label-position": "Pozycja etykiety", + "label-position-left": "Po lewej", + "label-position-top": "Na górze" }, - "liquid-level-card":{ - "layout-simple":"Prosty", - "layout-percentage":"Procentowy", - "layout-absolute":"Bezwzględny", - "layout":"Układ", - "background-overlay":"Nakładka na tło wartości", - "total-volume":"Całkowita objętość", - "tank":"Zbiornik", - "shape":"Kształt", - "datasource-units":"Jednostki źródła danych", - "widget-units":"Jednostki widżetu", - "decimals":"Miejsca dziesiętne", - "liquid":"Ciecz", - "liquid-color":"Kolor cieczy", - "value":"Wartość", - "value-font":"Czcionka wartości", - "level":"Poziom", - "last-update":"Ostatnia aktualizacja", - "shape-by-attribute":"Ustaw kształt zbiornika według nazwy atrybutu", - "tooltip-background":"Kolor tła", - "background-blur":"Rozmycie tła", - "tank-color":"Kolor zbiornika", - "static":"Statyczny", - "see-examples":"Zobacz przykłady", - "attribute":"Atrybut", - "shape-type":"Typ", - "v-oval":"Pionowy Owal", - "v-cylinder":"Pionowy Cylinder", - "v-capsule":"Pionowa Kapsuła", - "rectangle":"Prostokąt", - "h-oval":"Poziomy Owal", - "h-ellipse":"Pozioma Elipsa", - "h-dish-ends":"Poziome Zakończenia Dzbanka", - "h-cylinder":"Poziomy Cylinder", - "h-capsule":"Pozioma Kapsuła", - "h-elliptical_2_1":"Pozioma Elipsa 2:1", - "icon":"Ikona karty", - "title":"Tytuł karty", - "units":"Jednostki", - "color-and-font":"Kolor i czcionka", - "shape-attribute-name":"Nazwa atrybutu", - "total-volume-required":"Wymagana jest całkowita objętość.", - "attribute-name-required":"Wymagana jest nazwa atrybutu.", - "attribute-key-not-set":"Klucz atrybutu '{{attributeName}}' nie jest ustawiony", - "attribute-key-invalid":"Klucz atrybutu '{{attributeName}}' jest nieprawidłowy" + "value-card": { + "layout": "Layout", + "layout-square": "Square", + "layout-vertical": "Vertical", + "layout-centered": "Centered", + "layout-simplified": "Simplified", + "layout-horizontal": "Horizontal", + "layout-horizontal-reversed": "Horizontal reversed", + "label": "Label", + "icon": "Icon", + "value": "Value", + "date": "Date", + "value-card-style": "Value card style" }, - "aggregated-value-card":{ - "subtitle":"Podtytuł", - "chart":"Wykres", - "values":"Wartości", - "value-appearance":"Wygląd wartości", - "position":"Pozycja", - "position-center":"Środek", - "position-right-top":"Prawy górny róg", - "position-right-bottom":"Prawy dolny róg", - "position-left-top":"Lewy górny róg", - "position-left-bottom":"Lewy dolny róg", - "font":"Czcionka", - "color":"Kolor", - "arrow":"Strzałka", - "display-up-down-arrow":"Wyświetl strzałkę w górę/dół", - "add-value":"Dodaj wartość", - "remove-value":"Usuń wartość", - "no-values":"Brak skonfigurowanych wartości", - "aggregation":"Agregacja", - "aggregated-value-card-style":"Styl karty zagregowanej wartości", - "auto-scale":"Automatyczne skalowanie" + "aggregated-value-card": { + "subtitle": "Subtitle", + "chart": "Chart", + "values": "Values", + "value-appearance": "Value appearance", + "position": "Position", + "position-center": "Center", + "position-right-top": "Right top", + "position-right-bottom": "Right bottom", + "position-left-top": "Left top", + "position-left-bottom": "Left bottom", + "font": "Font", + "color": "Color", + "arrow": "Arrow", + "display-up-down-arrow": "Display Up/Down arrow", + "add-value": "Add value", + "remove-value": "Remove value", + "no-values": "No values configured", + "aggregation": "Aggregation", + "aggregated-value-card-style": "Aggregated value card style" }, - "value-chart-card":{ - "layout":"Układ", - "layout-left":"Lewo", - "layout-right":"Prawo", - "auto-scale":"Automatyczna skala", - "icon":"Ikona", - "value":"Wartość", - "chart":"Wykres", - "value-chart-card-style":"Styl karty wartości z wykresem" + "alarm-count": { + "alarm-count-card-style": "Alarm count card style" }, - "progress-bar":{ - "layout":"Układ", - "layout-default":"Domyślny", - "layout-simplified":"Uproszczony", - "auto-scale":"Automatyczna skala", - "icon":"Ikona", - "value":"Wartość", - "range":"Zakres", - "min":"min", - "max":"max", - "range-ticks":"Podziałka zakresu", - "bar":"Pasek", - "bar-color":"Kolor paska", - "bar-background":"Tło paska", - "progress-bar-card-style":"Styl karty paska postępu" + "entity-count": { + "entity-count-card-style": "Entity count card style" }, - "alarm-count":{ - "alarm-count-card-style":"Styl karty licznika alarmów" + "count": { + "layout": "Layout", + "layout-column": "Column", + "layout-row": "Row", + "label": "Label", + "icon": "Icon", + "icon-background": "Icon background", + "value": "Value", + "chevron": "Chevron" }, - "entity-count":{ - "entity-count-card-style":"Entity count card style" + "table": { + "common-table-settings": "Wspólne ustawienia tabeli", + "enable-search": "Włącz wyszukiwanie", + "enable-sticky-header": "Zawsze wyświetlaj nagłówek", + "enable-sticky-action": "Zawsze wyświetlaj kolumnę działań", + "hidden-cell-button-display-mode": "Tryb wyświetlania akcji przycisków ukrytych komórek", + "show-empty-space-hidden-action": "Pokaż pustą przestrzeń zamiast działania przycisku ukrytej komórki", + "dont-reserve-space-hidden-action": "Nie rezerwuj miejsca na ukryte przyciski akcji", + "display-timestamp": "Wyświetl kolumnę sygnatury czasowej", + "display-milliseconds": "Wyświetl znacznik czasu w milisekundach", + "display-pagination": "Wyświetl paginację", + "default-page-size": "Domyślny rozmiar strony", + "use-entity-label-tab-name": "Użyj etykiety obiektu w nazwie karty", + "hide-empty-lines": "Ukryj puste linie", + "row-style": "Styl rzędu", + "use-row-style-function": "Użyj funkcji stylu wiersza", + "row-style-function": "Funkcja stylu wiersza", + "cell-style": "Styl komórki", + "use-cell-style-function": "Użyj funkcji stylu komórki", + "cell-style-function": "Funkcja stylu komórki", + "cell-content": "Zawartość komórki", + "use-cell-content-function": "Użyj funkcji zawartości komórki", + "cell-content-function": "Funkcja zawartości komórki", + "show-latest-data-column": "Pokaż ostatnią kolumnę danych", + "latest-data-column-order": "Najnowsza kolejność kolumn danych", + "entities-table-title": "Tytuł tabeli obiektów", + "enable-select-column-display": "Włącz wybrane kolumny do wyświetlenia", + "display-entity-name": "Wyświetl kolumnę z nazwą obiektu", + "entity-name-column-title": "Tytuł kolumny z nazwą obiektu", + "display-entity-label": "Wyświetl kolumnę etykiety obiektu", + "entity-label-column-title": "Tytuł kolumny etykiety obiektu", + "display-entity-type": "Wyświetl kolumnę typu obiektu", + "default-sort-order": "Domyślny porządek sortowania", + "custom-title": "Niestandardowy tytuł nagłówka", + "column-width": "Szerokość kolumny (px lub %)", + "default-column-visibility": "Domyślna widoczność kolumny", + "column-visibility-visible": "Widoczna", + "column-visibility-hidden": "Ukryta", + "column-visibility-hidden-mobile": "Ukryta w trybie mobilnym", + "column-selection-to-display": "Wybór kolumny w „Kolumnach do wyświetlenia”", + "column-selection-to-display-enabled": "Włączona", + "column-selection-to-display-disabled": "Wyłączona", + "column-export-option": "Uwzględnij kolumnę w eksporcie", + "column-export-option-always": "Zawsze", + "column-export-option-only-visible": "Tylko jeśli kolumna jest widoczna", + "column-export-option-never": "Nigdy", + "alarms-table-title": "Tytuł tabeli alarmów", + "enable-alarms-selection": "Włącz wybór alarmów", + "enable-alarms-search": "Włącz wyszukiwanie alarmów", + "enable-alarm-filter": "Włącz filtr alarmowy", + "display-alarm-details": "Wyświetl szczegóły alarmu", + "allow-alarms-ack": "Zezwalaj na potwierdzanie alarmów", + "allow-alarms-clear": "Zezwól na usunięcie alarmów", + "display-alarm-activity": "Wyświetl aktywność alarmową", + "allow-alarms-assign": "Zezwalaj na przypisywanie alarmów", + "blob-entities-table-title": "Tytuł tabeli obiektów BLOB", + "display-created-time": "Wyświetl utworzoną kolumnę czasu", + "display-type": "Kolumna typu wyświetlania", + "display-customer": "Wyświetl kolumnę klienta", + "no-data-display-message": "Alternatywny komunikat „Brak danych do wyświetlenia”.", + "force-default-blob-entity-type": "Wymuś domyślny typ obiektu typu BLOB", + "scheduler-events-table-title": "Tytuł tabeli zdarzeń planisty", + "force-default-event-type": "Wymuś domyślny typ zdarzenia", + "columns": "Columns", + "column-settings": "Column settings", + "remove-column": "Remove column", + "add-column": "Add column", + "no-columns": "No columns configured", + "columns-to-display": "Columns to display", + "table-header": "Table header", + "header-buttons": "Header buttons", + "table-buttons": "Table buttons", + "pagination": "Pagination", + "rows": "Rows", + "timeseries-column-error": "At least one timeseries column should be specified", + "table-tabs": "Table tabs", + "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode" }, - "count":{ - "layout":"Układ", - "layout-column":"Kolumna", - "layout-row":"Wiersz", - "label":"Etykieta", - "icon":"Ikona", - "icon-background":"Tło ikony", - "value":"Wartość", - "chevron":"Strzałka", - "auto-scale":"Automatyczna skala" + "scheduler": { + "enabled-scheduler-event-views": "Włączono widoki zdarzeń planisty", + "enabled-scheduler-event-views-both": "Obydwa", + "enabled-scheduler-event-views-list": "Tylko widok listy", + "enabled-scheduler-event-views-calendar": "Tylko widok kalendarza", + "display-name": "Wyświetlana nazwa", + "type-name": "Wpisz nazwę", + "display-originator-select": "Wyświetl wybór obiektu inicjaującego", + "display-message-type-select": "Wyświetl wybór typu wiadomości", + "display-metadata-table": "Wyświetl tabelę metadanych wiadomości", + "configuration-html-template": "Szablon HTML konfiguracji", + "custom-event-types": "Niestandardowe typy zdarzeń", + "no-custom-event-types": "Nie skonfigurowano niestandardowych typów zdarzeń", + "add-custom-event-type": "Dodaj niestandardowy typ zdarzenia" }, - "table":{ - "common-table-settings":"Wspólne ustawienia tabeli", - "enable-search":"Włącz wyszukiwanie", - "enable-sticky-header":"Zawsze wyświetlaj nagłówek", - "enable-sticky-action":"Zawsze wyświetlaj kolumnę akcji", - "hidden-cell-button-display-mode":"Tryb wyświetlania ukrytego przycisku akcji w komórce", - "show-empty-space-hidden-action":"Pokaż pustą przestrzeń zamiast ukrytego przycisku akcji w komórce", - "dont-reserve-space-hidden-action":"Nie rezerwuj miejsca na ukryte przyciski akcji", - "display-timestamp":"Znak czasu", - "display-pagination":"Wyświetl paginację", - "default-page-size":"Domyślny rozmiar strony", - "use-entity-label-tab-name":"Użyj etykiety encji w nazwie karty", - "hide-empty-lines":"Ukryj puste wiersze", - "row-style":"Styl wiersza", - "use-row-style-function":"Użyj funkcji stylu wiersza", - "row-style-function":"Funkcja stylu wiersza", - "cell-style":"Styl komórki", - "use-cell-style-function":"Użyj funkcji stylu komórki", - "cell-style-function":"Funkcja stylu komórki", - "cell-content":"Zawartość komórki", - "use-cell-content-function":"Użyj funkcji zawartości komórki", - "cell-content-function":"Funkcja zawartości komórki", - "show-latest-data-column":"Pokaż kolumnę z najnowszymi danymi", - "latest-data-column-order":"Kolejność kolumny z najnowszymi danymi", - "entities-table-title":"Tytuł tabeli encji", - "enable-select-column-display":"Włącz wybór kolumn do wyświetlenia", - "display-entity-name":"Wyświetl kolumnę z nazwą encji", - "entity-name-column-title":"Tytuł kolumny z nazwą encji", - "display-entity-label":"Wyświetl kolumnę z etykietą encji", - "entity-label-column-title":"Tytuł kolumny z etykietą encji", - "display-entity-type":"Wyświetl kolumnę z typem encji", - "default-sort-order":"Domyślna kolejność sortowania", - "custom-title":"Niestandardowy tytuł nagłówka", - "column-width":"Szerokość kolumny (px lub %)", - "default-column-visibility":"Domyślna widoczność kolumny", - "column-visibility-visible":"Widoczna", - "column-visibility-hidden":"Ukryta", - "column-visibility-hidden-mobile":"Ukryta w trybie mobilnym", - "column-selection-to-display":"Wybór kolumn w 'Kolumny do wyświetlenia'", - "column-selection-to-display-enabled":"Włączony", - "column-selection-to-display-disabled":"Wyłączony", - "alarms-table-title":"Tytuł tabeli alarmów", - "enable-alarms-selection":"Włącz wybór alarmów", - "enable-alarms-search":"Włącz wyszukiwanie alarmów", - "enable-alarm-filter":"Włącz filtr alarmów", - "display-alarm-details":"Wyświetl szczegóły alarmu", - "allow-alarms-ack":"Zezwalaj na potwierdzanie alarmów", - "allow-alarms-clear":"Zezwalaj na usuwanie alarmów", - "display-alarm-activity":"Wyświetl aktywność alarmu", - "allow-alarms-assign":"Zezwalaj na przypisywanie alarmów", - "columns":"Kolumny", - "column-settings":"Ustawienia kolumn", - "remove-column":"Usuń kolumnę", - "add-column":"Dodaj kolumnę", - "no-columns":"Brak skonfigurowanych kolumn", - "columns-to-display":"Kolumny do wyświetlenia", - "table-header":"Nagłówek tabeli", - "header-buttons":"Przyciski nagłówka", - "table-buttons":"Przyciski tabeli", - "pagination":"Paginacja", - "rows":"Wiersze", - "timeseries-column-error":"Należy określić co najmniej jedną kolumnę szeregów czasowych", - "alarm-column-error":"Należy określić co najmniej jedną kolumnę alarmów", - "table-tabs":"Karty tabeli", - "show-cell-actions-menu-mobile":"Pokaż menu rozwijane akcji komórek w trybie mobilnym" + "value-source": { + "value-source": "Źródło wartości", + "predefined-value": "Predefiniowana wartość", + "entity-attribute": "Wartość pobrana z atrybutu obiektu", + "value": "Wartość", + "source-entity-alias": "Alias obiektu źródłowego", + "source-entity-attribute": "Atrybut obiektu źródłowego" }, - "wind-speed-direction":{ - "layout":"Układ", - "layout-default":"Domyślny", - "layout-advanced":"Zaawansowany", - "layout-simplified":"Uproszczony", - "values":"Wartości", - "wind-direction":"Kierunek wiatru", - "center-value":"Wartość środkowa", - "icon":"Ikona", - "arrow":"Strzałka", - "ticks":"Kreski", - "labels-type":"Typ etykiet", - "directional-names":"Nazwy kierunkowe", - "degrees":"Stopnie", - "major-ticks":"Główne kreski", - "minor-ticks":"Drobne kreski", - "wind-speed-direction-card-style":"Styl karty prędkości i kierunku wiatru", - "ticks-color":"Kolor kresek", - "ticks-labels-type":"Typ etykiet kreskowych", - "arrow-color":"Kolor strzałki" + "widget-font": { + "font-settings": "Font settings", + "font-family": "Rodzina czcionek", + "size": "Rozmiar", + "relative-font-size": "Względny rozmiar czcionki (w procentach)", + "font-style": "Styl", + "font-style-normal": "Normalna", + "font-style-italic": "Włoska", + "font-style-oblique": "Skośna", + "font-weight": "Grubość", + "font-weight-normal": "Normalna", + "font-weight-bold": "Pogrubiona", + "font-weight-bolder": "Bardziej pogrubiona", + "font-weight-lighter": "Cieńsza", + "color": "Kolor", + "shadow-color": "Kolor cienia", + "preview": "Preview", + "line-height": "Line height", + "auto": "Auto" }, - "value-source":{ - "value-source":"Źródło wartości", - "predefined-value":"Wartość predefiniowana", - "entity-attribute":"Atrybut jednostki", - "value":"Wartość", - "source-entity-alias":"Alias jednostki źródłowej", - "source-entity-attribute":"Atrybut jednostki źródłowej" + "home": { + "no-data-available": "Brak dostępnych danych" }, - "widget-font":{ - "font-settings":"Ustawienia czcionki", - "font-family":"Rodzina czcionek", - "size":"Rozmiar", - "relative-font-size":"Względny rozmiar czcionki (procenty)", - "font-style":"Styl", - "font-style-normal":"Normalny", - "font-style-italic":"Kursywa", - "font-style-oblique":"Pochyły", - "font-weight":"Grubość", - "font-weight-normal":"Normalna", - "font-weight-bold":"Pogrubiona", - "font-weight-bolder":"Grubsza", - "font-weight-lighter":"Lżejsza", - "color":"Kolor", - "shadow-color":"Kolor cienia", - "preview":"Podgląd", - "line-height":"Wysokość wiersza", - "auto":"Automatyczne" + "system-info": { + "cpu": "CPU (procesor)", + "ram": "Pamięć RAM", + "disk": "Dysk", + "cpu-warning-text": "Wysokie wykorzystanie procesora. Zoptymalizuj działanie systemu aby zapobiec awarii systemu.", + "cpu-critical-text": "Krytycznie wysokie użycie procesora. Zoptymalizuj działanie systemu aby zapobiec awarii systemu.", + "ram-warning-text": "Kończy się rezerwa pamięci RAM. Zoptymalizuj działanie systemu, lub zwiększ pamięć RAM aby zapobiec awarii systemu.", + "ram-critical-text": "Krytycznie niska rezerwa pamięci RAM. Zoptymalizuj działanie systemu, lub zwiększ pamięć RAM aby zapobiec awarii systemu.", + "disk-warning-text": "Kończy się miejsce na dysku. Aby zapobiec utracie danych, zwolnij lub zwiększ miejsce na dysku.", + "disk-critical-text": "Krytycznie mało miejsca na dysku. Aby zapobiec utracie danych, zwolnij lub zwiększ miejsce na dysku." }, - "home":{ - "no-data-available":"Brak dostępnych danych" + "cluster-info": { + "service-id": "Identyfikator usługi", + "service-type": "Rodzaj usługi", + "no-data": "Brak danych" }, - "system-info":{ - "cpu":"Procesor", - "ram":"RAM", - "disk":"Dysk", - "cpu-warning-text":"Wysokie zużycie procesora. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu.", - "cpu-critical-text":"Krytycznie wysokie zużycie procesora. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu.", - "ram-warning-text":"Niski zapas pamięci RAM. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu lub zwiększ rozmiar pamięci RAM.", - "ram-critical-text":"Krytycznie niski zapas pamięci RAM. Aby uniknąć awarii systemu, zoptymalizuj wydajność systemu lub zwiększ rozmiar pamięci RAM.", - "disk-warning-text":"Niskie miejsce na dysku. Aby uniknąć utraty danych, zwolnij miejsce lub zwiększ rozmiar dysku.", - "disk-critical-text":"Krytycznie niskie miejsce na dysku. Aby uniknąć utraty danych, zwolnij miejsce lub zwiększ rozmiar dysku." + "transport-messages": { + "title": "Komunikaty transportowe", + "info": "Wszystkie wiadomości pochodzące z urządzeń" }, - "cluster-info":{ - "service-id":"ID usługi", - "service-type":"Typ usługi", - "no-data":"Brak danych" + "activity": { + "title": "Działalność" }, - "transport-messages":{ - "title":"Wiadomości transportowe", - "info":"Wszystkie wiadomości odebrane od urządzeń" + "documentation": { + "title": "Dokumentacja", + "add-link": "Dodaj link", + "add-link-title": "Dodaj link do dokumentacji", + "name": "Nazwa", + "name-required": "Nazwa jest wymagana.", + "link": "Połączyć", + "link-required": "Link jest wymagany.", + "columns": "Kolumny" }, - "activity":{ - "title":"Aktywność" + "quick-links": { + "title": "Szybkie linki", + "add-link": "Dodaj link", + "add-link-title": "Dodaj szybki link", + "quick-link": "Szybki link", + "quick-link-required": "Wymagany jest szybki link.", + "no-links-matching": "Nie znaleziono linku '{{name}}'.", + "columns": "Kolumny" }, - "documentation":{ - "title":"Dokumentacja", - "add-link":"Dodaj link", - "add-link-title":"Dodaj link do dokumentacji", - "name":"Nazwa", - "name-required":"Nazwa jest wymagana.", - "link":"Link", - "link-required":"Link jest wymagany.", - "columns":"Kolumny" + "recent-dashboards": { + "title": "Panele", + "last": "Ostatnio oglądane", + "starred": "Oznaczone gwiazdką", + "name": "Nazwa", + "last-viewed": "Ostatnio oglądane", + "no-last-viewed-dashboards": "Jeszcze nie ma ostatnio oglądanych paneli" }, - "quick-links":{ - "title":"Szybkie linki", - "add-link":"Dodaj link", - "add-link-title":"Dodaj szybki link", - "quick-link":"Szybki link", - "quick-link-required":"Szybki link jest wymagany.", - "no-links-matching":"Nie znaleziono linków pasujących do '{{name}}'.", - "columns":"Kolumny" + "configured-features": { + "title": "Skonfigurowane funkcje", + "info": "Stan funkcji wymagających konfiguracji", + "white-labeling-feature": "White labeling", + "email-feature": "E-mail", + "sms-feature": "SMS-em", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2FA", + "feature-configured": "Funkcja jest skonfigurowana.\nKliknij, aby skonfigurować", + "feature-not-configured": "Funkcja nie jest skonfigurowana.\nKliknij, aby skonfigurować" }, - "recent-dashboards":{ - "title":"Dashboardy", - "last":"Ostatnio przeglądane", - "starred":"Ulubione", - "name":"Nazwa", - "last-viewed":"Ostatnio przeglądane", - "no-last-viewed-dashboards":"Nie oglądałeś jeszcze żadnych dashboardów" + "version-info": { + "title": "Wersja", + "contact-us": "Skontaktuj się z nami", + "current-version": "Obecna wersja", + "current": "Aktualna", + "available-version": "Dostępna wersja", + "available": "DostępnaS", + "upgrade": "Aktualizacja", + "version-is-up-to-date": "Wersja jest aktualna" }, - "configured-features":{ - "title":"Skonfigurowane funkcje", - "info":"Status funkcji wymagających konfiguracji", - "email-feature":"Email", - "sms-feature":"SMS", - "slack-feature":"Slack", - "oauth2-feature":"OAuth 2", - "2fa-feature":"2FA", - "feature-configured":"Funkcja jest skonfigurowana.\nKliknij, aby skonfigurować", - "feature-not-configured":"Funkcja nie jest skonfigurowana.\nKliknij, aby skonfigurować" + "license-info": { + "title": "Informacje o licencji", + "view-all": "Pokaż wszystkie", + "license-portal": "Portal licencji", + "devices-info": "{count} / { max, plural, =0 {∞} other {#} } Urządzeń", + "assets-info": "{count} / { max, plural, =0 {∞} other {#} } Zasobów", + "dashboards-info": "{count} / { max, plural, =0 {∞} other {#} } Paneli", + "integrations-info": "{count} / { max, plural, =0 {∞} other {#} } Integracji", + "community-support": "Pomoc społeczności", + "white-labeling": "White labeling", + "unlimited-datapoints-and-messages": "Nieograniczone punkty danych i wiadomości", + "unlimited-api-calls": "Nieograniczone wywołania API" }, - "version-info":{ - "title":"Wersja", - "contact-us":"Skontaktuj się z nami", - "current-version":"Aktualna wersja", - "current":"Aktualna", - "available-version":"Dostępna wersja", - "available":"Dostępna", - "upgrade":"Aktualizuj", - "version-is-up-to-date":"Wersja jest aktualna" + "usage-info": { + "title": "Wykorzystanie", + "entities": "Obiekty", + "api-calls": "Wywołania API" }, - "usage-info":{ - "title":"Użycie", - "entities":"Encje", - "api-calls":"Wywołania API" + "functions": { + "title": "Funkcje", + "pe-feature-tooltip": "Tylko na ThingsBoard\nProfessional Edition", + "switch-to-pe": "Przejdź na PE", + "alarms": "Alarmy", + "dashboards": "Panele", + "entities-and-relations": "Obiekty i Relacje", + "profiles": "Profile", + "advanced-features": "Zaawansowane funkcje", + "notification-center": "Centrum powiadomień", + "api-usage": "Użycie API", + "customers": "Klienci", + "customers-hierarchy": "Hierarchia klientów", + "roles-and-permissions": "Role i Pozwolenia", + "groups": "Grupy", + "integrations": "Integracje", + "solution-templates": "Szablony rozwiązań", + "scheduler": "Planista", + "white-labeling": "White-labeling" }, - "functions":{ - "title":"Funkcje", - "pe-feature-tooltip":"Tylko w ThingsBoard\nProfessional Edition", - "switch-to-pe":"Przełącz do PE", - "alarms":"Alarmy", - "dashboards":"Dashboards", - "entities-and-relations":"Encje i relacje", - "profiles":"Profile", - "advanced-features":"Zaawansowane funkcje", - "notification-center":"Centrum powiadomień", - "api-usage":"Użycie API", - "customers":"Klienci", - "customers-hierarchy":"Hierarchia klientów", - "roles-and-permissions":"Role i uprawnienia", - "groups":"Grupy", - "integrations":"Integracje", - "solution-templates":"Szablony rozwiązań", - "scheduler":"Harmonogram", - "white-labeling":"White-labeling" + "devices": { + "view-docs": "Zobacz dokumenty", + "inactive": "Nieaktywne", + "active": "Aktywne", + "total": "Wszystkie" }, - "devices":{ - "view-docs":"Zobacz dokumentację", - "inactive":"Nieaktywne", - "active":"Aktywne", - "total":"Razem" + "alarms": { + "critical": "Krytyczne", + "assigned-to-me": "Przydzielone do mnie", + "total": "Wszystkie" }, - "alarms":{ - "critical":"Krytyczne", - "assigned-to-me":"Przypisane do mnie", - "total":"Razem" + "solution-templates": { + "title": "Szablony rozwiązań", + "prototype-plan": "Plan prototypu", + "startup-plan": "Plan startowy" }, - "getting-started":{ - "get-started":"Rozpocznij", - "finish":"Zakończ", - "done-welcome-title":"Witamy na pokładzie", - "done-welcome-text":"Świetnie sobie poradziłeś!", - "sys-admin":{ - "step1":{ - "title":"Utwórz Najemcę i Administratora Najemcy", - "content":"

Najemca to osoba lub organizacja, która posiada lub wytwarza urządzenia i zasoby. Najemca może mieć wielu administratorów najemców, klientów, urządzeń i zasobów.

Administrator najemcy może tworzyć i zarządzać urządzeniami, zasobami, klientami i pulpitami w ramach konta najemcy.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", - "how-to-create-tenant":"Jak utworzyć Najemcę i Administratora Najemcy" + "getting-started": { + "get-started": "Jak zacząć?", + "finish": "Koniec", + "done-welcome-title": "Witaj na pokładzie", + "done-welcome-text": "Świetnie sobie z tym poradziłeś!", + "sys-admin": { + "step1": { + "title": "Stwórz Tenanta i Administratora Tenanta", + "content": "

Tenant to osoba fizyczna lub organizacja posiadająca lub produkująca urządzenia i zasoby.

Administrator Tenanta może tworzyć urządzenia, zasoby, klientów i panele oraz zarządzać nimi w ramach konta Tenanta.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-tenant": "Jak stworzyć Tenanta i Administratora Tenanta" + }, + "step2": { + "title": "Skonfiguruj funkcję: Serwer pocztowy", + "content": "

Konfiguracja serwera pocztowego jest niezbędna do aktywacji użytkownika, odzyskiwania hasła i dostarczania powiadomień o alarmach.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-configure-mail-server": "Jak skonfigurować serwer pocztowy" }, - "step2":{ - "title":"Skonfiguruj funkcję: Serwer poczty", - "content":"

Konfiguracja serwera poczty jest niezbędna do aktywacji użytkowników, odzyskiwania hasła i dostarczania powiadomień o alarmach.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", - "how-to-configure-mail-server":"Jak skonfigurować serwer poczty" + "step3": { + "title": "Skonfiguruj funkcję: dostawca SMS", + "content": "

Skonfiguruj dostawców SMS, aby powiadamiali klientów o alarmach za pomocą wiadomości SMS.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-configure-sms-provider": "Jak skonfigurować dostawcę SMS" }, - "step3":{ - "title":"Skonfiguruj funkcję: Dostawca SMS", - "content":"

Skonfiguruj dostawców SMS, aby informować klientów o alarmach za pomocą wiadomości SMS.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", - "how-to-configure-sms-provider":"Jak skonfigurować dostawcę SMS" + "step4": { + "title": "Skonfiguruj funkcję: White labeling", + "content": "

Z łatwością dostosuj logo i kolorystykę swojej firmy lub produktu bez kodowania i ponownego uruchamiania usługi.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-configure-white-labeling": "Jak white labeling" }, - "step4":{ - "title":"Skonfiguruj funkcję: 2FA", - "content":"

Popraw bezpieczeństwo kont platformy za pomocą uwierzytelniania dwuetapowego.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", - "how-to-configure-2fa":"Jak skonfigurować 2FA" + "step5": { + "title": "Skonfiguruj funkcję: 2FA", + "content": "

Popraw bezpieczeństwo kont platformy dzięki uwierzytelnianiu dwuskładnikowemu.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-configure-2fa": "Jak skonfigurować 2FA" }, - "step5":{ - "title":"Skonfiguruj funkcję: OAuth 2", - "content":"

Uprość logowanie dla użytkowników najemcy i klientów za pomocą funkcji jednokrotnego logowania za pośrednictwem OAuth 2.0.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", - "how-to-configure-oauth2":"Jak skonfigurować OAuth 2" + "step6": { + "title": "Skonfiguruj funkcję: OAuth 2", + "content": "

Uprość logowanie użytkowników Tenantów i klientów dzięki funkcji pojedynczego logowania za pośrednictwem protokołu OAuth 2.0.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-configure-oauth2": "Jak skonfigurować OAuth 2" }, - "step6":{ - "title":"Skonfiguruj funkcję: Slack", - "content":"

Rozprowadzaj powiadomienia do użytkowników najemców i klientów za pośrednictwem Slack zgodnie z zasadami powiadomień, które ustawisz.

Przeczytaj dokumentację, aby się dowiedzieć, jak to zrobić:

", - "how-to-configure-notifications":"Jak skonfigurować Slack" + "step7": { + "title": "Skonfiguruj funkcję: Slack", + "content": "

Rozpowszechniaj powiadomienia do użytkowników Tenantów i klientów za pośrednictwem Slacka zgodnie z ustawionymi regułami powiadomień.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-configure-notifications": "Jak skonfigurować Slacka" } }, - "tenant-admin":{ - "step1":{ - "title":"Utwórz urządzenie", - "content":"

Dodaj swoje pierwsze urządzenie do platformy za pomocą interfejsu użytkownika. Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", - "how-to-create-device":"Jak utworzyć urządzenie" + "tenant-admin": { + "step1": { + "title": "Stwórz urządzenie", + "content": "

Udostępnimy Twoje pierwsze urządzenie na platformie za pośrednictwem interfejsu użytkownika. Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić

", + "how-to-create-device": "Jak stworzyć urządzenie" }, - "step2":{ - "title":"Podłącz urządzenie", - "content-before":"

Aby podłączyć urządzenie, potrzebujesz danych uwierzytelniających urządzenia. Zalecamy korzystanie z domyślnie generowanych danych uwierzytelniających, czyli token dostępu, zgodnie z tym przewodnikiem.

  • Przejdź do tabeli urządzeń
  • Kliknij w wiersz urządzenia, aby otworzyć szczegóły urządzenia
  • Naciśnij przycisk „Kopiuj token dostępu”

Użyj prostych poleceń, aby publikować dane za pomocą protokołu HTTP. Nie zapomnij zamienić $ACCESS_TOKEN na swój token dostępu do urządzenia:

", - "ubuntu":{ - "install-curl":"Zainstaluj cURL dla Ubuntu:" + "step2": { + "title": "Podłącz urządzenie", + "content-before": "

Aby podłączyć urządzenie, musisz uzyskać dane uwierzytelniające urządzenia.

  • Przejdź do tabeli urządzeń
  • Kliknij wiersz urządzenia, aby otworzyć szczegóły urządzenia
  • Naciśnij przycisk „Kopiuj token dostępu”

Użyj prostych poleceń, aby publikować dane za pośrednictwem protokołu HTTP. Nie zapomnij, żeby zastąpić $ACCESS_TOKEN tokenem dostępu twojego urządzenia:

", + "ubuntu": { + "install-curl": "Zainstaluj cURL dla Ubuntu:" }, - "macos":{ - "install-curl":"Zainstaluj cURL dla MacOS:" + "macos": { + "install-curl": "Zainstaluj cURL dla MacOS:" }, - "windows":{ - "install-curl":"Od Windows 10 b17063 cURL jest dostępny domyślnie." + "windows": { + "install-curl": "Począwszy od systemu Windows 10 b17063, cURL jest dostępny domyślnie." }, - "replace-access-token":"Zamień $ACCESS_TOKEN na token dostępu do swojego urządzenia:", - "content-after":"

Możesz także używać innych protokołów, takich jak MQTT, CoAP, itp.

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", - "how-to-connect-device":"Jak podłączyć urządzenie" + "replace-access-token": "Zastąp $ACCESS_TOKEN tokenem twojego urządzenia:", + "content-after": "

Można także używać innych protokołów, takich jak MQTT, CoAP itp.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-connect-device": "Jak podłączyć urządzenie" }, - "step3":{ - "title":"Utwórz pulpit", - "content":"

Utwórz pulpit, aby wizualizować dane z jednostek, takich jak zasoby, urządzenia, itp.

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", - "how-to-create-dashboard":"Jak utworzyć pulpit" + "step3": { + "title": "Utwórz panel", + "content": "

Utwórz panel, aby wizualizować dane z obiektów, takich jak zasoby, urządzenia itp.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-dashboard": "Jak stworzyć Panel" }, - "step4":{ - "title":"Skonfiguruj reguły alarmowe", - "alarm-rules":"Reguły alarmowe", - "content":"

Podnieśmy alarm, gdy temperatura osiągnie 25°C. Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", - "how-to-configure-alarm-rules":"Jak skonfigurować reguły alarmowe" + "step4": { + "title": "Skonfiguruj reguły alarmów", + "alarm-rules": "Zasady alarmowania", + "content": "

Podnieśmy alarm, gdy temperatura osiągnie 25°C.

", + "how-to-configure-alarm-rules": "Jak skonfigurować reguły alarmów" }, - "step5":{ - "title":"Utwórz alarm", - "content-before":"

Aby uruchomić alarm, prześlij nową wartość telemetrii równej lub wyższej niż 26°C.

", - "replace-access-token":"Zamień $ACCESS_TOKEN na token dostępu do swojego urządzenia:", - "content-after":"

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", - "how-to-create-alarm":"Jak utworzyć alarm" + "step5": { + "title": "Utwórz alarm", + "content-before": "

Aby wywołać alarm, wyślij nową wartość telemetryczną wynoszącą 26°C lub wyższą.

", + "replace-access-token": "Zastąp $ACCESS_TOKEN tokenem twojego urządzenia:", + "content-after": "

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-alarm": "Jak utworzyć alarm" }, - "step6":{ - "title":"Utwórz klienta i przypisz pulpit", - "content":"

Tworząc pulpity dla końcowych użytkowników, użytkownik klienta może widzieć tylko swoje urządzenia, a dane innego klienta będą ukryte.

Przeczytaj dokumentację, aby dowiedzieć się, jak to zrobić:

", - "how-to-create-customer-and-assign-dashboard":"Jak utworzyć klienta i przypisać pulpit" + "step6": { + "title": "Utwórz klienta i udostępnij panel", + "content": "

Tworząc panele użytkownika końcowego, użytkownik klient może widzieć tylko swoje własne urządzenia, a dane innych klientów zostaną ukryte.

Postępuj zgodnie z dokumentacją, aby dowiedzieć się, jak to zrobić:

", + "how-to-create-customer-and-share-dashboard": "Jak utworzyć Klienta i udostępnić Panel" } } } }, - "color":{ - "color":"Kolor" + "icon": { + "icon": "Ikona", + "icons": "Icons", + "select-icon": "Wybierz ikonę", + "material-icons": "Ikony materialne", + "show-all": "Pokaż wszystkie ikony", + "search-icon": "Search icon", + "no-icons-found": "No icons found for '{{iconSearch}}'" }, - "icon":{ - "icon":"Ikona", - "icons":"Ikony", - "select-icon":"Wybierz ikonę", - "material-icons":"Ikony materiałowe", - "show-all":"Pokaż wszystkie ikony", - "search-icon":"Wyszukaj ikonę", - "no-icons-found":"Nie znaleziono ikon dla '{{iconSearch}}'" + "subscription": { + "entity-limit-text": "Możesz jednak uaktualnić swój plan subskrypcji, aby zwiększyć swoje limity.", + "upgrade-your-plan": "Uaktualnij plan subskrypcji", + "white-labeling-feature": "Funkcja white labeling", + "white-labeling-text-full": "Zmień nazwę interfejsu internetowego platformy ThingsBoard za pomocą logo Twojej firmy lub produktu oraz schematu kolorów w 2 minuty.

Usuń opcję „Powered By” w stopce panelu.
Nie jest wymagane kodowanie ani ponowne uruchamianie usługi. Pozwól także swoim klientom na White Labeling swoich interfejsów", + "enable-white-labeling": "Włącz teraz funkcję white labeling, aktualizując swój plan subskrypcji!", + "read-more": "Czytaj więcej", + "white-labeling-video-text": "Obejrzyj samouczek wideo poniżej, aby zobaczyć, jak działa ta funkcja!" + }, + "subscription-error": { + "title": "Naruszenie subskrypcji", + "warning-title": "Ostrzeżenie dotyczące subskrypcji", + "upgrade-subscription-plan": "Uaktualnij swój plan subskrypcji", + "upgrade-subscription-plan-to-install-solution-template": "Aby zainstalować rozwiązanie {{solutionTemplateName}}, musisz uaktualnić swoją subskrypcję co najmniej do planu {{planName}}!", + "limit-reached": { + "device-count": "Osiągnięto maksymalną liczbę urządzeń ({{value}}) dozwolone przez Twój plan abonamentowy!", + "asset-count": "Osiągnąłeś maksymalną liczbę zasobów ({{value}}) dozwolone przez Twój plan abonamentowy!" + }, + "feature-disabled": { + "white-labeling": "Funkcja white labeling nie jest dozwolona w Twoim planie subskrypcji!" + } }, - "phone-input":{ - "phone-input-label":"Numer telefonu", - "phone-input-required":"Numer telefonu jest wymagany", - "phone-input-validation":"Numer telefonu jest nieprawidłowy lub niemożliwy", - "phone-input-pattern":"Nieprawidłowy numer telefonu. Powinien być w formacie E.164, np. {{phoneNumber}}", - "phone-input-hint":"Numer telefonu w formacie E.164, np. {{phoneNumber}}" + "phone-input": { + "phone-input-label": "Numer telefonu", + "phone-input-required": "Numer telefonu jest wymagany", + "phone-input-validation": "Numer telefonu jest nieprawidłowy lub niemożliwy", + "phone-input-pattern": "Nieprawidłowy numer telefonu. Powinien być w formacie E.164, np. {{phoneNumber}}", + "phone-input-hint": "Numer telefonu w formacie E.164, np. {{phoneNumber}}" }, - "custom":{ - "widget-action":{ - "action-cell-button":"Przycisk w komórce akcji", - "row-click":"Po kliknięciu w wiersz", - "polygon-click":"Po kliknięciu w wielokąt", - "marker-click":"Po kliknięciu w marker", - "circle-click":"Po kliknięciu w okrąg", - "tooltip-tag-action":"Akcja etykiety narzędziowej", - "node-selected":"Po wybraniu węzła", - "element-click":"Po kliknięciu w element HTML", - "pie-slice-click":"Po kliknięciu w kawałek tarty", - "row-double-click":"Po podwójnym kliknięciu w wiersz", - "card-click":"Po kliknięciu w kartę" + "custom": { + "widget-action": { + "action-cell-button": "Przycisk komórki akcji", + "row-click": "Po kliknięciu wiersza", + "polygon-click": "Po kliknięciu poligonu", + "marker-click": "Po kliknięciu znacznika", + "circle-click": "Po kliknięciu okręgu", + "tooltip-tag-action": "Akcja tagu podpowiedzi", + "node-selected": "Po wybraniu węzła", + "element-click": "Po kliknięciu elementu HTML", + "pie-slice-click": "Po kliknięciu wycinka", + "row-double-click": "Po dwukrotnym kliknięciu wiersza", + "card-click": "On card click" } }, - "paginator":{ - "items-per-page":"Elementy na stronie:", - "first-page-label":"Pierwsza strona", - "last-page-label":"Ostatnia strona", - "next-page-label":"Następna strona", - "previous-page-label":"Poprzednia strona", - "items-per-page-separator":"z" + "paginator": { + "items-per-page": "Przedmioty na stronę:", + "first-page-label": "Pierwsza strona", + "last-page-label": "Ostatnia strona", + "next-page-label": "Następna strona", + "previous-page-label": "Poprzednia strona", + "items-per-page-separator": "z" }, - "language":{ - "language":"Język" + "language": { + "language": "Język" } } From e9956cbd10cb1aff90b524a8ab4791cbcab9e72d Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 6 May 2024 09:32:36 +0300 Subject: [PATCH 051/121] UI: Fixed models --- .../profile/device/lwm2m/lwm2m-profile-config.models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 79e9bb6514..4262e47970 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -175,7 +175,7 @@ export interface ClientLwM2mSettings { edrxCycle?: number; pagingTransmissionWindow?: number; psmActivityTimer?: number; - defaultObjectIDVer: string; + defaultObjectIDVer: ObjectIDVer; } export interface ObservableAttributes { From 31276ddb42d4ac49e579cc1f4b91fe74ca4fff86 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 6 May 2024 13:59:11 +0300 Subject: [PATCH 052/121] UI: Refactor data aggregator to support time-series aggregation key processing in latest widgets --- ui-ngx/src/app/core/api/data-aggregator.ts | 23 +++++++++---------- .../src/app/shared/models/time/time.models.ts | 4 ++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index 68baa7d04a..d437bd73d0 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -18,7 +18,6 @@ import { AggKey, IndexedSubscriptionData, } from '@app/shared/models/telemetry/t import { AggregationType, calculateAggIntervalWithSubscriptionTimeWindow, - calculateInterval, calculateIntervalComparisonEndTime, calculateIntervalEndTime, calculateIntervalStartEndTime, @@ -28,7 +27,7 @@ import { SubscriptionTimewindow } from '@shared/models/time/time.models'; import { UtilsService } from '@core/services/utils.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils'; +import { deepClone, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils'; import { DataEntry, DataSet, IndexedData } from '@shared/models/widget.models'; import BTree from 'sorted-btree'; import Timeout = NodeJS.Timeout; @@ -49,7 +48,8 @@ class AggDataMap { constructor( private subsTw: SubscriptionTimewindow, - private endTs: number + private endTs: number, + private aggType: AggregationType ){}; set(ts: number, data: AggData) { @@ -64,9 +64,9 @@ class AggDataMap { this.map.delete(ts); } - findDataForTs(ts: number, noAggregation: boolean): AggData | undefined { + findDataForTs(ts: number): AggData | undefined { if (ts >= this.endTs) { - this.updateLastInterval(ts + 1, noAggregation); + this.updateLastInterval(ts + 1); } const pair = this.map.getPairOrNextLower(ts, this.reusePair); if (pair) { @@ -79,17 +79,16 @@ class AggDataMap { } calculateAggInterval(timestamp: number): [number, number] { - return calculateInterval(this.subsTw.startTs, this.endTs, this.subsTw.aggregation.interval, this.subsTw.tsOffset, this.subsTw.timezone, timestamp); + return calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, this.endTs, timestamp, this.aggType); } - updateLastInterval(endTs: number, noAggregation?: boolean) { + updateLastInterval(endTs: number) { if (endTs > this.endTs) { this.endTs = endTs; const lastTs = this.map.maxKey(); if (lastTs) { const data = this.map.get(lastTs); - const interval = isDefined(noAggregation) && !noAggregation ? this.calculateAggInterval(data.ts) : - calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts); + const interval = calculateAggIntervalWithSubscriptionTimeWindow(this.subsTw, endTs, data.ts, this.aggType); data.interval = interval; data.ts = interval[0] + Math.floor((interval[1] - interval[0]) / 2); } @@ -417,7 +416,7 @@ export class DataAggregator { const noAggregation = aggType === AggregationType.NONE; let aggKeyData = aggregationMap.aggMap[id]; if (!aggKeyData) { - aggKeyData = new AggDataMap(this.subsTw, this.endTs); + aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType); aggregationMap.aggMap[id] = aggKeyData; } const keyData = data[id]; @@ -451,14 +450,14 @@ export class DataAggregator { const noAggregation = aggType === AggregationType.NONE; let aggKeyData = this.aggregationMap.aggMap[id]; if (!aggKeyData) { - aggKeyData = new AggDataMap(this.subsTw, this.endTs); + aggKeyData = new AggDataMap(this.subsTw, this.endTs, aggType); this.aggregationMap.aggMap[id] = aggKeyData; } const keyData = data[id]; keyData.forEach((kvPair) => { const timestamp = kvPair[0]; const value = DataAggregator.convertValue(kvPair[1], noAggregation); - let aggData = aggKeyData.findDataForTs(timestamp, noAggregation); + let aggData = aggKeyData.findDataForTs(timestamp); if (!aggData) { let interval: [number, number] = [timestamp, timestamp]; if (!noAggregation) { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index deec2ee8f9..9336f47d90 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -1116,8 +1116,8 @@ export const endIntervalDate = (current: moment_.Moment, interval: IntervalType) }; export const calculateAggIntervalWithSubscriptionTimeWindow - = (subsTw: SubscriptionTimewindow, endTs: number, timestamp: number): [number, number] => { - if (subsTw.aggregation.type === AggregationType.NONE) { + = (subsTw: SubscriptionTimewindow, endTs: number, timestamp: number, aggType?: AggregationType): [number, number] => { + if ((aggType || subsTw.aggregation.type) === AggregationType.NONE) { return [timestamp, timestamp]; } else { return calculateInterval(subsTw.startTs, endTs, subsTw.aggregation.interval, subsTw.tsOffset, subsTw.timezone, timestamp); From 9add3118095062877c5893a112cfd2b379fb0d68 Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Mon, 6 May 2024 16:37:57 +0300 Subject: [PATCH 053/121] Update feature_request.md Link change to a more accurate Acceptable Use document --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 005141b2e5..9430e961d5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -22,4 +22,4 @@ Add any other context or screenshots about the feature request here. _____________________________________________________ **Disclaimer** -We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://opensource.guide/building-community/), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies)), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From d5d3a022b14c72c92c5add984859587b3852b788 Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Mon, 6 May 2024 16:38:39 +0300 Subject: [PATCH 054/121] Update ---bug-report.md Fixed link --- .github/ISSUE_TEMPLATE/---bug-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index e883305f89..a126a28e34 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -64,4 +64,4 @@ Please feel free to add any other context about the problem here. ________________________________________________________________ **Disclaimer** -We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://opensource.guide/building-community/), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From 0597a6365be82689a7a52a61580e9d2873ad3b3a Mon Sep 17 00:00:00 2001 From: AlexDoanTB <47384595+AlexDoanTB@users.noreply.github.com> Date: Mon, 6 May 2024 16:39:25 +0300 Subject: [PATCH 055/121] Update question.md Fix link --- .github/ISSUE_TEMPLATE/question.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index a7c2b94488..7f4becfe82 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -27,4 +27,4 @@ Clear and concise details. ___________________________________________________________ **Disclaimer** -We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://opensource.guide/building-community/), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. +We appreciate your contribution whether it is a bug report, feature request, or pull request with improvement (hopefully). Please comply with the [Community ethics policy](https://docs.github.com/en/site-policy/acceptable-use-policies/github-acceptable-use-policies), and do not expect us to answer your requests immediately. Also, do not treat *GitHub issues* as a support channel. From b78c4fd21abf26e812d294162631fabdcf7744bb Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 6 May 2024 12:04:00 +0200 Subject: [PATCH 056/121] Add per-tenant rate limits for emails sending --- .../server/service/mail/DefaultMailService.java | 13 +++++++++++++ application/src/main/resources/thingsboard.yml | 2 ++ .../server/common/data/limit/LimitedApi.java | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 29db1d7d76..470127246d 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -22,6 +22,7 @@ import jakarta.xml.bind.DatatypeConverter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; import org.springframework.core.NestedRuntimeException; @@ -33,6 +34,7 @@ import org.springframework.stereotype.Service; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.TbEmail; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -43,6 +45,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -84,6 +87,12 @@ public class DefaultMailService implements MailService { @Autowired private TbMailContextComponent tbMailContextComponent; + @Autowired + private RateLimitService rateLimitService; + + @Value("${mail.per_tenant_rate_limits:}") + private String perTenantRateLimitConfig; + private TbMailSender mailSender; private String mailFrom; @@ -214,6 +223,10 @@ public class DefaultMailService implements MailService { private void sendMail(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender, long timeout) throws ThingsboardException { if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) { + if (tenantId != null && !tenantId.isSysTenantId() && StringUtils.isNotEmpty(perTenantRateLimitConfig) && + !rateLimitService.checkRateLimit(LimitedApi.EMAILS, (Object) tenantId, perTenantRateLimitConfig)) { + throw new ThingsboardException("Rate limit for emails sending is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); + } try { MimeMessage mailMsg = javaMailSender.createMimeMessage(); boolean multipart = (tbEmail.getImages() != null && !tbEmail.getImages().isEmpty()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 191d810c64..2fc2473851 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -164,6 +164,8 @@ mail: oauth2: # Interval for checking refresh token expiration in seconds(by default, 1 day). refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}" + # Rate limits for sending mails per tenant + per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}" # Usage statistics parameters usage: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 1a0f038c90..48f8fdfc92 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -39,7 +39,8 @@ public enum LimitedApi { TWO_FA_VERIFICATION_CODE_SEND(false, true), TWO_FA_VERIFICATION_CODE_CHECK(false, true), TRANSPORT_MESSAGES_PER_TENANT("transport messages", true), - TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false); + TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), + EMAILS("emails", true); private Function configExtractor; @Getter From b750077c717132f0dd366338789e9ba044e16343 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 6 May 2024 12:18:53 +0200 Subject: [PATCH 057/121] Minor refactoring for mail rate limits --- .../service/mail/DefaultMailService.java | 3 +- .../exception/RateLimitExceededException.java | 30 +++++++++++++++++++ .../server/common/data/limit/LimitedApi.java | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 470127246d..04bf1f40a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.RateLimitExceededException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -225,7 +226,7 @@ public class DefaultMailService implements MailService { if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) { if (tenantId != null && !tenantId.isSysTenantId() && StringUtils.isNotEmpty(perTenantRateLimitConfig) && !rateLimitService.checkRateLimit(LimitedApi.EMAILS, (Object) tenantId, perTenantRateLimitConfig)) { - throw new ThingsboardException("Rate limit for emails sending is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS); + throw new RateLimitExceededException(LimitedApi.EMAILS); } try { MimeMessage mailMsg = javaMailSender.createMimeMessage(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java new file mode 100644 index 0000000000..27be0f65d2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/RateLimitExceededException.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.exception; + +import org.thingsboard.server.common.data.limit.LimitedApi; + +public class RateLimitExceededException extends AbstractRateLimitException { + + public RateLimitExceededException(String message) { + super(message); + } + + public RateLimitExceededException(LimitedApi api) { + super("Rate limit for " + api.getLabel() + " is exceeded"); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 48f8fdfc92..4afead3435 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -40,7 +40,7 @@ public enum LimitedApi { TWO_FA_VERIFICATION_CODE_CHECK(false, true), TRANSPORT_MESSAGES_PER_TENANT("transport messages", true), TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), - EMAILS("emails", true); + EMAILS("emails sending", true); private Function configExtractor; @Getter From b8480cd456d1ef98f18e075c593e419c25f3b126 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 13:30:40 +0200 Subject: [PATCH 058/121] MailSenderInternalExecutorService hotfix (cherry picked from commit 5c5b93d6a6f8b2b4ac1dbd3b42d0b564206f2c70) --- .../service/mail/DefaultMailService.java | 2 +- .../MailSenderInternalExecutorService.java | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 04bf1f40a0..176449a213 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -80,7 +80,7 @@ public class DefaultMailService implements MailService { private TbApiUsageStateService apiUsageStateService; @Autowired - private MailExecutorService mailExecutorService; + private MailSenderInternalExecutorService mailExecutorService; @Autowired private PasswordResetExecutorService passwordResetExecutorService; diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java new file mode 100644 index 0000000000..ac57946916 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java @@ -0,0 +1,48 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +package org.thingsboard.server.service.mail; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.AbstractListeningExecutor; + +@Component +public class MailSenderInternalExecutorService extends AbstractListeningExecutor { + + @Value("${actors.rule.mail_thread_pool_size}") + private int mailExecutorThreadPoolSize; + + @Override + protected int getThreadPollSize() { + return mailExecutorThreadPoolSize; + } + +} From 38d37224f3f5834bfb72d5a449d247faa2d0ceab Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 16:06:08 +0200 Subject: [PATCH 059/121] DefaultMailService with timeout by mail-service-watchdog --- .../service/mail/DefaultMailService.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 176449a213..b9c7f71d66 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.mail; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; import freemarker.template.Configuration; import freemarker.template.Template; import jakarta.xml.bind.DatatypeConverter; @@ -32,6 +33,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.TbEmail; import org.thingsboard.server.cache.limits.RateLimitService; @@ -53,11 +55,14 @@ import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import jakarta.mail.internet.MimeMessage; import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -94,6 +99,8 @@ public class DefaultMailService implements MailService { @Value("${mail.per_tenant_rate_limits:}") private String perTenantRateLimitConfig; + private final ScheduledExecutorService scheduler; + private TbMailSender mailSender; private String mailFrom; @@ -105,6 +112,7 @@ public class DefaultMailService implements MailService { this.freemarkerConfig = freemarkerConfig; this.adminSettingsService = adminSettingsService; this.apiUsageClient = apiUsageClient; + this.scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog")); } @PostConstruct @@ -112,6 +120,13 @@ public class DefaultMailService implements MailService { updateMailConfiguration(); } + @PreDestroy + public void destroy() { + if (scheduler != null) { + scheduler.shutdownNow(); + } + } + @Override public void updateMailConfiguration() { AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); @@ -429,8 +444,11 @@ public class DefaultMailService implements MailService { } private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) { + var submittedMail = Futures.withTimeout( + mailExecutorService.submit(() -> mailSender.send(msg)), + timeout, TimeUnit.MILLISECONDS, scheduler); try { - mailExecutorService.submit(() -> mailSender.send(msg)).get(timeout, TimeUnit.MILLISECONDS); + submittedMail.get(timeout, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { log.debug("Error during mail submission", e); throw new RuntimeException("Timeout!"); From 97f0078288b70dd2911688e7991c46b24aa58e85 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:11:04 +0200 Subject: [PATCH 060/121] mvn license:format --- .../MailSenderInternalExecutorService.java | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java index ac57946916..03aea6c7f9 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java @@ -1,32 +1,17 @@ /** - * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * Copyright © 2016-2024 The Thingsboard Authors * - * Copyright © 2016-2024 ThingsBoard, Inc. All Rights Reserved. + * 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 * - * NOTICE: All information contained herein is, and remains - * the property of ThingsBoard, Inc. and its suppliers, - * if any. The intellectual and technical concepts contained - * herein are proprietary to ThingsBoard, Inc. - * and its suppliers and may be covered by U.S. and Foreign Patents, - * patents in process, and are protected by trade secret or copyright law. + * http://www.apache.org/licenses/LICENSE-2.0 * - * Dissemination of this information or reproduction of this material is strictly forbidden - * unless prior written permission is obtained from COMPANY. - * - * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, - * managers or contractors who have executed Confidentiality and Non-disclosure agreements - * explicitly covering such access. - * - * The copyright notice above does not evidence any actual or intended publication - * or disclosure of this source code, which includes - * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. - * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, - * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT - * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, - * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. - * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION - * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, - * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.thingsboard.server.service.mail; From 3d26e854250a21d56093fd530ffde713680185b1 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:26:20 +0200 Subject: [PATCH 061/121] Mail service minor refactoring --- .../server/service/mail/DefaultMailService.java | 11 +++++------ .../mail/MailSenderInternalExecutorService.java | 4 ++++ .../server/service/mail/TbMailContextComponent.java | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index b9c7f71d66..d68837e1f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import freemarker.template.Configuration; import freemarker.template.Template; -import jakarta.xml.bind.DatatypeConverter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -99,7 +98,7 @@ public class DefaultMailService implements MailService { @Value("${mail.per_tenant_rate_limits:}") private String perTenantRateLimitConfig; - private final ScheduledExecutorService scheduler; + private final ScheduledExecutorService timeoutScheduler; private TbMailSender mailSender; @@ -112,7 +111,7 @@ public class DefaultMailService implements MailService { this.freemarkerConfig = freemarkerConfig; this.adminSettingsService = adminSettingsService; this.apiUsageClient = apiUsageClient; - this.scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog")); + this.timeoutScheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("mail-service-watchdog")); } @PostConstruct @@ -122,8 +121,8 @@ public class DefaultMailService implements MailService { @PreDestroy public void destroy() { - if (scheduler != null) { - scheduler.shutdownNow(); + if (timeoutScheduler != null) { + timeoutScheduler.shutdownNow(); } } @@ -446,7 +445,7 @@ public class DefaultMailService implements MailService { private void sendMailWithTimeout(JavaMailSender mailSender, MimeMessage msg, long timeout) { var submittedMail = Futures.withTimeout( mailExecutorService.submit(() -> mailSender.send(msg)), - timeout, TimeUnit.MILLISECONDS, scheduler); + timeout, TimeUnit.MILLISECONDS, timeoutScheduler); try { submittedMail.get(timeout, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java index 03aea6c7f9..0c8feff6c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/MailSenderInternalExecutorService.java @@ -19,6 +19,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.common.util.AbstractListeningExecutor; +/** + * Executor have the sole purpose to send mails. It should be used only by Mail Service. + * For other purposes please use the MailExecutorService component + * */ @Component public class MailSenderInternalExecutorService extends AbstractListeningExecutor { diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java index 4df152936f..97c20fdcda 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.dao.settings.AdminSettingsService; -import org.thingsboard.server.queue.util.TbCoreComponent; @Component @Data From 4daa229d83bba1b0d6a2caab4aa6535fb7038b15 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:31:02 +0200 Subject: [PATCH 062/121] MAIL_PER_TENANT_RATE_LIMITS example --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2fc2473851..4c4fad1238 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -164,7 +164,7 @@ mail: oauth2: # Interval for checking refresh token expiration in seconds(by default, 1 day). refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}" - # Rate limits for sending mails per tenant + # Rate limits for sending mails per tenant. As example for 1000 per minute and 10000 per hour is "1000:60,10000:3600" per_tenant_rate_limits: "${MAIL_PER_TENANT_RATE_LIMITS:}" # Usage statistics parameters From 9b5962e447c8b8d2cac5329772891cefaab4efe7 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 18:52:37 +0200 Subject: [PATCH 063/121] DefaultMailService tbMailContextComponent renamed to the ctx --- .../thingsboard/server/service/mail/DefaultMailService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index d68837e1f6..257dfa14b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -90,7 +90,7 @@ public class DefaultMailService implements MailService { private PasswordResetExecutorService passwordResetExecutorService; @Autowired - private TbMailContextComponent tbMailContextComponent; + private TbMailContextComponent ctx; @Autowired private RateLimitService rateLimitService; @@ -131,7 +131,7 @@ public class DefaultMailService implements MailService { AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); if (settings != null) { JsonNode jsonConfig = settings.getJsonValue(); - mailSender = new TbMailSender(tbMailContextComponent, jsonConfig); + mailSender = new TbMailSender(ctx, jsonConfig); mailFrom = jsonConfig.get("mailFrom").asText(); timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT); } else { @@ -146,7 +146,7 @@ public class DefaultMailService implements MailService { @Override public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException { - TbMailSender testMailSender = new TbMailSender(tbMailContextComponent, jsonConfig); + TbMailSender testMailSender = new TbMailSender(ctx, jsonConfig); String mailFrom = jsonConfig.get("mailFrom").asText(); String subject = messages.getMessage("test.message.subject", null, Locale.US); long timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT); From cfab9c2d349a765f37983c0181300de822a54079 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 6 May 2024 21:49:46 +0200 Subject: [PATCH 064/121] Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts --- pom.xml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pom.xml b/pom.xml index c06166e497..c3451544a4 100755 --- a/pom.xml +++ b/pom.xml @@ -1476,6 +1476,12 @@ org.apache.httpcomponents httpclient ${apache-httpclient.version} + + + commons-logging + commons-logging + + org.apache.httpcomponents @@ -1943,6 +1949,12 @@ org.elasticsearch.client elasticsearch-rest-client ${elasticsearch.version} + + + commons-logging + commons-logging + + org.javadelight @@ -1968,11 +1980,23 @@ com.amazonaws aws-java-sdk-sqs ${aws.sdk.version} + + + commons-logging + commons-logging + + com.amazonaws aws-java-sdk-sns ${aws.sdk.version} + + + commons-logging + commons-logging + + com.google.cloud @@ -2102,6 +2126,10 @@ org.slf4j * + + commons-logging + commons-logging + com.github.spotbugs spotbugs-annotations @@ -2136,6 +2164,12 @@ mockserver-netty ${mock-server.version} test + + + commons-logging + commons-logging + + org.mock-server @@ -2168,6 +2202,12 @@ com.google.firebase firebase-admin ${firebase-admin.version} + + + commons-logging + commons-logging + + org.eclipse.jgit @@ -2203,6 +2243,12 @@ org.apache.xmlgraphics batik-transcoder ${apache-xmlgraphics.version} + + + commons-logging + commons-logging + + org.apache.xmlgraphics From 2b377d8605a35a1fe5abb14c6824ac2e031405b5 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 7 May 2024 10:01:44 +0300 Subject: [PATCH 065/121] UI: Fixed paste hex color with prefix --- .../app/shared/components/color-picker/hex-input.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts index fcd91a2397..9d65cf02a6 100644 --- a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.ts @@ -59,14 +59,14 @@ export class HexInputComponent { } public onInputChange(event: KeyboardEvent, inputValue: string): void { - const value = inputValue.toLowerCase(); + const value = inputValue.replace('#', '').toLowerCase(); if ( ((event.keyCode === 13 || event.key.toLowerCase() === 'enter') && value.length === 3) || value.length === 6 || value.length === 8 ) { const hex = parseInt(value, 16); const hexStr = hex.toString(16); - if (hexStr.padStart(value.length, '0') === value && this.value !== value) { + if (hexStr.padStart(value.length, '0') === value && this.value.toLowerCase() !== value) { const newColor = new Color(`#${value}`); this.colorChange.emit(newColor); } From 953c2150023ccb081782ff5217c7a52c262d58ea Mon Sep 17 00:00:00 2001 From: rusikv Date: Tue, 7 May 2024 11:08:39 +0300 Subject: [PATCH 066/121] UI: added separate mobile qrcode widget to cards bundle --- .../json/system/widget_bundles/cards.json | 1 + .../widget_bundles/home_page_widgets.json | 2 +- .../widget_types/home_mobile_app_qr_code.json | 21 +++ .../widget_types/mobile_app_qr_code.json | 10 +- .../src/app/core/http/mobile-app.service.ts | 10 +- .../cards/mobile-app-qr-code-widge.models.ts | 50 ++++++ .../mobile-app-qrcode-widget.component.html | 6 +- .../mobile-app-qrcode-widget.component.scss | 3 +- .../lib/mobile-app-qrcode-widget.component.ts | 14 +- ...app-qr-code-widget-settings.component.html | 130 +++++++++++++++ ...app-qr-code-widget-settings.component.scss | 19 +++ ...e-app-qr-code-widget-settings.component.ts | 156 ++++++++++++++++++ .../lib/settings/widget-settings.module.ts | 6 + .../admin/mobile-app-settings.component.ts | 6 +- .../app/shared/models/mobile-app.models.ts | 2 +- .../dashboard/tenant_admin_home_page.json | 12 +- ui-ngx/src/form.scss | 3 - 17 files changed, 415 insertions(+), 36 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 39996839f1..5a839d4c82 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -17,6 +17,7 @@ "cards.label_widget", "cards.dashboard_state_widget", "cards.qr_code", + "cards.mobile_app_qr_code", "cards.attributes_card", "cards.html_card", "cards.html_value_card", diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index 5465f750e5..0af30e4df2 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -14,6 +14,6 @@ "home_page_widgets.documentation_links", "home_page_widgets.dashboards", "home_page_widgets.usage_info", - "home_page_widgets.mobile_app_qr_code" + "home_page_widgets.home_mobile_app_qr_code" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json new file mode 100644 index 0000000000..88109fc902 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json @@ -0,0 +1,21 @@ +{ + "fqn": "home_page_widgets.home_mobile_app_qr_code", + "name": "Mobile app QR code", + "deprecated": false, + "image": null, + "description": null, + "descriptor": { + "type": "static", + "sizeX": 6, + "sizeY": 3, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "", + "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\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"Mobile app QR code\",\"dropShadow\":true}" + }, + "tags": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index 2e3600d05b..82ce9025ed 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -1,21 +1,21 @@ { - "fqn": "home_page_widgets.mobile_app_qr_code", + "fqn": "cards.mobile_app_qr_code", "name": "Mobile app QR code", "deprecated": false, "image": null, "description": null, "descriptor": { "type": "static", - "sizeX": 6, + "sizeX": 7.5, "sizeY": 3, "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnInit();\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.mobileAppQrcodeWidget.ngOnDestroy();\n}", + "controllerScript": "self.onInit = function() {\n}\n", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "", - "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\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}" + "settingsDirective": "tb-mobile-app-qr-code-widget-settings", + "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\":{\"useDefaultApp\":true,\"androidConfig\":{\"enabled\":true,\"appPackage\":\"\",\"sha256CertFingerprints\":\"\"},\"iosConfig\":{\"enabled\":true,\"appId\":\"\"},\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgeStyle\":\"ORIGINAL\",\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"}},\"title\":\"Mobile app QR code\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" }, "tags": null } \ No newline at end of file diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index 75d5f337bc..bb4dc6c863 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -18,7 +18,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileAppSettings } from '@shared/models/mobile-app.models'; +import { MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; @Injectable({ providedIn: 'root' @@ -30,12 +30,12 @@ export class MobileAppService { ) { } - public getMobileAppSettings(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); } - public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + public saveMobileAppSettings(mobileAppSettings: MobileAppQRCodeSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); } public getMobileAppDeepLink( config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts new file mode 100644 index 0000000000..4027e0445b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AndroidConfig, + BadgePosition, + BadgeStyle, + IosConfig, + QRCodeConfig +} from '@shared/models/mobile-app.models'; + +export type MobileAppQrCodeWidgetSettings = { + useDefaultApp: boolean; + androidConfig: AndroidConfig; + iosConfig: IosConfig; + qrCodeConfig: Omit; +} + +export const mobileAppQrCodeWidgetDefaultSettings: MobileAppQrCodeWidgetSettings = { + useDefaultApp: true, + androidConfig: { + enabled: true, + appPackage: '', + sha256CertFingerprints: '' + }, + iosConfig: { + enabled: true, + appId: '' + }, + qrCodeConfig: { + badgeEnabled: true, + badgeStyle: BadgeStyle.ORIGINAL, + badgePosition: BadgePosition.RIGHT, + qrCodeLabelEnabled: true, + qrCodeLabel: 'Scan to connect or download mobile app' + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 53f95cb57e..e3f9ca11c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -15,15 +15,15 @@ limitations under the License. --> -
admin.mobile-app.connect-mobile-app
+
admin.mobile-app.connect-mobile-app
-
+ [class.tb-badge-container]="!previewMode && !ctx"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index 1d088ec482..5dafcc7521 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -17,7 +17,6 @@ @import '../../../../../../scss/constants'; :host { - width: 100%; height: 100%; display: flex; flex-direction: column; @@ -25,12 +24,14 @@ } .tb-title { + padding-bottom: 12px; align-self: start; font-weight: 600; font-size: 20px; line-height: 24px; letter-spacing: 0.1px; color: rgba(0, 0, 0, 0.76); + @media #{$mat-md-lg} { font-weight: 500; font-size: 14px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index c9c8541613..7296b47f80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -18,13 +18,14 @@ import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestr import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; -import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; +import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; import { MobileAppService } from '@core/http/mobile-app.service'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; @Component({ selector: 'tb-mobile-app-qrcode-widget', @@ -41,7 +42,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI previewMode: boolean; @Input() - set mobileAppSettings(settings: MobileAppSettings) { + set mobileAppSettings(settings: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings) { if (settings) { this.mobileAppSettingsValue = settings; } @@ -59,7 +60,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI badgePosition = BadgePosition; badgeStyleURLMap = badgeStyleURLMap; - private mobileAppSettingsValue: MobileAppSettings; + private mobileAppSettingsValue: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings; private deepLinkTTL: number; constructor(protected store: Store, @@ -71,9 +72,8 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI ngOnInit(): void { if (this.ctx) { - this.ctx.$scope.mobileAppQrcodeWidget = this; - } - if (!this.mobileAppSettings) { + this.mobileAppSettings = this.ctx.settings; + } else { this.mobileAppService.getMobileAppSettings().subscribe((settings => { this.mobileAppSettings = settings; this.cd.detectChanges(); @@ -104,7 +104,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI updateQRCode(link: string) { import('qrcode').then((QRCode) => { - QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 125}); + QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 90 }); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html new file mode 100644 index 0000000000..cdef956141 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html @@ -0,0 +1,130 @@ + +
+
+
+
admin.mobile-app.applications
+ + {{ 'admin.mobile-app.default' | translate }} + {{ 'admin.mobile-app.custom' | translate }} + +
+
+
+ + {{ 'admin.mobile-app.android' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-package-name' | translate }}
+ + + + warning + + +
+
+
+
+
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
+ + + + warning + + +
+
+
+
+
+ + {{ 'admin.mobile-app.ios' | translate }} + +
+
+
+
{{ 'admin.mobile-app.app-id' | translate }}
+ + + + warning + + +
+
+
+
+
+
+
+ + {{ 'admin.mobile-app.badges' | translate }} + + + + + {{ badgeStyle.value | translate }} + + + + + + + {{ badgePosition.value | translate }} + + + +
+
+
+
+ + {{ 'admin.mobile-app.label' | translate }} + + + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss new file mode 100644 index 0000000000..d0a2c9ac16 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-fixed-width { + min-width: 230px; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts new file mode 100644 index 0000000000..61999d7d41 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from "@angular/core"; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms"; +import { WidgetSettings, WidgetSettingsComponent } from "@shared/models/widget.models"; +import { AppState } from '@core/core.state'; +import { Store } from "@ngrx/store"; +import { badgePositionTranslationsMap, badgeStyleTranslationsMap } from '@shared/models/mobile-app.models'; +import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; + +@Component({ + selector: 'tb-mobile-app-qr-code-widget-settings', + templateUrl: './mobile-app-qr-code-widget-settings.component.html', + styleUrls: ['/mobile-app-qr-code-widget-settings.component.scss', './../widget-settings.scss'] +}) +export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsComponent { + + mobileAppQRCodeWidgetSettingsForm: UntypedFormGroup; + + badgePositionTranslationsMap = badgePositionTranslationsMap; + badgeStyleTranslationsMap = badgeStyleTranslationsMap; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.mobileAppQRCodeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...mobileAppQrCodeWidgetDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.mobileAppQRCodeWidgetSettingsForm = this.fb.group({ + useDefaultApp: [settings.useDefaultApp, []], + androidConfig: this.fb.group({ + enabled: [settings.androidConfig.enabled, []], + appPackage: [settings.androidConfig.appPackage, []], + sha256CertFingerprints: [settings.androidConfig.sha256CertFingerprints, []] + }), + iosConfig: this.fb.group({ + enabled: [settings.iosConfig.enabled, []], + appId: [settings.iosConfig.appId, []] + }), + qrCodeConfig: this.fb.group({ + badgeEnabled: [settings.qrCodeConfig.badgeEnabled, []], + badgeStyle: [{value: settings.qrCodeConfig.badgeStyle, disabled: true}, []], + badgePosition: [{value: settings.qrCodeConfig.badgePosition, disabled: true}, []], + qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled, []], + qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel, []] + }) + }); + } + + protected validatorTriggers(): string[] { + return ['useDefaultApp', 'androidConfig.enabled', 'iosConfig.enabled', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled']; + } + + protected updateValidators(emitEvent: boolean) { + const useDefaultApp = this.mobileAppQRCodeWidgetSettingsForm.get('useDefaultApp').value; + const androidEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.enabled').value; + const iosEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.enabled').value; + const badgeEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').value; + const qrCodeLabelEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value; + + if (useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators(); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } + + if (androidEnabled) { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + } + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + if (badgeEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators(); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); + } + if (!iosEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + + if (iosEnabled) { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + } + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + if (badgeEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + if (!useDefaultApp) { + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators(); + } + if (!androidEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + } + + if (badgeEnabled) { + if (androidEnabled || iosEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); + } + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); + } + + if (qrCodeLabelEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable(); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable(); + } + + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').updateValueAndValidity({emitEvent}); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity({emitEvent}); + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index c71f0fd658..f093c4c568 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -354,10 +354,14 @@ import { import { RadarChartWidgetSettingsComponent } from '@home/components/widget/lib/settings/chart/radar-chart-widget-settings.component'; +import { + MobileAppQrCodeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component'; @NgModule({ declarations: [ QrCodeWidgetSettingsComponent, + MobileAppQrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, @@ -490,6 +494,7 @@ import { ], exports: [ QrCodeWidgetSettingsComponent, + MobileAppQrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, TimeseriesTableLatestKeySettingsComponent, @@ -620,6 +625,7 @@ export class WidgetSettingsModule { export const widgetSettingsComponentsMap: {[key: string]: Type} = { 'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent, + 'tb-mobile-app-qr-code-widget-settings': MobileAppQrCodeWidgetSettingsComponent, 'tb-timeseries-table-widget-settings': TimeseriesTableWidgetSettingsComponent, 'tb-timeseries-table-key-settings': TimeseriesTableKeySettingsComponent, 'tb-timeseries-table-latest-key-settings': TimeseriesTableLatestKeySettingsComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index c342b68d1f..aa2aca3cb9 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -27,7 +27,7 @@ import { badgePositionTranslationsMap, BadgeStyle, badgeStyleTranslationsMap, - MobileAppSettings + MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; @Component({ @@ -39,7 +39,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf mobileAppSettingsForm: FormGroup; - mobileAppSettings: MobileAppSettings; + mobileAppSettings: MobileAppQRCodeSettings; private readonly destroy$ = new Subject(); @@ -179,7 +179,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf }); } - private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { + private processMobileAppSettings(mobileAppSettings: MobileAppQRCodeSettings): void { this.mobileAppSettings = {...mobileAppSettings}; this.mobileAppSettingsForm.reset(this.mobileAppSettings); } diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts index 0787f17747..02139bd913 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -16,7 +16,7 @@ import { TenantId } from '@shared/models/id/tenant-id'; -export interface MobileAppSettings { +export interface MobileAppQRCodeSettings { tenantId: TenantId; useDefaultApp: boolean; androidConfig: AndroidConfig; diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index d850e640b7..e69b43b1fe 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1145,7 +1145,7 @@ "id": "8e71a398-caf5-540d-cec5-6e5dc264343e" }, "b9d704a2-f579-dbff-f123-f953d92081fd": { - "typeFullFqn": "system.home_page_widgets.mobile_app_qr_code", + "typeFullFqn": "system.home_page_widgets.home_mobile_app_qr_code", "type": "static", "sizeX": 6, "sizeY": 3, @@ -1176,12 +1176,10 @@ "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", "padding": "8px", - "settings": { - "cardHtml": "
HTML code here
", - "cardCss": ".card {\n font-weight: bold;\n font-size: 32px;\n color: #999;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}" - }, - "title": "HTML Card", - "dropShadow": false + "settings": {}, + "title": "Mobile app QR code", + "dropShadow": false, + "enableFullscreen": false }, "row": 0, "col": 0, diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index a692994144..fa1eab0ece 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -290,9 +290,6 @@ &.shrink { flex: 0; } - &.no-gap { - gap: 0; - } } .tb-form-panel, .tb-form-row { From 012457e0db9801fd023d9aac28195985573ab5e8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 7 May 2024 11:55:06 +0300 Subject: [PATCH 067/121] Add mobileQrEnabled to system params --- .../server/controller/SystemInfoController.java | 10 ++++++++++ .../thingsboard/server/common/data/SystemParams.java | 1 + 2 files changed, 11 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index cb2033b29d..bc12171306 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -34,10 +34,13 @@ import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.mobile.MobileAppSettings; +import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.mobile.MobileAppSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -45,6 +48,7 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Hidden @@ -72,6 +76,9 @@ public class SystemInfoController extends BaseController { @Autowired private EntitiesVersionControlService versionControlService; + @Autowired + private MobileAppSettingsService mobileAppSettingsService; + @PostConstruct public void init() { JsonNode info = buildInfoObject(); @@ -135,6 +142,9 @@ public class SystemInfoController extends BaseController { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); } + systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID)) + .map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) + .orElse(false)); return systemParams; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index e022e56c5b..5b4312f7b0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -31,4 +31,5 @@ public class SystemParams { JsonNode userSettings; long maxDatapointsLimit; long maxResourceSize; + boolean mobileQrEnabled; } From 0d7d9b481bd93008f44c022c5927c98a4d4e4fa2 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 7 May 2024 12:09:36 +0300 Subject: [PATCH 068/121] Refactoring for mobile settings api --- .../main/data/upgrade/3.6.4/schema_update.sql | 5 +++-- .../MobileApplicationController.java | 7 ++++--- .../secret}/MobileAppSecretService.java | 2 +- .../secret}/MobileAppSecretServiceImpl.java | 2 +- .../secret}/MobileSecretCaffeineCache.java | 2 +- .../secret}/MobileSecretEvictEvent.java | 2 +- .../secret}/MobileSecretRedisCache.java | 2 +- .../mobile/BaseMobileAppSettingsService.java | 20 ++++++++----------- .../mobile/MobileAppSettingsRedisCache.java | 2 +- .../model/sql/MobileAppSettingsEntity.java | 9 +++++---- .../main/resources/sql/schema-entities.sql | 7 ++++--- 11 files changed, 30 insertions(+), 30 deletions(-) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileAppSecretService.java (94%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileAppSecretServiceImpl.java (98%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileSecretCaffeineCache.java (96%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileSecretEvictEvent.java (93%) rename application/src/main/java/org/thingsboard/server/service/{qr => mobile/secret}/MobileSecretRedisCache.java (96%) diff --git a/application/src/main/data/upgrade/3.6.4/schema_update.sql b/application/src/main/data/upgrade/3.6.4/schema_update.sql index 287991ffd1..9a5204379c 100644 --- a/application/src/main/data/upgrade/3.6.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.4/schema_update.sql @@ -157,11 +157,12 @@ DELETE FROM asset_profile WHERE name ='TbServiceQueue'; CREATE TABLE IF NOT EXISTS mobile_app_settings ( id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, created_time bigint NOT NULL, - tenant_id uuid UNIQUE NOT NULL, + tenant_id uuid NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), - qr_code_config VARCHAR(100000) + qr_code_config VARCHAR(100000), + CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id) ); -- MOBILE APP SETTINGS TABLE CREATE END \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java index ddd49ab975..626f6f3b0f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/MobileApplicationController.java @@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.mobile.MobileAppSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.qr.MobileAppSecretService; +import org.thingsboard.server.service.mobile.secret.MobileAppSecretService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -84,7 +84,7 @@ public class MobileApplicationController extends BaseController { public static final String ANDROID_APPLICATION_STORE_LINK = "https://play.google.com/store/apps/details?id=org.thingsboard.demo.app"; public static final String APPLE_APPLICATION_STORE_LINK = "https://apps.apple.com/us/app/thingsboard-live/id1594355695"; public static final String SECRET = "secret"; - public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-live secret key"; + public static final String SECRET_PARAM_DESCRIPTION = "A string value representing short-lived secret key"; public static final String DEFAULT_APP_DOMAIN = "demo.thingsboard.io"; public static final String DEEP_LINK_PATTERN = "https://%s/api/noauth/qr?secret=%s&ttl=%s"; @@ -164,7 +164,7 @@ public class MobileApplicationController extends BaseController { notes = "Returns the token of the User based on the provided secret key.") @GetMapping(value = "/api/noauth/qr/{secret}") public JwtPair getUserTokenByMobileSecret(@Parameter(description = SECRET_PARAM_DESCRIPTION) - @PathVariable(SECRET) String secret) throws ThingsboardException { + @PathVariable(SECRET) String secret) throws ThingsboardException { checkParameter(SECRET, secret); return mobileAppSecretService.getJwtPair(secret); } @@ -184,4 +184,5 @@ public class MobileApplicationController extends BaseController { .build(); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java index 943fd45c54..83c1b55c6e 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretService.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretService.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.security.model.JwtPair; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java similarity index 98% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java index f8d8629d10..bef31622ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileAppSecretServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileAppSecretServiceImpl.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java index 916d58afd7..ce21041d27 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretCaffeineCache.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretCaffeineCache.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java index 1f97e9ab07..b015291d78 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretEvictEvent.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretEvictEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java rename to application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java index d29a104ae9..81fa8672f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/qr/MobileSecretRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.qr; +package org.thingsboard.server.service.mobile.secret; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.connection.RedisConnectionFactory; diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java index 86de46fed1..4e8a2cd503 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/BaseMobileAppSettingsService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.mobile; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.id.TenantId; @@ -28,9 +27,10 @@ import org.thingsboard.server.common.data.mobile.IosConfig; import org.thingsboard.server.common.data.mobile.MobileAppSettings; import org.thingsboard.server.common.data.mobile.QRCodeConfig; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import java.util.Map; + import static org.thingsboard.server.dao.service.Validator.validateId; @Service @@ -51,16 +51,12 @@ public class BaseMobileAppSettingsService extends AbstractCachedEntityService { private JsonNode qrCodeConfig; public MobileAppSettingsEntity(MobileAppSettings mobileAppSettings) { - if (mobileAppSettings.getId() != null) { - this.setId(mobileAppSettings.getId().getId()); - } + this.setId(mobileAppSettings.getUuidId()); this.setCreatedTime(mobileAppSettings.getCreatedTime()); this.tenantId = mobileAppSettings.getTenantId().getId(); this.useDefaultApp = mobileAppSettings.isUseDefaultApp(); this.androidConfig = toJson(mobileAppSettings.getAndroidConfig()); this.iosConfig = toJson(mobileAppSettings.getIosConfig()); this.qrCodeConfig = toJson(mobileAppSettings.getQrCodeConfig()); - } + } @Override public MobileAppSettings toData() { @@ -81,4 +81,5 @@ public class MobileAppSettingsEntity extends BaseSqlEntity { mobileAppSettings.setQrCodeConfig(fromJson(qrCodeConfig, QRCodeConfig.class)); return mobileAppSettings; } + } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3a96580011..f2978b7f4a 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -900,9 +900,10 @@ CREATE TABLE IF NOT EXISTS queue_stats ( CREATE TABLE IF NOT EXISTS mobile_app_settings ( id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY, created_time bigint NOT NULL, - tenant_id uuid UNIQUE NOT NULL, + tenant_id uuid NOT NULL, use_default_app boolean, android_config VARCHAR(1000), ios_config VARCHAR(1000), - qr_code_config VARCHAR(100000) -); \ No newline at end of file + qr_code_config VARCHAR(100000), + CONSTRAINT mobile_app_settings_tenant_id_unq_key UNIQUE (tenant_id) +); From 63156a69df0e13b63268fad3f629ae9c998a6bb3 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 7 May 2024 13:59:56 +0300 Subject: [PATCH 069/121] added ability to transfer msg to originator default rule chain and set default value for maxRuleNodeExecutionsPerMessage from tenant profile --- .../update/DefaultDataUpdateService.java | 22 ++ .../DefaultTenantProfileConfiguration.java | 2 +- .../engine/flow/TbRuleChainInputNode.java | 66 +++- .../TbRuleChainInputNodeConfiguration.java | 5 +- .../engine/flow/TbRuleChainInputNodeTest.java | 299 ++++++++++++++++++ 5 files changed, 382 insertions(+), 12 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 06cbdc2230..9626190f1b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -39,6 +39,7 @@ import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.sql.JpaExecutorService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.utils.TbNodeUpgradeUtils; @@ -77,6 +78,8 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private CustomerService customerService; + @Autowired + private TenantProfileService tenantProfileService; @Override public void updateData(String fromVersion) throws Exception { @@ -88,12 +91,31 @@ public class DefaultDataUpdateService implements DataUpdateService { case "3.6.4": log.info("Updating data from version 3.6.4 to 3.7.0 ..."); updateCustomersWithTheSameTitle(); + updateMaxRuleNodeExecsPerMessage(); break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } } + private void updateMaxRuleNodeExecsPerMessage() { + var tenantProfiles = new PageDataIterable<>( + link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE); + tenantProfiles.forEach(tenantProfile -> { + var configurationOpt = tenantProfile.getProfileConfiguration(); + configurationOpt.ifPresent(configuration -> { + if (configuration.getMaxRuleNodeExecsPerMessage() == 0) { + configuration.setMaxRuleNodeExecutionsPerMessage(1000); + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + } catch (Exception e) { + log.error("Failed to update tenant profile with id: {} due to: ", tenantProfile.getId(), e); + } + } + }); + }); + } + private void updateCustomersWithTheSameTitle() { var customers = new ArrayList(); new PageDataIterable<>(pageLink -> diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index a0cfbfe1e1..a1a16743be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -59,7 +59,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxJSExecutions; private long maxTbelExecutions; private long maxDPStorageDays; - private int maxRuleNodeExecutionsPerMessage; + private int maxRuleNodeExecutionsPerMessage = 1000; private long maxEmails; private Boolean smsEnabled; private long maxSms; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java index 4baec1c882..0b30d02842 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java @@ -15,6 +15,8 @@ */ package org.thingsboard.rule.engine.flow; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -22,10 +24,14 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; +import java.util.Optional; import java.util.UUID; @Slf4j @@ -33,11 +39,13 @@ import java.util.UUID; type = ComponentType.FLOW, name = "rule chain", configClazz = TbRuleChainInputNodeConfiguration.class, - nodeDescription = "transfers the message to another rule chain", - nodeDetails = "Allows to nest the rule chain similar to single rule node. " + - "The incoming message is forwarded to the input node of the specified target rule chain. " + - "The target rule chain may produce multiple labeled outputs. " + - "You may use the outputs to forward the results of processing to other rule nodes.", + version = 1, + nodeDescription = "Transfers the message to another rule chain", + nodeDetails = "The incoming message is forwarded to the input node of target rule chain. " + + "If 'Forward message to the originator's default rule chain' is enabled, " + + "then target rule chain might be resolved dynamically based on incoming message originator. " + + "In this case rule chain specified in the configuration will be used as fallback rule chain.

" + + "Output connections: Any connection(s) produced by output node(s) in the target rule chain.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFlowNodeRuleChainInputConfig", relationTypes = {}, @@ -46,19 +54,57 @@ import java.util.UUID; ) public class TbRuleChainInputNode implements TbNode { - private TbRuleChainInputNodeConfiguration config; private RuleChainId ruleChainId; + private boolean forwardMsgToDefaultRuleChain; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbRuleChainInputNodeConfiguration.class); - this.ruleChainId = new RuleChainId(UUID.fromString(config.getRuleChainId())); + TbRuleChainInputNodeConfiguration config = TbNodeUtils.convert(configuration, TbRuleChainInputNodeConfiguration.class); + if (config.getRuleChainId() == null) { + throw new TbNodeException("Rule chain must be set!", true); + } + UUID ruleChainUUID; + try { + ruleChainUUID = UUID.fromString(config.getRuleChainId()); + } catch (Exception e) { + throw new TbNodeException("Failed to parse rule chain id: " + config.getRuleChainId(), true); + } + ruleChainId = new RuleChainId(ruleChainUUID); ctx.checkTenantEntity(ruleChainId); + forwardMsgToDefaultRuleChain = config.isForwardMsgToDefaultRuleChain(); } @Override - public void onMsg(TbContext ctx, TbMsg msg) { - ctx.input(msg, ruleChainId); + public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { + RuleChainId targetRuleChainId = forwardMsgToDefaultRuleChain ? + getOriginatorDefaultRuleChainId(ctx, msg).orElse(ruleChainId) : ruleChainId; + ctx.input(msg, targetRuleChainId); } + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + boolean hasChanges = false; + switch (fromVersion) { + case 0 -> { + if (!oldConfiguration.has("forwardMsgToDefaultRuleChain")) { + hasChanges = true; + ((ObjectNode) oldConfiguration).put("forwardMsgToDefaultRuleChain", false); + } + } + default -> { + } + } + return new TbPair<>(hasChanges, oldConfiguration); + } + + private Optional getOriginatorDefaultRuleChainId(TbContext ctx, TbMsg msg) { + return Optional.ofNullable( + switch (msg.getOriginator().getEntityType()) { + case DEVICE -> + ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceId) msg.getOriginator()).getDefaultRuleChainId(); + case ASSET -> + ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetId) msg.getOriginator()).getDefaultRuleChainId(); + default -> null; + }); + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java index 30c2a49821..84ddb779f7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java @@ -22,10 +22,13 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; public class TbRuleChainInputNodeConfiguration implements NodeConfiguration { private String ruleChainId; + private boolean forwardMsgToDefaultRuleChain; @Override public TbRuleChainInputNodeConfiguration defaultConfiguration() { - return new TbRuleChainInputNodeConfiguration(); + TbRuleChainInputNodeConfiguration configuration = new TbRuleChainInputNodeConfiguration(); + configuration.setForwardMsgToDefaultRuleChain(false); + return configuration; } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java new file mode 100644 index 0000000000..579ed7f035 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeTest.java @@ -0,0 +1,299 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.flow; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; +import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; +import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class TbRuleChainInputNodeTest extends AbstractRuleNodeUpgradeTest { + + private final TenantId TENANT_ID = new TenantId(UUID.fromString("4ba69ea5-6b27-42df-ab66-e7a727a67027")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("97731954-2147-4176-8f1a-d14f1b73e4e6")); + private final AssetId ASSET_ID = new AssetId(UUID.fromString("841a47bd-4e8e-4ea5-88e6-420da0d70e51")); + + private TbRuleChainInputNode node; + private TbRuleChainInputNodeConfiguration config; + private TbNodeConfiguration nodeConfiguration; + + @Mock + private TbContext ctxMock; + @Mock + private RuleEngineDeviceProfileCache deviceProfileCacheMock; + @Mock + private RuleEngineAssetProfileCache assetProfileCacheMock; + + @BeforeEach + public void setUp() { + node = spy(new TbRuleChainInputNode()); + config = new TbRuleChainInputNodeConfiguration().defaultConfiguration(); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + } + + @ParameterizedTest + @MethodSource + public void givenValidConfig_whenInit_thenOk(String ruleChainIdStr, boolean forwardMsgToDefaultRuleChain) throws TbNodeException { + //GIVEN + config.setRuleChainId(ruleChainIdStr); + config.setForwardMsgToDefaultRuleChain(forwardMsgToDefaultRuleChain); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + //WHEN + assertThatCode(() -> node.init(ctxMock, nodeConfiguration)) + .doesNotThrowAnyException(); + + //THEN + verify(ctxMock).checkTenantEntity(new RuleChainId(UUID.fromString(ruleChainIdStr))); + } + + private static Stream givenValidConfig_whenInit_thenOk() { + return Stream.of( + Arguments.of("45bba7c4-04bf-419b-ae03-6ceb9724f10e", false), + Arguments.of("52d57e1b-70bb-480e-bcc4-6710e1dcc9d8", true) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"91acbce0-079fdb", "", " ", "my test string"}) + public void givenInvalidRuleChainId_whenInit_thenThrowsException(String ruleChainIdStr) { + //GIVEN + config.setRuleChainId(ruleChainIdStr); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + //WHEN-THEN + Assertions.assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Failed to parse rule chain id: " + ruleChainIdStr); + } + + @Test + public void givenRuleChainIdIsNotSet_whenInit_thenThrowsException() { + assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration)) + .isInstanceOf(TbNodeException.class) + .hasMessage("Rule chain must be set!"); + } + + @Test + public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToDeviceDefaultRuleChain() throws TbNodeException { + //GIVEN + DeviceProfile deviceProfile = new DeviceProfile(); + RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("196e3cd5-68b8-421e-a0cf-1d44fa377cdf")); + deviceProfile.setDefaultRuleChainId(defaultRuleChainId); + + TbMsg msg = getMsg(DEVICE_ID); + + String ruleChainIdFromConfigStr = "acbc924f-7f95-4a9b-a854-e4822deb74c7"; + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock); + when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue(); + assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId); + + RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId"); + assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr))); + } + + @Test + public void givenForwardMsgToDefaultIsTrue_whenOnMsg_thenShouldTransferToAssetDefaultRuleChain() throws TbNodeException { + //GIVEN + AssetProfile assetProfile = new AssetProfile(); + RuleChainId defaultRuleChainId = new RuleChainId(UUID.fromString("f0a3cd58-980c-4730-a40c-8f59064d2065")); + assetProfile.setDefaultRuleChainId(defaultRuleChainId); + + TbMsg msg = getMsg(ASSET_ID); + + String ruleChainIdFromConfigStr = "56f1c0b8-1a00-4ce0-b3ab-a1416d7cc429"; + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock); + when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + RuleChainId expectedRuleChainId = ruleChainArgumentCaptor.getValue(); + assertThat(expectedRuleChainId).isEqualTo(defaultRuleChainId); + + RuleChainId ruleChainId = (RuleChainId) ReflectionTestUtils.getField(node, "ruleChainId"); + assertThat(ruleChainId).isEqualTo(new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr))); + } + + @Test + public void givenForwardMsgToDefaultIsTrueWithoutDeviceDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { + //GIVEN + DeviceProfile deviceProfile = new DeviceProfile(); + + TbMsg msg = getMsg(DEVICE_ID); + + String ruleChainIdFromConfigStr = "357c2785-e7cc-46a8-9797-957180dabdeb"; + RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getDeviceProfileCache()).thenReturn(deviceProfileCacheMock); + when(deviceProfileCacheMock.get(any(TenantId.class), any(DeviceId.class))).thenReturn(deviceProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); + } + + @Test + public void givenForwardMsgToDefaultIsTrueWithoutAssetDefaultRuleChain_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { + //GIVEN + AssetProfile assetProfile = new AssetProfile(); + + TbMsg msg = getMsg(ASSET_ID); + + String ruleChainIdFromConfigStr = "12883c3d-c10b-4d5b-b606-a59385a920bc"; + RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(true); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + when(ctxMock.getAssetProfileCache()).thenReturn(assetProfileCacheMock); + when(assetProfileCacheMock.get(any(TenantId.class), any(AssetId.class))).thenReturn(assetProfile); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); + } + + @Test + public void givenRuleChainInConfig_whenOnMsg_thenShouldTransferToRuleChainFromConfig() throws TbNodeException { + //GIVEN + String ruleChainIdFromConfigStr = "3c02c8b3-645c-4e67-aac5-f984f59471d1"; + RuleChainId ruleChainIdFromConfig = new RuleChainId(UUID.fromString(ruleChainIdFromConfigStr)); + + TbMsg msg = getMsg(DEVICE_ID); + + config.setRuleChainId(ruleChainIdFromConfigStr); + config.setForwardMsgToDefaultRuleChain(false); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + node.init(ctxMock, nodeConfiguration); + + //WHEN + node.onMsg(ctxMock, msg); + + //THEN + ArgumentCaptor ruleChainArgumentCaptor = ArgumentCaptor.forClass(RuleChainId.class); + verify(ctxMock).input(eq(msg), ruleChainArgumentCaptor.capture()); + assertThat(ruleChainArgumentCaptor.getValue()).isEqualTo(ruleChainIdFromConfig); + } + + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { + return Stream.of( + //config for version 0 + Arguments.of(0, + "{\"ruleChainId\": null}", + true, + "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}" + ), + //config for version 1 with upgrade from version 0 + Arguments.of(1, + "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}", + false, + "{\"ruleChainId\": null, \"forwardMsgToDefaultRuleChain\": false}" + ) + ); + } + + @Override + protected TbNode getTestNode() { + return node; + } + + private TbMsg getMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + } +} From 994bb6c825607c897cae913d55c8ad8f762c9974 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 7 May 2024 14:01:23 +0300 Subject: [PATCH 070/121] Add search by type for assets where missing --- .../server/dao/sql/asset/AssetRepository.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index eb83a5805e..bf4798fa4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -42,7 +42,8 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + - " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -55,7 +56,7 @@ public interface AssetRepository extends JpaRepository, Expor "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + " OR ilike(p.name, CONCAT('%', :textSearch, '%')) = true " + - " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(c.title, CONCAT('%', :textSearch, '%')) = true)") Page findAssetInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -63,7 +64,8 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + - " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true " + + " OR ilike(a.type, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -72,7 +74,7 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.assetProfileId = :profileId " + "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + - " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true)") Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, @Param("profileId") UUID profileId, @Param("searchText") String searchText, @@ -86,7 +88,8 @@ public interface AssetRepository extends JpaRepository, Expor "AND a.customerId = :customerId " + "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true " + - " OR ilike(c.title, CONCAT('%', :searchText, '%')) = true) ") + " OR ilike(c.title, CONCAT('%', :searchText, '%')) = true " + + " OR ilike(p.name, CONCAT('%', :searchText, '%')) = true) ") Page findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -101,7 +104,7 @@ public interface AssetRepository extends JpaRepository, Expor @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.type = :type " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + - " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :textSearch, '%')) = true)") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -198,7 +201,7 @@ public interface AssetRepository extends JpaRepository, Expor "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + "AND a.type = :type " + "AND (:searchText IS NULL OR ilike(a.name, CONCAT('%', :searchText, '%')) = true " + - " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") + " OR ilike(a.label, CONCAT('%', :searchText, '%')) = true) ") Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, From c3f1e186c53764c48397da209c4b18afa66d531c Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 7 May 2024 14:49:30 +0300 Subject: [PATCH 071/121] lwm2m: object-id-version default 1.0 --- .../lwm2m-device-profile-transport-configuration.component.ts | 4 ++-- .../profile/device/lwm2m/lwm2m-profile-config.models.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index 8aeb3e58d8..fbc72d9498 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -123,7 +123,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro edrxCycle: [{disabled: true, value: 0}, Validators.required], psmActivityTimer: [{disabled: true, value: 0}, Validators.required], pagingTransmissionWindow: [{disabled: true, value: 0}, Validators.required], - defaultObjectIDVer: [ObjectIDVer.V1_1, Validators.required] + defaultObjectIDVer: [ObjectIDVer.V1_0, Validators.required] }) }); @@ -282,7 +282,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro pagingTransmissionWindow: this.configurationValue.clientLwM2mSettings.pagingTransmissionWindow || DEFAULT_PAGING_TRANSMISSION_WINDOW, psmActivityTimer: this.configurationValue.clientLwM2mSettings.psmActivityTimer || DEFAULT_PSM_ACTIVITY_TIMER, - defaultObjectIDVer: this.configurationValue.clientLwM2mSettings.defaultObjectIDVer || ObjectIDVer.V1_1 + defaultObjectIDVer: this.configurationValue.clientLwM2mSettings.defaultObjectIDVer || ObjectIDVer.V1_0 } }, {emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 4262e47970..8c1bb56a49 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -202,7 +202,7 @@ export function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSetting fwUpdateStrategy: 1, swUpdateStrategy: 1, powerMode: PowerMode.DRX, - defaultObjectIDVer: ObjectIDVer.V1_1 + defaultObjectIDVer: ObjectIDVer.V1_0 }; } From 15d189ead730245e29dcf07d959e34cfb8693e48 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 7 May 2024 15:13:52 +0300 Subject: [PATCH 072/121] Refactoring for consumer manager --- .../HousekeeperReprocessingService.java | 2 +- .../housekeeper/HousekeeperService.java | 2 +- .../queue/DefaultTbCoreConsumerService.java | 4 +- .../queue/consumer/QueueConsumerManager.java | 109 ------------------ .../processing/AbstractConsumerService.java | 2 +- .../common/consumer/QueueConsumerManager.java | 13 ++- .../service/DefaultTransportService.java | 3 +- .../DefaultClusterVersionControlService.java | 3 +- 8 files changed, 15 insertions(+), 123 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java index ae16f925a0..37a55c99d6 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java @@ -27,11 +27,11 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperService import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.housekeeper.HousekeeperConfig; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; import javax.annotation.PreDestroy; import java.util.LinkedHashSet; diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 06e38d0160..eea92eb75b 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -27,13 +27,13 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.gen.transport.TransportProtos.ToHousekeeperServiceMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.housekeeper.HousekeeperConfig; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor; import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService; -import org.thingsboard.server.service.queue.consumer.QueueConsumerManager; import javax.annotation.PreDestroy; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index abfcd47a46..fbef0cfd29 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -210,20 +210,20 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService>builder() - .key("usage-stats") .name("TB Usage Stats") .msgPackProcessor(this::processUsageStatsMsg) .pollInterval(pollInterval) .consumerCreator(queueFactory::createToUsageStatsServiceMsgConsumer) .consumerExecutor(consumersExecutor) + .threadPrefix("usage-stats") .build(); this.firmwareStatesConsumer = QueueConsumerManager.>builder() - .key("firmware") .name("TB Ota Package States") .msgPackProcessor(this::processFirmwareMsgs) .pollInterval(pollInterval) .consumerCreator(queueFactory::createToOtaPackageStateServiceMsgConsumer) .consumerExecutor(consumersExecutor) + .threadPrefix("firmware") .build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java deleted file mode 100644 index e7b3a2f2b7..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/queue/consumer/QueueConsumerManager.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.queue.consumer; - -import lombok.Builder; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.queue.TbQueueConsumer; -import org.thingsboard.server.queue.TbQueueMsg; - -import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.function.Supplier; - -@Slf4j -public class QueueConsumerManager { - - private final String name; - private final MsgPackProcessor msgPackProcessor; - private final long pollInterval; - private final ExecutorService consumerExecutor; - private final String threadPrefix; - - @Getter - private final TbQueueConsumer consumer; - private volatile boolean stopped; - - @Builder - public QueueConsumerManager(String name, MsgPackProcessor msgPackProcessor, - long pollInterval, Supplier> consumerCreator, - ExecutorService consumerExecutor, String threadPrefix) { - this.name = name; - this.pollInterval = pollInterval; - this.msgPackProcessor = msgPackProcessor; - this.consumerExecutor = consumerExecutor; - this.threadPrefix = threadPrefix; - this.consumer = consumerCreator.get(); - } - - public void subscribe() { - consumer.subscribe(); - } - - public void subscribe(Set partitions) { - consumer.subscribe(partitions); - } - - public void launch() { - log.info("[{}] Launching consumer", name); - consumerExecutor.submit(() -> { - if (threadPrefix != null) { - ThingsBoardThreadFactory.addThreadNamePrefix(threadPrefix); - } - try { - consumerLoop(consumer); - } catch (Throwable e) { - log.error("Failure in consumer loop", e); - } - log.info("[{}] Consumer stopped", name); - }); - } - - private void consumerLoop(TbQueueConsumer consumer) { - while (!stopped && !consumer.isStopped()) { - try { - List msgs = consumer.poll(pollInterval); - if (msgs.isEmpty()) { - continue; - } - msgPackProcessor.process(msgs, consumer); - } catch (Exception e) { - if (!consumer.isStopped()) { - log.warn("Failed to process messages from queue", e); - try { - Thread.sleep(pollInterval); - } catch (InterruptedException interruptedException) { - log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException); - } - } - } - } - } - - public void stop() { - log.debug("[{}] Stopping consumer", name); - stopped = true; - consumer.unsubscribe(); - } - - public interface MsgPackProcessor { - void process(List msgs, TbQueueConsumer consumer) throws Exception; - } -} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 2967143dd4..85227e50ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -85,12 +85,12 @@ public abstract class AbstractConsumerService>builder() - .key("notifications") .name("TB Notifications") .msgPackProcessor(this::processNotifications) .pollInterval(getNotificationPollDuration()) .consumerCreator(this::createNotificationsConsumer) .consumerExecutor(consumersExecutor) + .threadPrefix("notifications") .build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java index bf103bef60..90f4cf050c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueConsumerManager.java @@ -31,25 +31,25 @@ import java.util.function.Supplier; @Slf4j public class QueueConsumerManager { - private final String key; private final String name; private final MsgPackProcessor msgPackProcessor; private final long pollInterval; private final ExecutorService consumerExecutor; + private final String threadPrefix; @Getter private final TbQueueConsumer consumer; private volatile boolean stopped; @Builder - public QueueConsumerManager(String key, String name, MsgPackProcessor msgPackProcessor, + public QueueConsumerManager(String name, MsgPackProcessor msgPackProcessor, long pollInterval, Supplier> consumerCreator, - ExecutorService consumerExecutor) { - this.key = key; + ExecutorService consumerExecutor, String threadPrefix) { this.name = name; this.pollInterval = pollInterval; this.msgPackProcessor = msgPackProcessor; this.consumerExecutor = consumerExecutor; + this.threadPrefix = threadPrefix; this.consumer = consumerCreator.get(); } @@ -64,7 +64,9 @@ public class QueueConsumerManager { public void launch() { log.info("[{}] Launching consumer", name); consumerExecutor.submit(() -> { - ThingsBoardThreadFactory.addThreadNamePrefix(key); + if (threadPrefix != null) { + ThingsBoardThreadFactory.addThreadNamePrefix(threadPrefix); + } try { consumerLoop(consumer); } catch (Throwable e) { @@ -104,4 +106,5 @@ public class QueueConsumerManager { public interface MsgPackProcessor { void process(List msgs, TbQueueConsumer consumer) throws Exception; } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index d208fab29e..8b7b88c261 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -232,9 +232,8 @@ public class DefaultTransportService extends TransportActivityManager implements ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer(); transportApiRequestTemplate.init(); - consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("consumer")); + consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); transportNotificationsConsumer = QueueConsumerManager.>builder() - .key("transport") .name("TB Transport") .msgPackProcessor(this::processNotificationMsgs) .pollInterval(notificationsPollDuration) diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java index a5a7021d8d..34171c19cd 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java @@ -132,14 +132,13 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe @PostConstruct public void init() { - consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("consumer")); + consumerExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("vc-consumer")); var threadFactory = ThingsBoardThreadFactory.forName("vc-io-thread"); for (int i = 0; i < ioPoolSize; i++) { ioThreads.add(MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(threadFactory))); } producer = producerProvider.getTbCoreNotificationsMsgProducer(); consumer = QueueConsumerManager.>builder() - .key("vc") .name("TB Version Control") .msgPackProcessor(this::processMsgs) .pollInterval(pollDuration) From 6514f6c8676711ef4291daf7e1e66f3fca9dc666 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 7 May 2024 15:51:30 +0300 Subject: [PATCH 073/121] added Login audit log for activateUser --- .../org/thingsboard/server/controller/AuthController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 2898689760..571f32d171 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -256,7 +256,9 @@ public class AuthController extends BaseController { } } - return tokenFactory.createTokenPair(securityUser); + var tokenPair = tokenFactory.createTokenPair(securityUser); + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null); + return tokenPair; } @ApiOperation(value = "Reset password (resetPassword)", From bdf577710e6dc7d8101bd80ecf7157ec9db1b8fb Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 7 May 2024 15:41:23 +0200 Subject: [PATCH 074/121] Mocks the few JUnit4 classes that testcontainers needs for successful compilation. Signed-off-by: Oleksandra Matviienko --- msa/black-box-tests/pom.xml | 5 +++++ pom.xml | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index f433b1930c..4c81e0f31f 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -43,6 +43,11 @@ testcontainers test + + software.xdev + testcontainers-junit4-mock + test + org.zeroturnaround zt-exec diff --git a/pom.xml b/pom.xml index 74c095c857..bf15107acd 100755 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,7 @@ 5.4.0 2.2 1.19.7 + 1.0.1 1.12 4.19.1 5.8.0 @@ -2125,6 +2126,14 @@ + + + software.xdev + testcontainers-junit4-mock + ${testcontainers-junit4-mock.version} + test + org.zeroturnaround zt-exec From d163b5679c3d2cbef72fa521aa5f6c8fede6a1d9 Mon Sep 17 00:00:00 2001 From: rusikv Date: Tue, 7 May 2024 17:14:31 +0300 Subject: [PATCH 075/121] UI: mobile qrcode improvements --- ...ts => mobile-app-qr-code-widget.models.ts} | 0 .../mobile-app-qrcode-widget.component.html | 3 +- .../mobile-app-qrcode-widget.component.scss | 2 +- .../lib/mobile-app-qrcode-widget.component.ts | 7 ++- ...e-app-qr-code-widget-settings.component.ts | 2 +- .../admin/mobile-app-settings.component.ts | 3 +- .../android_store_en_black_badge.svg | 49 ++++++++++++------- .../dashboard/tenant_admin_home_page.json | 4 +- 8 files changed, 45 insertions(+), 25 deletions(-) rename ui-ngx/src/app/modules/home/components/widget/lib/cards/{mobile-app-qr-code-widge.models.ts => mobile-app-qr-code-widget.models.ts} (100%) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widge.models.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index e3f9ca11c3..35092807dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -20,9 +20,10 @@
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index 5dafcc7521..6d229abb0a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -51,7 +51,7 @@ } .tb-badge-container { - @media #{$mat-lt-lg} { + @media #{$mat-md} { display: none; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index 7296b47f80..c7557c75c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -25,7 +25,7 @@ import { UtilsService } from '@core/services/utils.service'; import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; +import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; @Component({ selector: 'tb-mobile-app-qrcode-widget', @@ -104,7 +104,10 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI updateQRCode(link: string) { import('qrcode').then((QRCode) => { - QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 90 }); + QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 100 }); + // QRCode.toCanvas(this.canvasRef.nativeElement, link); + // this.canvasRef.nativeElement.style.width = '4.6vw'; + // this.canvasRef.nativeElement.style.height = '4.6vw'; }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts index 61999d7d41..662a9d3dd3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -20,7 +20,7 @@ import { WidgetSettings, WidgetSettingsComponent } from "@shared/models/widget.m import { AppState } from '@core/core.state'; import { Store } from "@ngrx/store"; import { badgePositionTranslationsMap, badgeStyleTranslationsMap } from '@shared/models/mobile-app.models'; -import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widge.models'; +import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; @Component({ selector: 'tb-mobile-app-qr-code-widget-settings', diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index aa2aca3cb9..7959b1b800 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -29,6 +29,7 @@ import { badgeStyleTranslationsMap, MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; +import { AuthService } from '@core/auth/auth.service'; @Component({ selector: 'tb-mobile-app-settings', @@ -48,7 +49,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf constructor(protected store: Store, private mobileAppService: MobileAppService, - public fb: FormBuilder) { + private fb: FormBuilder) { super(store); this.buildMobileAppSettingsForm(); this.mobileAppService.getMobileAppSettings() diff --git a/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg index 02b88dab6f..2ef3c8fc10 100644 --- a/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg +++ b/ui-ngx/src/assets/android-ios-stores-badges/android_store_en_black_badge.svg @@ -1,22 +1,35 @@ - - - - - - - - - - - - - - - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index e69b43b1fe..95e33e211b 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1258,7 +1258,9 @@ "sizeX": 35, "sizeY": 16, "row": 42, - "col": 85 + "col": 85, + "mobileOrder": 5, + "mobileHeight": 7 } }, "gridSettings": { From 2bbeb6ab34c0c67249159032e809ec639b86a9b9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 7 May 2024 18:05:22 +0300 Subject: [PATCH 076/121] UI: Added support mobileQrEnabled settings in tenant admin home page dashboard --- ui-ngx/src/app/core/auth/auth.models.ts | 1 + ui-ngx/src/app/core/auth/auth.reducer.ts | 1 + ui-ngx/src/app/core/auth/auth.selectors.ts | 11 + .../home-links/home-links-routing.module.ts | 17 +- .../dashboard/tenant_admin_home_page.json | 189 +++++++++++++----- 5 files changed, 167 insertions(+), 52 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 7f76f25854..b73847ef83 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -24,6 +24,7 @@ export interface SysParamsState { hasRepository: boolean; tbelEnabled: boolean; persistDeviceStateToTelemetry: boolean; + mobileQrEnabled: boolean; userSettings: UserSettings; maxResourceSize: number; } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index f755438600..cc9d01532a 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -29,6 +29,7 @@ const emptyUserAuthState: AuthPayload = { hasRepository: false, tbelEnabled: false, persistDeviceStateToTelemetry: false, + mobileQrEnabled: false, maxResourceSize: 0, userSettings: initialUserSettings }; diff --git a/ui-ngx/src/app/core/auth/auth.selectors.ts b/ui-ngx/src/app/core/auth/auth.selectors.ts index 0cb13c9865..0a97ef5665 100644 --- a/ui-ngx/src/app/core/auth/auth.selectors.ts +++ b/ui-ngx/src/app/core/auth/auth.selectors.ts @@ -78,6 +78,17 @@ export const selectPersistDeviceStateToTelemetry = createSelector( (state: AuthState) => state.persistDeviceStateToTelemetry ); +export const selectMobileQrEnabled = createSelector( + selectAuthState, + (state: AuthState) => state.mobileQrEnabled +); + +export const selectPersistDeviceStateToTelemetryAndMobileQrEnabled = createSelector( + selectPersistDeviceStateToTelemetry, + selectMobileQrEnabled, + (persistDeviceStateToTelemetry, mobileQrEnabled) => ({persistDeviceStateToTelemetry, mobileQrEnabled}) +); + export const selectUserSettings = createSelector( selectAuthState, (state: AuthState) => state.userSettings diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index 6c5789b218..3b6b909225 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -25,7 +25,7 @@ import { DashboardService } from '@core/http/dashboard.service'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { map } from 'rxjs/operators'; -import { getCurrentAuthUser, selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors'; +import { getCurrentAuthUser, selectPersistDeviceStateToTelemetryAndMobileQrEnabled } from '@core/auth/auth.selectors'; import { EntityKeyType } from '@shared/models/query/query.models'; import { ResourcesService } from '@core/services/resources.service'; @@ -35,16 +35,25 @@ const customerUserHomePageJson = '/assets/dashboard/customer_user_home_page.json const updateDeviceActivityKeyFilterIfNeeded = (store: Store, dashboard$: Observable): Observable => - store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe( - mergeMap((persistToTelemetry) => dashboard$.pipe( + store.pipe(select(selectPersistDeviceStateToTelemetryAndMobileQrEnabled)).pipe( + mergeMap((params) => dashboard$.pipe( map((dashboard) => { - if (persistToTelemetry) { + if (params.persistDeviceStateToTelemetry) { for (const filterId of Object.keys(dashboard.configuration.filters)) { if (['Active Devices', 'Inactive Devices'].includes(dashboard.configuration.filters[filterId].filter)) { dashboard.configuration.filters[filterId].keyFilters[0].key.type = EntityKeyType.TIME_SERIES; } } } + if (params.mobileQrEnabled) { + for (const widgetId of Object.keys(dashboard.configuration.widgets)) { + if (dashboard.configuration.widgets[widgetId].config.title === 'Select show mobile QR code') { + dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction = + (dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction as string) + .replace('\'${mobileQrEnabled}\'', String(params.mobileQrEnabled)); + } + } + } return dashboard; }) )) diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index 95e33e211b..b9cba6eb08 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -101,38 +101,6 @@ "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4", "typeFullFqn": "system.home_page_widgets.documentation_links" }, - "a23185ad-dc46-806c-0e50-5b21fb080ace": { - "type": "static", - "sizeX": 7.5, - "sizeY": 6.5, - "config": { - "datasources": [], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, - "showTitle": false, - "backgroundColor": "rgb(255, 255, 255)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "16px", - "settings": { - "columns": 3 - }, - "title": "Getting started", - "dropShadow": false, - "enableFullscreen": false, - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "showLegend": false - }, - "row": 0, - "col": 0, - "id": "a23185ad-dc46-806c-0e50-5b21fb080ace", - "typeFullFqn": "system.home_page_widgets.getting_started" - }, "ebbd0a6e-8a47-e770-5086-7f4974250f2d": { "type": "static", "sizeX": 7.5, @@ -1144,7 +1112,108 @@ "col": 0, "id": "8e71a398-caf5-540d-cec5-6e5dc264343e" }, - "b9d704a2-f579-dbff-f123-f953d92081fd": { + "35bda856-2495-4f72-e35e-925bac8b8da7": { + "typeFullFqn": "system.cards.markdown_card", + "type": "latest", + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1715005524783, + "endTimeMs": 1715091924783 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextPattern": "### Markdown/HTML card\n - **Current entity**: ${entityName}.\n - **Current value**: ${Random}.", + "markdownTextFunction": "let html = '
';\nlet supportMobile = '${mobileQrEnabled}' || false;\nif (supportMobile && typeof supportMobile === 'boolean') {\n html += '';\n} else {\n html += '';\n}\nhtml += '
';\nreturn html;", + "applyDefaultMarkdownStyle": false, + "markdownCss": "" + }, + "title": "Select show mobile QR code", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": { + "border": "none", + "boxShadow": "none", + "padding": 0 + }, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "displayTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "35bda856-2495-4f72-e35e-925bac8b8da7" + }, + "ed33e56f-293b-aaaa-ad19-0cf499dc926f": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 3 + }, + "title": "Getting started", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "ed33e56f-293b-aaaa-ad19-0cf499dc926f", + "typeFullFqn": "system.home_page_widgets.getting_started" + }, + "8c5207d8-7103-4dc2-db48-d61c0c8c61fd": { "typeFullFqn": "system.home_page_widgets.home_mobile_app_qr_code", "type": "static", "sizeX": 6, @@ -1183,7 +1252,7 @@ }, "row": 0, "col": 0, - "id": "b9d704a2-f579-dbff-f123-f953d92081fd" + "id": "8c5207d8-7103-4dc2-db48-d61c0c8c61fd" } }, "states": { @@ -1208,13 +1277,6 @@ "col": 28, "mobileHide": true }, - "a23185ad-dc46-806c-0e50-5b21fb080ace": { - "sizeX": 35, - "sizeY": 42, - "row": 0, - "col": 85, - "mobileHide": true - }, "ebbd0a6e-8a47-e770-5086-7f4974250f2d": { "sizeX": 28, "sizeY": 16, @@ -1254,13 +1316,11 @@ "col": 43, "mobileOrder": 0 }, - "b9d704a2-f579-dbff-f123-f953d92081fd": { + "35bda856-2495-4f72-e35e-925bac8b8da7": { "sizeX": 35, - "sizeY": 16, - "row": 42, - "col": 85, - "mobileOrder": 5, - "mobileHeight": 7 + "sizeY": 58, + "row": 0, + "col": 85 } }, "gridSettings": { @@ -1330,6 +1390,39 @@ } } } + }, + "mobile_app_qr_code": { + "name": "Mobile App QR code", + "root": false, + "layouts": { + "main": { + "widgets": { + "ed33e56f-293b-aaaa-ad19-0cf499dc926f": { + "sizeX": 120, + "sizeY": 45, + "row": 0, + "col": 0 + }, + "8c5207d8-7103-4dc2-db48-d61c0c8c61fd": { + "sizeX": 120, + "sizeY": 17, + "row": 45, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 120, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 20 + } + } + } } }, "entityAliases": { @@ -1465,4 +1558,4 @@ } }, "name": "Tenant Administrator Home Page" -} +} \ No newline at end of file From cbb78878c2d23bb174dad2d88b2f9ee746f6242b Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Wed, 8 May 2024 00:09:12 +0200 Subject: [PATCH 077/121] Junit 4 to 5 migration in application Signed-off-by: Oleksandra Matviienko --- .../discovery/HashPartitionServiceTest.java | 16 ++-- .../DefaultTbApiUsageStateServiceTest.java | 36 ++------- .../RuleChainMsgConstructorTest.java | 74 +++++++++---------- .../service/limits/RateLimitServiceTest.java | 19 +++-- .../TbRuleEngineQueueConsumerManagerTest.java | 18 +++-- .../service/sms/smpp/SmppSmsSenderTest.java | 12 +-- 6 files changed, 75 insertions(+), 100 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java index 4d389b4210..dbef55539f 100644 --- a/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/queue/discovery/HashPartitionServiceTest.java @@ -20,12 +20,12 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.RandomStringUtils; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.DataConstants; @@ -67,7 +67,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @Slf4j -@RunWith(MockitoJUnitRunner.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class HashPartitionServiceTest { public static final int ITERATIONS = 1000000; @@ -82,7 +82,7 @@ public class HashPartitionServiceTest { private String hashFunctionName = "murmur3_128"; - @Before + @BeforeEach public void setup() throws Exception { serviceInfoProvider = mock(TbServiceInfoProvider.class); applicationEventPublisher = mock(ApplicationEventPublisher.class); @@ -173,7 +173,7 @@ public class HashPartitionServiceTest { for (Map.Entry entry : data) { System.out.println(entry.getKey() + ": " + entry.getValue()); } - Assert.assertTrue(diffPercent < maxDiffPercent); + Assertions.assertTrue(diffPercent < maxDiffPercent); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index e32e3aaa57..051214c771 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -15,24 +15,16 @@ */ package org.thingsboard.server.service.apiusage; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; -import org.mockito.junit.MockitoJUnitRunner; -import org.thingsboard.rule.engine.api.MailService; -import org.thingsboard.server.cluster.TbClusterService; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.tenant.TenantService; -import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; -import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.service.executors.DbCallbackExecutorService; import java.util.UUID; @@ -40,27 +32,11 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.never; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { - @Mock - TenantService tenantService; - @Mock - TimeseriesService tsService; - @Mock - TbClusterService clusterService; - @Mock - PartitionService partitionService; @Mock TenantApiUsageState tenantUsageStateMock; - @Mock - ApiUsageStateService apiUsageStateService; - @Mock - TbTenantProfileCache tenantProfileCache; - @Mock - MailService mailService; - @Mock - DbCallbackExecutorService dbExecutor; TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); @@ -68,7 +44,7 @@ public class DefaultTbApiUsageStateServiceTest { @InjectMocks DefaultTbApiUsageStateService service; - @Before + @BeforeEach public void setUp() { } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java index d6bfbf1036..ec43753596 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java @@ -17,11 +17,11 @@ package org.thingsboard.server.service.edge.rpc.constructor; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -44,7 +44,7 @@ import java.util.Optional; import java.util.UUID; @Slf4j -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class RuleChainMsgConstructorTest { private static final String RPC_CONNECTION_TYPE = "RPC"; @@ -53,7 +53,7 @@ public class RuleChainMsgConstructorTest { private TenantId tenantId; - @Before + @BeforeEach public void setup() { ruleChainMsgConstructorV1 = new RuleChainMsgConstructorV1(); tenantId = new TenantId(UUID.randomUUID()); @@ -98,10 +98,10 @@ public class RuleChainMsgConstructorTest { } private void assetV_3_3_3_and_V_3_4_0(RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) { - Assert.assertEquals("First rule node index incorrect!", 3, ruleChainMetadataUpdateMsg.getFirstNodeIndex()); - Assert.assertEquals("Nodes count incorrect!", 12, ruleChainMetadataUpdateMsg.getNodesCount()); - Assert.assertEquals("Connections count incorrect!", 13, ruleChainMetadataUpdateMsg.getConnectionsCount()); - Assert.assertEquals("Rule chain connections count incorrect!", 0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); + Assertions.assertEquals(3, ruleChainMetadataUpdateMsg.getFirstNodeIndex(), "First rule node index incorrect!"); + Assertions.assertEquals(12, ruleChainMetadataUpdateMsg.getNodesCount(), "Nodes count incorrect!"); + Assertions.assertEquals(13, ruleChainMetadataUpdateMsg.getConnectionsCount(), "Connections count incorrect!"); + Assertions.assertEquals(0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(), "Rule chain connections count incorrect!"); compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); @@ -129,10 +129,10 @@ public class RuleChainMsgConstructorTest { ruleChainMetaData, EdgeVersion.V_3_3_0); - Assert.assertEquals("First rule node index incorrect!", 2, ruleChainMetadataUpdateMsg.getFirstNodeIndex()); - Assert.assertEquals("Nodes count incorrect!", 10, ruleChainMetadataUpdateMsg.getNodesCount()); - Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); - Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); + Assertions.assertEquals(2, ruleChainMetadataUpdateMsg.getFirstNodeIndex(),"First rule node index incorrect!"); + Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getNodesCount(), "Nodes count incorrect!"); + Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getConnectionsCount(),"Connections count incorrect!"); + Assertions.assertEquals(1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(), "Rule chain connections count incorrect!"); compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); @@ -146,13 +146,12 @@ public class RuleChainMsgConstructorTest { compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); - Assert.assertEquals("From index incorrect!", 2, ruleChainConnection.getFromIndex()); - Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType()); - Assert.assertEquals("Additional info incorrect!", - "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", - ruleChainConnection.getAdditionalInfo()); - Assert.assertTrue("Target rule chain id MSB incorrect!", ruleChainConnection.getTargetRuleChainIdMSB() != 0); - Assert.assertTrue("Target rule chain id LSB incorrect!", ruleChainConnection.getTargetRuleChainIdLSB() != 0); + Assertions.assertEquals(2, ruleChainConnection.getFromIndex(), "From index incorrect!"); + Assertions.assertEquals(TbNodeConnectionType.SUCCESS, ruleChainConnection.getType(), "Type index incorrect!"); + Assertions.assertEquals("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", + ruleChainConnection.getAdditionalInfo(),"Additional info incorrect!"); + Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdMSB() != 0,"Target rule chain id MSB incorrect!"); + Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdLSB() != 0,"Target rule chain id LSB incorrect!"); assertCheckpointRuleNodeConfiguration( ruleChainMetadataUpdateMsg.getNodesList(), @@ -171,10 +170,10 @@ public class RuleChainMsgConstructorTest { ruleChainMetaData1, EdgeVersion.V_3_3_0); - Assert.assertEquals("First rule node index incorrect!", 7, ruleChainMetadataUpdateMsg.getFirstNodeIndex()); - Assert.assertEquals("Nodes count incorrect!", 10, ruleChainMetadataUpdateMsg.getNodesCount()); - Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); - Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); + Assertions.assertEquals(7, ruleChainMetadataUpdateMsg.getFirstNodeIndex(), "First rule node index incorrect!"); + Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getNodesCount(),"Nodes count incorrect!"); + Assertions.assertEquals(10, ruleChainMetadataUpdateMsg.getConnectionsCount(),"Connections count incorrect!"); + Assertions.assertEquals(1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount(),"Rule chain connections count incorrect!"); compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(1)); @@ -188,13 +187,12 @@ public class RuleChainMsgConstructorTest { compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); - Assert.assertEquals("From index incorrect!", 7, ruleChainConnection.getFromIndex()); - Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType()); - Assert.assertEquals("Additional info incorrect!", - "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", - ruleChainConnection.getAdditionalInfo()); - Assert.assertTrue("Target rule chain id MSB incorrect!", ruleChainConnection.getTargetRuleChainIdMSB() != 0); - Assert.assertTrue("Target rule chain id LSB incorrect!", ruleChainConnection.getTargetRuleChainIdLSB() != 0); + Assertions.assertEquals(7, ruleChainConnection.getFromIndex(),"From index incorrect!"); + Assertions.assertEquals(TbNodeConnectionType.SUCCESS, ruleChainConnection.getType(), "Type index incorrect!"); + Assertions.assertEquals( "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", + ruleChainConnection.getAdditionalInfo(), "Additional info incorrect!"); + Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdMSB() != 0, "Target rule chain id MSB incorrect!"); + Assertions.assertTrue(ruleChainConnection.getTargetRuleChainIdLSB() != 0, "Target rule chain id LSB incorrect!"); assertCheckpointRuleNodeConfiguration( ruleChainMetadataUpdateMsg.getNodesList(), @@ -206,15 +204,15 @@ public class RuleChainMsgConstructorTest { Optional checkpointRuleNodeOpt = nodesList.stream() .filter(rn -> "org.thingsboard.rule.engine.flow.TbCheckpointNode".equals(rn.getType())) .findFirst(); - Assert.assertTrue(checkpointRuleNodeOpt.isPresent()); + Assertions.assertTrue(checkpointRuleNodeOpt.isPresent()); RuleNodeProto checkpointRuleNode = checkpointRuleNodeOpt.get(); - Assert.assertEquals(expectedConfiguration, checkpointRuleNode.getConfiguration()); + Assertions.assertEquals(expectedConfiguration, checkpointRuleNode.getConfiguration()); } private void compareNodeConnectionInfoAndProto(NodeConnectionInfo expected, org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto actual) { - Assert.assertEquals(expected.getFromIndex(), actual.getFromIndex()); - Assert.assertEquals(expected.getToIndex(), actual.getToIndex()); - Assert.assertEquals(expected.getType(), actual.getType()); + Assertions.assertEquals(expected.getFromIndex(), actual.getFromIndex()); + Assertions.assertEquals(expected.getToIndex(), actual.getToIndex()); + Assertions.assertEquals(expected.getType(), actual.getType()); } private RuleChainMetaData createRuleChainMetaData(RuleChainId ruleChainId, Integer firstNodeIndex, List nodes, List connections) { diff --git a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java index 95a2c63f52..2e3dd8ee26 100644 --- a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java @@ -15,11 +15,12 @@ */ package org.thingsboard.server.service.limits; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.cache.limits.DefaultRateLimitService; import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.TenantProfile; @@ -35,21 +36,19 @@ import org.thingsboard.server.dao.tenant.DefaultTbTenantProfileCache; import java.util.List; import java.util.UUID; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class RateLimitServiceTest { private RateLimitService rateLimitService; private DefaultTbTenantProfileCache tenantProfileCache; private TenantId tenantId; - @Before + @BeforeEach public void beforeEach() { tenantProfileCache = Mockito.mock(DefaultTbTenantProfileCache.class); rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100); @@ -102,10 +101,10 @@ public class RateLimitServiceTest { private void testRateLimits(LimitedApi limitedApi, int max, Object level) { for (int i = 1; i <= max; i++) { boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); - assertTrue(success); + Assertions.assertTrue(success); } boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); - assertFalse(success); + Assertions.assertFalse(success); } private void updateTenantProfileConfiguration(DefaultTenantProfileConfiguration profileConfiguration) { diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 8e0b294241..25b541d42d 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -18,13 +18,15 @@ package org.thingsboard.server.service.queue.ruleengine; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.testcontainers.shaded.org.apache.commons.lang3.RandomUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; @@ -91,7 +93,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @Slf4j -@RunWith(MockitoJUnitRunner.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class TbRuleEngineQueueConsumerManagerTest { @Mock @@ -121,7 +123,7 @@ public class TbRuleEngineQueueConsumerManagerTest { private AtomicInteger totalConsumedMsgs; private AtomicInteger totalProcessedMsgs; - @Before + @BeforeEach public void beforeEach() { ruleEngineConsumerContext = new TbRuleEngineConsumerContext( actorContext, statsFactory, spy(new TbRuleEngineSubmitStrategyFactory()), @@ -177,7 +179,7 @@ public class TbRuleEngineQueueConsumerManagerTest { consumerManager = new TbRuleEngineQueueConsumerManager(ruleEngineConsumerContext, queueKey); } - @After + @AfterEach public void afterEach() { consumerManager.stop(); consumerManager.awaitStop(); diff --git a/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java index 96a3daa06d..7db9e01bc8 100644 --- a/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.service.sms.smpp; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; import org.smpp.Session; import org.smpp.pdu.SubmitSMResp; import org.thingsboard.server.common.data.StringUtils; @@ -35,14 +35,14 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class SmppSmsSenderTest { SmppSmsSender smppSmsSender; SmppSmsProviderConfiguration smppConfig; Session smppSession; - @Before + @BeforeEach public void beforeEach() throws Exception { Constructor constructor = SmppSmsSender.class.getDeclaredConstructor(); constructor.setAccessible(true); From 843151ba4491c836e85045a4991d730794cd8fb9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 May 2024 11:56:29 +0300 Subject: [PATCH 078/121] UI: Fixed error JIT Compilation and correct process --- .../dynamic-component-factory.service.ts | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index 00bc0c3e64..8414e6ff0e 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -17,7 +17,6 @@ import { Compiler, Component, - ComponentFactory, Injectable, Injector, NgModule, @@ -26,9 +25,9 @@ import { Type, ɵresetCompiledComponents } from '@angular/core'; -import { from, Observable } from 'rxjs'; +import { from, Observable, of } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { catchError, map, mergeMap } from 'rxjs/operators'; +import { mergeMap } from 'rxjs/operators'; @NgModule() export abstract class DynamicComponentModule implements OnDestroy { @@ -79,33 +78,32 @@ export class DynamicComponentFactoryService { declarations: [comp], imports: moduleImports })(class DynamicComponentInstanceModule extends DynamicComponentModule {}); - return from(this.compiler.compileModuleAsync(dynamicComponentInstanceModule)).pipe( - map((module) => { - let moduleRef: NgModuleRef; - try { - moduleRef = module.create(this.injector); - } catch (e) { - this.compiler.clearCacheFor(module.moduleType); - throw e; - } - this.dynamicComponentModulesMap.set(comp, { - moduleRef, - moduleType: module.moduleType - }); - return { - componentType: comp, - componentModuleRef: moduleRef - }; - }), - catchError((error) => { - if (compileAttempt === 1) { - ɵresetCompiledComponents(); - return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); - } else { - throw error; - } - }) - ); + try { + const module = this.compiler.compileModuleSync(dynamicComponentInstanceModule); + let moduleRef: NgModuleRef; + try { + moduleRef = module.create(this.injector); + } catch (e) { + this.compiler.clearCacheFor(module.moduleType); + throw e; + } + this.dynamicComponentModulesMap.set(comp, { + moduleRef, + moduleType: module.moduleType + }); + return of( { + componentType: comp, + componentModuleRef: moduleRef + }); + } catch (error) { + if (compileAttempt === 1) { + ɵresetCompiledComponents(); + return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); + } else { + console.error(error); + throw error; + } + } }) ); } From c0d2d5449bfbb4044e9335ef719ce01c8c049b0c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 8 May 2024 13:08:54 +0300 Subject: [PATCH 079/121] Fix finding of attribute keys by entity id and scope --- .../sql/attributes/AttributeKvRepository.java | 4 +- .../dao/sql/attributes/JpaAttributeDao.java | 2 +- .../attributes/BaseAttributesServiceTest.java | 42 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index 073dfd7d95..ddd6c3c881 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -45,7 +45,7 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByDeviceProfileId(@Param("tenantId") UUID tenantId, - @Param("deviceProfileId") UUID deviceProfileId); + @Param("deviceProfileId") UUID deviceProfileId); @Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE " + "entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true) @@ -58,6 +58,6 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, - @Param("attributeType") String attributeType); + @Param("attributeType") int attributeType); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java index 3bf8e43c70..320e0e67fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java @@ -173,7 +173,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @Override public List findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List entityIds, String attributeType) { return attributeKvRepository - .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), attributeType) + .findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId()) .stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index 707155c86c..98a8cf9d47 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -47,6 +48,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Andrew Shvayka */ @@ -221,6 +224,45 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { Assert.assertEquals(NEW_VALUE, value.get(1)); } + @Test + public void testFindAllKeysByEntityId() { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123"); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); + + Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId)); + assertThat(keys).containsOnly("key1", "key2"); + }); + } + + @Test + public void testFindAllKeysByEntityIdAndAttributeType() { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123"); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); + + Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name()); + assertThat(keys).containsOnly("key1", "key2"); + }); + } + + @Test + public void testFindAllByEntityIdAndAttributeType() { + var tenantId = new TenantId(UUID.randomUUID()); + var deviceId = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key1", "123"); + saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); + + Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + List attributes = attributesService.findAll(tenantId, deviceId, AttributeScope.SERVER_SCOPE).get(); + assertThat(attributes).extracting(KvEntry::getKey).containsOnly("key1", "key2"); + }); + } + private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { var scope = AttributeScope.SERVER_SCOPE; var key = "TEST"; From a6e47c3b133607a644d26d2be1c394cc3f6a44de Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 8 May 2024 13:50:30 +0300 Subject: [PATCH 080/121] Minor refactoring for fields naming --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 83ffba634f..e4ac64378e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1900,10 +1900,10 @@ "min-evaluation-period": "Minimum evaluation period", "max-evaluation-period": "Maximum evaluation period" }, - "default-object-id": "Default object Version (Attribute)", + "default-object-id": "Default Object Version (Attribute)", "default-object-id-ver": { - "v1-0": "\"1.0\" is the object's version value if the object's version is empty.", - "v1-1": "\"1.1\" is the object's version value if the object's version is empty." + "v1-0": "1.0", + "v1-1": "1.1" } }, "snmp": { From 938b60f8406e29356577f60d6ec834e211e1a882 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 8 May 2024 14:27:51 +0300 Subject: [PATCH 081/121] Fix maxRuleNodeExecutionsPerMessage (failed test TenantProfileEdgeTest.testTenantProfiles) --- .../data/tenant/profile/DefaultTenantProfileConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index a1a16743be..a0cfbfe1e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -59,7 +59,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxJSExecutions; private long maxTbelExecutions; private long maxDPStorageDays; - private int maxRuleNodeExecutionsPerMessage = 1000; + private int maxRuleNodeExecutionsPerMessage; private long maxEmails; private Boolean smsEnabled; private long maxSms; From b16d8484e91798ec77a6e73f1726e5159de160b8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 8 May 2024 14:50:11 +0300 Subject: [PATCH 082/121] Minor refactoring for notifications consumers --- .../queue/processing/AbstractConsumerService.java | 2 +- .../server/common/msg/queue/ServiceType.java | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 85227e50ab..a264f912cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -85,7 +85,7 @@ public abstract class AbstractConsumerService>builder() - .name("TB Notifications") + .name(getServiceType().getLabel() + " Notifications") .msgPackProcessor(this::processNotifications) .pollInterval(getNotificationPollDuration()) .consumerCreator(this::createNotificationsConsumer) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java index c823ea4d73..f3a0e47d09 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/ServiceType.java @@ -15,11 +15,23 @@ */ package org.thingsboard.server.common.msg.queue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter public enum ServiceType { - TB_CORE, TB_RULE_ENGINE, TB_TRANSPORT, JS_EXECUTOR, TB_VC_EXECUTOR; + TB_CORE("TB Core"), + TB_RULE_ENGINE("TB Rule Engine"), + TB_TRANSPORT("TB Transport"), + JS_EXECUTOR("JS Executor"), + TB_VC_EXECUTOR("TB VC Executor"); + + private final String label; public static ServiceType of(String serviceType) { return ServiceType.valueOf(serviceType.replace("-", "_").toUpperCase()); } + } From 62d9f76c57f7f66ddd5dc1c2c20c2984c1caebb1 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 8 May 2024 16:02:51 +0300 Subject: [PATCH 083/121] Use local variable to create new alarm instead of field variable - fix inconsistent state of currentAlarm field in case createAlarm is failed --- .../rule/engine/profile/AlarmState.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 281fdcb16a..f03d1e46ba 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -244,27 +244,27 @@ class AlarmState { return null; } } else { - currentAlarm = new Alarm(); - currentAlarm.setType(alarmDefinition.getAlarmType()); - currentAlarm.setAcknowledged(false); - currentAlarm.setCleared(false); - currentAlarm.setSeverity(severity); + var newAlarm = new Alarm(); + newAlarm.setType(alarmDefinition.getAlarmType()); + newAlarm.setAcknowledged(false); + newAlarm.setCleared(false); + newAlarm.setSeverity(severity); long startTs = dataSnapshot.getTs(); if (startTs == 0L) { startTs = System.currentTimeMillis(); } - currentAlarm.setStartTs(startTs); - currentAlarm.setEndTs(currentAlarm.getStartTs()); - currentAlarm.setDetails(createDetails(ruleState)); - currentAlarm.setOriginator(originator); - currentAlarm.setTenantId(ctx.getTenantId()); - currentAlarm.setPropagate(alarmDefinition.isPropagate()); - currentAlarm.setPropagateToOwner(alarmDefinition.isPropagateToOwner()); - currentAlarm.setPropagateToTenant(alarmDefinition.isPropagateToTenant()); + newAlarm.setStartTs(startTs); + newAlarm.setEndTs(startTs); + newAlarm.setDetails(createDetails(ruleState)); + newAlarm.setOriginator(originator); + newAlarm.setTenantId(ctx.getTenantId()); + newAlarm.setPropagate(alarmDefinition.isPropagate()); + newAlarm.setPropagateToOwner(alarmDefinition.isPropagateToOwner()); + newAlarm.setPropagateToTenant(alarmDefinition.isPropagateToTenant()); if (alarmDefinition.getPropagateRelationTypes() != null) { - currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); + newAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); } - AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(currentAlarm)); + AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(newAlarm)); currentAlarm = result.getAlarm(); return TbAlarmResult.fromAlarmResult(result); } From 6c499fd342edb7e87989527143c95c54f0a8849f Mon Sep 17 00:00:00 2001 From: Kulikov <44275303+nickAS21@users.noreply.github.com> Date: Wed, 8 May 2024 16:09:37 +0300 Subject: [PATCH 084/121] LwM2M: default object version (#10731) * lwm2m: object-id-version default 1.0 * lwm2m: delete translate * lwm2m: delete translate3 * lwm2m: object id ver comment4 * lwm2m: object id ver comment5 * lwm2m: object id ver comment6 --- .../device/DeviceBulkImportService.java | 5 +- .../profile/lwm2m/OtherConfiguration.java | 1 + .../lwm2m/server/LwM2mServerListener.java | 6 +- .../server/LwM2mVersionedModelProvider.java | 8 +- .../lwm2m/server/client/LwM2mClient.java | 102 ++++++++++-------- .../server/client/LwM2mClientContextImpl.java | 2 +- .../ota/DefaultLwM2MOtaUpdateService.java | 14 +-- .../server/store/util/LwM2MClientSerDes.java | 9 ++ .../uplink/DefaultLwM2mUplinkMsgHandler.java | 28 ++--- .../server/uplink/LwM2mUplinkMsgHandler.java | 2 + .../lwm2m/utils/LwM2MTransportUtil.java | 81 +------------- .../main/resources/lwm2m/device_profile.json | 9 +- .../resources/lwm2m/models/test-model.xml | 4 +- .../src/main/resources/lwm2m/resource.json | 2 +- 14 files changed, 115 insertions(+), 158 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 0c3146a6c6..38f44898ee 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; -import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration; @@ -68,6 +67,8 @@ import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import static org.eclipse.leshan.core.LwM2m.Version.V1_0; + @Service @TbCoreComponent @RequiredArgsConstructor @@ -256,7 +257,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); transportConfiguration.setBootstrap(Collections.emptyList()); - transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null)); + transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap())); DeviceProfileData deviceProfileData = new DeviceProfileData(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java index 36179502ad..7ab5ee3b56 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java @@ -39,4 +39,5 @@ public class OtherConfiguration extends PowerSavingConfiguration { private Long pagingTransmissionWindow; private String fwUpdateResource; private String swUpdateResource; + private String defaultObjectIDVer; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index b59914e2ac..7797242b51 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -29,6 +29,7 @@ import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.registration.RegistrationListener; import org.eclipse.leshan.server.registration.RegistrationUpdate; import org.eclipse.leshan.server.send.SendListener; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.util.Collection; @@ -101,7 +102,10 @@ public class LwM2mServerListener { @Override public void onResponse(SingleObservation observation, Registration registration, ObserveResponse response) { if (registration != null) { - service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), registration), response); + LwM2mClient lwM2MClient = service.getClientContext().getClientByEndpoint(registration.getEndpoint()); + if (lwM2MClient != null) { + service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), lwM2MClient), response); + } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 3e407bdfc9..78f76adc1d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -27,10 +27,10 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.Map; import java.util.Optional; @@ -107,7 +107,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public ObjectModel getObjectModel(int objectId) { - String version = String.valueOf(registration.getSupportedVersion(objectId)); + LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()); + String version = lwM2mClient.getSupportedObjectVersion(objectId).toString(); if (version != null) { return this.getObjectModelDynamic(objectId, version); } @@ -116,7 +117,8 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { @Override public Collection getObjectModels() { - Map supportedObjects = this.registration.getSupportedObject(); + LwM2mClient lwM2mClient = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()); + Map supportedObjects = lwM2mClient.getSupportedClientObjects(); Collection result = new ArrayList<>(supportedObjects.size()); for (Map.Entry supportedObject : supportedObjects.entrySet()) { ObjectModel objectModel = this.getObjectModelDynamic(supportedObject.getKey(), String.valueOf(supportedObject.getValue())); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index cb391b27e9..e267561cd2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -21,7 +21,10 @@ import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.LwM2m; +import org.eclipse.leshan.core.LwM2m.Version; +import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.link.attributes.Attribute; +import org.eclipse.leshan.core.link.lwm2m.MixedLwM2mLink; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -37,6 +40,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.PowerMode; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; @@ -61,8 +65,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; -import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.getVerFromPathIdVerOrId; @@ -129,6 +133,12 @@ public class LwM2mClient { @Setter private UUID lastSentRpcId; + @Setter + private LwM2m.Version defaultObjectIDVer; + + @Getter + private Map supportedClientObjects; + public Object clone() throws CloneNotSupportedException { return super.clone(); } @@ -142,6 +152,7 @@ public class LwM2mClient { this.state = LwM2MClientState.CREATED; this.lock = new ReentrantLock(); this.retryAttempts = new AtomicInteger(0); + this.supportedClientObjects = null; } public void init(ValidateDeviceCredentialsResponse credentials, UUID sessionId) { @@ -153,12 +164,14 @@ public class LwM2mClient { this.edrxCycle = credentials.getDeviceInfo().getEdrxCycle(); this.psmActivityTimer = credentials.getDeviceInfo().getPsmActivityTimer(); this.pagingTransmissionWindow = credentials.getDeviceInfo().getPagingTransmissionWindow(); + this.defaultObjectIDVer = getObjectIDVerFromDeviceProfile(credentials.getDeviceProfile()); } public void setRegistration(Registration registration) { this.registration = registration; this.clientSupportContentFormats = clientSupportContentFormat(registration); this.defaultContentFormat = calculateDefaultContentFormat(registration); + this.setSupportedClientObjects(); } public void lock() { @@ -197,6 +210,15 @@ public class LwM2mClient { builder.setDeviceType(deviceProfile.getName()); } + private LwM2m.Version getObjectIDVerFromDeviceProfile(DeviceProfile deviceProfile) { + String defaultObjectIdVer = ((Lwm2mDeviceProfileTransportConfiguration)deviceProfile + .getProfileData() + .getTransportConfiguration()) + .getClientLwM2mSettings() + .getDefaultObjectIDVer(); + return new Version(defaultObjectIdVer == null ? LWM2M_OBJECT_VERSION_DEFAULT : defaultObjectIdVer); + } + public void refreshSessionId(String nodeId) { UUID newId = UUID.randomUUID(); SessionInfoProto.Builder builder = SessionInfoProto.newBuilder().mergeFrom(session); @@ -240,67 +262,28 @@ public class LwM2mClient { } } - public Object getResourceValue(String pathRezIdVer, String pathRezId) { - String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer; - if (this.resources.get(pathRez) != null) { - return this.resources.get(pathRez).getLwM2mResource().getValue(); - } - return null; - } - - public Object getResourceNameByRezId(String pathRezIdVer, String pathRezId) { - String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer; - if (this.resources.get(pathRez) != null) { - return this.resources.get(pathRez).getResourceModel().name; - } - return null; - } - - public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) { - LwM2mPath pathIds = getLwM2mPathFromString(pathObjectInstanceIdVer); - if (pathIds.isObjectInstance()) { - Set rezIds = modelProvider.getObjectModel(registration) - .getObjectModel(pathIds.getObjectId()).resources.entrySet() - .stream() - .filter(map -> resourceName.equals(map.getValue().name)) - .map(map -> map.getKey()) - .collect(Collectors.toSet()); - return rezIds.size() > 0 ? String.valueOf(rezIds.stream().findFirst().get()) : null; - } - return null; - } - public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) { LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); - String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); + String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) .getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null; } - public boolean isResourceMultiInstances(String pathIdVer, LwM2mModelProvider modelProvider) { - var resourceModel = getResourceModel(pathIdVer, modelProvider); - if (resourceModel != null && resourceModel.multiple != null) { - return resourceModel.multiple; - } else { - return false; - } - } - public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) { try { LwM2mPath pathIds = getLwM2mPathFromString(pathIdVer); - String verSupportedObject = String.valueOf(registration.getSupportedObject().get(pathIds.getObjectId())); + String verSupportedObject = String.valueOf(this.getSupportedObjectVersion(pathIds.getObjectId())); String verRez = getVerFromPathIdVerOrId(pathIdVer); return verRez != null && verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration) .getObjectModel(pathIds.getObjectId()) : null; } catch (Exception e) { if (registration == null) { log.error("[{}] Failed Registration is null, GetObjectModelRegistration. ", this.endpoint, e); - } else if (registration.getSupportedObject() == null) { + } else if (this.getSupportedClientObjects() == null) { log.error("[{}] Failed SupportedObject in Registration, GetObjectModelRegistration.", this.endpoint, e); } else { - log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, registration.getSupportedObject(), e); + log.error("[{}] Failed ModelProvider.getObjectModel [{}] in Registration. ", this.endpoint, this.getSupportedClientObjects(), e); } return null; } @@ -371,7 +354,7 @@ public class LwM2mClient { public String isValidObjectVersion(String path) { LwM2mPath pathIds = getLwM2mPathFromString(path); - LwM2m.Version verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId()); + LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId()); if (verSupportedObject == null) { return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); } else { @@ -460,5 +443,34 @@ public class LwM2mClient { return new LwM2mPath(fromVersionedIdToObjectId(path)); } + public LwM2m.Version getDefaultObjectIDVer() { + return this.defaultObjectIDVer == null ? new Version(LWM2M_OBJECT_VERSION_DEFAULT) : this.defaultObjectIDVer; + } + + public LwM2m.Version getSupportedObjectVersion(Integer objectid) { + return this.supportedClientObjects.get(objectid); + } + + private void setSupportedClientObjects(){ + this.supportedClientObjects = new ConcurrentHashMap<>(); + for (Link link: this.registration.getSortedObjectLinks()) { + MixedLwM2mLink mixedLwM2mLink = (MixedLwM2mLink)link; + if(!mixedLwM2mLink.getPath().isRoot()){ + LwM2mPath lwM2mPath = mixedLwM2mLink.getPath(); + if (lwM2mPath.isObject()) { + LwM2m.Version ver; + if (mixedLwM2mLink.getAttributes().get("ver")!= null) { + ver = (Version) mixedLwM2mLink.getAttributes().get("ver").getValue(); + } else { + ver = getDefaultObjectIDVer(); + } + this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); + } else if (this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ + this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); + } + } + } + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 4f35489250..851b517db6 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -396,7 +396,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { Arrays.stream(client.getRegistration().getObjectLinks()).forEach(link -> { LwM2mPath pathIds = new LwM2mPath(link.getUriReference()); if (!pathIds.isRoot()) { - clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client.getRegistration())); + clientObjects.add(convertObjectIdToVersionedId(link.getUriReference(), client)); } }); return (clientObjects.size() > 0) ? clientObjects : null; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index f9d23383cd..0b5e995ea5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -481,7 +481,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } private void startUpdateUsingUrl(LwM2mClient client, String id, String url) { - String targetIdVer = convertObjectIdToVersionedId(id, client.getRegistration()); + String targetIdVer = convertObjectIdToVersionedId(id, client); TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, targetIdVer)); } @@ -512,10 +512,10 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } switch (strategy) { case OBJ_5_BINARY: - startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client.getRegistration()), otaPackageId); + startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_5_ID, client), otaPackageId); break; case OBJ_19_BINARY: - startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client.getRegistration()), otaPackageId); + startUpdateUsingBinary(client, convertObjectIdToVersionedId(FW_PACKAGE_19_ID, client), otaPackageId); break; case OBJ_5_TEMP_URL: startUpdateUsingUrl(client, FW_URL_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString()); @@ -534,7 +534,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl LwM2MSoftwareUpdateStrategy strategy = info.getStrategy(); switch (strategy) { case BINARY: - startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client.getRegistration()), otaPackageId); + startUpdateUsingBinary(client, convertObjectIdToVersionedId(SW_PACKAGE_ID, client), otaPackageId); break; case TEMP_URL: startUpdateUsingUrl(client, SW_PACKAGE_URI_ID, info.getBaseUrl() + "/" + FIRMWARE_UPDATE_COAP_RESOURCE + "/" + otaPackageId.toString()); @@ -566,19 +566,19 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } private void executeFwUpdate(LwM2mClient client) { - String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client.getRegistration()); + String fwExecuteVerId = convertObjectIdToVersionedId(FW_EXECUTE_ID, client); TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(fwExecuteVerId).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, fwExecuteVerId)); } private void executeSwInstall(LwM2mClient client) { - String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client.getRegistration()); + String swInstallVerId = convertObjectIdToVersionedId(SW_INSTALL_ID, client); TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInstallVerId).timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInstallVerId)); } private void executeSwUninstallForUpdate(LwM2mClient client) { - String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client.getRegistration()); + String swInInstallVerId = convertObjectIdToVersionedId(SW_UN_INSTALL_ID, client); TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(swInInstallVerId).params("1").timeout(clientContext.getRequestTimeout(client)).build(); downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, swInInstallVerId)); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 17fef8acb0..21cdc356a4 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -107,6 +107,8 @@ public class LwM2MClientSerDes { if (client.getPagingTransmissionWindow() != null) { o.addProperty("pagingTransmissionWindow", client.getPagingTransmissionWindow()); } + o.addProperty("defaultObjectIDVer", client.getDefaultObjectIDVer().toString()); + if (client.getRegistration() != null) { String registrationAddress = client.getRegistration().getAddress().toString(); JsonNode registrationNode = registrationSerDes.jSerialize(client.getRegistration()); @@ -339,6 +341,13 @@ public class LwM2MClientSerDes { pagingTransmissionWindowField.set(lwM2mClient, pagingTransmissionWindow.getAsLong()); } + JsonElement defaultObjectIDVer = o.get("defaultObjectIDVer"); + if (defaultObjectIDVer != null) { + Field defaultObjectIDVerField = lwM2mClientClass.getDeclaredField("defaultObjectIDVer"); + defaultObjectIDVerField.setAccessible(true); + defaultObjectIDVerField.set(lwM2mClient, defaultObjectIDVer.getAsString()); + } + JsonElement registration = o.get("registration"); if (registration != null) { lwM2mClient.setRegistration(registrationSerDes.deserialize(toJsonNode(registration.getAsString()))); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index fc95bb3728..bfc9f32dd8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -368,7 +368,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mPath path = entry.getKey(); LwM2mNode node = entry.getValue(); LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); - String stringPath = convertObjectIdToVersionedId(path.toString(), registration); + String stringPath = convertObjectIdToVersionedId(path.toString(), lwM2MClient); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider); if (objectModelVersion != null) { if (node instanceof LwM2mObject) { @@ -584,27 +584,27 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String path, Mode mode, int code) { Registration registration = lwM2MClient.getRegistration(); if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { - if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, registration))) { + if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) { otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, lwM2MClient))) { otaService.onCurrentFirmwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, lwM2MClient))) { otaService.onCurrentFirmwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, lwM2MClient))) { otaService.onCurrentFirmwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, lwM2MClient))) { otaService.onCurrentFirmwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, lwM2MClient))) { otaService.onCurrentFirmwareDeliveryMethodUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, lwM2MClient))) { otaService.onCurrentSoftwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, lwM2MClient))) { otaService.onCurrentSoftwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, lwM2MClient))) { otaService.onCurrentSoftwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, lwM2MClient))) { otaService.onCurrentSoftwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, registration))) { + } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, lwM2MClient))) { otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); } if (ResponseCode.BAD_REQUEST.getCode() > code) { @@ -969,6 +969,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } + public LwM2mClientContext getClientContext(){ + return this.clientContext; + }; + private Map getNamesFromProfileForSharedAttributes(LwM2mClient lwM2MClient) { Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getProfileId()); return profile.getObserveAttr().getKeyName(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 65c7fb9654..0896963be2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import java.util.Collection; import java.util.Optional; @@ -77,4 +78,5 @@ public interface LwM2mUplinkMsgHandler { LwM2mValueConverter getConverter(); + LwM2mClientContext getClientContext(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index db8398cd57..e2e31f2768 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -20,16 +20,13 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.elements.config.Configuration; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectLoader; -import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; -import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.util.Hex; -import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; @@ -49,8 +46,6 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState; -import java.util.Arrays; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -84,56 +79,6 @@ public class LwM2MTransportUtil { public static final String LOG_LWM2M_WARN = "warn"; public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0; - public enum LwM2MClientStrategy { - CLIENT_STRATEGY_1(1, "Read only resources marked as observation"), - CLIENT_STRATEGY_2(2, "Read all client resources"); - - public int code; - public String type; - - LwM2MClientStrategy(int code, String type) { - this.code = code; - this.type = type; - } - - public static LwM2MClientStrategy fromStrategyClientByType(String type) { - for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) { - if (to.type.equals(type)) { - return to; - } - } - throw new IllegalArgumentException(String.format("Unsupported Client Strategy type : %s", type)); - } - - public static LwM2MClientStrategy fromStrategyClientByCode(int code) { - for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) { - if (to.code == code) { - return to; - } - } - throw new IllegalArgumentException(String.format("Unsupported Client Strategy code : %s", code)); - } - } - - public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath - resourcePath) throws CodecException { - switch (type) { - case BOOLEAN: - case INTEGER: - case FLOAT: - return String.valueOf(valueOld).equals(String.valueOf(valueNew)); - case TIME: - return ((Date) valueOld).getTime() == ((Date) valueNew).getTime(); - case STRING: - case OBJLNK: - return valueOld.equals(valueNew); - case OPAQUE: - return Arrays.equals(Hex.decodeHex(((String) valueOld).toCharArray()), Hex.decodeHex(((String) valueNew).toCharArray())); - default: - throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type); - } - } - public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) { String path = fromVersionedIdToObjectId(pathIdVer); LwM2mOtaConvert lwM2mOtaConvert = new LwM2mOtaConvert(); @@ -210,22 +155,8 @@ public class LwM2MTransportUtil { return null; } - public static String validPathIdVer(String pathIdVer, Registration registration) throws - IllegalArgumentException { - if (!pathIdVer.contains(LWM2M_SEPARATOR_PATH)) { - throw new IllegalArgumentException(String.format("Error:")); - } else { - String[] keyArray = pathIdVer.split(LWM2M_SEPARATOR_PATH); - if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) { - return pathIdVer; - } else { - return convertObjectIdToVersionedId(pathIdVer, registration); - } - } - } - - public static String convertObjectIdToVersionedId(String path, Registration registration) { - String ver = String.valueOf(registration.getSupportedObject().get(new LwM2mPath(path).getObjectId())); + public static String convertObjectIdToVersionedId(String path, LwM2mClient lwM2MClient) { + String ver = String.valueOf(lwM2MClient.getSupportedObjectVersion(new LwM2mPath(path).getObjectId())); return convertObjectIdToVerId(path, ver); } public static String convertObjectIdToVerId(String path, String ver) { @@ -243,14 +174,6 @@ public class LwM2MTransportUtil { } } - public static String validateObjectVerFromKey(String key) { - try { - return (key.split(LWM2M_SEPARATOR_PATH)[1].split(LWM2M_SEPARATOR_KEY)[1]); - } catch (Exception e) { - return ObjectModel.DEFAULT_VERSION; - } - } - /** * "UNSIGNED_INTEGER": // Number -> Integer Example: * Alarm Timestamp [32-bit unsigned integer] diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json index cc78187938..c75fe9ce7a 100644 --- a/monitoring/src/main/resources/lwm2m/device_profile.json +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -12,14 +12,14 @@ "transportConfiguration": { "observeAttr": { "observe": [ - "/3_1.1/0/0" + "/3_1.0/0/0" ], "attribute": [], "telemetry": [ - "/3_1.1/0/0" + "/3_1.0/0/0" ], "keyName": { - "/3_1.1/0/0": "testData" + "/3_1.0/0/0": "testData" }, "attributeLwm2m": {} }, @@ -44,8 +44,7 @@ "clientOnlyObserveAfterConnect": 1, "fwUpdateStrategy": 1, "swUpdateStrategy": 1, - "powerMode": "DRX", - "compositeOperationsSupport": false + "powerMode": "DRX" }, "bootstrapServerUpdateEnable": false, "type": "LWM2M" diff --git a/monitoring/src/main/resources/lwm2m/models/test-model.xml b/monitoring/src/main/resources/lwm2m/models/test-model.xml index b13934531d..7b71b5e783 100644 --- a/monitoring/src/main/resources/lwm2m/models/test-model.xml +++ b/monitoring/src/main/resources/lwm2m/models/test-model.xml @@ -23,9 +23,9 @@ 3 - urn:oma:lwm2m:oma:3:1.1 + urn:oma:lwm2m:oma:3 1.1 - 1.1 + 1.0 Single Mandatory diff --git a/monitoring/src/main/resources/lwm2m/resource.json b/monitoring/src/main/resources/lwm2m/resource.json index 3f4ec5c3fe..dcc0182bdd 100644 --- a/monitoring/src/main/resources/lwm2m/resource.json +++ b/monitoring/src/main/resources/lwm2m/resource.json @@ -2,5 +2,5 @@ "title": "", "resourceType": "LWM2M_MODEL", "fileName": "test-model.xml", - "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMTwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4xPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K" + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjQgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozPC9PYmplY3RVUk4+CiAgICAgICAgPExXTTJNVmVyc2lvbj4xLjE8L0xXTTJNVmVyc2lvbj4KICAgICAgICA8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CiAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgPE1hbmRhdG9yeT5NYW5kYXRvcnk8L01hbmRhdG9yeT4KICAgICAgICA8UmVzb3VyY2VzPgogICAgICAgICAgICA8SXRlbSBJRD0iMCI+CiAgICAgICAgICAgICAgICA8TmFtZT5UZXN0IGRhdGE8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUZXN0IGRhdGFdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgPC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo=" } \ No newline at end of file From 65fd836d9ffdb675a3f5873270d12a6c03097708 Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 8 May 2024 17:16:11 +0300 Subject: [PATCH 085/121] fis_bug: LwM2MClientSerDes test --- .../lwm2m/server/client/LwM2mClient.java | 37 +++++++++---------- .../server/store/util/LwM2MClientSerDes.java | 3 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index e267561cd2..405050bd71 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -24,7 +24,6 @@ import org.eclipse.leshan.core.LwM2m; import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.link.attributes.Attribute; -import org.eclipse.leshan.core.link.lwm2m.MixedLwM2mLink; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -211,11 +210,14 @@ public class LwM2mClient { } private LwM2m.Version getObjectIDVerFromDeviceProfile(DeviceProfile deviceProfile) { - String defaultObjectIdVer = ((Lwm2mDeviceProfileTransportConfiguration)deviceProfile - .getProfileData() - .getTransportConfiguration()) - .getClientLwM2mSettings() - .getDefaultObjectIDVer(); + String defaultObjectIdVer = null; + if (deviceProfile != null) { + defaultObjectIdVer = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile + .getProfileData() + .getTransportConfiguration()) + .getClientLwM2mSettings() + .getDefaultObjectIDVer(); + } return new Version(defaultObjectIdVer == null ? LWM2M_OBJECT_VERSION_DEFAULT : defaultObjectIdVer); } @@ -454,20 +456,17 @@ public class LwM2mClient { private void setSupportedClientObjects(){ this.supportedClientObjects = new ConcurrentHashMap<>(); for (Link link: this.registration.getSortedObjectLinks()) { - MixedLwM2mLink mixedLwM2mLink = (MixedLwM2mLink)link; - if(!mixedLwM2mLink.getPath().isRoot()){ - LwM2mPath lwM2mPath = mixedLwM2mLink.getPath(); - if (lwM2mPath.isObject()) { - LwM2m.Version ver; - if (mixedLwM2mLink.getAttributes().get("ver")!= null) { - ver = (Version) mixedLwM2mLink.getAttributes().get("ver").getValue(); - } else { - ver = getDefaultObjectIDVer(); - } - this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); - } else if (this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ - this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); + LwM2mPath lwM2mPath = new LwM2mPath(link.getUriReference()); + if (lwM2mPath.isObject()) { + LwM2m.Version ver; + if (link.getAttributes().get("ver")!= null) { + ver = (Version) link.getAttributes().get("ver").getValue(); + } else { + ver = getDefaultObjectIDVer(); } + this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); + } else if (this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ + this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 21cdc356a4..8cafc2bfd0 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -23,6 +23,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.protobuf.util.JsonFormat; import lombok.SneakyThrows; +import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mNodeException; @@ -345,7 +346,7 @@ public class LwM2MClientSerDes { if (defaultObjectIDVer != null) { Field defaultObjectIDVerField = lwM2mClientClass.getDeclaredField("defaultObjectIDVer"); defaultObjectIDVerField.setAccessible(true); - defaultObjectIDVerField.set(lwM2mClient, defaultObjectIDVer.getAsString()); + defaultObjectIDVerField.set(lwM2mClient, new Version(defaultObjectIDVer.getAsString())); } JsonElement registration = o.get("registration"); From 814bc331aebc09facd586d958cb8ba4245f2372c Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 8 May 2024 18:06:18 +0300 Subject: [PATCH 086/121] fixed tests related to the login audit log --- .../server/controller/AuditLogControllerTest.java | 6 +++--- .../thingsboard/server/controller/TwoFactorAuthTest.java | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java index 31f398dc03..5005e6ad34 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AuditLogControllerTest.java @@ -122,7 +122,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Assert.assertEquals(11, loadedAuditLogs.size()); + Assert.assertEquals(11 + 1, loadedAuditLogs.size()); loadedAuditLogs = new ArrayList<>(); pageLink = new TimePageLink(5); @@ -136,7 +136,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Assert.assertEquals(11, loadedAuditLogs.size()); + Assert.assertEquals(11 + 1, loadedAuditLogs.size()); loadedAuditLogs = new ArrayList<>(); pageLink = new TimePageLink(5); @@ -150,7 +150,7 @@ public class AuditLogControllerTest extends AbstractControllerTest { } } while (pageData.hasNext()); - Assert.assertEquals(11, loadedAuditLogs.size()); + Assert.assertEquals(11 + 1, loadedAuditLogs.size()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 38286589bd..63703ae27b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -298,15 +298,13 @@ public class TwoFactorAuthTest extends AbstractControllerTest { logInWithPreVerificationToken(username, password); await("async audit log saving").during(1, TimeUnit.SECONDS); - assertThat(getLogInAuditLogs()).isEmpty(); - assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo() - .get("lastLoginTs")).isNull(); doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") .andExpect(status().isBadRequest()); + // there is the first login audit log after user activation await("async audit log saving").atMost(1, TimeUnit.SECONDS) - .until(() -> getLogInAuditLogs().size() == 1); + .until(() -> getLogInAuditLogs().size() == 2); assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> { assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE); assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect"); @@ -316,7 +314,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig)) .andExpect(status().isOk()); await("async audit log saving").atMost(1, TimeUnit.SECONDS) - .until(() -> getLogInAuditLogs().size() == 2); + .until(() -> getLogInAuditLogs().size() == 3); assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> { assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS); assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username); From 6809114c0506d061fcd4f775d13b1104c83f00bb Mon Sep 17 00:00:00 2001 From: nick Date: Wed, 8 May 2024 19:10:53 +0300 Subject: [PATCH 087/121] fis_bug: setSupportedClientObjects() if link is root --- .../server/transport/lwm2m/server/client/LwM2mClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 405050bd71..705ed45995 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -465,7 +465,7 @@ public class LwM2mClient { ver = getDefaultObjectIDVer(); } this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); - } else if (this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ + } else if (lwM2mPath.getObjectId() != null && this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); } } From 305945133c78411b43728c6d0d609183293e02e6 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 9 May 2024 11:06:50 +0300 Subject: [PATCH 088/121] Import system images from tenant dashboards. Do not create tenant dashboards for test spring profile. --- .../server/service/entitiy/AbstractTbEntityService.java | 9 +++++++++ .../service/entitiy/tenant/DefaultTbTenantService.java | 5 ++++- .../server/service/install/InstallScripts.java | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 0c91d72ea5..438cb58b91 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -39,11 +40,15 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; @Slf4j public abstract class AbstractTbEntityService { + @Autowired + private Environment env; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -67,6 +72,10 @@ public abstract class AbstractTbEntityService { @Lazy private EntitiesVersionControlService vcService; + protected boolean isTestProfile() { + return Set.of(this.env.getActiveProfiles()).contains("test"); + } + protected T checkNotNull(T reference) throws ThingsboardException { return checkNotNull(reference, "Requested item wasn't found!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java index 8096722940..74061ee9e5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.entitiy.tenant; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; @@ -52,7 +53,9 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T Tenant savedTenant = tenantService.saveTenant(tenant, tenantId -> { installScripts.createDefaultRuleChains(tenantId); installScripts.createDefaultEdgeRuleChains(tenantId); - installScripts.createDefaultTenantDashboards(tenantId, null); + if (!isTestProfile()) { + installScripts.createDefaultTenantDashboards(tenantId, null); + } }); tenantProfileCache.evict(savedTenant.getId()); diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 999079e913..9db2a99e7f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -317,7 +317,8 @@ public class InstallScripts { @SneakyThrows public void loadSystemImages() { log.info("Loading system images..."); - Stream dashboardsFiles = Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)); + Stream dashboardsFiles = Stream.concat(Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR)), + Files.list(Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DASHBOARDS_DIR))); try (dashboardsFiles) { dashboardsFiles.forEach(file -> { try { From bc82e2084fa3c0eb81be7fc8f45b6cd0b1893e81 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 9 May 2024 12:09:58 +0300 Subject: [PATCH 089/121] Fix testUsageInfo --- .../java/org/thingsboard/server/controller/HomePageApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java index 31c82408c1..55af29ab0b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/HomePageApiTest.java @@ -92,7 +92,7 @@ public class HomePageApiTest extends AbstractControllerTest { @MockBean private SmsService smsService; - private static final int DEFAULT_DASHBOARDS_COUNT = 1; + private static final int DEFAULT_DASHBOARDS_COUNT = 0; //For system administrator @Test From 5dc91d857c7e066ea6321117bf30da8a99422f37 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 9 May 2024 12:22:07 +0300 Subject: [PATCH 090/121] fixed platform allowable values for oauth in swagger --- .../org/thingsboard/server/controller/OAuth2Controller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index c0a9d6d26f..325cc3b025 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -68,7 +68,7 @@ public class OAuth2Controller extends BaseController { @Parameter(description = "Platform type to search OAuth2 clients for which " + "the usage with this platform type is allowed in the settings. " + "If platform type is not one of allowable values - it will just be ignored", - schema = @Schema(allowableValues = "WEB, ANDROID, IOS")) + schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"})) @RequestParam(required = false) String platform) throws ThingsboardException { if (log.isDebugEnabled()) { log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); From 0bc04222ba91302efce90c62b849ed6c83729770 Mon Sep 17 00:00:00 2001 From: rusikv Date: Thu, 9 May 2024 16:44:38 +0300 Subject: [PATCH 091/121] UI: added mobile app qr code to sys admin home page --- ui-ngx/src/app/core/services/utils.service.ts | 2 +- .../mobile-app-qrcode-widget.component.scss | 6 + .../home-links/home-links-routing.module.ts | 40 ++- .../assets/dashboard/sys_admin_home_page.json | 229 +++++++++++++++--- .../dashboard/tenant_admin_home_page.json | 13 +- 5 files changed, 232 insertions(+), 58 deletions(-) diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index d7238c450d..097c3b8d49 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -30,7 +30,7 @@ import { guid, hashCode, isDefined, - isDefinedAndNotNull, isNotEmptyStr, + isDefinedAndNotNull, isString, isUndefined, objToBase64, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index 6d229abb0a..d00baa119e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -33,6 +33,7 @@ color: rgba(0, 0, 0, 0.76); @media #{$mat-md-lg} { + padding-bottom: 0; font-weight: 500; font-size: 14px; line-height: 20px; @@ -43,6 +44,11 @@ .tb-qrcode-label { font-size: 14px; color: rgba(0, 0, 0, 0.54); + + @media #{$mat-md-lg} { + font-size: 11px; + line-height: 16px; + } } .tb-qrcode { diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index 3b6b909225..9c6adf91ca 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -25,7 +25,11 @@ import { DashboardService } from '@core/http/dashboard.service'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { map } from 'rxjs/operators'; -import { getCurrentAuthUser, selectPersistDeviceStateToTelemetryAndMobileQrEnabled } from '@core/auth/auth.selectors'; +import { + getCurrentAuthUser, + selectMobileQrEnabled, + selectPersistDeviceStateToTelemetryAndMobileQrEnabled +} from '@core/auth/auth.selectors'; import { EntityKeyType } from '@shared/models/query/query.models'; import { ResourcesService } from '@core/services/resources.service'; @@ -45,20 +49,32 @@ const updateDeviceActivityKeyFilterIfNeeded = (store: Store, } } } - if (params.mobileQrEnabled) { - for (const widgetId of Object.keys(dashboard.configuration.widgets)) { - if (dashboard.configuration.widgets[widgetId].config.title === 'Select show mobile QR code') { - dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction = - (dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction as string) - .replace('\'${mobileQrEnabled}\'', String(params.mobileQrEnabled)); - } - } - } - return dashboard; + return params.mobileQrEnabled ? toggleMobileQRCodeDisplay(dashboard) : dashboard; }) )) ); +const toggleMobileQRCodeDisplayIfNeeded = (store: Store, + dashboard$: Observable): Observable => + store.pipe(select(selectMobileQrEnabled)).pipe( + mergeMap((mobileQrEnabled) => dashboard$.pipe( + map((dashboard) => { + return mobileQrEnabled ? toggleMobileQRCodeDisplay(dashboard) : dashboard; + }) + )) + ); + +const toggleMobileQRCodeDisplay = (dashboard: HomeDashboard) => { + for (const widgetId of Object.keys(dashboard.configuration.widgets)) { + if (dashboard.configuration.widgets[widgetId].config.title === 'Select show mobile QR code') { + dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction = + (dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction as string) + .replace('\'${mobileQrEnabled}\'', String(true)); + } + } + return dashboard; +} + export const homeDashboardResolver: ResolveFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, @@ -73,7 +89,7 @@ export const homeDashboardResolver: ResolveFn = ( const authority = getCurrentAuthUser(store).authority; switch (authority) { case Authority.SYS_ADMIN: - dashboard$ = resourcesService.loadJsonResource(sysAdminHomePageJson); + dashboard$ = toggleMobileQRCodeDisplayIfNeeded(store, resourcesService.loadJsonResource(sysAdminHomePageJson)); break; case Authority.TENANT_ADMIN: dashboard$ = updateDeviceActivityKeyFilterIfNeeded(store, resourcesService.loadJsonResource(tenantAdminHomePageJson)); diff --git a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json index a47103059e..43998afbcb 100644 --- a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json @@ -377,38 +377,6 @@ "id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4", "typeFullFqn": "system.home_page_widgets.documentation_links" }, - "a23185ad-dc46-806c-0e50-5b21fb080ace": { - "type": "static", - "sizeX": 7.5, - "sizeY": 6.5, - "config": { - "datasources": [], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, - "showTitle": false, - "backgroundColor": "rgb(255, 255, 255)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "16px", - "settings": { - "columns": 3 - }, - "title": "Getting started", - "dropShadow": false, - "enableFullscreen": false, - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "showLegend": false - }, - "row": 0, - "col": 0, - "id": "a23185ad-dc46-806c-0e50-5b21fb080ace", - "typeFullFqn": "system.home_page_widgets.getting_started" - }, "b35fa1b0-7f95-0b23-fade-d066653a40e1": { "type": "latest", "sizeX": 5, @@ -2640,6 +2608,152 @@ "row": 0, "col": 0, "id": "b9174a99-2a00-3dbe-0eae-927fd8506345" + }, + "c02d59a0-c979-5ec6-7f58-a5d89ff66236": { + "type": "static", + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "datasources": [], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "16px", + "settings": { + "columns": 3 + }, + "title": "Getting started", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "showLegend": false + }, + "row": 0, + "col": 0, + "id": "c02d59a0-c979-5ec6-7f58-a5d89ff66236", + "typeFullFqn": "system.home_page_widgets.getting_started" + }, + "53d5f2dd-d432-5362-745f-63bf4487dc96": { + "typeFullFqn": "system.home_page_widgets.home_page_mobile_app_qr_code", + "type": "static", + "sizeX": 6, + "sizeY": 3, + "config": { + "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": {}, + "title": "Mobile app QR code", + "dropShadow": false, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "53d5f2dd-d432-5362-745f-63bf4487dc96" + }, + "0d34fa85-914f-30de-e3dd-7b7879f8d593": { + "typeFullFqn": "system.cards.markdown_card", + "type": "latest", + "sizeX": 5, + "sizeY": 3.5, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1715012840937, + "endTimeMs": 1715099240937 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "useMarkdownTextFunction": true, + "markdownTextPattern": "### Markdown/HTML card\n - **Current entity**: ${entityName}.\n - **Current value**: ${Random}.", + "markdownTextFunction": "let html = '
';\nlet supportMobile = '${mobileQrEnabled}' || false;\nif (supportMobile && typeof supportMobile === 'boolean') {\n ctx.dashboardWidget.widgetLayout.mobileHide = false;\n html += '';\n} else {\n ctx.dashboardWidget.widgetLayout.mobileHide = true;\n html += '';\n}\nhtml += '
';\nreturn html;", + "applyDefaultMarkdownStyle": false, + "markdownCss": "" + }, + "title": "Select show mobile QR code", + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": { + "border": "none", + "boxShadow": "none", + "padding": 0 + }, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "showLegend": false, + "useDashboardTimewindow": true, + "displayTimewindow": true, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "" + }, + "row": 0, + "col": 0, + "id": "0d34fa85-914f-30de-e3dd-7b7879f8d593" } }, "states": { @@ -2695,13 +2809,6 @@ "col": 0, "mobileHide": true }, - "a23185ad-dc46-806c-0e50-5b21fb080ace": { - "sizeX": 35, - "sizeY": 58, - "row": 0, - "col": 85, - "mobileHide": true - }, "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8": { "sizeX": 44, "sizeY": 15, @@ -2709,6 +2816,14 @@ "mobileHeight": 7, "row": 0, "col": 0 + }, + "0d34fa85-914f-30de-e3dd-7b7879f8d593": { + "sizeX": 35, + "sizeY": 58, + "row": 0, + "col": 85, + "mobileHide": false, + "mobileHeight": 7 } }, "gridSettings": { @@ -2936,6 +3051,40 @@ } } } + }, + "mobile_app_qr_code": { + "name": "Mobile App QR code", + "root": false, + "layouts": { + "main": { + "widgets": { + "c02d59a0-c979-5ec6-7f58-a5d89ff66236": { + "sizeX": 120, + "sizeY": 45, + "row": 0, + "col": 0, + "mobileHide": true + }, + "53d5f2dd-d432-5362-745f-63bf4487dc96": { + "sizeX": 120, + "sizeY": 17, + "row": 45, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 120, + "margin": 12, + "outerMargin": false, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 20 + } + } + } } }, "entityAliases": { @@ -3051,4 +3200,4 @@ } }, "name": "System Administrator Home Page" -} \ No newline at end of file +} diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index b9cba6eb08..cb42df7839 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1150,7 +1150,7 @@ "settings": { "useMarkdownTextFunction": true, "markdownTextPattern": "### Markdown/HTML card\n - **Current entity**: ${entityName}.\n - **Current value**: ${Random}.", - "markdownTextFunction": "let html = '
';\nlet supportMobile = '${mobileQrEnabled}' || false;\nif (supportMobile && typeof supportMobile === 'boolean') {\n html += '';\n} else {\n html += '';\n}\nhtml += '
';\nreturn html;", + "markdownTextFunction": "let html = '
';\nlet supportMobile = '${mobileQrEnabled}' || false;\nif (supportMobile && typeof supportMobile === 'boolean') {\n ctx.dashboardWidget.widgetLayout.mobileHide = false;\n html += '';\n} else {\n ctx.dashboardWidget.widgetLayout.mobileHide = true;\n html += '';\n}\nhtml += '
';\nreturn html;", "applyDefaultMarkdownStyle": false, "markdownCss": "" }, @@ -1214,7 +1214,7 @@ "typeFullFqn": "system.home_page_widgets.getting_started" }, "8c5207d8-7103-4dc2-db48-d61c0c8c61fd": { - "typeFullFqn": "system.home_page_widgets.home_mobile_app_qr_code", + "typeFullFqn": "system.home_page_widgets.home_page_mobile_app_qr_code", "type": "static", "sizeX": 6, "sizeY": 3, @@ -1320,7 +1320,9 @@ "sizeX": 35, "sizeY": 58, "row": 0, - "col": 85 + "col": 85, + "mobileHide": false, + "mobileHeight": 10 } }, "gridSettings": { @@ -1401,7 +1403,8 @@ "sizeX": 120, "sizeY": 45, "row": 0, - "col": 0 + "col": 0, + "mobileHide": true }, "8c5207d8-7103-4dc2-db48-d61c0c8c61fd": { "sizeX": 120, @@ -1558,4 +1561,4 @@ } }, "name": "Tenant Administrator Home Page" -} \ No newline at end of file +} From 14ebfe7975e5e8211240d3c9b08ee82fa862a905 Mon Sep 17 00:00:00 2001 From: rusikv Date: Thu, 9 May 2024 18:25:24 +0300 Subject: [PATCH 092/121] UI: mobile app qrcode widgets update --- .../system/widget_bundles/home_page_widgets.json | 2 +- .../widget_types/home_mobile_app_qr_code.json | 16 +++++++++++----- .../system/widget_types/mobile_app_qr_code.json | 9 +++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index 0af30e4df2..cb8d0ed915 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -14,6 +14,6 @@ "home_page_widgets.documentation_links", "home_page_widgets.dashboards", "home_page_widgets.usage_info", - "home_page_widgets.home_mobile_app_qr_code" + "home_page_widgets.home_page_mobile_app_qr_code" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json index 88109fc902..0e88188e55 100644 --- a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json @@ -1,9 +1,9 @@ { - "fqn": "home_page_widgets.home_mobile_app_qr_code", - "name": "Mobile app QR code", + "fqn": "home_page_widgets.home_page_mobile_app_qr_code", + "name": "Home page mobile app QR code", "deprecated": false, "image": null, - "description": null, + "description": "Enables users to open the ThingsBoard mobile app by scanning a QR code from Home page.", "descriptor": { "type": "static", "sizeX": 6, @@ -15,7 +15,13 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "", - "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\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"Mobile app QR code\",\"dropShadow\":true}" + "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\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"Home page mobile app QR code\",\"dropShadow\":false,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" }, - "tags": null + "tags": [ + "ios", + "android", + "qr code", + "mobile application", + "home page" + ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index 82ce9025ed..d84c83bfb6 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -3,7 +3,7 @@ "name": "Mobile app QR code", "deprecated": false, "image": null, - "description": null, + "description": "Enables users to open the ThingsBoard mobile app by scanning the QR code from dashboard.", "descriptor": { "type": "static", "sizeX": 7.5, @@ -17,5 +17,10 @@ "settingsDirective": "tb-mobile-app-qr-code-widget-settings", "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\":{\"useDefaultApp\":true,\"androidConfig\":{\"enabled\":true,\"appPackage\":\"\",\"sha256CertFingerprints\":\"\"},\"iosConfig\":{\"enabled\":true,\"appId\":\"\"},\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgeStyle\":\"ORIGINAL\",\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"}},\"title\":\"Mobile app QR code\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" }, - "tags": null + "tags": [ + "ios", + "android", + "qr code", + "mobile application" + ] } \ No newline at end of file From 21872468e2548674f99f43f276f291fccaf1e871 Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 10 May 2024 11:08:57 +0300 Subject: [PATCH 093/121] UI: added preview images for mobile app qr code widgets --- .../widget_types/home_mobile_app_qr_code.json | 2 +- .../widget_types/mobile_app_qr_code.json | 2 +- .../lib/mobile-app-qrcode-widget.component.ts | 19 +++++++++---------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json index 0e88188e55..e8e21f5d89 100644 --- a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json @@ -2,7 +2,7 @@ "fqn": "home_page_widgets.home_page_mobile_app_qr_code", "name": "Home page mobile app QR code", "deprecated": false, - "image": null, + "image": "tb-image:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0iI2ZmZiIgcng9IjQiLz48cmVjdCB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE1OSIgeD0iLjUiIHk9Ii41IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIuMSIgcng9IjMuNSIvPjxnIGNsaXAtcGF0aD0idXJsKCNhKSI+PHJlY3Qgd2lkdGg9IjEzNyIgaGVpZ2h0PSIxMzciIHg9IjMxLjUiIHk9IjExLjUiIGZpbGw9IiNmZmYiIHJ4PSI4LjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTY4LjUgMTEuNWgtMTM3djEzN2gxMzd2LTEzN1oiLz48bWFzayBpZD0iYiIgd2lkdGg9IjEyNCIgaGVpZ2h0PSIxMjQiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDUyLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTQyLjcgNjAuOWgtNC4zVjY1aDQuM1Y2MVpNNDIuNyA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNNDIuNyA5NC45aC00LjNWOTloNC4zVjk1Wk00NyA1Ni42aC00LjNWNjFoNC4ydi00LjNaTTQ3IDgyLjFoLTQuM3Y0LjNoNC4yVjgyWk00NyA4Ni40aC00LjN2NC4yaDQuMnYtNC4yWk00NyA5NC45aC00LjNWOTloNC4yVjk1Wk00NyAxMDMuNGgtNC4zdjQuMmg0LjJ2LTQuMlpNNTEuMiA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDYwLjloLTQuM1Y2NWg0LjNWNjFaTTUxLjIgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDc3LjloLTQuM1Y4Mmg0LjNWNzhaTTUxLjIgODIuMWgtNC4zdjQuM2g0LjNWODJaTTUxLjIgOTAuNmgtNC4zVjk1aDQuM3YtNC4zWk01MS4yIDk5LjFoLTQuM3Y0LjNoNC4zVjk5Wk01NS40IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk01NS40IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWk01OS43IDY1LjFoLTQuM3Y0LjNoNC4zVjY1Wk01OS43IDY5LjRoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgODYuNGgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTkuNyA5MC42aC00LjNWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNNjMuOSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk02My45IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTYzLjkgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNjMuOSA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNNjguMSA1Mi40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA2MC45SDY0VjY1aDQuMlY2MVpNNjguMSA2OS40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA3Ny45SDY0VjgyaDQuMlY3OFpNNjguMSA4Ni40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA5NC45SDY0Vjk5aDQuMlY5NVpNNjguMSAxMDMuNEg2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcyLjQgNTIuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNjkuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNzcuOUg2OFY4Mmg0LjNWNzhaTTcyLjQgODIuMUg2OHY0LjNoNC4zVjgyWk03Mi40IDg2LjRINjh2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Mi40IDkwLjZINjhWOTVoNC4zdi00LjNaTTcyLjQgOTkuMUg2OHY0LjNoNC4zVjk5Wk03Mi40IDEwMy40SDY4djQuMmg0LjN2LTQuMlpNNzYuNiAyMi43aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDMxLjJoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNzYuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNNzYuNiA0OC4xaC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDYwLjloLTQuMlY2NWg0LjJWNjFaTTc2LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTc2LjYgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNzYuNiA5NC45aC00LjJWOTloNC4yVjk1Wk03Ni42IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDExNi4xaC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyOC44aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk04MC45IDE4LjRoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSAyN2gtNC4zdjQuMkg4MXYtNC4zWk04MC45IDMxLjJoLTQuM3Y0LjJIODF2LTQuMlpNODAuOSAzNS40aC00LjN2NC4zSDgxdi00LjNaTTgwLjkgMzkuN2gtNC4zdjQuMkg4MXYtNC4yWk04MC45IDQ4LjFoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSA2MC45aC00LjNWNjVIODFWNjFaTTgwLjkgNjUuMWgtNC4zdjQuM0g4MVY2NVpNODAuOSA3My42aC00LjNWNzhIODF2LTQuM1pNODAuOSA4Mi4xaC00LjN2NC4zSDgxVjgyWk04MC45IDk0LjloLTQuM1Y5OUg4MVY5NVpNODAuOSA5OS4xaC00LjN2NC4zSDgxVjk5Wk04MC45IDEwMy40aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTExLjloLTQuM3Y0LjJIODF2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODAuOSAxMTYuMWgtNC4zdjQuMkg4MXYtNC4yWk04MC45IDEyNC42aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTM3LjNoLTQuM3Y0LjNIODF2LTQuM1pNODUuMSAxOC40SDgxdjQuM0g4NXYtNC4zWk04NS4xIDMxLjJIODF2NC4ySDg1di00LjJaTTg1LjEgMzUuNEg4MXY0LjNIODV2LTQuM1pNODUuMSA0My45SDgxdjQuMkg4NVY0NFpNODUuMSA1Ni42SDgxVjYxSDg1di00LjNaTTg1LjEgNzMuNkg4MVY3OEg4NXYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik04NS4xIDc3LjlIODFWODJIODVWNzhaTTg1LjEgODIuMUg4MXY0LjNIODVWODJaTTg1LjEgODYuNEg4MXY0LjJIODV2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODUuMSA5MC42SDgxVjk1SDg1di00LjNaTTg1LjEgOTkuMUg4MXY0LjNIODVWOTlaTTg1LjEgMTExLjlIODF2NC4ySDg1di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTg1LjEgMTE2LjFIODF2NC4ySDg1di00LjJaTTg1LjEgMTMzSDgxdjQuM0g4NXYtNC4yWk04OS40IDIyLjdIODV2NC4yaDQuM3YtNC4yWk04OS40IDMxLjJIODV2NC4yaDQuM3YtNC4yWk04OS40IDM1LjRIODV2NC4zaDQuM3YtNC4zWk04OS40IDQ4LjFIODV2NC4zaDQuM3YtNC4zWk04OS40IDUyLjRIODV2NC4yaDQuM3YtNC4yWk04OS40IDU2LjZIODVWNjFoNC4zdi00LjNaTTg5LjQgNjUuMUg4NXY0LjNoNC4zVjY1Wk04OS40IDEwMy40SDg1djQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMDcuNkg4NXY0LjNoNC4zdi00LjNaTTg5LjQgMTExLjlIODV2NC4yaDQuM3YtNC4yWk04OS40IDEyOC44SDg1djQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMzNIODV2NC4zaDQuM3YtNC4yWk05My42IDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTkzLjYgMjIuN2gtNC4ydjQuMmg0LjJ2LTQuMlpNOTMuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNOTMuNiAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk05My42IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk05My42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNOTMuNiA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNOTMuNiA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDczLjZoLTQuMlY3OGg0LjJ2LTQuM1pNOTMuNiA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNOTMuNiA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDkwLjZoLTQuMlY5NWg0LjJ2LTQuM1pNOTMuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOTMuNiAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNOTcuOSAxOC40aC00LjN2NC4zSDk4di00LjNaTTk3LjkgMjIuN2gtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDM1LjRoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSA0OC4xaC00LjN2NC4zSDk4di00LjNaTTk3LjkgNTIuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDYwLjloLTQuM1Y2NUg5OFY2MVpNOTcuOSA2NS4xaC00LjN2NC4zSDk4VjY1Wk05Ny45IDczLjZoLTQuM1Y3OEg5OHYtNC4zWk05Ny45IDgyLjFoLTQuM3Y0LjNIOThWODJaTTk3LjkgODYuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDk0LjloLTQuM1Y5OUg5OFY5NVpNOTcuOSA5OS4xaC00LjN2NC4zSDk4Vjk5Wk05Ny45IDEwMy40aC00LjN2NC4ySDk4di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTk3LjkgMTA3LjZoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSAxMTYuMWgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDEyNC42aC00LjN2NC4ySDk4di00LjJaTTk3LjkgMTMzaC00LjN2NC4zSDk4di00LjJaTTEwMi4xIDIyLjdIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA0My45SDk4djQuMmg0LjJWNDRaTTEwMi4xIDUyLjRIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA2OS40SDk4djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgNzMuNkg5OFY3OGg0LjJ2LTQuM1pNMTAyLjEgOTAuNkg5OFY5NWg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgOTQuOUg5OFY5OWg0LjJWOTVaTTEwMi4xIDEwMy40SDk4djQuMmg0LjJ2LTQuMlpNMTAyLjEgMTMzSDk4djQuM2g0LjJ2LTQuMlpNMTAyLjEgMTM3LjNIOTh2NC4zaDQuMnYtNC4zWk0xMDYuNCAxOC40SDEwMnY0LjNoNC4zdi00LjNaTTEwNi40IDIyLjdIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgNDguMUgxMDJ2NC4zaDQuM3YtNC4zWk0xMDYuNCA2MC45SDEwMlY2NWg0LjNWNjFaTTEwNi40IDczLjZIMTAyVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMDYuNCA3Ny45SDEwMlY4Mmg0LjNWNzhaTTEwNi40IDgyLjFIMTAydjQuM2g0LjNWODJaTTEwNi40IDg2LjRIMTAydjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTA2LjQgOTAuNkgxMDJWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEwNi40IDk0LjlIMTAyVjk5aDQuM1Y5NVpNMTA2LjQgMTAzLjRIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTE2LjFIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTMzSDEwMnY0LjNoNC4zdi00LjJaTTEwNi40IDEzNy4zSDEwMnY0LjNoNC4zdi00LjNaTTExMC42IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMTAuNiAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTAuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTEwLjYgNDguMWgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTEwLjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWk0xMTAuNiA5MC42aC00LjJWOTVoNC4ydi00LjNaTTExMC42IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk0xMTAuNiAxMDcuNmgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTExMC42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTAuNiAxMzNoLTQuMnY0LjNoNC4ydi00LjJaTTExMC42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTQuOSAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA0OC4xaC00LjN2NC4zaDQuM3YtNC4zWk0xMTQuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTE0LjkgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSA3Ny45aC00LjNWODJoNC4zVjc4Wk0xMTQuOSA4Mi4xaC00LjN2NC4zaDQuM1Y4MlpNMTE0LjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgOTkuMWgtNC4zdjQuM2g0LjNWOTlaTTExNC45IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSAxMTYuMWgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgMTMzaC00LjN2NC4zaDQuM3YtNC4yWk0xMTQuOSAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNMTE5LjEgMTguNEgxMTV2NC4zaDQuMnYtNC4zWk0xMTkuMSAzNS40SDExNXY0LjNoNC4ydi00LjNaTTExOS4xIDM5LjdIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNDMuOUgxMTV2NC4yaDQuMlY0NFpNMTE5LjEgNjUuMUgxMTV2NC4zaDQuMlY2NVpNMTE5LjEgNjkuNEgxMTV2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTkuMSA3My42SDExNVY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNzcuOUgxMTVWODJoNC4yVjc4Wk0xMTkuMSA4Mi4xSDExNXY0LjNoNC4yVjgyWk0xMTkuMSA4Ni40SDExNXY0LjJoNC4ydi00LjJaTTExOS4xIDk0LjlIMTE1Vjk5aDQuMlY5NVpNMTE5LjEgMTAzLjRIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgMTA3LjZIMTE1djQuM2g0LjJ2LTQuM1pNMTE5LjEgMTE2LjFIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTI0LjZIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTMzSDExNXY0LjNoNC4ydi00LjJaTTExOS4xIDEzNy4zSDExNXY0LjNoNC4ydi00LjNaTTEyMy40IDMxLjJIMTE5djQuMmg0LjN2LTQuMlpNMTIzLjQgMzUuNEgxMTl2NC4zaDQuM3YtNC4zWk0xMjMuNCA1Ni42SDExOVY2MWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgNjAuOUgxMTlWNjVoNC4zVjYxWk0xMjMuNCA5MC42SDExOVY5NWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgOTQuOUgxMTlWOTloNC4zVjk1Wk0xMjMuNCA5OS4xSDExOXY0LjNoNC4zVjk5Wk0xMjMuNCAxMjAuM0gxMTl2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMjMuNCAxMjQuNkgxMTl2NC4yaDQuM3YtNC4yWk0xMjcuNiAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNMTI3LjYgMzEuMmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTI3LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgNDMuOWgtNC4ydjQuMmg0LjJWNDRaTTEyNy42IDQ4LjFoLTQuMnY0LjNoNC4ydi00LjNaTTEyNy42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTI3LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTEyNy42IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDc3LjloLTQuMlY4Mmg0LjJWNzhaTTEyNy42IDgyLjFoLTQuMnY0LjNoNC4yVjgyWk0xMjcuNiA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTI3LjYgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyNy42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTMzaC00LjJ2NC4zaDQuMnYtNC4yWk0xMzEuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMzEuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTMxLjkgNzcuOWgtNC4zVjgyaDQuM1Y3OFpNMTMxLjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTMxLjkgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNMTMxLjkgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTEzMS45IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzEuOSAxMzNoLTQuM3Y0LjNoNC4zdi00LjJaTTEzMS45IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzYuMSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA1Ni42aC00LjJWNjFoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDYwLjloLTQuMlY2NWg0LjJWNjFaTTEzNi4xIDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk0xMzYuMSA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA5NC45aC00LjJWOTloNC4yVjk1Wk0xMzYuMSAxMDMuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTExLjloLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSAxMjQuNmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTE0MC4zIDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTQwLjMgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTE0MC4zIDczLjZoLTQuMlY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQwLjMgNzcuOWgtNC4yVjgyaDQuMlY3OFpNMTQwLjMgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQwLjMgOTQuOWgtNC4yVjk5aDQuMlY5NVpNMTQwLjMgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xNDAuMyAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ0LjYgNjAuOWgtNC4zVjY1aDQuM1Y2MVoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ0LjYgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDEwNy42aC00LjN2NC4zaDQuM3YtNC4zWk0xNDQuNiAxMTEuOWgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMTE2LjFoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNDQuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ4LjggNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggNjAuOWgtNC4yVjY1aDQuMlY2MVpNMTQ4LjggNzMuNmgtNC4yVjc4aDQuMnYtNC4zWk0xNDguOCA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNMTQ4LjggODYuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ4LjggOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNDguOCA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTQ4LjggMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ4LjggMTM3LjNoLTQuMnY0LjNoNC4ydi00LjNaTTE1MyA1Ni42aC00LjJWNjFoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE1MyA2MC45aC00LjJWNjVoNC4zVjYxWk0xNTMgNjUuMWgtNC4ydjQuM2g0LjNWNjVaTTE1MyA5NC45aC00LjJWOTloNC4zVjk1Wk0xNTMgOTkuMWgtNC4ydjQuM2g0LjNWOTlaTTE1MyAxMDMuNGgtNC4ydjQuMmg0LjN2LTQuMlpNMTUzIDEyMC4zaC00LjJ2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTMgMTI0LjZoLTQuMnY0LjJoNC4zdi00LjJaTTE1MyAxMzcuM2gtNC4ydjQuM2g0LjN2LTQuM1pNMTU3LjMgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTcuMyA2MC45aC00LjJWNjVoNC4yVjYxWk0xNTcuMyA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNMTU3LjMgOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNTcuMyAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTE1Ny4zIDEzM2gtNC4ydjQuM2g0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTYxLjYgNTYuNmgtNC4zVjYxaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA2MC45aC00LjNWNjVoNC4zVjYxWk0xNjEuNiA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTYxLjYgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWk0xNjEuNiA5MC42aC00LjNWOTVoNC4zdi00LjNaTTE2MS42IDEwMy40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMDcuNmgtNC4zdjQuM2g0LjN2LTQuM1pNMTYxLjYgMTExLjloLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTYxLjYgMTMzaC00LjN2NC4zaDQuM3YtNC4yWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2IpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTYxLjYgMTguNEgzOC40djEyMy4yaDEyMy4yVjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJjIiB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMjIuN2gtNC4zdjQuMmg0LjN2LTQuMlpNNDIuNyAyN2gtNC4zdjQuMmg0LjN2LTQuM1pNNDIuNyAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMzkuN2gtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDIuNyA0My45aC00LjN2NC4yaDQuM1Y0NFpNNDcgMTguNGgtNC4zdjQuM2g0LjJ2LTQuM1pNNDcgNDMuOWgtNC4zdjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTguNGgtNC4zdjQuM2g0LjN2LTQuM1pNNTEuMiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk01NS40IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk01OS43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTU5LjcgNDMuOWgtNC4zdjQuMmg0LjNWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYzLjkgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNNjMuOSA0My45aC00LjJ2NC4yaDQuMlY0NFpNNjguMSAxOC40SDY0djQuM2g0LjJ2LTQuM1pNNjguMSAyMi43SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAyN0g2NHY0LjJoNC4ydi00LjNaTTY4LjEgMzEuMkg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMzUuNEg2NHY0LjNoNC4ydi00LjNaTTY4LjEgMzkuN0g2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgNDMuOUg2NHY0LjJoNC4yVjQ0WiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2MpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNjguMSAxOC40SDM4LjR2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJkIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjQ2IiB5PSIyNiIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDI3aC00LjN2NC4yaDQuM3YtNC4zWk01MS4yIDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMzUuNGgtNC4zdjQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNNTUuNCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDM1LjRoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTU5LjcgMzEuMmgtNC4zdjQuMmg0LjN2LTQuMlpNNTkuNyAzNS40aC00LjN2NC4zaDQuM3YtNC4zWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2QpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNTkuNyAyN0g0Ni45djEyLjdoMTIuOFYyNi45WiIvPjwvZz48bWFzayBpZD0iZSIgd2lkdGg9IjMxIiBoZWlnaHQ9IjMxIiB4PSIxMzEiIHk9IjE4IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTEzNi4xIDIyLjdoLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMzYuMSAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMzYuMSAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTQwLjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQwLjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0NC42IDQzLjloLTQuM3Y0LjJoNC4zVjQ0Wk0xNDguOCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTUzIDE4LjRoLTQuMnY0LjNoNC4zdi00LjNaTTE1MyA0My45aC00LjJ2NC4yaDQuM1Y0NFpNMTU3LjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTU3LjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2MS42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE2MS42IDIyLjdoLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDI3aC00LjN2NC4yaDQuM3YtNC4zWk0xNjEuNiAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xNjEuNiAzNS40aC00LjN2NC4zaDQuM3YtNC4zWk0xNjEuNiAzOS43aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48L21hc2s+PGcgbWFzaz0idXJsKCNlKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE2MS42IDE4LjRoLTI5Ljd2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJmIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjE0MCIgeT0iMjYiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHN0eWxlPSJtYXNrLXR5cGU6bHVtaW5hbmNlIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTE0NC42IDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0OC44IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xNDguOCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNTMgMjdoLTQuMnY0LjJoNC4zdi00LjNaTTE1MyAzMS4yaC00LjJ2NC4yaDQuM3YtNC4yWk0xNTMgMzUuNGgtNC4ydjQuM2g0LjN2LTQuM1oiLz48L21hc2s+PGcgbWFzaz0idXJsKCNmKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE1MyAyN2gtMTIuN3YxMi43aDEyLjhWMjYuOVoiLz48L2c+PG1hc2sgaWQ9ImciIHdpZHRoPSIzMSIgaGVpZ2h0PSIzMSIgeD0iMzgiIHk9IjExMSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExNi4xaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEyNC42aC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyOC44aC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEzM2gtNC4zdjQuM2g0LjN2LTQuMlpNNDIuNyAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNNDcgMTExLjloLTQuM3Y0LjJoNC4ydi00LjJaTTQ3IDEzNy4zaC00LjN2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01MS4yIDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01NS40IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk01OS43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01OS43IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02My45IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk02OC4xIDExMS45SDY0djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNjguMSAxMTYuMUg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMTIwLjNINjR2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02OC4xIDEyNC42SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAxMjguOEg2NHY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgMTMzSDY0djQuM2g0LjJ2LTQuMlpNNjguMSAxMzcuM0g2NHY0LjNoNC4ydi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjZykiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik02OC4xIDExMS45SDM4LjR2MjkuN2gyOS43di0yOS43WiIvPjwvZz48bWFzayBpZD0iaCIgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiB4PSI0NiIgeT0iMTIwIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjaCkiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik01OS43IDEyMC4zSDQ2Ljl2MTIuOGgxMi44di0xMi44WiIvPjwvZz48L2c+PGRlZnM+PGNsaXBQYXRoIGlkPSJhIj48cmVjdCB3aWR0aD0iMTM3IiBoZWlnaHQ9IjEzNyIgeD0iMzEuNSIgeT0iMTEuNSIgZmlsbD0iI2ZmZiIgcng9IjguMiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg==", "description": "Enables users to open the ThingsBoard mobile app by scanning a QR code from Home page.", "descriptor": { "type": "static", diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index d84c83bfb6..223678bbdd 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -2,7 +2,7 @@ "fqn": "cards.mobile_app_qr_code", "name": "Mobile app QR code", "deprecated": false, - "image": null, + "image": "tb-image:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0iI2ZmZiIgcng9IjQiLz48cmVjdCB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE1OSIgeD0iLjUiIHk9Ii41IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIuMSIgcng9IjMuNSIvPjxnIGNsaXAtcGF0aD0idXJsKCNhKSI+PHJlY3Qgd2lkdGg9IjEzNyIgaGVpZ2h0PSIxMzciIHg9IjMxLjUiIHk9IjExLjUiIGZpbGw9IiNmZmYiIHJ4PSI4LjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTY4LjUgMTEuNWgtMTM3djEzN2gxMzd2LTEzN1oiLz48bWFzayBpZD0iYiIgd2lkdGg9IjEyNCIgaGVpZ2h0PSIxMjQiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDUyLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTQyLjcgNjAuOWgtNC4zVjY1aDQuM1Y2MVpNNDIuNyA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNNDIuNyA5NC45aC00LjNWOTloNC4zVjk1Wk00NyA1Ni42aC00LjNWNjFoNC4ydi00LjNaTTQ3IDgyLjFoLTQuM3Y0LjNoNC4yVjgyWk00NyA4Ni40aC00LjN2NC4yaDQuMnYtNC4yWk00NyA5NC45aC00LjNWOTloNC4yVjk1Wk00NyAxMDMuNGgtNC4zdjQuMmg0LjJ2LTQuMlpNNTEuMiA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDYwLjloLTQuM1Y2NWg0LjNWNjFaTTUxLjIgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDc3LjloLTQuM1Y4Mmg0LjNWNzhaTTUxLjIgODIuMWgtNC4zdjQuM2g0LjNWODJaTTUxLjIgOTAuNmgtNC4zVjk1aDQuM3YtNC4zWk01MS4yIDk5LjFoLTQuM3Y0LjNoNC4zVjk5Wk01NS40IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk01NS40IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWk01OS43IDY1LjFoLTQuM3Y0LjNoNC4zVjY1Wk01OS43IDY5LjRoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgODYuNGgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTkuNyA5MC42aC00LjNWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNNjMuOSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk02My45IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTYzLjkgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNjMuOSA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNNjguMSA1Mi40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA2MC45SDY0VjY1aDQuMlY2MVpNNjguMSA2OS40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA3Ny45SDY0VjgyaDQuMlY3OFpNNjguMSA4Ni40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA5NC45SDY0Vjk5aDQuMlY5NVpNNjguMSAxMDMuNEg2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcyLjQgNTIuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNjkuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNzcuOUg2OFY4Mmg0LjNWNzhaTTcyLjQgODIuMUg2OHY0LjNoNC4zVjgyWk03Mi40IDg2LjRINjh2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Mi40IDkwLjZINjhWOTVoNC4zdi00LjNaTTcyLjQgOTkuMUg2OHY0LjNoNC4zVjk5Wk03Mi40IDEwMy40SDY4djQuMmg0LjN2LTQuMlpNNzYuNiAyMi43aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDMxLjJoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNzYuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNNzYuNiA0OC4xaC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDYwLjloLTQuMlY2NWg0LjJWNjFaTTc2LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTc2LjYgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNzYuNiA5NC45aC00LjJWOTloNC4yVjk1Wk03Ni42IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDExNi4xaC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyOC44aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk04MC45IDE4LjRoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSAyN2gtNC4zdjQuMkg4MXYtNC4zWk04MC45IDMxLjJoLTQuM3Y0LjJIODF2LTQuMlpNODAuOSAzNS40aC00LjN2NC4zSDgxdi00LjNaTTgwLjkgMzkuN2gtNC4zdjQuMkg4MXYtNC4yWk04MC45IDQ4LjFoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSA2MC45aC00LjNWNjVIODFWNjFaTTgwLjkgNjUuMWgtNC4zdjQuM0g4MVY2NVpNODAuOSA3My42aC00LjNWNzhIODF2LTQuM1pNODAuOSA4Mi4xaC00LjN2NC4zSDgxVjgyWk04MC45IDk0LjloLTQuM1Y5OUg4MVY5NVpNODAuOSA5OS4xaC00LjN2NC4zSDgxVjk5Wk04MC45IDEwMy40aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTExLjloLTQuM3Y0LjJIODF2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODAuOSAxMTYuMWgtNC4zdjQuMkg4MXYtNC4yWk04MC45IDEyNC42aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTM3LjNoLTQuM3Y0LjNIODF2LTQuM1pNODUuMSAxOC40SDgxdjQuM0g4NXYtNC4zWk04NS4xIDMxLjJIODF2NC4ySDg1di00LjJaTTg1LjEgMzUuNEg4MXY0LjNIODV2LTQuM1pNODUuMSA0My45SDgxdjQuMkg4NVY0NFpNODUuMSA1Ni42SDgxVjYxSDg1di00LjNaTTg1LjEgNzMuNkg4MVY3OEg4NXYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik04NS4xIDc3LjlIODFWODJIODVWNzhaTTg1LjEgODIuMUg4MXY0LjNIODVWODJaTTg1LjEgODYuNEg4MXY0LjJIODV2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODUuMSA5MC42SDgxVjk1SDg1di00LjNaTTg1LjEgOTkuMUg4MXY0LjNIODVWOTlaTTg1LjEgMTExLjlIODF2NC4ySDg1di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTg1LjEgMTE2LjFIODF2NC4ySDg1di00LjJaTTg1LjEgMTMzSDgxdjQuM0g4NXYtNC4yWk04OS40IDIyLjdIODV2NC4yaDQuM3YtNC4yWk04OS40IDMxLjJIODV2NC4yaDQuM3YtNC4yWk04OS40IDM1LjRIODV2NC4zaDQuM3YtNC4zWk04OS40IDQ4LjFIODV2NC4zaDQuM3YtNC4zWk04OS40IDUyLjRIODV2NC4yaDQuM3YtNC4yWk04OS40IDU2LjZIODVWNjFoNC4zdi00LjNaTTg5LjQgNjUuMUg4NXY0LjNoNC4zVjY1Wk04OS40IDEwMy40SDg1djQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMDcuNkg4NXY0LjNoNC4zdi00LjNaTTg5LjQgMTExLjlIODV2NC4yaDQuM3YtNC4yWk04OS40IDEyOC44SDg1djQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMzNIODV2NC4zaDQuM3YtNC4yWk05My42IDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTkzLjYgMjIuN2gtNC4ydjQuMmg0LjJ2LTQuMlpNOTMuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNOTMuNiAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk05My42IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk05My42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNOTMuNiA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNOTMuNiA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDczLjZoLTQuMlY3OGg0LjJ2LTQuM1pNOTMuNiA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNOTMuNiA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDkwLjZoLTQuMlY5NWg0LjJ2LTQuM1pNOTMuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOTMuNiAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNOTcuOSAxOC40aC00LjN2NC4zSDk4di00LjNaTTk3LjkgMjIuN2gtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDM1LjRoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSA0OC4xaC00LjN2NC4zSDk4di00LjNaTTk3LjkgNTIuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDYwLjloLTQuM1Y2NUg5OFY2MVpNOTcuOSA2NS4xaC00LjN2NC4zSDk4VjY1Wk05Ny45IDczLjZoLTQuM1Y3OEg5OHYtNC4zWk05Ny45IDgyLjFoLTQuM3Y0LjNIOThWODJaTTk3LjkgODYuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDk0LjloLTQuM1Y5OUg5OFY5NVpNOTcuOSA5OS4xaC00LjN2NC4zSDk4Vjk5Wk05Ny45IDEwMy40aC00LjN2NC4ySDk4di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTk3LjkgMTA3LjZoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSAxMTYuMWgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDEyNC42aC00LjN2NC4ySDk4di00LjJaTTk3LjkgMTMzaC00LjN2NC4zSDk4di00LjJaTTEwMi4xIDIyLjdIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA0My45SDk4djQuMmg0LjJWNDRaTTEwMi4xIDUyLjRIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA2OS40SDk4djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgNzMuNkg5OFY3OGg0LjJ2LTQuM1pNMTAyLjEgOTAuNkg5OFY5NWg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgOTQuOUg5OFY5OWg0LjJWOTVaTTEwMi4xIDEwMy40SDk4djQuMmg0LjJ2LTQuMlpNMTAyLjEgMTMzSDk4djQuM2g0LjJ2LTQuMlpNMTAyLjEgMTM3LjNIOTh2NC4zaDQuMnYtNC4zWk0xMDYuNCAxOC40SDEwMnY0LjNoNC4zdi00LjNaTTEwNi40IDIyLjdIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgNDguMUgxMDJ2NC4zaDQuM3YtNC4zWk0xMDYuNCA2MC45SDEwMlY2NWg0LjNWNjFaTTEwNi40IDczLjZIMTAyVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMDYuNCA3Ny45SDEwMlY4Mmg0LjNWNzhaTTEwNi40IDgyLjFIMTAydjQuM2g0LjNWODJaTTEwNi40IDg2LjRIMTAydjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTA2LjQgOTAuNkgxMDJWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEwNi40IDk0LjlIMTAyVjk5aDQuM1Y5NVpNMTA2LjQgMTAzLjRIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTE2LjFIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTMzSDEwMnY0LjNoNC4zdi00LjJaTTEwNi40IDEzNy4zSDEwMnY0LjNoNC4zdi00LjNaTTExMC42IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMTAuNiAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTAuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTEwLjYgNDguMWgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTEwLjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWk0xMTAuNiA5MC42aC00LjJWOTVoNC4ydi00LjNaTTExMC42IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk0xMTAuNiAxMDcuNmgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTExMC42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTAuNiAxMzNoLTQuMnY0LjNoNC4ydi00LjJaTTExMC42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTQuOSAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA0OC4xaC00LjN2NC4zaDQuM3YtNC4zWk0xMTQuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTE0LjkgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSA3Ny45aC00LjNWODJoNC4zVjc4Wk0xMTQuOSA4Mi4xaC00LjN2NC4zaDQuM1Y4MlpNMTE0LjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgOTkuMWgtNC4zdjQuM2g0LjNWOTlaTTExNC45IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSAxMTYuMWgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgMTMzaC00LjN2NC4zaDQuM3YtNC4yWk0xMTQuOSAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNMTE5LjEgMTguNEgxMTV2NC4zaDQuMnYtNC4zWk0xMTkuMSAzNS40SDExNXY0LjNoNC4ydi00LjNaTTExOS4xIDM5LjdIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNDMuOUgxMTV2NC4yaDQuMlY0NFpNMTE5LjEgNjUuMUgxMTV2NC4zaDQuMlY2NVpNMTE5LjEgNjkuNEgxMTV2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTkuMSA3My42SDExNVY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNzcuOUgxMTVWODJoNC4yVjc4Wk0xMTkuMSA4Mi4xSDExNXY0LjNoNC4yVjgyWk0xMTkuMSA4Ni40SDExNXY0LjJoNC4ydi00LjJaTTExOS4xIDk0LjlIMTE1Vjk5aDQuMlY5NVpNMTE5LjEgMTAzLjRIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgMTA3LjZIMTE1djQuM2g0LjJ2LTQuM1pNMTE5LjEgMTE2LjFIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTI0LjZIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTMzSDExNXY0LjNoNC4ydi00LjJaTTExOS4xIDEzNy4zSDExNXY0LjNoNC4ydi00LjNaTTEyMy40IDMxLjJIMTE5djQuMmg0LjN2LTQuMlpNMTIzLjQgMzUuNEgxMTl2NC4zaDQuM3YtNC4zWk0xMjMuNCA1Ni42SDExOVY2MWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgNjAuOUgxMTlWNjVoNC4zVjYxWk0xMjMuNCA5MC42SDExOVY5NWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgOTQuOUgxMTlWOTloNC4zVjk1Wk0xMjMuNCA5OS4xSDExOXY0LjNoNC4zVjk5Wk0xMjMuNCAxMjAuM0gxMTl2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMjMuNCAxMjQuNkgxMTl2NC4yaDQuM3YtNC4yWk0xMjcuNiAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNMTI3LjYgMzEuMmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTI3LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgNDMuOWgtNC4ydjQuMmg0LjJWNDRaTTEyNy42IDQ4LjFoLTQuMnY0LjNoNC4ydi00LjNaTTEyNy42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTI3LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTEyNy42IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDc3LjloLTQuMlY4Mmg0LjJWNzhaTTEyNy42IDgyLjFoLTQuMnY0LjNoNC4yVjgyWk0xMjcuNiA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTI3LjYgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyNy42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTMzaC00LjJ2NC4zaDQuMnYtNC4yWk0xMzEuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMzEuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTMxLjkgNzcuOWgtNC4zVjgyaDQuM1Y3OFpNMTMxLjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTMxLjkgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNMTMxLjkgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTEzMS45IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzEuOSAxMzNoLTQuM3Y0LjNoNC4zdi00LjJaTTEzMS45IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzYuMSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA1Ni42aC00LjJWNjFoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDYwLjloLTQuMlY2NWg0LjJWNjFaTTEzNi4xIDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk0xMzYuMSA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA5NC45aC00LjJWOTloNC4yVjk1Wk0xMzYuMSAxMDMuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTExLjloLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSAxMjQuNmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTE0MC4zIDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTQwLjMgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTE0MC4zIDczLjZoLTQuMlY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQwLjMgNzcuOWgtNC4yVjgyaDQuMlY3OFpNMTQwLjMgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQwLjMgOTQuOWgtNC4yVjk5aDQuMlY5NVpNMTQwLjMgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xNDAuMyAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ0LjYgNjAuOWgtNC4zVjY1aDQuM1Y2MVoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ0LjYgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDEwNy42aC00LjN2NC4zaDQuM3YtNC4zWk0xNDQuNiAxMTEuOWgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMTE2LjFoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNDQuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ4LjggNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggNjAuOWgtNC4yVjY1aDQuMlY2MVpNMTQ4LjggNzMuNmgtNC4yVjc4aDQuMnYtNC4zWk0xNDguOCA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNMTQ4LjggODYuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ4LjggOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNDguOCA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTQ4LjggMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ4LjggMTM3LjNoLTQuMnY0LjNoNC4ydi00LjNaTTE1MyA1Ni42aC00LjJWNjFoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE1MyA2MC45aC00LjJWNjVoNC4zVjYxWk0xNTMgNjUuMWgtNC4ydjQuM2g0LjNWNjVaTTE1MyA5NC45aC00LjJWOTloNC4zVjk1Wk0xNTMgOTkuMWgtNC4ydjQuM2g0LjNWOTlaTTE1MyAxMDMuNGgtNC4ydjQuMmg0LjN2LTQuMlpNMTUzIDEyMC4zaC00LjJ2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTMgMTI0LjZoLTQuMnY0LjJoNC4zdi00LjJaTTE1MyAxMzcuM2gtNC4ydjQuM2g0LjN2LTQuM1pNMTU3LjMgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTcuMyA2MC45aC00LjJWNjVoNC4yVjYxWk0xNTcuMyA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNMTU3LjMgOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNTcuMyAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTE1Ny4zIDEzM2gtNC4ydjQuM2g0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTYxLjYgNTYuNmgtNC4zVjYxaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA2MC45aC00LjNWNjVoNC4zVjYxWk0xNjEuNiA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTYxLjYgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWk0xNjEuNiA5MC42aC00LjNWOTVoNC4zdi00LjNaTTE2MS42IDEwMy40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMDcuNmgtNC4zdjQuM2g0LjN2LTQuM1pNMTYxLjYgMTExLjloLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTYxLjYgMTMzaC00LjN2NC4zaDQuM3YtNC4yWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2IpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTYxLjYgMTguNEgzOC40djEyMy4yaDEyMy4yVjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJjIiB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMjIuN2gtNC4zdjQuMmg0LjN2LTQuMlpNNDIuNyAyN2gtNC4zdjQuMmg0LjN2LTQuM1pNNDIuNyAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMzkuN2gtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDIuNyA0My45aC00LjN2NC4yaDQuM1Y0NFpNNDcgMTguNGgtNC4zdjQuM2g0LjJ2LTQuM1pNNDcgNDMuOWgtNC4zdjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTguNGgtNC4zdjQuM2g0LjN2LTQuM1pNNTEuMiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk01NS40IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk01OS43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTU5LjcgNDMuOWgtNC4zdjQuMmg0LjNWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYzLjkgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNNjMuOSA0My45aC00LjJ2NC4yaDQuMlY0NFpNNjguMSAxOC40SDY0djQuM2g0LjJ2LTQuM1pNNjguMSAyMi43SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAyN0g2NHY0LjJoNC4ydi00LjNaTTY4LjEgMzEuMkg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMzUuNEg2NHY0LjNoNC4ydi00LjNaTTY4LjEgMzkuN0g2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgNDMuOUg2NHY0LjJoNC4yVjQ0WiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2MpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNjguMSAxOC40SDM4LjR2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJkIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjQ2IiB5PSIyNiIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDI3aC00LjN2NC4yaDQuM3YtNC4zWk01MS4yIDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMzUuNGgtNC4zdjQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNNTUuNCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDM1LjRoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTU5LjcgMzEuMmgtNC4zdjQuMmg0LjN2LTQuMlpNNTkuNyAzNS40aC00LjN2NC4zaDQuM3YtNC4zWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2QpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNTkuNyAyN0g0Ni45djEyLjdoMTIuOFYyNi45WiIvPjwvZz48bWFzayBpZD0iZSIgd2lkdGg9IjMxIiBoZWlnaHQ9IjMxIiB4PSIxMzEiIHk9IjE4IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTEzNi4xIDIyLjdoLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMzYuMSAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMzYuMSAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTQwLjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQwLjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0NC42IDQzLjloLTQuM3Y0LjJoNC4zVjQ0Wk0xNDguOCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTUzIDE4LjRoLTQuMnY0LjNoNC4zdi00LjNaTTE1MyA0My45aC00LjJ2NC4yaDQuM1Y0NFpNMTU3LjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTU3LjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2MS42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE2MS42IDIyLjdoLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDI3aC00LjN2NC4yaDQuM3YtNC4zWk0xNjEuNiAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xNjEuNiAzNS40aC00LjN2NC4zaDQuM3YtNC4zWk0xNjEuNiAzOS43aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48L21hc2s+PGcgbWFzaz0idXJsKCNlKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE2MS42IDE4LjRoLTI5Ljd2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJmIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjE0MCIgeT0iMjYiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHN0eWxlPSJtYXNrLXR5cGU6bHVtaW5hbmNlIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTE0NC42IDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0OC44IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xNDguOCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNTMgMjdoLTQuMnY0LjJoNC4zdi00LjNaTTE1MyAzMS4yaC00LjJ2NC4yaDQuM3YtNC4yWk0xNTMgMzUuNGgtNC4ydjQuM2g0LjN2LTQuM1oiLz48L21hc2s+PGcgbWFzaz0idXJsKCNmKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE1MyAyN2gtMTIuN3YxMi43aDEyLjhWMjYuOVoiLz48L2c+PG1hc2sgaWQ9ImciIHdpZHRoPSIzMSIgaGVpZ2h0PSIzMSIgeD0iMzgiIHk9IjExMSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExNi4xaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEyNC42aC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyOC44aC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEzM2gtNC4zdjQuM2g0LjN2LTQuMlpNNDIuNyAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNNDcgMTExLjloLTQuM3Y0LjJoNC4ydi00LjJaTTQ3IDEzNy4zaC00LjN2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01MS4yIDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01NS40IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk01OS43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01OS43IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02My45IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk02OC4xIDExMS45SDY0djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNjguMSAxMTYuMUg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMTIwLjNINjR2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02OC4xIDEyNC42SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAxMjguOEg2NHY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgMTMzSDY0djQuM2g0LjJ2LTQuMlpNNjguMSAxMzcuM0g2NHY0LjNoNC4ydi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjZykiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik02OC4xIDExMS45SDM4LjR2MjkuN2gyOS43di0yOS43WiIvPjwvZz48bWFzayBpZD0iaCIgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiB4PSI0NiIgeT0iMTIwIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjaCkiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik01OS43IDEyMC4zSDQ2Ljl2MTIuOGgxMi44di0xMi44WiIvPjwvZz48L2c+PGRlZnM+PGNsaXBQYXRoIGlkPSJhIj48cmVjdCB3aWR0aD0iMTM3IiBoZWlnaHQ9IjEzNyIgeD0iMzEuNSIgeT0iMTEuNSIgZmlsbD0iI2ZmZiIgcng9IjguMiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg==", "description": "Enables users to open the ThingsBoard mobile app by scanning the QR code from dashboard.", "descriptor": { "type": "static", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index c7557c75c2..5085d43aec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -71,13 +71,15 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI } ngOnInit(): void { - if (this.ctx) { - this.mobileAppSettings = this.ctx.settings; - } else { - this.mobileAppService.getMobileAppSettings().subscribe((settings => { - this.mobileAppSettings = settings; - this.cd.detectChanges(); - })); + if (!this.previewMode) { + if (this.ctx) { + this.mobileAppSettings = this.ctx.settings; + } else { + this.mobileAppService.getMobileAppSettings().subscribe((settings => { + this.mobileAppSettings = settings; + this.cd.detectChanges(); + })); + } } } @@ -105,9 +107,6 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI updateQRCode(link: string) { import('qrcode').then((QRCode) => { QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 100 }); - // QRCode.toCanvas(this.canvasRef.nativeElement, link); - // this.canvasRef.nativeElement.style.width = '4.6vw'; - // this.canvasRef.nativeElement.style.height = '4.6vw'; }); } From 68fe7d4a17b22373f6a4cf2b259a13aa5af38e5b Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 10 May 2024 12:09:21 +0300 Subject: [PATCH 094/121] fix_bug: if setSupportedClientObjectsis is not supported LwM2m.Version --- .../server/transport/lwm2m/server/client/LwM2mClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 705ed45995..141aa83b7e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -460,7 +460,7 @@ public class LwM2mClient { if (lwM2mPath.isObject()) { LwM2m.Version ver; if (link.getAttributes().get("ver")!= null) { - ver = (Version) link.getAttributes().get("ver").getValue(); + ver = new LwM2m.Version(link.getAttributes().get("ver").getValue().toString()); } else { ver = getDefaultObjectIDVer(); } From 2eee2bc4416575ba297a5a541ed39c3f08c42850 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 10 May 2024 13:41:13 +0300 Subject: [PATCH 095/121] fix_bug: monitoring - resources --- .../monitoring/service/transport/TransportHealthChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java index bee65d1f37..4b04f5d8de 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -145,7 +145,7 @@ public abstract class TransportHealthChecker { TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); From 39a9cefb9df634ae2858bfa421c005a30522ede2 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 10 May 2024 13:45:50 +0300 Subject: [PATCH 096/121] Jwt token factory refactored due to PE --- .../server/service/security/model/token/JwtTokenFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index f2e276a0ef..a47d0034a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -210,11 +210,12 @@ public class JwtTokenFactory { ZonedDateTime currentTime = ZonedDateTime.now(); + claimsBuilder.expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())); + return Jwts.builder() .claims(claimsBuilder.build()) .issuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) .issuedAt(Date.from(currentTime.toInstant())) - .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) .signWith(getSecretKey(false), Jwts.SIG.HS512); } From ffbb2c7c73e9443acb5a90f44a678f626c5b927c Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 10 May 2024 16:46:32 +0300 Subject: [PATCH 097/121] UI: refactor mobile app qr code widgets validationm, cleanup widgets json --- .../widget_types/home_mobile_app_qr_code.json | 2 +- .../widget_types/mobile_app_qr_code.json | 2 +- ...app-qr-code-widget-settings.component.html | 9 ++ ...e-app-qr-code-widget-settings.component.ts | 106 +++++-------- .../admin/mobile-app-settings.component.html | 9 ++ .../admin/mobile-app-settings.component.ts | 149 +++++++++--------- .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 131 insertions(+), 147 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json index e8e21f5d89..908533b279 100644 --- a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "", - "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\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"Home page mobile app QR code\",\"dropShadow\":false,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Home page mobile app QR code\",\"dropShadow\":true}" }, "tags": [ "ios", diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index 223678bbdd..ccb9bea73f 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-mobile-app-qr-code-widget-settings", - "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\":{\"useDefaultApp\":true,\"androidConfig\":{\"enabled\":true,\"appPackage\":\"\",\"sha256CertFingerprints\":\"\"},\"iosConfig\":{\"enabled\":true,\"appId\":\"\"},\"qrCodeConfig\":{\"badgeEnabled\":true,\"badgeStyle\":\"ORIGINAL\",\"badgePosition\":\"RIGHT\",\"qrCodeLabelEnabled\":true,\"qrCodeLabel\":\"Scan to connect or download mobile app\"}},\"title\":\"Mobile app QR code\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "defaultConfig": "{\n \"showTitle\": true,\n \"backgroundColor\": \"rgb(255, 255, 255)\",\n \"color\": \"rgba(0, 0, 0, 0.87)\",\n \"padding\": \"8px\",\n \"settings\": {\n \"useDefaultApp\": true,\n \"androidConfig\": {\n \"enabled\": true,\n \"appPackage\": \"\",\n \"sha256CertFingerprints\": \"\"\n },\n \"iosConfig\": {\n \"enabled\": true,\n \"appId\": \"\"\n },\n \"qrCodeConfig\": {\n \"badgeEnabled\": true,\n \"badgeStyle\": \"ORIGINAL\",\n \"badgePosition\": \"RIGHT\",\n \"qrCodeLabelEnabled\": true,\n \"qrCodeLabel\": \"Scan to connect or download mobile app\"\n }\n },\n \"title\": \"Mobile app QR code\",\n \"dropShadow\": true,\n \"enableFullscreen\": false,\n \"widgetStyle\": {},\n \"widgetCss\": \"\",\n \"pageSize\": 1024,\n \"noDataDisplayMessage\": \"\",\n \"showTitleIcon\": false,\n \"titleTooltip\": \"\",\n \"titleStyle\": {\n \"fontSize\": \"16px\",\n \"fontWeight\": 400\n }\n}" }, "tags": [ "ios", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html index cdef956141..d474714216 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html @@ -123,6 +123,15 @@ + + warning +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts index 662a9d3dd3..664dbe3327 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -49,22 +49,22 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.mobileAppQRCodeWidgetSettingsForm = this.fb.group({ - useDefaultApp: [settings.useDefaultApp, []], + useDefaultApp: [settings.useDefaultApp], androidConfig: this.fb.group({ - enabled: [settings.androidConfig.enabled, []], - appPackage: [settings.androidConfig.appPackage, []], - sha256CertFingerprints: [settings.androidConfig.sha256CertFingerprints, []] + enabled: [settings.androidConfig.enabled], + appPackage: [settings.androidConfig.appPackage, [Validators.required]], + sha256CertFingerprints: [settings.androidConfig.sha256CertFingerprints, [Validators.required]] }), iosConfig: this.fb.group({ - enabled: [settings.iosConfig.enabled, []], - appId: [settings.iosConfig.appId, []] + enabled: [settings.iosConfig.enabled], + appId: [settings.iosConfig.appId, [Validators.required]] }), qrCodeConfig: this.fb.group({ - badgeEnabled: [settings.qrCodeConfig.badgeEnabled, []], - badgeStyle: [{value: settings.qrCodeConfig.badgeStyle, disabled: true}, []], - badgePosition: [{value: settings.qrCodeConfig.badgePosition, disabled: true}, []], - qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled, []], - qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel, []] + badgeEnabled: [settings.qrCodeConfig.badgeEnabled], + badgeStyle: [{value: settings.qrCodeConfig.badgeStyle, disabled: true}], + badgePosition: [{value: settings.qrCodeConfig.badgePosition, disabled: true}], + qrCodeLabelEnabled: [settings.qrCodeConfig.qrCodeLabelEnabled], + qrCodeLabel: [settings.qrCodeConfig.qrCodeLabel, [Validators.required]] }) }); } @@ -81,76 +81,46 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon const qrCodeLabelEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value; if (useDefaultApp) { - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators(); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators(); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); } else { - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); - } - - if (androidEnabled) { - if (!useDefaultApp) { - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); + if (androidEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); } - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); - if (badgeEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); - } - } else { - if (!useDefaultApp) { - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').clearValidators(); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); - } - if (!iosEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); + if (iosEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); } } - if (iosEnabled) { - if (!useDefaultApp) { - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); - } - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); - if (badgeEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); - } - } else { - if (!useDefaultApp) { - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').clearValidators(); - } - if (!androidEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); - } + if (!androidEnabled && !iosEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } - if (badgeEnabled) { - if (androidEnabled || iosEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable(); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable(); + if (androidEnabled || iosEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + if (badgeEnabled) { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); + } else { + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } - } else { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable(); - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgePosition').disable(); } if (qrCodeLabelEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); } else { - this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable(); + this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); } - - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').updateValueAndValidity({emitEvent}); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity({emitEvent}); - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').updateValueAndValidity({emitEvent}); } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html index 5b1cb710e0..88c6c287d5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html @@ -140,6 +140,15 @@ + + warning +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index 7959b1b800..96e8e89e6a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -29,7 +29,6 @@ import { badgeStyleTranslationsMap, MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; -import { AuthService } from '@core/auth/auth.service'; @Component({ selector: 'tb-mobile-app-settings', @@ -58,95 +57,49 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf takeUntil(this.destroy$) ).subscribe(value => { if (value) { - this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); - this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); + this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); } else { - this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); - this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); + if (this.mobileAppSettingsForm.get('androidConfig.enabled').value) { + this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); + } + if (this.mobileAppSettingsForm.get('iosConfig.enabled').value) { + this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); + } } - this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); - this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); }); this.mobileAppSettingsForm.get('androidConfig.enabled').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { - if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { - if (value) { - this.mobileAppSettingsForm.get('androidConfig.appPackage').setValidators([Validators.required]); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').setValidators([Validators.required]); - } else { - this.mobileAppSettingsForm.get('androidConfig.appPackage').clearValidators(); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').clearValidators(); - } - this.mobileAppSettingsForm.get('androidConfig.appPackage').updateValueAndValidity(); - this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').updateValueAndValidity(); - } - if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { - if (value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); - if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); - } - } else { - if (!this.mobileAppSettingsForm.get('iosConfig.enabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); - } - } - } + this.androidEnableChanged(value); }); this.mobileAppSettingsForm.get('iosConfig.enabled').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { - if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { - if (value) { - this.mobileAppSettingsForm.get('iosConfig.appId').setValidators([Validators.required]); - } else { - this.mobileAppSettingsForm.get('iosConfig.appId').clearValidators(); - } - this.mobileAppSettingsForm.get('iosConfig.appId').updateValueAndValidity(); - } - if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { - if (value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable(); - if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); - } - } else { - if (!this.mobileAppSettingsForm.get('androidConfig.enabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); - } - } - } + this.iosEnableChanged(value); }); this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { if (value) { if (this.mobileAppSettingsForm.get('androidConfig.enabled').value || this.mobileAppSettingsForm.get('iosConfig.enabled').value) { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); } } else { - this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable(); - this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); } }); this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { if (value) { - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable(); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); } else { - this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable(); + this.mobileAppSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); } }); } @@ -159,23 +112,23 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf private buildMobileAppSettingsForm() { this.mobileAppSettingsForm = this.fb.group({ - useDefaultApp: [true, []], + useDefaultApp: [true], androidConfig: this.fb.group({ - enabled: [true, []], - appPackage: ['', []], - sha256CertFingerprints: ['', []] + enabled: [true], + appPackage: [{value: '', disabled: true}, [Validators.required]], + sha256CertFingerprints: [{value: '', disabled: true}, [Validators.required]] }), iosConfig: this.fb.group({ - enabled: [true, []], - appId: ['', []] + enabled: [true], + appId: [{value: '', disabled: true}, [Validators.required]] }), qrCodeConfig: this.fb.group({ - showOnHomePage: [true, []], - badgeEnabled: [true, []], - badgeStyle: [{value: BadgeStyle.ORIGINAL, disabled: true}, []], - badgePosition: [{value: BadgePosition.RIGHT, disabled: true}, []], - qrCodeLabelEnabled: [true, []], - qrCodeLabel: ['', []] + showOnHomePage: [true], + badgeEnabled: [true], + badgeStyle: [{value: BadgeStyle.ORIGINAL, disabled: true}], + badgePosition: [{value: BadgePosition.RIGHT, disabled: true}], + qrCodeLabelEnabled: [true], + qrCodeLabel: ['', [Validators.required]] }) }); } @@ -185,6 +138,48 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf this.mobileAppSettingsForm.reset(this.mobileAppSettings); } + private androidEnableChanged(value: boolean): void { + if (value) { + if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + this.mobileAppSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); + } + } else { + this.mobileAppSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); + } + this.updateBadgeControls(value); + } + + private iosEnableChanged(value: boolean): void { + if (value) { + if (!this.mobileAppSettingsForm.get('useDefaultApp').value) { + this.mobileAppSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); + } + } else { + this.mobileAppSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); + } + this.updateBadgeControls(value); + } + + private updateBadgeControls(value: boolean) { + if (this.mobileAppSettingsForm.get('qrCodeConfig.showOnHomePage').value) { + if (value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').enable({emitEvent: false}); + if (this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').enable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').enable({emitEvent: false}); + } + } else { + if (!this.mobileAppSettingsForm.get('iosConfig.enabled').value && !this.mobileAppSettingsForm.get('androidConfig.enabled').value) { + this.mobileAppSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); + this.mobileAppSettingsForm.get('qrCodeConfig.badgePosition').disable({emitEvent: false}); + } + } + } + } + save(): void { this.mobileAppSettings = {...this.mobileAppSettings, ...this.mobileAppSettingsForm.getRawValue()}; this.mobileAppService.saveMobileAppSettings(this.mobileAppSettings) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index a78067e830..d79b468a8e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -440,6 +440,7 @@ "original": "Original", "white": "White", "label": "Label", + "label-required": "Label is required", "right": "Right", "left": "Left", "set": "Set", From 9d71bb77ea396839c4e4685871d1c5a6a670de23 Mon Sep 17 00:00:00 2001 From: rusikv Date: Fri, 10 May 2024 18:06:11 +0300 Subject: [PATCH 098/121] UI: clean up sysadmin, tenant home pages jsons --- .../assets/dashboard/sys_admin_home_page.json | 62 +------------------ .../dashboard/tenant_admin_home_page.json | 61 +----------------- 2 files changed, 2 insertions(+), 121 deletions(-) diff --git a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json index 43998afbcb..4984de6e29 100644 --- a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json @@ -2614,12 +2614,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "datasources": [], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, "showTitle": false, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -2632,9 +2626,7 @@ "enableFullscreen": false, "widgetStyle": {}, "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "showLegend": false + "noDataDisplayMessage": "" }, "row": 0, "col": 0, @@ -2647,28 +2639,6 @@ "sizeX": 6, "sizeY": 3, "config": { - "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)", @@ -2679,7 +2649,6 @@ "enableFullscreen": false, "widgetStyle": {}, "widgetCss": "", - "pageSize": 1024, "noDataDisplayMessage": "" }, "row": 0, @@ -2692,31 +2661,6 @@ "sizeX": 5, "sizeY": 3.5, "config": { - "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1715012840937, - "endTimeMs": 1715099240937 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -2744,11 +2688,7 @@ "fontSize": "16px", "fontWeight": 400 }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", - "pageSize": 1024, "noDataDisplayMessage": "" }, "row": 0, diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index cb42df7839..6c1bed741e 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1118,31 +1118,6 @@ "sizeX": 5, "sizeY": 3.5, "config": { - "datasources": [], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1715005524783, - "endTimeMs": 1715091924783 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": false, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", @@ -1170,11 +1145,7 @@ "fontSize": "16px", "fontWeight": 400 }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", - "pageSize": 1024, "noDataDisplayMessage": "" }, "row": 0, @@ -1186,12 +1157,6 @@ "sizeX": 7.5, "sizeY": 6.5, "config": { - "datasources": [], - "timewindow": { - "realtime": { - "timewindowMs": 60000 - } - }, "showTitle": false, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", @@ -1204,9 +1169,7 @@ "enableFullscreen": false, "widgetStyle": {}, "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "showLegend": false + "noDataDisplayMessage": "" }, "row": 0, "col": 0, @@ -1219,28 +1182,6 @@ "sizeX": 6, "sizeY": 3, "config": { - "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)", From 87ed660fc922734880d29311158c9e7e53a3d91a Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Sun, 12 May 2024 22:56:45 +0200 Subject: [PATCH 099/121] fixed swagger allowableValues --- .../server/controller/ComponentDescriptorController.java | 6 +++--- .../thingsboard/server/controller/OtaPackageController.java | 2 +- .../data/sms/config/SmppSmsProviderConfiguration.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 5271de2329..6967b3c13b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -68,9 +68,9 @@ public class ComponentDescriptorController extends BaseController { @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @ResponseBody public List getComponentDescriptorsByType( - @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", requiredMode = Schema.RequiredMode.REQUIRED)) + @Parameter(description = "Type of the Rule Node", schema = @Schema(allowableValues = {"ENRICHMENT", "FILTER,TRANSFORMATION", "ACTION,EXTERNAL"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("componentType") String strComponentType, - @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE")) + @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"})) @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkParameter("componentType", strComponentType); return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType)); @@ -85,7 +85,7 @@ public class ComponentDescriptorController extends BaseController { public List getComponentDescriptorsByTypes( @Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true) @RequestParam("componentTypes") String[] strComponentTypes, - @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = "CORE,EDGE")) + @Parameter(description = "Type of the Rule Chain", schema = @Schema(allowableValues = {"CORE", "EDGE"})) @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkArrayParameter("componentTypes", strComponentTypes); Set componentTypes = new HashSet<>(); diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index f2b77c3b7b..ee7bfcc0dc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -198,7 +198,7 @@ public class OtaPackageController extends BaseController { @ResponseBody public PageData getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("deviceProfileId") String strDeviceProfileId, - @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = "FIRMWARE, SOFTWARE")) + @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) @PathVariable("type") String strType, @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java index 5fc0c14d61..549770c539 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java @@ -89,7 +89,7 @@ public class SmppSmsProviderConfiguration implements SmsProviderConfiguration { @Schema(description = "Address range", requiredMode = Schema.RequiredMode.NOT_REQUIRED) private String addressRange; - @Schema(allowableValues = "0-10,13-14", + @Schema(allowableValues = {"0-10" ,"13-14"}, description = "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free, used as default)\n" + "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))\n" + "2 - Octet Unspecified (8-bit binary)\n" + From e10ba543a0b813eb0dd784d6ba07b7621f0c9c7c Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 13 May 2024 11:12:20 +0200 Subject: [PATCH 100/121] fixed all allowableValues --- .../thingsboard/server/controller/CustomerController.java | 2 +- .../server/controller/EntityViewController.java | 4 ++-- .../server/controller/NotificationController.java | 8 +++----- .../thingsboard/server/common/data/audit/AuditLog.java | 2 +- .../server/common/data/event/DebugEventFilter.java | 2 +- .../server/common/data/event/LifeCycleEventFilter.java | 2 +- .../common/data/event/RuleNodeDebugEventFilter.java | 2 +- .../server/common/data/plugin/ComponentDescriptor.java | 2 +- .../server/common/data/security/DeviceCredentials.java | 2 +- 9 files changed, 12 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index a46cbe7dba..9ee8ae6cf8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -164,7 +164,7 @@ public class CustomerController extends BaseController { @RequestParam int page, @Parameter(description = CUSTOMER_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "email", "country, city"})) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "email", "country", "city"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 8b33aeeb15..783b451a6d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -229,7 +229,7 @@ public class EntityViewController extends BaseController { @RequestParam(required = false) String type, @Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name, type"})) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "type"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { @@ -293,7 +293,7 @@ public class EntityViewController extends BaseController { @RequestParam(required = false) String type, @Parameter(description = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name, type"})) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "type"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 96f38daf35..fb32ebd30b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -106,8 +106,6 @@ public class NotificationController extends BaseController { private final NotificationCenter notificationCenter; private final NotificationSettingsService notificationSettingsService; - private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP"; - @ApiOperation(value = "Get notifications (getNotifications)", notes = "Returns the page of notifications for current user." + NEW_LINE + PAGE_DATA_PARAMETERS + @@ -175,7 +173,7 @@ public class NotificationController extends BaseController { @RequestParam(required = false) String sortOrder, @Parameter(description = "To search for unread notifications only") @RequestParam(defaultValue = "false") boolean unreadOnly, - @Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) + @Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"})) @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) throws ThingsboardException { // no permissions @@ -188,7 +186,7 @@ public class NotificationController extends BaseController { AVAILABLE_FOR_ANY_AUTHORIZED_USER) @GetMapping("/notifications/unread/count") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) + public Integer getUnreadNotificationsCount(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"})) @RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) { return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId()); @@ -211,7 +209,7 @@ public class NotificationController extends BaseController { AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PutMapping("/notifications/read") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {DELIVERY_METHOD_ALLOWABLE_VALUES})) + public void markAllNotificationsAsRead(@Parameter(description = "Delivery method", schema = @Schema(allowableValues = {"WEB", "MOBILE_APP"})) @RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod, @AuthenticationPrincipal SecurityUser user) { // no permissions diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java index bc7ac85755..fdbbe4436c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java @@ -49,7 +49,7 @@ public class AuditLog extends BaseData { private ActionType actionType; @Schema(description = "JsonNode represented action data", accessMode = Schema.AccessMode.READ_ONLY) private JsonNode actionData; - @Schema(description = "String represented Action status", example = "SUCCESS", allowableValues = "SUCCESS,FAILURE", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "String represented Action status", example = "SUCCESS", allowableValues = {"SUCCESS", "FAILURE"}, accessMode = Schema.AccessMode.READ_ONLY) private ActionStatus actionStatus; @Schema(description = "Failure action details info. An empty string in case of action status type 'SUCCESS', otherwise includes stack trace of the caused exception.", accessMode = Schema.AccessMode.READ_ONLY) private String actionFailureDetails; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java index c810dfdbb2..17c94cc4e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEventFilter.java @@ -25,7 +25,7 @@ public abstract class DebugEventFilter implements EventFilter { @Schema(description = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") protected String server; - @Schema(description = "Boolean value to filter the errors", allowableValues = "false, true") + @Schema(description = "Boolean value to filter the errors", allowableValues = {"false", "true"}) protected boolean isError; @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") protected String errorStr; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java index 643dd0bef2..3c07a699c0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java @@ -27,7 +27,7 @@ public class LifeCycleEventFilter implements EventFilter { protected String server; @Schema(description = "String value representing the lifecycle event type", example = "STARTED") protected String event; - @Schema(description = "String value representing status of the lifecycle event", allowableValues = "Success, Failure") + @Schema(description = "String value representing status of the lifecycle event", allowableValues = {"Success", "Failure"}) protected String status; @Schema(description = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") protected String errorStr; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java index 6f99de5986..3d43b86a6b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/RuleNodeDebugEventFilter.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.StringUtils; @Schema public class RuleNodeDebugEventFilter extends DebugEventFilter { - @Schema(description = "String value representing msg direction type (incoming to entity or outcoming from entity)", allowableValues = "IN, OUT") + @Schema(description = "String value representing msg direction type (incoming to entity or outcoming from entity)", allowableValues = {"IN", "OUT"}) protected String msgDirectionType; @Schema(description = "String value representing the entity id in the event body (originator of the message)", example = "de9d54a0-2b7a-11ec-a3cc-23386423d98f") protected String entityId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java index d58ca2d62d..4275180ede 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java @@ -39,7 +39,7 @@ public class ComponentDescriptor extends BaseData { @Getter @Setter private ComponentType type; @Schema(description = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT") @Getter @Setter private ComponentScope scope; - @Schema(description = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = "USER_PREFERENCE, ENABLED, SINGLETON", example = "ENABLED") + @Schema(description = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = Schema.AccessMode.READ_ONLY, allowableValues = {"USER_PREFERENCE", "ENABLED", "SINGLETON"}, example = "ENABLED") @Getter @Setter private ComponentClusteringMode clusteringMode; @Length(fieldName = "name") @Schema(description = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = Schema.AccessMode.READ_ONLY, example = "Custom Rule Node") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java index 24d8cc1175..e269f0438d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java @@ -70,7 +70,7 @@ public class DeviceCredentials extends BaseData implements this.deviceId = deviceId; } - @Schema(description = "Type of the credentials", allowableValues ="ACCESS_TOKEN, X509_CERTIFICATE, MQTT_BASIC, LWM2M_CREDENTIALS") + @Schema(description = "Type of the credentials", allowableValues = {"ACCESS_TOKEN", "X509_CERTIFICATE", "MQTT_BASIC", "LWM2M_CREDENTIALS"}) @Override public DeviceCredentialsType getCredentialsType() { return credentialsType; From 9c30fce48db61cf82c98cf378b1f6216c9ae70e9 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 14 May 2024 11:48:11 +0300 Subject: [PATCH 101/121] Added more logs to DefaultTbApiUsageStateService --- .../service/apiusage/BaseApiUsageState.java | 10 ++++++++++ .../apiusage/DefaultTbApiUsageStateService.java | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java index 2d5026429a..9eac721dfb 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java @@ -199,6 +199,16 @@ public abstract class BaseApiUsageState { return getApiUsageState().getEntityId(); } + @Override + public String toString() { + return "BaseApiUsageState{" + + "apiUsageState=" + apiUsageState + + ", currentCycleTs=" + currentCycleTs + + ", nextCycleTs=" + nextCycleTs + + ", currentHourTs=" + currentHourTs + + '}'; + } + @Data @Builder public static class StatsCalculationResult { diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index aef3a4ddcf..60bf529ace 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -17,6 +17,8 @@ package org.thingsboard.server.service.apiusage; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; @@ -67,8 +69,6 @@ import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.partition.AbstractPartitionBasedService; import org.thingsboard.server.service.telemetry.InternalTelemetryService; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -184,6 +184,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService if (newHourTs != hourTs) { usageState.setHour(newHourTs); } + if (log.isTraceEnabled()) { + log.trace("[{}][{}] Processing usage stats from {} (currentCycleTs={}, currentHourTs={}): {}", tenantId, ownerId, serviceId, ts, newHourTs, values); + } updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); Set apiFeatures = new HashSet<>(); for (UsageStatsKVProto statsItem : values) { @@ -210,6 +213,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } finally { updateLock.unlock(); } + log.trace("[{}][{}] Saving new stats: {}", tenantId, ownerId, updatedEntries); tsWsService.saveAndNotifyInternal(tenantId, usageState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK); if (!result.isEmpty()) { persistAndNotify(usageState, result); @@ -410,6 +414,9 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService myUsageStates.values().forEach(state -> { if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) { state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); + if (log.isTraceEnabled()) { + log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs()); + } saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values())); if (state.getEntityType() == EntityType.TENANT && !state.getEntityId().equals(TenantId.SYS_TENANT_ID)) { TenantId tenantId = state.getTenantId(); @@ -417,6 +424,8 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } } }); + } catch (Throwable e) { + log.error("Failed to check start of next cycle", e); } finally { updateLock.unlock(); } @@ -483,7 +492,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService } } state.setGaugeReportInterval(gaugeReportInterval); - log.debug("[{}] Initialized state: {}", ownerId, storedState); + log.debug("[{}][{}] Initialized state: {}", tenantId, ownerId, state); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, ownerId); if (tpi.isMyPartition()) { addEntityState(tpi, state); From 278f6aec067082719d824bd21038005f5f1fee71 Mon Sep 17 00:00:00 2001 From: kalytka Date: Wed, 15 May 2024 13:35:17 +0300 Subject: [PATCH 102/121] Added Lithuanian language --- .../assets/locale/locale.constant-en_US.json | 1 + .../assets/locale/locale.constant-lt_LT.json | 7954 +++++++++++++++++ 2 files changed, 7955 insertions(+) create mode 100644 ui-ngx/src/assets/locale/locale.constant-lt_LT.json diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e16dcee50b..6996e37699 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -7336,6 +7336,7 @@ "ja_JP": "日本語", "ka_GE": "ქართული", "ko_KR": "한국어", + "lt_LT": "Lietuvių", "lv_LV": "Latviešu", "nl_BE": "Koninkrijk België", "pl_PL": "Polski", diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json new file mode 100644 index 0000000000..5ca808146c --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -0,0 +1,7954 @@ +{ + "access": { + "unauthorized": "Neautorizuotas", + "unauthorized-access": "Neautorizuota prieiga", + "unauthorized-access-text": "Prieiga prie šio funkcionalumo leidžiama tik prisijungusiam vartotojui!", + "access-forbidden": "Prieiga uždrausta", + "access-forbidden-text": "Neturite teisių prieigai!
Bandykite prisijungti kitu vartotoju.", + "refresh-token-expired": "Sesijos galiojimas baigėsi", + "refresh-token-failed": "Sesijos atnaujinti nepavyksta", + "permission-denied": "Leidimas nesuteiktas", + "permission-denied-text": "Šio veiksmo atlikti negalite!" + }, + "account": { + "account": "Account", + "notification-settings": "Notification settings" + }, + "action": { + "activate": "Aktyvuoti", + "suspend": "Sustabdyti", + "save": "Išsaugoti", + "saveAs": "Išsaugoti kaip", + "move": "Perkelti", + "cancel": "Atmesti", + "ok": "OK", + "delete": "Panaikinti", + "add": "Pridėti", + "yes": "Taip", + "no": "Ne", + "update": "Atnaujinti", + "remove": "Panaikinti", + "search": "Paieška", + "clear-search": "Išvalyti", + "assign": "Priskirti", + "unassign": "Atsieti", + "share": "Pasidalinti", + "make-private": "Riboti matomumą", + "make-public": "Neriboti matomumo", + "apply": "Taikyti", + "apply-changes": "Pritaikyti pakeitimus", + "edit-mode": "Redagavimo režimas", + "enter-edit-mode": "Redagavimo režimas", + "decline-changes": "Atšaukti pakeitimus", + "open": "Atverti", + "decline": "Atšaukti", + "close": "Uždaryti", + "back": "Atgal", + "run": "Paleisti", + "sign-in": "Prisijunkite!", + "edit": "Redaguoti", + "view": "Peržiūrėti", + "create": "Kurti", + "drag": "Vilkti", + "refresh": "Atnaujinti", + "undo": "Atšaukti", + "copy": "Kopijuoti", + "paste": "Įklijuoti", + "copy-reference": "Kopijuoti nuorodą", + "paste-reference": "Įklijuoti nuorodą", + "import": "Importuoti", + "export": "Eksportuoti", + "share-via": "Pasidalinti per {{provider}}", + "select": "Pasirinkti", + "continue": "Tęsti", + "discard-changes": "Atsisakyti pakeitimų", + "download": "Parsisiųsti", + "next": "Sekantis", + "next-with-label": "Sekantis: {{label}}", + "read-more": "Skaityti daugiau", + "hide": "Paslėpti", + "done": "Atlikta", + "print": "Spausdinti", + "restore": "Atkurti", + "confirm": "Patvirtinti", + "more": "Daugiau", + "less": "Mažiau", + "skip": "Praleisti", + "send": "Siųsti", + "reset": "Nustatyti iš naujo", + "show-more": "Rodyti daugiau", + "dont-show-again": "Daugiau neberodyti", + "see-documentation": "Žiūrėti dokumentacijoje", + "clear": "Išvalyti" + }, + "aggregation": { + "aggregation": "Agregavimas", + "function": "Duomenų aggregavimo funkcija", + "limit": "Maksimali reikšmė", + "group-interval": "Grupavimo intervalas", + "min": "Min", + "max": "Max", + "avg": "Vidurkis", + "sum": "Sum", + "count": "Kiekis", + "none": "Nėra" + }, + "admin": { + "settings": "Settings", + "general": "General", + "general-settings": "General Settings", + "home-settings": "Home Settings", + "home": "Home", + "outgoing-mail": "Mail Server", + "outgoing-mail-settings": "Outgoing Mail Server Settings", + "system-settings": "System Settings", + "test-mail-sent": "Test mail was successfully sent!", + "base-url": "Base URL", + "base-url-required": "Base URL is required.", + "prohibit-different-url": "Prohibit to use hostname from the client request headers", + "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", + "mail-from": "Mail From", + "mail-from-required": "Mail From is required.", + "smtp-protocol": "SMTP protocol", + "smtp-host": "SMTP host", + "smtp-host-required": "SMTP host is required.", + "smtp-port": "SMTP port", + "smtp-port-required": "You must supply a smtp port.", + "smtp-port-invalid": "That doesn't look like a valid smtp port.", + "timeout-msec": "Timeout (msec)", + "timeout-required": "Timeout is required.", + "timeout-invalid": "That doesn't look like a valid timeout.", + "enable-tls": "Enable TLS", + "tls-version": "TLS version", + "enable-proxy": "Enable proxy", + "proxy-host": "Proxy host", + "proxy-host-required": "Proxy host is required.", + "proxy-port": "Proxy port", + "proxy-port-required": "Proxy port is required.", + "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", + "proxy-user": "Proxy user", + "proxy-password": "Proxy password", + "change-password": "Change password", + "send-test-mail": "Send test mail", + "use-system-mail-settings": "Use System Mail Server Settings", + "mail-templates": "Mail Templates", + "mail-template-settings": "Mail Templates Settings", + "use-system-mail-template-settings": "Use System Mail Templates", + "mail-template": { + "mail-template": "Mail template", + "test": "Test email message", + "activation": "Account activation message", + "account-activated": "Account activated message", + "account-lockout": "Account lockout message", + "reset-password": "Reset password message", + "password-was-reset": "Password was reset message", + "user-activated": "User activated message", + "user-registered": "User registered message", + "api-usage-state-enabled": "Api usage state enabled", + "api-usage-state-warning": "Api usage state warning", + "api-usage-state-disabled": "Api usage state disabled", + "two-fa-verification": "2FA verification message" + }, + "mail-subject": "Mail Subject", + "mail-body": "Mail body", + "sms-provider": "SMS provider", + "sms-provider-settings": "SMS provider settings", + "use-system-sms-settings": "Use System SMS provider settings", + "sms-provider-type": "SMS provider type", + "sms-provider-type-required": "SMS provider type is required.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "AWS Access Key ID", + "aws-access-key-id-required": "AWS Access Key ID is required", + "aws-secret-access-key": "AWS Secret Access Key", + "aws-secret-access-key-required": "AWS Secret Access Key is required", + "aws-region": "AWS Region", + "aws-region-required": "AWS Region is required", + "number-from": "Phone Number From", + "number-from-required": "Phone Number From is required.", + "number-to": "Phone Number To", + "number-to-required": "Phone Number To is required.", + "phone-number-hint": "Phone Number in E.164 format, ex. +19995550123", + "phone-number-hint-twilio": "Phone Number in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.", + "phone-number-pattern-twilio": "Invalid phone number. Should be in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX.", + "sms-message": "SMS message", + "sms-message-required": "SMS message is required.", + "sms-message-max-length": "SMS message can't be longer 1600 characters", + "twilio-account-sid": "Twilio Account SID", + "twilio-account-sid-required": "Twilio Account SID is required", + "twilio-account-token": "Twilio Account Token", + "twilio-account-token-required": "Twilio Account Token is required", + "send-test-sms": "Send test SMS", + "test-sms-sent": "Test SMS was successfully sent!", + "security-settings": "Security settings", + "password-policy": "Password policy", + "minimum-password-length": "Minimum password length", + "minimum-password-length-required": "Minimum password length is required", + "minimum-password-length-range": "Minimum password length should be in a range from 5 to 50", + "minimum-uppercase-letters": "Minimum number of uppercase letters", + "minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative", + "minimum-lowercase-letters": "Minimum number of lowercase letters", + "minimum-lowercase-letters-range": "Minimum number of lowercase letters can't be negative", + "minimum-digits": "Minimum number of digits", + "minimum-digits-range": "Minimum number of digits can't be negative", + "minimum-special-characters": "Minimum number of special characters", + "minimum-special-characters-range": "Minimum number of special characters can't be negative", + "password-expiration-period-days": "Password expiration period in days", + "password-expiration-period-days-range": "Password expiration period in days can't be negative", + "password-reuse-frequency-days": "Password reuse frequency in days", + "password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative", + "allow-whitespace": "Allow whitespace", + "general-policy": "General policy", + "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked", + "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", + "user-lockout-notification-email": "In case user account lockout, send notification to email", + "domain-name": "Domain name", + "domain-name-unique": "Domain name and protocol need to unique.", + "domain-name-max-length": "Domain name should be less than 256", + "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", + "connection-settings": "Connection settings", + "oauth2": { + "access-token-uri": "Access token URI", + "access-token-uri-required": "Access token URI is required.", + "activate-user": "Activate user", + "add-domain": "Add domain", + "delete-domain": "Delete domain", + "add-provider": "Add provider", + "delete-provider": "Delete provider", + "allow-user-creation": "Allow user creation", + "always-fullscreen": "Always fullscreen", + "authorization-uri": "Authorization URI", + "authorization-uri-required": "Authorization URI is required.", + "client-authentication-method": "Client authentication method", + "client-id": "Client ID", + "client-id-required": "Client ID is required.", + "client-id-max-length": "Client ID should be less than 256", + "client-secret": "Client secret", + "client-secret-required": "Client secret is required.", + "client-secret-max-length": "Client secret should be less than 2049", + "custom-setting": "Custom settings", + "customer-name-pattern": "Customer name pattern", + "customer-name-pattern-max-length": "Customer name pattern should be less than 256", + "parent-customer-name-pattern": "Parent customer name pattern", + "user-groups-name-pattern": "User groups name pattern", + "default-dashboard-name": "Default dashboard name", + "default-dashboard-name-max-length": "Default dashboard name should be less than 256", + "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", + "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", + "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", + "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", + "email-attribute-key": "Email attribute key", + "email-attribute-key-required": "Email attribute key is required.", + "email-attribute-key-max-length": "Email attribute key should be less than 32", + "first-name-attribute-key": "First name attribute key", + "first-name-attribute-key-max-length": "First name attribute key should be less than 32", + "general": "General", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "Last name attribute key", + "last-name-attribute-key-max-length": "Last name attribute key should be less than 32", + "login-button-icon": "Login button icon", + "login-button-label": "Provider label", + "login-button-label-placeholder": "Login with $(Provider label)", + "login-button-label-required": "Label is required.", + "login-provider": "Login provider", + "mapper": "Mapper", + "new-domain": "New domain", + "oauth2": "OAuth2", + "password-max-length": "Password should be less than 256", + "redirect-uri-template": "Redirect URI template", + "copy-redirect-uri": "Copy redirect URI", + "registration-id": "Registration ID", + "registration-id-required": "Registration ID is required.", + "registration-id-unique": "Registration ID need to unique for the system.", + "scope": "Scope", + "scope-required": "Scope is required.", + "tenant-name-pattern": "Tenant name pattern", + "tenant-name-pattern-required": "Tenant name pattern is required.", + "tenant-name-pattern-max-length": "Tenant name pattern ishould be less than 256", + "tenant-name-strategy": "Tenant name strategy", + "type": "Mapper type", + "uri-pattern-error": "Invalid URI format.", + "url": "URL", + "url-pattern": "Invalid URL format.", + "url-required": "URL is required.", + "url-max-length": "URL should be less than 256", + "user-info-uri": "User info URI", + "user-info-uri-required": "User info URI is required.", + "username-max-length": "User name should be less than 256", + "user-name-attribute-name": "User name attribute key", + "user-name-attribute-name-required": "User name attribute key is required", + "protocol": "Protocol", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Enable OAuth2 settings", + "domains": "Domains", + "mobile-apps": "Mobile applications", + "no-mobile-apps": "No applications configured", + "mobile-package": "Application package", + "mobile-package-placeholder": "Ex.: my.example.app", + "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", + "mobile-package-unique": "Application package must be unique.", + "mobile-app-secret": "Application secret", + "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", + "copy-mobile-app-secret": "Copy application secret", + "add-mobile-app": "Add application", + "delete-mobile-app": "Delete application info", + "providers": "Providers", + "platform-web": "Web", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "All platforms", + "smtp-provider": "SMTP provider", + "allowed-platforms": "Allowed platforms", + "authentication": "Authentication", + "basic": "Basic", + "provider": "Provider", + "redirect-url": "Redirect URI", + "domain-name": "Domain name", + "redirect-url-template": "Redirect URI template", + "microsoft-tenant-id": "Directory (tenant) Id", + "microsoft-tenant-id-required": "Directory (tenant) Id is required", + "token-uri": "Token URI", + "token-uri-required": "Token URI is required", + "redirect-uri": "Redirect URI", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "Custom", + "generate-access-token": "Generate access token", + "update-access-token": "Update access token", + "access-token-status": "Access token status:", + "token-status-generated": "generated", + "token-status-not-generated": "not generated" + }, + "smpp-provider": { + "smpp-version": "SMPP version", + "smpp-host": "SMPP host", + "smpp-host-required": "SMPP host is required", + "smpp-port": "SMPP port", + "smpp-port-required": "SMPP port is required", + "system-id": "System ID", + "system-id-required": "System ID is required", + "password": "Password", + "password-required": "Password is required", + "type-settings": "Type settings", + "source-settings": "Source settings", + "destination-settings": "Destination settings", + "additional-settings": "Additional settings", + "system-type": "System type", + "bind-type": "Bind type", + "service-type": "Service type", + "source-address": "Source address", + "source-ton": "Source TON", + "source-npi": "Source NPI", + "destination-ton": "Destination TON (Type of Number)", + "destination-npi": "Destination NPI (Numbering Plan Identification)", + "address-range": "Address range", + "coding-scheme": "Coding scheme", + "bind-type-tx": "Transmitter", + "bind-type-rx": "Receiver", + "bind-type-trx": "Transciever", + "ton-unknown": "Unknown", + "ton-international": "International", + "ton-national": "National", + "ton-network-specific": "Network Specific", + "ton-subscriber-number": "Subscriber Number", + "ton-alphanumeric": "Alphanumeric", + "ton-abbreviated": "Abbreviated", + "npi-unknown": "0 - Unknown", + "npi-isdn": "1 - ISDN/telephone numbering plan (E163/E164)", + "npi-data-numbering-plan": "3 - Data numbering plan (X.121)", + "npi-telex-numbering-plan": "4 - Telex numbering plan (F.69)", + "npi-land-mobile": "6 - Land Mobile (E.212)", + "npi-national-numbering-plan": "8 - National numbering plan", + "npi-private-numbering-plan": "9 - Private numbering plan", + "npi-ermes-numbering-plan": "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - Internet (IP)", + "npi-wap-client-id": "18 - WAP Client Id (to be defined by WAP Forum)", + "scheme-smsc": "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free)", + "scheme-ia5": "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - Octet Unspecified (8-bit binary)", + "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - Octet Unspecified (8-bit binary)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Cyrillic (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Latin/Hebrew (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - Pictogram Encoding", + "scheme-music-codes": "10 - Music Codes (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - Extended Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)" + }, + "queue-select-name": "Select queue name", + "queue-name": "Name", + "queue-name-required": "Queue name is required!", + "queues": "Queues", + "queue-partitions": "Partitions", + "queue-submit-strategy": "Submit strategy", + "queue-processing-strategy": "Processing strategy", + "queue-configuration": "Queue configuration", + "repository-settings": "Repository settings", + "repository": "Repository", + "repository-url": "Repository URL", + "repository-url-required": "Repository URL is required.", + "default-branch": "Default branch name", + "repository-read-only": "Read-only", + "show-merge-commits": "Show merge commits", + "authentication-settings": "Authentication settings", + "auth-method": "Authentication method", + "auth-method-username-password": "Password / access token", + "auth-method-username-password-hint": "GitHub users must use access tokens with write permissions to the repository.", + "auth-method-private-key": "Private key", + "password-access-token": "Password / access token", + "change-password-access-token": "Change password / access token", + "private-key": "Private key", + "drop-private-key-file-or": "Drag and drop a private key file or", + "passphrase": "Passphrase", + "enter-passphrase": "Enter passphrase", + "change-passphrase": "Change passphrase", + "check-access": "Check access", + "check-repository-access-success": "Repository access successfully verified!", + "delete-repository-settings-title": "Are you sure you want to delete repository settings?", + "delete-repository-settings-text": "Be careful, after the confirmation the repository settings will be removed and version control feature will be unavailable.", + "auto-commit-settings": "Auto-commit settings", + "auto-commit": "Auto-commit", + "auto-commit-entities": "Auto-commit entities", + "no-auto-commit-entities-prompt": "No entities configured for auto-commit", + "delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?", + "delete-auto-commit-settings-text": "Be careful, after the confirmation the auto-commit settings will be removed and auto-commit will be disabled for all entities.", + "2fa": { + "2fa": "Two-factor authentication", + "available-providers": "Available providers", + "issuer-name": "Issuer name", + "issuer-name-required": "Issuer name is required.", + "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", + "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", + "number-of-checking-attempts": "Number of checking attempts", + "number-of-checking-attempts-pattern": "Number of checking attempts must be a positive integer.", + "number-of-checking-attempts-required": "Number of checking attempts is required.", + "number-of-codes": "Number of codes", + "number-of-codes-pattern": "Number of codes must be a positive integer.", + "number-of-codes-required": "Number of codes is required.", + "provider": "Provider", + "retry-verification-code-period": "Retry verification code period (sec)", + "retry-verification-code-period-pattern": "Minimal period time is 5 sec", + "retry-verification-code-period-required": "Retry verification code period is required.", + "total-allowed-time-for-verification": "Total allowed time for verification (sec)", + "total-allowed-time-for-verification-pattern": "Minimal total allowed time is 60 sec", + "total-allowed-time-for-verification-required": "Total allowed time is required.", + "use-system-two-factor-auth-settings": "Use system two factor auth settings", + "verification-code-check-rate-limit": "Verification code check rate limit", + "verification-code-lifetime": "Verification code lifetime (sec)", + "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", + "verification-code-lifetime-required": "Verification code lifetime is required.", + "verification-message-template": "Verification message template", + "verification-limitations": "Verification limitations", + "verification-message-template-pattern": "Verification message need to contains pattern: ${code}", + "verification-message-template-required": "Verification message template is required.", + "within-time": "Within time (sec)", + "within-time-pattern": "Time must be a positive integer.", + "within-time-required": "Time is required." + }, + "jwt": { + "security-settings": "JWT security settings", + "issuer-name": "Issuer name", + "issuer-name-required": "Issuer name is required.", + "signings-key": "Signing key", + "signings-key-hint": "Base64 encoded string representing at least 256 bits of data.", + "signings-key-required": "Signing key is required.", + "signings-key-min-length": "Signing key must be at least 256 bits of data.", + "signings-key-base64": "Signing key must be base64 format.", + "expiration-time": "Token expiration time (sec)", + "expiration-time-required": "Token expiration time is required.", + "expiration-time-pattern": "Token expiration time be a positive integer.", + "expiration-time-min": "Minimum time is 60 seconds (1 minute).", + "refresh-expiration-time": "Refresh token expiration time (sec)", + "refresh-expiration-time-required": "Refresh token expiration time is required.", + "refresh-expiration-time-pattern": "Refresh token expiration time be a positive integer.", + "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", + "refresh-expiration-time-less-token": "Refresh token time must be greater token time.", + "generate-key": "Generate key", + "info-header": "All users will be to re-logined", + "info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets." + }, + "resources": "Resources", + "notifications": "Notifications", + "notifications-settings": "Notifications settings", + "slack-api-token": "Slack API token", + "slack": "Slack", + "slack-settings": "Slack settings" + }, + "alarm": { + "alarm": "Įspėjimas", + "alarms": "Įspėjimai", + "all-alarms": "Visi Įspėjimai", + "select-alarm": "Pasirinkti įspėjimą", + "no-alarms-matching": "Su '{{entity}}' susijusių įspėjimų nėra.", + "alarm-required": "Įspėjimas yra privalomas", + "alarm-filter": "Įspėjimų filtras", + "filter": "Filtras", + "alarm-status": "Įspėjimo statusas", + "alarm-status-list": "Įspėjimo statusų sąrašas", + "any-status": "Visi statusai", + "search-status": { + "ANY": "Visi", + "ACTIVE": "Aktyvūs", + "CLEARED": "Atmesti", + "ACK": "Pripažinti", + "UNACK": "Nepatvirtinti" + }, + "display-status": { + "ACTIVE_UNACK": "Aktyvūs nepatvirtinti", + "ACTIVE_ACK": "Aktyvūs pripažinti", + "CLEARED_UNACK": "Atmesti nepatvirtinti", + "CLEARED_ACK": "Atmesti pripažinti" + }, + "no-alarms-prompt": "Įspėjimų nėra", + "created-time": "Sukūrimo laikas", + "type": "Tipas", + "severity": "Lygis", + "originator": "Iniciatorius", + "originator-type": "Iniciatoriaus tipas", + "details": "Pastabos", + "originator-label": "Iniciatoriaus pavadinimas", + "assign": "Priskirti", + "assignments": "Priskyrimai", + "assignee": "Atsakingas", + "assignee-id": "Atsakingo asmens ID", + "assignee-first-name": "Atsakingo asmens vardas", + "assignee-last-name": "Atsakingo asmens pavardė", + "assignee-email": "Atsakingo asmens e-paštas", + "unassigned": "Panaikinti priskyrimą", + "assignee-not-set": "Visi", + "status": "Statusas", + "alarm-details": "Įspėjimo pastabos", + "start-time": "Pradžia", + "assign-time": "Priskyrimo laikas", + "end-time": "Pabaiga", + "ack-time": "Pripažinimo laikas", + "clear-time": "Atmetimo laikas", + "duration": "Trukmė", + "alarm-severity-list": "Įspėjimų lygiai", + "any-severity": "Visi lygiai", + "severity-critical": "Kritinis", + "severity-major": "Svarbus", + "severity-minor": "Antraeilis", + "severity-warning": "Įspėjimas", + "severity-indeterminate": "Nenustatytas", + "acknowledge": "Patvirtinti", + "clear": "Atmesti", + "delete": "Panaikinti", + "search": "Įspėjimo paieška", + "selected-alarms": "Pasirinkta { count, plural, =1 {1 įspėjimas} other {# įspėjimai} }", + "no-data": "Nėra duomenų", + "polling-interval": "Įspėjimų tikrinimo intervalas (sek)", + "polling-interval-required": "Reikalingas įspėjimų tikrinimo intervalas", + "min-polling-interval-message": "Mažiausias įspėjimų tikrinimo intervalas yra 1 sekundė.", + "aknowledge-alarms-title": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "aknowledge-alarms-text": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "aknowledge-alarm-title": "Pripažinti įspėjimą", + "aknowledge-alarm-text": "Pripažinti įspėjimą?", + "selected-alarms-are-acknowledged": "Pasirinkti įspėjimai pripažinti", + "clear-alarms-title": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "clear-alarms-text": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "clear-alarm-title": "Atmesti įspėjimą", + "clear-alarm-text": "Atmesti įspėjimą?", + "delete-alarms-title": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "delete-alarms-text": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "selected-alarms-are-cleared": "Pasirinkti įspėjimai atmesti", + "alarm-status-filter": "Įspėjimų statusai", + "alarm-filter-title": "Įspėjimų filtras", + "assigned": "Priskirta", + "filter-title": "Filtras", + "max-count-load": "Didžiausias rodomų įspėjimų skaičius (0 – neribotas)", + "max-count-load-required": "Reikalingas didžiausias rodomų įspėjimų skaičius (0 – neribotas).", + "max-count-load-error-min": "Mažiausia reikšmė yra 0.", + "fetch-size": "Fetch size", + "fetch-size-required": "Fetch size is required.", + "fetch-size-error-min": "Mažiausia rekšmė yra 10.", + "alarm-type-list": "Įspėjimų tipai", + "any-type": "Visi tipai", + "assigned-to-current-user": "Priskirta prisijungusiam vartotojui", + "assigned-to-me": "Priskirta man", + "search-propagated-alarms": "Ieškoti išplatintų įspėjimų", + "comments": "Įspėjimo pastabos", + "show-more": "Rodyti daugiau", + "additional-info": "Papildoma informacija", + "alarm-type": "Įspėjimo tipas", + "enter-alarm-type": "Įveskite įspėjimo tipą", + "no-alarm-types-matching": "Nėra įspėjimų tipų, kurie atitiktų '{{entitySubtype}}'.", + "alarm-type-list-empty": "Nepasirinktas įspėjimų tipas" + }, + "alarm-activity": { + "add": "Pridėti komentarą...", + "alarm-comment": "Įspėjimo komentaras", + "comments": "Komentarai", + "delete-alarm-comment": "Ištrinti komentarą?", + "refresh": "Atnaujinti", + "oldest-first": "Seniausi viršuje", + "newest-first": "Naujausi viršuje", + "activity": "Veikla", + "export": "Eksportuoti į CSV", + "author": "Sukūrė", + "created-date": "Sukūrimo dara", + "edited-date": "Redagavimo data", + "text": "Tekstas", + "system": "Sistema" + }, + "alias": { + "add": "Pridėti pseudonimą", + "edit": "Redaguoti pseudonimą", + "name": "Pseudonimas", + "name-required": "Reikalingas pseudonimas", + "duplicate-alias": "Toks pseudonimas sistemoje jau yra.", + "filter-type-single-entity": "Vienas subjektas", + "filter-type-entity-group": "Subjektų grupė", + "filter-type-entity-list": "Subjektų sąrašas", + "filter-type-entity-name": "Subjekto pavadinimas", + "filter-type-entity-type": "Subjekto tipas", + "filter-type-entity-group-list": "Subjektų grupių sąrašas", + "filter-type-entity-group-name": "Subjektų grupės pavadinimas", + "filter-type-entities-by-group-name": "Subjektai pagal grupės pavadinimą", + "filter-type-state-entity": "Subjektas iš skydelio būsenos", + "filter-type-state-entity-description": "Subjektas iš skydelio būsenos parametrų", + "filter-type-state-entity-owner": "Skydelio būsenos subjekto savininkas", + "filter-type-state-entity-owner-description": "Subjekto, iš skydelio būsenos parametrų, savininkas", + "filter-type-asset-type": "Turto tipas", + "filter-type-asset-type-description": "Turtas, kurio tipas '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Turtas, kurio tipas '{{assetTypes}}' ir kurio pavadinimas prasideda '{{prefix}}'", + "filter-type-device-type": "Įrenginio tipas", + "filter-type-device-type-description": "Įrenginiai, kurių tipas '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Įrenginiai, kurių tipas '{{deviceTypes}}' ir kurių pavadinimai '{{prefix}}'", + "filter-type-entity-view-type": "Subjekto rodinio tipas", + "filter-type-entity-view-type-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}' ir kurių pavadinimai prasideda '{{prefix}}'", + "filter-type-edge-type": "Edge type", + "filter-type-edge-type-description": "Edges of type '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Edges of type '{{edgeTypes}}' and with name starting with '{{prefix}}'", + "filter-type-relations-query": "Ryšių užklausa", + "filter-type-relations-query-description": "{{entities}} kurie turi {{relationType}} ryšį {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Edge search query", + "filter-type-edge-search-query-description": "Edges with types {{edgeTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-scheduler-event": "Scheduler events", + "filter-type-scheduler-event-type-description": "Scheduler events with type '{{eventType}}'", + "filter-type-scheduler-event-originator-description": "Scheduler events of specific originator", + "filter-type-scheduler-event-type-originator-description": "Scheduler events of specific originator with '{{eventType}}'", + "filter-type-asset-search-query": "Turto paieškos užklausa", + "filter-type-asset-search-query-description": "{{assetTypes}} tipo turtas, turintis {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Įrenginio paieškos užklausa", + "filter-type-device-search-query-description": "{{deviceTypes}} tipo įrenginiai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Subjektų rodinio paieškos užklausa", + "filter-type-entity-view-search-query-description": "{{entityViewTypes}} tipo subjektai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "Api Usage State", + "entity-filter": "Subjektų filtras", + "resolve-multiple": "Traktuoti kaip kelis subjektus", + "filter-type": "Filtro tipas", + "filter-type-required": "Filtro tipas būtinas.", + "entity-filter-no-entity-matched": "Subjektų, atitinkančių filtro kriterijus, nerasta.", + "no-entity-filter-specified": "Subjektų filtras nenustatytas", + "root-state-entity": "Subjektą, iš skydelio būsenos, naudoti kaip pagrindinį", + "group-state-entity": "Skydelio būsenos subjektą naudoti kaip subjektų grupę", + "group-state-entity-owner": "Skydelio būsenos subjektą naudoti kaip subjektų grupės sąvininką", + "last-level-relation": "Gauti tik paskutinio lygio ryšį", + "root-entity": "Pagridinis subjektas", + "state-entity-parameter-name": "Būsenos subjekto parametro pavadinimas", + "default-state-entity": "Numatytasis būsenos subjektas", + "default-state-entity-group": "Numatytoji būsenos subjektų grupė", + "default-entity-parameter-name": "Pagal nutylėjimą", + "max-relation-level": "Maksimalus ryšių lygis", + "unlimited-level": "Neribojamas lygis", + "state-entity": "Skydelio būsenos subjektas", + "entities-of-group-state-entity": "Subjektai iš skydelio būsenos subjektų grupės", + "all-entities": "Visi subjektai", + "any-relation": "visi", + "originator": "Iniciatorius", + "originator-state-entity": "Skydelio būsenos subjektą naudoti kaip iniciatorių", + "scheduler-event-type": "Scheduler event type" + }, + "asset": { + "all": "Visas", + "all-assets": "Visas turtas", + "groups": "Grupės", + "shared": "Bendrinamas", + "asset": "Turtas", + "assets": "Turtas", + "management": "Turto valdymas", + "view-assets": "Peržiūrėti turtą", + "add": "Pridėti turtą", + "asset-type-max-length": "Turto tipas turi būti trumpesnis nei 256 simboliai", + "assign-to-customer": "Priskirti klientui", + "assign-asset-to-customer": "Turtą priskirti klientui", + "assign-asset-to-customer-text": "Pasirinkite turtą, kurį norite priskiri klientui", + "no-assets-text": "Turto nerasta", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskiriamas turtas", + "public": "Viešas", + "assignedToCustomer": "Priskirti klientui", + "make-public": "Neriboti turto matomumo", + "make-private": "Riboti turto matomumą", + "unassign-from-customer": "Atsieti nuo kliento", + "delete": "Panaikinti turtą", + "asset-public": "Turtas yra viešas", + "asset-type": "Turto tipas", + "asset-type-required": "Turto tipas būtinas.", + "select-asset-type": "Pasirinkite turto tipą", + "enter-asset-type": "Įveskite turto tipą", + "any-asset": "Visas turtas", + "no-asset-types-matching": "Turto tipas, atitinkantis '{{entitySubtype}}', nerastas.", + "asset-type-list-empty": "Nepasirinkti turto tipai.", + "asset-types": "Turto tipai", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "label-max-length": "Vardas turi būti trumpesnis nei 256 simboliai", + "description": "Aprašymas", + "type": "Tipas", + "type-required": "Tipas yra prvalomas.", + "details": "Pastabos", + "events": "Įvykiai", + "add-asset-text": "Pridėti turtą", + "asset-details": "Pastabos apie turtą", + "assign-assets": "Priskirti turtą", + "assign-assets-text": "Priskirti { count, plural, =1 {1 turtą} other {# turtus} } to customer", + "assign-asset-to-edge-title": "Assign Asset(s) To Edge", + "assign-asset-to-edge-text": "Please select the assets to assign to the edge", + "delete-assets": "Pašalinti turtą", + "unassign-assets": "Atsieti turtą", + "unassign-assets-action-title": "Atsieti { count, plural, =1 {1 turtą} other {# turtus} } nuo kliento?", + "assign-new-asset": "Prisirti naują turtą", + "delete-asset-title": "Pašalinti '{{assetName}}' turtą?", + "delete-asset-text": "Būkite dėmesingi, po patvirtinimo, turtas ir su juo susijusi informacija, bus pašalinta.", + "delete-assets-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }?", + "delete-assets-action-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }", + "delete-assets-text": "Būkite dėmesingi, po patvirtinimo, visi turto įrašai ir su jais susijusi informacija, bus pašalinta.", + "make-public-asset-title": "Ar tikrai norite neriboti turto '{{assetName}}' matomumo?", + "make-public-asset-text": "Po patvirtinimo turtas ir visa su juo susijusi informacija bus prieinama visiems.", + "make-private-asset-title": "Ar tikrai norite riboti turto '{{assetName}}' matomumą?", + "make-private-asset-text": "Po patvirtinimo, turtas ir visa su juo susijusi informacija nebus prieinama kitiems.", + "unassign-asset-title": "Ar tikrai norite atsieti turtą '{{assetName}}'?", + "unassign-asset-text": "Po patvirtinimo turtas bus atsietas ir klientas neteks prieigos prie jo.", + "unassign-asset": "Atsieti turtą", + "unassign-assets-title": "Ar tikrai norite atsieti { count, plural, =1 {1 turtą} other {# turtus} }?", + "unassign-assets-text": "Po patvirtinimo visas turtas bus atsietas ir klientas neteks prieigos prie jo.", + "copyId": "Kopijuoti turto Id", + "idCopiedMessage": "Turto Id nukopijuotas į iškarpinę", + "select-asset": "Pasirinkti turtą", + "no-assets-matching": "Turtas atitinkantis '{{entity}}' nerastas.", + "asset-required": "Turtas būtinas", + "name-starts-with": "Turto pavadinimas prasideda", + "help-text": "naudokite simbolį '%' pagal tai, kaip norite ieškoti: '%turto_pavadinimo_fragmentas%', '%turto_pavadinimo_pabaiga', 'turto_pavadinimo_pradžia%'.", + "search": "Turto paieška", + "select-group-to-add": "Pasirinkti grupę, į kurią turtą pridėti", + "select-group-to-move": "Pasirinkite grupę, į kurią turtą perkelti", + "remove-assets-from-group": "Ar tikrai norite { count, plural, =1 {1 turtą} other {# turtus} } pašalinti iš grupės '{{entityGroup}}'?", + "group": "Turto grupė", + "list-of-groups": "{ count, plural, =1 {Viena turto grupė} other { # Turto grupių sąrašas} }", + "group-name-starts-with": "Turto grupės, kurių pavadinimai prasideda '{{prefix}}'", + "import": "Importuoti turtą", + "asset-file": "Turto failas", + "label": "Etiketė", + "assign-asset-to-edge": "Assign Asset(s) To Edge", + "unassign-asset-from-edge": "Unassign asset", + "unassign-asset-from-edge-title": "Are you sure you want to unassign the asset '{{assetName}}'?", + "unassign-asset-from-edge-text": "After the confirmation the asset will be unassigned and won't be accessible by the edge.", + "unassign-assets-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 asset} other {# assets} }?", + "unassign-assets-from-edge-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the edge.", + "selected-assets": "Pasirinkta { count, plural, =1 {1 turtas} other {# turtai} }" + }, + "attribute": { + "attributes": "Atributai", + "latest-telemetry": "Paskutinė telemetrija", + "no-latest-telemetry": "Telemetrijos nėra", + "attributes-scope": "Subjekto atributų kontekstas", + "scope-telemetry": "Telemetrija", + "scope-latest-telemetry": "Paskutinė telemetrija", + "scope-client": "Kliento atributai", + "scope-server": "Serverio atributai", + "scope-shared": "Bendrinami atributai", + "add": "Pridėti atributą", + "add-attribute-prompt": "Pridėkite atributą", + "key": "Raktas", + "key-max-length": "Raktas negali viršyti 256 simbolių", + "last-update-time": "Atnaujinimo laikas", + "key-required": "Atributo raktas būtinas.", + "value": "Reikšmė", + "value-required": "Atributo reikšmė būtina.", + "telemetry-key-required": "Telemetrijos raktas būtinas", + "telemetry-value-required": "Telemetrijos reikšmė būtina", + "delete-attributes-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 atributą} other {# atributus} }?", + "delete-attributes-text": "Būkite dėmesingi, po patvirtinimo pasirinkti atributai bus pašalinti.", + "delete-attributes": "Pašalinti atributus", + "enter-attribute-value": "Įveskite atributo reikšmę", + "show-on-widget": "Rodyti valdiklyje", + "widget-mode": "Valdiklio režimas", + "next-widget": "Sekantis valdiklis", + "prev-widget": "Ankstesnis valdiklis", + "add-to-dashboard": "Pridėti į skydelį", + "add-widget-to-dashboard": "Pridėti valdiklį į skydelį", + "selected-attributes": "Pasirinkta { count, plural, =1 {1 atributas} other {# atributai} }", + "selected-telemetry": "Pasirinkta { count, plural, =1 {1 telemetrijos blokas} other {# telemetrijos blokai} }", + "no-attributes-text": "Atributų nėra", + "no-telemetry-text": "Telemetrijos nėra", + "copy-key": "Kopijuoti raktą", + "add-telemetry": "Pridėti telemetriją", + "copy-value": "Kopijuoti reikšmę", + "delete-timeseries": { + "start-time": "Pradžia", + "ends-on": "Pabaiga", + "strategy": "Strategija", + "delete-strategy": "Šalinimo startegija", + "all-data": "Pašalinti visus duomenis", + "all-data-except-latest-value": "Pašalinti visus duomenis išskyrus naujausią reikšmę", + "latest-value": "Pašalinti naujausią reikšmę", + "all-data-for-time-period": "Pašalinti visus duomenis periodui", + "rewrite-latest-value": "Perrašyti naujausią reikšmę" + } + }, + "api-usage": { + "api-features": "API features", + "api-usage": "Api Usage", + "alarm": "Alarm", + "alarms-created": "Alarms created", + "alarms-created-daily-activity": "Alarms created daily activity", + "alarms-created-hourly-activity": "Alarms created hourly activity", + "alarms-created-monthly-activity": "Alarms created monthly activity", + "data-points": "Data points", + "data-points-storage-days": "Data points storage days", + "device-api": "Device API", + "email": "Email", + "email-messages": "Email messages", + "email-messages-daily-activity": "Email messages daily activity", + "email-messages-monthly-activity": "Email messages monthly activity", + "exceptions": "Exceptions", + "executions": "Executions", + "javascript": "JavaScript", + "javascript-executions": "JavaScript executions", + "javascript-functions": "JavaScript functions", + "javascript-functions-daily-activity": "JavaScript functions daily activity", + "javascript-functions-hourly-activity": "JavaScript functions hourly activity", + "javascript-functions-monthly-activity": "JavaScript functions monthly activity", + "latest-error": "Latest Error", + "messages": "Messages", + "notifications": "Notifications", + "notifications-email-sms": "Notifications (Email/SMS)", + "notifications-hourly-activity": "Notifications hourly activity", + "permanent-failures": "${entityName} Permanent Failures", + "permanent-timeouts": "${entityName} Permanent Timeouts", + "processing-failures": "${entityName} Processing Failures", + "processing-failures-and-timeouts": "Processing Failures and Timeouts", + "processing-timeouts": "${entityName} Processing Timeouts", + "queue-stats": "Queue Stats", + "rule-chain": "Rule Chain", + "rule-engine": "Rule Engine", + "rule-engine-daily-activity": "Rule Engine daily activity", + "rule-engine-executions": "Rule Engine executions", + "rule-engine-hourly-activity": "Rule Engine hourly activity", + "rule-engine-monthly-activity": "Rule Engine monthly activity", + "rule-engine-statistics": "Rule Engine Statistics", + "rule-node": "Rule Node", + "sms": "SMS", + "sms-messages": "SMS messages", + "sms-messages-daily-activity": "SMS messages daily activity", + "sms-messages-monthly-activity": "SMS messages monthly activity", + "successful": "${entityName} Successful", + "telemetry": "Telemetry", + "telemetry-persistence": "Telemetry persistence", + "telemetry-persistence-daily-activity": "Telemetry persistence daily activity", + "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", + "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", + "transport": "Transport", + "transport-daily-activity": "Transport daily activity", + "transport-data-points": "Transport data points", + "transport-hourly-activity": "Transport hourly activity", + "transport-messages": "Transport messages", + "transport-monthly-activity": "Transport monthly activity", + "view-details": "View details", + "view-statistics": "View statistics" + }, + "api-limit": { + "cassandra-queries": "Cassandra queries", + "entity-version-creation": "Entity version creation", + "entity-version-load": "Entity version load", + "integration-messages": "Integration messages", + "integration-messages-per-device": "Integration messages per device", + "notification-requests": "Notification requests", + "notification-requests-per-rule": "Notification requests per rule", + "reports-generation": "Reports generation", + "rest-api-requests": "REST API requests", + "rest-api-requests-per-customer": "REST API requests per customer", + "transport-messages": "Transport messages", + "transport-messages-per-device": "Transport messages per device", + "ws-updates-per-session": "WS updates per session" + }, + "audit-log": { + "audit": "Auditas", + "audit-logs": "Audito žurnalas", + "timestamp": "Laiko žyma", + "entity-type": "Subjektas", + "entity-name": "Pavadinimas", + "user": "Vartotojas", + "type": "Veiksmas", + "status": "Statusas", + "details": "Informacija", + "type-added": "Pridėta", + "type-deleted": "Pašalinta", + "type-updated": "Atnaujinta", + "type-attributes-updated": "Attributai atnaujinti", + "type-attributes-deleted": "Atributai pašalinti", + "type-rpc-call": "RPC iškvietimas", + "type-credentials-updated": "Įgaliojimai atnaujinti", + "type-assigned-to-customer": "Priskirta klientui", + "type-unassigned-from-customer": "Atsieta nuo kliento", + "type-assigned-to-edge": "Assigned to Edge", + "type-unassigned-from-edge": "Unassigned from Edge", + "type-activated": "Aktyvuota", + "type-suspended": "Sustabdyta", + "type-credentials-read": "Įgaliojimų skaitymas", + "type-attributes-read": "Atributų skaitymas", + "type-added-to-entity-group": "Įtraukta į grupę", + "type-removed-from-entity-group": "Pašalinta iš grupės", + "type-relation-add-or-update": "ryšys atnaujintas", + "type-relation-delete": "Ryšys panaikintas", + "type-relations-delete": "Visi ryšiai panaikinti", + "type-alarm-ack": "Pripažinta", + "type-alarm-clear": "Išvalyta", + "type-alarm-assign": "Priskitas", + "type-alarm-unassign": "Atsietas", + "type-added-comment": "Pridėtas komentaras", + "type-updated-comment": "Atnaujintas komentaras", + "type-deleted-comment": "Pašalintas komentaras", + "type-rest-api-rule-engine-call": "Rule engine REST API call", + "type-made-public": "Padaryti viešu", + "type-made-private": "Padaryti privačiu", + "type-login": "Prisijungti", + "type-logout": "Atsijungti", + "type-lockout": "Užblokuoti", + "status-success": "Sėkmė", + "status-failure": "Nesėkmė", + "audit-log-details": "Informacija apie autido žurnalą", + "no-audit-logs-prompt": "Žurnalo įrašų nėra", + "action-data": "Informacija apie veiksmus", + "failure-details": "Informacija apie nesėkmę", + "search": "Žurnalo įrašų paieška", + "clear-search": "Išvalyti paiešką", + "type-assigned-from-tenant": "Priskirta valdytojo", + "type-assigned-to-tenant": "Priskirta valdytojui", + "type-provision-success": "Device provisioned", + "type-provision-failure": "Device provisioning was failed", + "type-timeseries-updated": "Telemetrija atnaujinta", + "type-timeseries-deleted": "Telemetrija pašalinta", + "type-owner-changed": "Savininkas pakeistas", + "type-sms-sent": "SMS sent" + }, + "confirm-on-exit": { + "message": "Turite neišsaugotų pakeitimų. Ar tikrai norite palikti šį puslapį?", + "html-message": "Turite neišsaugotų pakeitimų.
Ar tikrai norite palikti šį puslapį?", + "title": "Neišsaugoti pakeitimai" + }, + "contact": { + "country": "Šalis", + "city": "Miestas", + "state": "Rajonas", + "postal-code": "Pašto kodas", + "postal-code-invalid": "Neteisingas pašto kodo formatas.", + "address": "Adresas", + "address2": "Adresas 2", + "phone": "Telefonas", + "email": "El. paštas", + "no-address": "Adresas nenurodytas", + "state-max-length": "Rajono pavadinimas negali viršyti 256 simbolių", + "phone-max-length": "Telefono numeris negali viršyti 256 simbolių", + "city-max-length": "Miesto pavadinimas negali viršyti 256 simbolių" + }, + "common": { + "username": "Vartoto vardas", + "password": "Slaptažodis", + "enter-username": "Įveskite vartotojo vardą", + "enter-password": "Įveskite slaptažodį", + "enter-search": "Įveskite paieškos kriterijus", + "created-time": "Sukūrimo laikas", + "loading": "Įkeliama...", + "proceed": "Tęsti", + "open-details-page": "Informacija", + "not-found": "Nerasta", + "documentation": "Dokumentacija" + }, + "converter": { + "converter": "Data converter", + "converters": "Data converters", + "select-converter": "Select data converter", + "no-converters-matching": "No data converters matching '{{entity}}' were found.", + "no-converters-found": "No data converters found.", + "converter-required": "Data converter is required", + "delete": "Delete converter", + "management": "Data converters management", + "add-converter-text": "Add new data converter", + "no-converters-text": "No data converters found", + "selected-converters": "{ count, plural, =1 {1 data converter} other {# data converters} } selected", + "delete-converter-title": "Are you sure you want to delete the data converter '{{converterName}}'?", + "delete-converter-text": "Be careful, after the confirmation the data converter and all related data will become unrecoverable.", + "delete-converters-title": "Are you sure you want to delete { count, plural, =1 {1 data converter} other {# data converters} }?", + "delete-converters-action-title": "Delete { count, plural, =1 {1 data converter} other {# data converters} }", + "delete-converters-text": "Be careful, after the confirmation all selected data converters will be removed and all related data will become unrecoverable.", + "events": "Events", + "add": "Add Data Converter", + "search": "Search data converters", + "converter-details": "Data converter details", + "details": "Details", + "copyId": "Copy converter Id", + "idCopiedMessage": "Converter Id has been copied to clipboard", + "debug-mode": "Debug mode", + "created-time": "Created time", + "name": "Name", + "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", + "description": "Description", + "decoder": "Decoder", + "encoder": "Encoder", + "test-decoder-fuction": "Test decoder function", + "test-encoder-fuction": "Test encoder function", + "test-with-this-message": "{{test}} with this message", + "decoder-input-params": "Decoder input parameters", + "encoder-input-params": "Encoder input parameters", + "payload": "Payload", + "payload-content-type": "Payload content type", + "payload-content": "Payload content", + "message": "Message", + "message-type": "Message type", + "message-type-required": "Message type is required", + "test": "Test", + "metadata": "Metadata", + "metadata-required": "Metadata entries can't be empty.", + "integration-metadata": "Integration metadata", + "integration-metadata-required": "Integration metadata entries can't be empty.", + "output": "Output", + "import": "Import converter", + "export": "Export converter", + "export-failed-error": "Unable to export converter: {{error}}", + "create-new-converter": "Create new converter", + "converter-file": "Converter file", + "invalid-converter-file-error": "Unable to import converter: Invalid converter data structure.", + "type": "Type", + "type-required": "Type is required.", + "type-uplink": "Uplink", + "type-downlink": "Downlink", + "update-only-keys-list": "Update only keys list", + "update-only-keys-list-hint": "The values associated with the provided keys will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and telemetry in the converter output.", + "add-key": "Add key" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binary (Base64)" + }, + "color": { + "color": "Color", + "color-picker": "Color picker", + "primary-colors": "Primary colors", + "accent-colors": "Accent colors", + "no-color-selected": "No color selected" + }, + "customer": { + "all": "Visi", + "all-customers": "Visi klientai", + "groups": "Grupės", + "shared": "Bendrinami", + "hierarchy": "Hierarchija", + "customer": "Klientas", + "customers": "Klientai", + "management": "Klientų valdymas", + "dashboard": "Klientų skydelis", + "dashboards": "Kliientų skydeliai", + "devices": "Kliento įrenginiai", + "entity-views": "Kliento subjektų rodiniai", + "assets": "Kliento turtas", + "public-dashboards": "Vieši skydeliai", + "public-devices": "Vieši įrenginiai", + "public-assets": "Viešas turtas", + "public-entity-views": "Vieši subjektų rodiniai", + "add": "Pridėti klientą", + "delete": "Pašalinti klientą", + "manage-customer-user-groups": "Kliento vartotojų grupės", + "manage-customer-groups": "Klientų grupės", + "manage-customer-device-groups": "Kliento įrenginių grupės", + "manage-customer-asset-groups": "Kliento turto grupės", + "manage-customer-entity-view-groups": "Kliento subjektų rodinių grupės", + "manage-customer-edge-groups": "Manage customer edge groups", + "manage-customer-dashboard-groups": "Kliento skydelių grupės", + "manage-customer-users": "Kliento vartotojai", + "manage-customers": "Klientai", + "manage-customer-devices": "Kliento įrenginiai", + "manage-customer-entity-views": "Kliento subjektų rodiniai", + "manage-customer-dashboards": "Kliento skydeliai", + "manage-public-devices": "Vieši įrenginiai", + "manage-public-dashboards": "Vieši skydeliai", + "manage-customer-assets": "Kliento turtas", + "manage-customer-edges": "Manage customer edges", + "manage-public-assets": "Viešas turtas", + "add-customer-text": "Pridėti naują klientą", + "no-customers-text": "Klientų nėra", + "customer-details": "Informacija apie klientą", + "delete-customer-title": "Ar tikrai norite pašalinti klientą '{{customerTitle}}'?", + "delete-customer-text": "Būkite dėmesingi, po patvirtinimo, kliento ir su juo susijusios informacijos atkurti nebegalėsite.", + "delete-customers-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 klientą} other {# klientus} }?", + "delete-customers-action-title": "Pašalinti { count, plural, =1 {1 klientą} other {# klientus} }", + "delete-customers-text": "Būkite dėmesingi, po patvirtinimo, klientų ir su jais susijusios informacijos atkurti nebegalėsite.", + "manage-user-groups": "Vartotjų grupės", + "manage-asset-groups": "turto grupės", + "manage-device-groups": "Įrenginių grupės", + "manage-dashboard-groups": "Skydelių grupės", + "manage-entity-view-groups": "Subjektų rodinių grupės", + "manage-edge-groups": "Manage edge groups", + "manage-users": "Vartotojai", + "manage-assets": "Turtas", + "manage-devices": "Įrenginiai", + "manage-dashboards": "Skydeliai", + "manage-entity-views": "Subjektų rodiniai", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "description": "Aprašymas", + "details": "Informacija", + "events": "Įvykiai", + "copyId": "Kopijuoti kliento Id", + "idCopiedMessage": "Kliento Id nukopijuotas į iškarpinę", + "select-customer": "Pasirinkti klientą", + "no-customers-matching": "Klientų, atitinkančių '{{entity}}' nėra.", + "customer-required": "Klientas būtinas", + "select-group-to-add": "Pasirinkite grupę, į kurią pridėti pažymėtus klientus", + "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pažymėtus klientus", + "remove-customers-from-group": "Ar tikrai { count, plural, =1 {1 klientą} other {# klientus} } norite pašalinti iš grupės '{{entityGroup}}'?", + "group": "Klientų grupė", + "list-of-groups": "{ count, plural, =1 {Viena kliento grupė} other {Sąrašas # klientų grupių} }", + "group-name-starts-with": "Klientų grupės, kurios prasideda '{{prefix}}'", + "select-default-customer": "Pasirinkite numatytąjį klientą", + "default-customer": "Numatytasis klientas", + "default-customer-required": "Numatytasis klientas būtinas, norint suderinti prietaisų skydelį valdytojo lygiu", + "allow-white-labeling": "Leisti tinkinti aplikaciją", + "search": "Klientų paieška", + "selected-customers": "Pasirinkta { count, plural, =1 {1 klientas} other {# kientai} }", + "edges": "Customer edge instances", + "manage-edges": "Manage edges" + }, + "customers-hierarchy": { + "customers-hierarchy": "Klientų hierarchija", + "open-nav-tree": "Atverti navigacijos medį", + "return-to-top-level": "Grįžti į aukščiausią lygį" + }, + "custom-menu": { + "custom-menu": "Vartotojo meniu", + "custom-menu-hint": "Aprašykite pasirinktinį meniu JSON formoje. Šiame JSON nurodomas vartotojo meniu elementų sąrašas." + }, + "custom-translation": { + "custom-translation": "Vartotojo vertimai", + "translation-map": "Vertimų žemėlapis", + "key": "Vertimo raktas", + "import": "Importuoti vertimą", + "export": "Eksportuoti vertimą", + "export-data": "Eksportuoti vertimo duomenis", + "import-data": "Importuoti vertimo duomenis", + "translation-file": "Vertimų failas", + "invalid-translation-file-error": "Nepavyksta importuoti vertimo failo: Neteisinga duomenų struktūra.", + "custom-translation-hint": "Aprašykite vartotojo vertimą JSON formatu žemiau. Šis JSON perrašys standartinį vertimą. Paspaudę 'Parsisiųsti lokalės failą', gausite standartinio vertimo failą, kuriame galite surasti reikalingas raktų-reikšmių poros ir jas pakeisti savo JSON.", + "download-locale-file": "Parsisųsti lokalės failą" + }, + "date": { + "last-update-n-ago": "Last update N ago", + "last-update-n-ago-text": "Last update {{ agoText }}", + "custom-date": "Custom date", + "format": "Format", + "preview": "Preview" + }, + "datetime": { + "date-from": "Data nuo", + "time-from": "Laikas nuo", + "date-to": "Data iki", + "time-to": "Laikas iki" + }, + "dashboard": { + "all": "Visi", + "all-dashboards": "Visi skydeliai", + "groups": "Grupės", + "shared": "Bendrinami", + "dashboard": "Skydelis", + "dashboards": "Skydeliai", + "management": "Skydelių valdymas", + "view-dashboards": "Paržiūrėti skydelius", + "add": "Pridėti skydelį", + "assign-dashboard-to-customer": "Skydelį (-ius) priskirti klientui", + "assign-dashboard-to-customer-text": "Pasirinkite skydelius, kuriuos norite priskirti klientui", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti skydelį (-ius)", + "assign-to-customer": "Priskirti kientui", + "unassign-from-customer": "Atsieti nuo kliento", + "make-public": "Skydelį padaryti viešą", + "make-private": "Skydelį padaryti privatų", + "manage-assigned-customers": "Valdyti priskirtus klientus", + "assigned-customers": "Priskirti klientai", + "assign-to-customers": "Skydelį (-ius) priskirti klientams", + "assign-to-customers-text": "Pasirinkite klientus, kuriems priskirti skydelį (-ius)", + "unassign-from-customers": "Skydelį (-ius) atsieti nuo klientų", + "unassign-from-customers-text": "Pasirinkite klientus, kuriems atsieti skydelį (-ius)", + "no-dashboards-text": "Skydelių nėra", + "no-widgets": "Valdikliai nesukonfigūruoti", + "add-widget": "Pridėti naują valdiklį", + "add-widget-button-text": "Pridėt valdiklį", + "title": "Pavadinimas", + "image": "Skydelio paveiksliukas", + "mobile-app-settings": "Mobiliosios aplikacijos nustatymai", + "mobile-order": "Skydelio eilės tvarka mobiliojoje aplikacijoje", + "mobile-hide": "Mobiliojoje aplikacijoje skydelį paslėpti", + "update-image": "Atnaujinti skydelio paveiksliuką", + "take-screenshot": "Padaryti ekrano nuotrauką", + "select-widget-title": "Pasirinkite valdiklį", + "select-widget-value": "{{title}}: pasirinkite valdiklį", + "select-widget-subtitle": "Galimų valdiklių sąrašas", + "delete": "Pašalinti skydelį", + "title-required": "Pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "description": "Aprašymas", + "details": "Informacija", + "dashboard-details": "Informacija", + "add-dashboard-text": "Pridėti naują skydelį", + "assign-dashboards": "Priskirti skydelius", + "assign-new-dashboard": "Priskirti naują skydelį", + "assign-dashboards-text": "Priskirti { count, plural, =1 {1 skydelį} other {# skydelius} } klientams", + "unassign-dashboards-action-text": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo klientų", + "delete-dashboards": "Panaikinti skydelius", + "unassign-dashboards": "Atsieti skydelius", + "unassign-dashboards-action-title": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo kliento", + "delete-dashboard-title": "Ar tikrai norite pašalinti skydelį '{{dashboardTitle}}'?", + "delete-dashboard-text": "Būkite dėmesingi, po patvirtinimo, skydelio ir su juo susijusios informacijos atkurti nebegalėsite.", + "delete-dashboards-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "delete-dashboards-action-title": "Pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }", + "delete-dashboards-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti skydeliai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", + "unassign-dashboard-title": "Ar tikrai norite atsieti skydelį '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Po patvrtinimo skydelis bus atsietas ir klientas jo nebematys.", + "unassign-dashboard": "Atsieti skydelį", + "unassign-dashboards-title": "Ar tikrai norite atsieti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "unassign-dashboards-text": "Po patvirtinimo visi skydeliai bus atsieti ir klientas jų nebematys.", + "public-dashboard-title": "Skydelis dabar yra viešas", + "public-dashboard-text": "Skydelis {{dashboardTitle}} dabar yra viešas ir pasiekiamas per šią nuorodą:", + "public-dashboard-notice": "Pastaba: Įrenginiai turi būti vieši, jei norite matyti jų duomenis.", + "public-dashboard-link": "Vieša skydelio nuoroda", + "public-dashboard-link-text": "Viešą sydelį {{dashboardTitle}} galima atidaryti per šią nuorodą:", + "public-dashboard-link-notice": "Pastaba: Įrenginiai, turtas ir subjektai turi būti vieši, jei norite matyti jų duomenis.", + "make-private-dashboard-title": "Ar tikrai norite skydelį '{{dashboardTitle}}' padaryti privačiu?", + "make-private-dashboard-text": "Po patvirtinimo, skydelis taps privačiu ir jo nematys kiti vartotojai.", + "make-private-dashboard": "Skydelį padaryti privačiu", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "select-dashboard": "Pasirinkite skydelį", + "no-dashboards-matching": "Skydelio, atitinkančio '{{entity}}' nėra.", + "dashboard-required": "Skydelis būtinas.", + "select-existing": "Pasirinkite egzistuojantį skydelį", + "create-new": "Sukurti naują skydelį", + "new-dashboard-title": "Naujas skydelio pavadinimas", + "open-dashboard": "Atidaryti skydelį", + "set-background": "Nustatyti foną", + "background-color": "Fono spalva", + "background-image": "Fono paveikslėlis", + "background-size-mode": "Fono dydis", + "no-image": "Paveikslėlis nepasirinktas", + "empty-image": "Nėra paveikslėlio", + "drop-image": "Užvilkite paveikslėlį arba spustelėkite ir pasirinkite failą.", + "maximum-upload-file-size": "Maksimalus įkeliamo failo dydis: {{ size }}", + "cannot-upload-file": "Failo įkelti nepavyko", + "settings": "Nustatymai", + "layout-settings": "Išdėstymo nustatymai", + "columns-count": "Stulpelių skaičius", + "columns-count-required": "Stulpelių skaičius būtinas.", + "min-columns-count-message": "Minimalus stulpelių skaičius - 10.", + "max-columns-count-message": "Maksimalus stulpelių skaičius - 1000.", + "widgets-margins": "Paraštės tarp valdiklių dysis", + "margin-required": "Paraštės dydžio reikšmė būtina.", + "min-margin-message": "Minimali paraštės dydžio reikšmė - 0.", + "max-margin-message": "Maksimali paraštės dydžio reikšmė - 50.", + "horizontal-margin": "Horizontalios paraštės dydis", + "horizontal-margin-required": "Horizontalios paraštės dydžio reikšmė būtina.", + "min-horizontal-margin-message": "Minimali horizontalios paraštės dydžio reikšmė - 0", + "max-horizontal-margin-message": "Maksimali horizontalios paraštės dydžio reikšmė - 50.", + "vertical-margin": "Vertikalios paraštės dydis", + "vertical-margin-required": "Vertikalios paraštės dydžio reikšmė būtina.", + "min-vertical-margin-message": "Minimali vertikalios paraštės dydžio reikšmė - 0.", + "max-vertical-margin-message": "Maksimali vertikalios paraštės dydžio reikšmė - 50.", + "apply-outer-margin": "Taikyti paraštes maketo šonuose", + "autofill-height": "Automatiškai pritaikyti aukštį", + "mobile-layout": "Mobiliojo režimo išdėstymo nustatymai", + "mobile-row-height": "Mobiliojo režimo eilutės aukštis pikseliais", + "mobile-row-height-required": "Mobiliojo režimo eilutės aukštis būtinas.", + "min-mobile-row-height-message": "Minimalus mobiliojo režimo eilutės aukštis yra 5 pikseliai.", + "max-mobile-row-height-message": "Maksimalus mobiliojo režimo eilutės aukštis yra 200 pikselių.", + "title-settings": "Pavadinimo nustatymai", + "display-title": "Rodyt skydelio pavadinimą", + "title-color": "Pavadinimo spalva", + "toolbar-settings": "Įrankių juostos nustatymai", + "hide-toolbar": "Paslėpti įrankių juostą", + "toolbar-always-open": "Įrankių juostą laikyti atdarytą", + "display-dashboards-selection": "Rodyti skydelių pasirinkimą", + "display-entities-selection": "Rodyti subjektų pasirinkimą", + "display-filters": "Rodyti filtrus", + "display-dashboard-timewindow": "Rodyti laiko langą", + "display-dashboard-export": "Rodyti eksportą", + "display-update-dashboard-image": "Display update dashboard image", + "dashboard-logo-settings": "Dashboard logo settings", + "display-dashboard-logo": "Display logo in dashboard fullscreen mode", + "dashboard-logo-image": "Dashboard logo image", + "advanced-settings": "Advanced settings", + "dashboard-css": "Dashboard CSS", + "import": "Importuoti skydelį", + "export": "Eksportuoti skydelį", + "export-failed-error": "Skydelio importuoti nepavyko: {{error}}", + "export-pdf": "Eksportuoti kaip PDF", + "export-png": "Eksportuoti kaip PNG", + "export-jpg": "Eksportuoti kaip JPEG", + "export-json-config": "Eksportuoti JSON konfigūraciją", + "download-dashboard-progress": "Generuojamas skydelis {{reportType}} ...", + "create-new-dashboard": "Sukurti naują skydelį", + "dashboard-file": "Skydelio failas", + "invalid-dashboard-file-error": "Skydelio importuoti nepavyko: neteisinga duomenų struktūra.", + "dashboard-import-missing-aliases-title": "Konfigūruoti importuoto skydelio naudojamus pseudonimus", + "create-new-widget": "Sukurti naują valdiklį", + "import-widget": "importuoti valdiklį", + "widget-file": "valdiklio failas", + "invalid-widget-file-error": "Valdiklio importuoti nepavyko: neteisinga duomenų struktūra.", + "widget-import-missing-aliases-title": "Konfigūruoti importuoto valdiklio naudojamus pseudonimus", + "open-toolbar": "Atidaryti skydelio įrankių juostą", + "close-toolbar": "Uždaryti įrankių juostą", + "configuration-error": "Konfigūracijos klaida", + "alias-resolution-error-title": "Skydelio pseudonimų konfigūracijos klaida", + "invalid-aliases-config": "Įrenginių, atitinkančių pseudonimų filtrus, nėra.
Susisiekite su sistemos administratoriumi.", + "select-devices": "Pasirinkite įrenginius", + "assignedToCustomer": "Priskirta klientui", + "assignedToCustomers": "Priskirta klientams", + "public": "Viešas", + "copyId": "Kopijuoti skydelio Id", + "idCopiedMessage": "Skydelio Id nukopijuotas į iškarpinę", + "public-link": "Vieša nuoroda", + "copy-public-link": "Kopijuoti viešą nuorodą", + "public-link-copied-message": "Skydelio vieša nuoroda nukopijuota į iškarpinę", + "manage-states": "Valdyti skydelio būsenas", + "states": "Skydelio būsenos", + "states-short": "Būsenos", + "search-states": "Skydelio būsenų paieška", + "selected-states": "Pasirinkta skydelio { count, plural, =1 {1 būsena} other {# būsenos} }", + "edit-state": "Redaguoti skydelio būseną", + "delete-state": "Panaikinti skydelio būseną", + "add-state": "Pridėti skydelio būseną", + "no-states-text": "Būsenų nėra", + "state": "Skydelio būsena", + "state-name": "Pavadinimas", + "state-name-required": "Skydelio pavadinimas būtinas.", + "state-id": "Būsenos Id", + "state-id-required": "Būsenos Id būtinas.", + "state-id-exists": "Skydelio būsena su tokiu pačiu Id jau yra.", + "is-root-state": "Pagrindinė būsena", + "delete-state-title": "Panaikinti skydelio būseną", + "delete-state-text": "Ar tikrai norite panaikinti skydelio būseną '{{stateName}}'?", + "show-details": "Rodyti detales", + "hide-details": "Slėpti detales", + "select-state": "Pasirinkite būseną", + "state-controller": "Būsenų valdiklis", + "search": "Skydelių paieška", + "selected-dashboards": "Pasirinkta { count, plural, =1 {1 skydelis} other {# skydeliai} }", + "home-dashboard": "Pagrindinis skydelis", + "home-dashboard-hide-toolbar": "Slėpti pagrindinio skydelio įrankių juostą", + "select-group-to-add": "Pasirinkite grupę, prie kurios pridėti pažymėtus skydelius", + "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pasirinktus skydelius", + "remove-dashboards-from-group": "Ar tikrai pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} } iš grupės '{{entityGroup}}'?", + "group": "Skydelių grupė", + "list-of-groups": "{ count, plural, =1 {Viena skydelių grupė} other {Sąrašas # skydelių grupių} }", + "group-name-starts-with": "Skydelių grupės, kurių pavadinimai prasideda '{{prefix}}'", + "unassign-dashboard-from-edge-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the edge.", + "unassign-dashboards-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-from-edge-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the edge.", + "assign-dashboard-to-edge": "Assign Dashboard(s) To Edge", + "assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge", + "non-existent-dashboard-state-error": "Dashboard state with id \"{{ stateId }}\" is not found", + "edit-mode": "Edit mode" + }, + "datakey": { + "settings": "Nustatymai", + "general": "Bendra", + "advanced": "Patyrusiam vartotojui", + "key": "Raktas", + "label": "Etiketė", + "color": "Spalva", + "units": "Specialieji simboliai, rodomi šalia reikšmės", + "decimals": "Skaičių kiekis po kablelio", + "data-generation-func": "Duomenų generavimo funkcija", + "use-data-post-processing-func": "Papildomo duomenų apdorojimo funkcija", + "configuration": "Duomenų raktų konfigūracija", + "timeseries": "Telemetrija", + "attributes": "Atributai", + "entity-field": "Subjekto laukas", + "alarm": "Įspėjimo laukai", + "timeseries-required": "Subjekto telemetrija būtina.", + "timeseries-or-attributes-required": "Subjekto telemetrija/atributai būtini.", + "alarm-fields-timeseries-or-attributes-required": "Įspėjimo laukai ar subjekto telemetrijos/atributai būtini.", + "maximum-timeseries-or-attributes": "Leidžiama daugiausiai { count, plural, =1 {1 viena telemetrija/atributas.} other {# telemetrijos/atributai} }", + "alarm-fields-required": "Įspėjimo laukai būtini.", + "function-types": "Funkcijų tipai", + "function-type": "Funkcijos tipas", + "function-types-required": "Funkcijos tipai būtini.", + "data-keys": "Duomenų raktai", + "data-key": "Duomenų raktas", + "data-keys-required": "Duomenų raktai būtini.", + "data-key-required": "Duomenų raktas būtinas.", + "alarm-keys": "Įspėjimo duomenų raktai", + "alarm-key": "Įspėjimo duomenų raktas", + "alarm-key-functions": "Alarm key functions", + "alarm-key-function": "Alarm key function", + "latest-keys": "Latest data keys", + "latest-key": "Latest data key", + "latest-key-functions": "Latest key functions", + "latest-key-function": "Latest key function", + "timeseries-keys": "Timeseries data keys", + "timeseries-key": "Timeseries data key", + "timeseries-key-functions": "Timeseries key functions", + "timeseries-key-function": "Timeseries key function", + "maximum-function-types": "Daugiausiai leidžiama { count, plural, =1 {1 funkcijos tipas.} other {# funkcijos tipai} }", + "time-description": "Dabartinės vertės laiko žyma;", + "value-description": "Dabartinė vertė;", + "prev-value-description": "Ankstesnio funkcijos kvietimo rezultatas;", + "time-prev-description": "Ankstesnės vertės laiko žyma;", + "prev-orig-value-description": "Pradinė ankstesnė vertė;", + "aggregation": "Agregavimas", + "aggregation-type-hint-common": "For performance reasons, the aggregated values calculation is available only for fixed time intervals like \"current day\", \"current month\", etc, and is not available for sliding window intervals like 'last 30 minutes' or 'last 24 hours'.", + "aggregation-type-none-hint": "Take latest value.", + "aggregation-type-min-hint": "Find minimum value among data points within a selected time window.", + "aggregation-type-max-hint": "Find maximum value among data points within a selected time window.", + "aggregation-type-avg-hint": "Calculate an average value among data points within a selected time window.", + "aggregation-type-sum-hint": "Sum all values of the data points within a selected time window.", + "aggregation-type-count-hint": "Total number of the data points within a selected time window.", + "delta-calculation": "Delta calculation", + "enable-delta-calculation": "Enable delta calculation", + "enable-delta-calculation-hint": "When enabled, the data key value is calculated based on the aggregated values for a selected time window and a specified comparison period. For performance reasons, the delta calculation is available only for history time windows and not for real-time values. For example, you may calculate the delta between the energy consumption for yesterday compared to the energy consumption for the day before yesterday.", + "delta-calculation-result": "Delta calculation result", + "delta-calculation-result-previous-value": "Previous value", + "delta-calculation-result-delta-absolute": "Delta (absolute)", + "delta-calculation-result-delta-percent": "Delta (percent)", + "source": "Source", + "latest": "Latest", + "latest-value": "Latest value", + "delta": "delta", + "percent": "percent", + "absolute": "absolute" + }, + "datasource": { + "type": "Duomenų šaltinio tipas", + "name": "Pavadinimas", + "label": "Etiketė", + "add-datasource-prompt": "Pridėkite duomenų šaltinį" + }, + "details": { + "details": "Informacija", + "edit-mode": "Redagavimo režimas", + "edit-json": "Redaguoti JSON", + "toggle-edit-mode": "Redagavimo režimas" + }, + "device": { + "all": "Visi", + "all-devices": "Visi įrenginiai", + "groups": "Grupės", + "shared": "Bendrinami", + "device": "Įrenginys", + "device-required": "Įrenginys būtinas.", + "devices": "Įrenginiai", + "management": "Įrenginių valdymas", + "view-devices": "Peržiūrėti įrenginius", + "device-alias": "Įrenginio pseudonimas", + "device-type-max-length": "Įrenginio tipas negali viršyti 256 simbolių", + "aliases": "Įrenginių pseudonimai aliases", + "no-alias-matching": "'{{alias}}' nerasta.", + "no-aliases-found": "Pseudonimų nėra.", + "no-key-matching": "'{{key}}' nerasta.", + "no-keys-found": "Raktų nėra.", + "create-new-alias": "Sukurti naują!", + "create-new-key": "Sukurti naują!", + "duplicate-alias-error": "'{{alias}}' dubliuojasi.
Įrenginio pseudonimai skydelyje turi būti unikalūs.", + "configure-alias": "Sukonfigūruoti '{{alias}}' pseudonimą", + "no-devices-matching": "Įrenginių, attinkančių '{{entity}}' nėra.", + "alias": "Pseudonimas", + "alias-required": "Įrenginio pseudonimas būtinas.", + "remove-alias": "Panaikinti įrenginio pseudonimą", + "add-alias": "Pridėti įrenginio pseudonimą", + "name-starts-with": "Įrenginio pavadinimas prasideda", + "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%įrenginio_pavadinimo_fragmentas%', '%įrenginio_pavadinimo_pabaiga', 'įrenginio_pavadinimo_pradžia%'.", + "device-list": "Įrenginių sąrašas", + "use-device-name-filter": "Naudoti filtrą", + "device-list-empty": "Įrenginiai nepasirinkti.", + "device-name-filter-required": "Įrenginio pavadinimo filtras turi būti nustatytas.", + "device-name-filter-no-device-matched": "Įrenginių, kurių pavadinimas prasideda '{{device}}' nėra.", + "add": "Pridėti įrenginį", + "assign-to-customer": "Priskirti kientui", + "assign-device-to-customer": "Įrenginį (-ius) priskirti klientui", + "assign-device-to-customer-text": "Pasirinkite įrenginius, kuriuos norite priskirti klientui", + "make-public": "Įrenginį padaryti viešu", + "make-private": "Įrenginį padaryti privačiu", + "no-devices-text": "Įrenginių nėra", + "assign-to-customer-text": "Pasirinkite klientą, kuriam norite priskirti įrenginį (-ius)", + "device-details": "Įrenginio informacija", + "add-device-text": "Pridėti naują įrenginį", + "credentials": "Įgaliojimai", + "manage-credentials": "Valdyti įgaliojimus", + "delete": "Panaikinti įrenginį", + "assign-devices": "priskirti įrenginius", + "assign-devices-text": "Priskirti { count, plural, =1 {1 įrenginį} other {# įrenginius} } klientui", + "delete-devices": "Panaikinti įrenginius", + "unassign-from-customer": "Atseti nuo kliento", + "unassign-devices": "Atsieti įrenginius", + "unassign-devices-action-title": "Atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} } nuo kliento", + "unassign-device-from-edge-title": "Are you sure you want to unassign the device '{{deviceName}}'?", + "unassign-device-from-edge-text": "After the confirmation the device will be unassigned and won't be accessible by the edge.", + "unassign-devices-from-edge": "Unassign devices from edge", + "assign-new-device": "Priskirti naują įrenginį", + "make-public-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti viešu?", + "make-public-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys bus vieši ir prieinami kitiems vartotojams.", + "make-private-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti privačiu?", + "make-private-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys taps privatūs ir nebus prieinami kitiems vartotojams.", + "view-credentials": "Peržiūrėti įgaliojimus", + "delete-device-title": "Ar tikrai norite panaikinti įrenginį '{{deviceName}}'?", + "delete-device-text": "Būkite dėmesingi, po patvirtinimo įrenginys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite", + "delete-devices-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", + "delete-devices-action-title": "Panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }", + "delete-devices-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti įrenginiai ir su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "unassign-device-title": "Ar tikrai norite atsieti įrenginį '{{deviceName}}'?", + "unassign-device-text": "Po patvirtinimo įrenginys bus atsietas ir klientas jo nebematys.", + "unassign-device": "Atsieti įrenginį", + "unassign-devices-title": "Ar tikrai norite atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", + "unassign-devices-text": "Po patvirtinimo visi pasirinkti įrenginiai bus atsieti nuo kliento.", + "device-credentials": "įrenginio įgaliojimai", + "loading-device-credentials": "Įkeliami įrenginio įgaliojimai...", + "credentials-type": "Įgaliojimų tipas", + "access-token": "Prieigos raktas", + "access-token-required": "Prieigos raktas būtinas.", + "access-token-invalid": "Prieigos rakto ilgis turi būti nuo 1 iki 32 simbolių.", + "certificate-pem-format": "Certificate in PEM format", + "certificate-pem-format-required": "Certificate is required.", + "copy-access-token": "Copy Access token", + "copy-certificate": "Copy Certificate", + "copy-client-id": "Copy Client ID", + "copy-user-name": "Copy User name", + "copy-password": "Copy Password", + "generate-client-id": "Generate Client ID", + "generate-user-name": "Generate User name", + "generate-password": "Generate Password", + "generate-access-token": "Generate Access Token", + "lwm2m-security-config": { + "identity": "Client Identity", + "identity-required": "Client Identity is required.", + "identity-tooltip": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", + "client-key": "Client Key", + "client-key-required": "Client Key is required.", + "client-key-tooltip-prk": "RPK public key or id must be in the standard [RFC7250] and encoded to Base64 format!", + "client-key-tooltip-psk": "PSK key must be in the standard [RFC4279] and HexDec format: 32, 64, 128 characters!", + "endpoint": "Endpoint Client Name", + "endpoint-required": "Endpoint Client Name is required.", + "client-public-key": "Client public key", + "client-public-key-hint": "If client public key is empty, the trusted certificate will be used", + "client-public-key-tooltip": "X509 public key must be in DER-encoded X509v3 format and support exclusively EC algorithm and then encoded to Base64 format!", + "mode": "Security config mode", + "client-tab": "Client Security Config", + "client-certificate": "Client certificate", + "bootstrap-tab": "Bootstrap Client", + "bootstrap-server": "Bootstrap Server", + "lwm2m-server": "LwM2M Server", + "client-publicKey-or-id": "Client Public Key or Id", + "client-publicKey-or-id-required": "Client Public Key or Id is required.", + "client-publicKey-or-id-tooltip-psk": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", + "client-publicKey-or-id-tooltip-rpk": "RPK public key or id must be in the standard [RFC7250] and encoded to Base64 format!", + "client-publicKey-or-id-tooltip-x509": "X509 public key must be in DER-encoded X509v3 format and support exclusively EC algorithm and then encoded to Base64 format", + "client-secret-key": "Client Secret Key", + "client-secret-key-required": "Client Secret Key is required.", + "client-secret-key-tooltip-psk": "PSK key must be in the standard [RFC4279] and HexDec format: 32, 64, 128 characters!", + "client-secret-key-tooltip-prk": "RPK secret key must be in PKCS_8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!", + "client-secret-key-tooltip-x509": "X509 secret key must be in PKCS_8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!" + }, + "client-id": "Client ID", + "client-id-pattern": "Contains invalid character.", + "user-name": "User Name", + "user-name-required": "User Name is required.", + "client-id-or-user-name-necessary": "Client ID and/or User Name are necessary", + "password": "Password", + "secret": "Secret", + "secret-required": "Secret is required.", + "device-type": "Įrenginio tipas", + "device-type-required": "Įrenginio tipas būtinas.", + "select-device-type": "Pasirinkite įrenginio tipą", + "enter-device-type": "Įveskite įrenginio tipą", + "any-device": "Bet kuris įrenginys", + "no-device-types-matching": "Įrenginio tipų, atitinkančių '{{entitySubtype}}', nėra.", + "device-type-list-empty": "Nepasirinktas įrenginio tipas!", + "device-types": "Įrenginių tipai", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "label-max-length": "Etiketė negali viršyti 256 simbolių", + "description": "Aprašymas", + "label": "Etiketė", + "events": "Įvykiai", + "details": "Informacija", + "copyId": "Kopijuoti įrenginio Id", + "copyAccessToken": "Kopijuoti prieigos raktą", + "copy-mqtt-authentication": "Copy MQTT credentials", + "idCopiedMessage": "Įrenginio Id nukopijuotas į iškarpinę", + "accessTokenCopiedMessage": "Įrenginio prieigos raktas nukopijuotas į iškarpinę", + "mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard", + "assignedToCustomer": "Priskirtas klientui", + "unable-delete-device-alias-title": "Įrenginio pseudonimo panaikinti nepavyko", + "unable-delete-device-alias-text": "Įrenginio pseudonimas '{{deviceAlias}}' nes jis naudojamas šiuose valdikliuose:
{{widgetsList}}", + "is-gateway": "Is gateway", + "overwrite-activity-time": "Overwrite activity time for connected device", + "device-filter": "Įrenginių filtras", + "device-filter-title": "Įrenginių filtras", + "filter-title": "Filtras", + "device-state": "Įrenginio būsena", + "state": "Būsena", + "any": "Visos", + "active": "Aktyvus", + "inactive": "Neaktyvus", + "public": "Viešas", + "device-public": "Įrenginys yra viešas", + "select-device": "Pasirinkite įrenginį", + "select-group-to-add": "Pasirinkite grupę, į kurią norite pridėti pažymėtus įrenginius", + "select-group-to-move": "Pasirinkite grupę, į kurią norite perkelti pažymėtus įrenginius", + "remove-devices-from-group": "Ar tikrai norite iš grupės '{{entityGroup}}' pašalinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", + "group": "Įrenginių grupė", + "list-of-groups": "{ count, plural, =1 {Viena įrenginio grupė} other {# įrenginių grupių sąrašas} }", + "group-name-starts-with": "Įrenginių grupės, kurių pavadinimai prasideda '{{prefix}}'", + "import": "Importuoti įrenginį", + "device-file": "Įrenginio failas", + "search": "Įrenginių paieška", + "selected-devices": "Pasirinkta { count, plural, =1 {1 įrenginys} other {# įrenginiai} }", + "device-configuration": "Įrenginių konfigūracija", + "transport-configuration": "Transport configuration", + "wizard": { + "device-details": "Informacija apie įrenginį" + }, + "unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 device} other {# devices} }?", + "unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge.", + "time": "Time", + "connectivity": { + "check-connectivity": "Check connectivity", + "device-created-check-connectivity": "Device created. Let's check connectivity!", + "loading-check-connectivity-command": "Loading check connectivity commands...", + "use-following-instructions": "Use the following instructions for sending telemetry on behalf of the device using shell", + "execute-following-command": "Execute the following command", + "install-curl-windows": "Starting Windows 10 b17063, cURL is available by default", + "install-curl-macos": "Starting Mac OS X 10.2 6C115 (Jaguar), cURL is available by default", + "install-mqtt-windows": "Use the instructions to download, install, setup and run mosquitto_pub", + "install-coap-client": "Use the instructions to download, install, setup and run coap-client", + "install-necessary-client-tools": "Install necessary client tools", + "mqtts-x509-command": "Use the following documentation to connect the device via MQTT with authorization X509", + "coaps-x509-command": "Use the following documentation to connect the device via CoAP over DTLS with authorization X509", + "snmp-command": "Use the following documentation to connect the device through the SNMP.", + "sparkplug-command": "Use the following documentation to connect the device through the MQTT Sparkplug.", + "lwm2m-command": "Use the following documentation to connect the device through the LWM2M." + } + }, + "asset-profile": { + "asset-profile": "Profilis", + "asset-profiles": "Asset profiles", + "all-asset-profiles": "Visi", + "add": "Add asset profile", + "edit": "Edit asset profile", + "asset-profile-details": "Asset profile details", + "no-asset-profiles-text": "No asset profiles found", + "search": "Search asset profiles", + "selected-asset-profiles": "{ count, plural, =1 {1 asset profile} other {# asset profiles} } selected", + "no-asset-profiles-matching": "No asset profile matching '{{entity}}' were found.", + "asset-profile-required": "Asset profile is required", + "idCopiedMessage": "Asset profile Id has been copied to clipboard", + "set-default": "Make asset profile default", + "delete": "Delete asset profile", + "copyId": "Copy asset profile Id", + "name-max-length": "Name should be less than 256", + "new-device-profile-name": "Asset profile name", + "new-device-profile-name-required": "Asset profile name is required.", + "name": "Name", + "name-required": "Name is required.", + "image": "Asset profile image", + "description": "Description", + "default": "Default", + "default-rule-chain": "Default rule chain", + "default-edge-rule-chain": "Default edge rule chain", + "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for assets of this asset profile", + "mobile-dashboard": "Mobile dashboard", + "mobile-dashboard-hint": "Used by mobile application as a asset details dashboard", + "select-queue-hint": "Select from a drop-down list.", + "delete-asset-profile-title": "Are you sure you want to delete the asset profile '{{assetProfileName}}'?", + "delete-asset-profile-text": "Be careful, after the confirmation the asset profile and all related data will become unrecoverable.", + "delete-asset-profiles-title": "Are you sure you want to delete { count, plural, =1 {1 asset profile} other {# asset profiles} }?", + "delete-asset-profiles-text": "Be careful, after the confirmation all selected asset profiles will be removed and all related data will become unrecoverable.", + "set-default-asset-profile-title": "Are you sure you want to make the asset profile '{{assetProfileName}}' default?", + "set-default-asset-profile-text": "After the confirmation the asset profile will be marked as default and will be used for new assets with no profile specified.", + "no-asset-profiles-found": "No asset profiles found.", + "create-new-asset-profile": "Create a new one!", + "create-asset-profile": "Create new asset profile", + "import": "Import asset profile", + "export": "Export asset profile", + "export-failed-error": "Unable to export asset profile: {{error}}", + "asset-profile-file": "Asset profile file", + "invalid-asset-profile-file-error": "Unable to import asset profile: Invalid asset profile data structure." + }, + "device-profile": { + "device-profile": "Profilis", + "device-profiles": "Device profiles", + "all-device-profiles": "Visi", + "add": "Add device profile", + "edit": "Edit device profile", + "device-profile-details": "Device profile details", + "no-device-profiles-text": "No device profiles found", + "search": "Search device profiles", + "selected-device-profiles": "{ count, plural, =1 {1 device profile} other {# device profiles} } selected", + "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", + "device-profile-required": "Device profile is required", + "idCopiedMessage": "Device profile Id has been copied to clipboard", + "set-default": "Make device profile default", + "delete": "Delete device profile", + "copyId": "Copy device profile Id", + "name-max-length": "Name should be less than 256", + "name": "Name", + "name-required": "Name is required.", + "type": "Profile type", + "type-required": "Profile type is required.", + "type-default": "Default", + "image": "Device profile image", + "transport-type": "Transport type", + "transport-type-required": "Transport type is required.", + "transport-type-default": "Default", + "transport-type-default-hint": "Supports basic MQTT, HTTP and CoAP transport", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Enables advanced MQTT transport settings", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "Enables advanced CoAP transport settings", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "LWM2M transport type", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "Specify SNMP transport configuration", + "transport-type-http": "HTTP", + "description": "Description", + "default": "Default", + "profile-configuration": "Profile configuration", + "transport-configuration": "Transport configuration", + "default-rule-chain": "Default rule chain", + "default-edge-rule-chain": "Default edge rule chain", + "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for devices of this device profile", + "mobile-dashboard": "Mobile dashboard", + "mobile-dashboard-hint": "Used by mobile application as a device details dashboard", + "select-queue-hint": "Select from a drop-down list.", + "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", + "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data including associated OTA updates will become unrecoverable.", + "delete-device-profiles-title": "Are you sure you want to delete { count, plural, =1 {1 device profile} other {# device profiles} }?", + "delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data including associated OTA updates will become unrecoverable.", + "set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?", + "set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.", + "no-device-profiles-found": "No device profiles found.", + "create-new-device-profile": "Create a new one!", + "mqtt-device-topic-filters": "MQTT device topic filters", + "mqtt-device-topic-filters-unique": "MQTT device topic filters need to be unique.", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) node.", + "mqtt-device-topic-filters-spark-plug-hint": "Allow connections from EoN nodes with Sparkplug B payload and topic format.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug metrics to store as attributes.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Names of SparkPlug metrics that will be stored as device attributes. All other metrics will be stored as device telemetry.", + "mqtt-device-payload-type": "MQTT device payload", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "Enable compatibility with other payload formats.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "When enabled, the platform will use a Protobuf payload format by default. If parsing fails, the platform will attempt to use JSON payload format. Useful for backward compatibility during firmware updates. For example, the initial release of the firmware uses Json, while the new release uses Protobuf. During the process of firmware update for the fleet of devices, it is required to support both Protobuf and JSON simultaneously. The compatibility mode introduces slight performance degradation, so it is recommended to disable this mode once all devices are updated.", + "mqtt-use-json-format-for-default-downlink-topics": "Use Json format for default downlink topics", + "mqtt-use-json-format-for-default-downlink-topics-hint": "When enabled, the platform will use Json payload format to push attributes and RPC via the following topics: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. This setting does not impact attribute and rpc subscriptions sent using new (v2) topics: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Where $request_id is an integer request identifier.", + "mqtt-send-ack-on-validation-exception": "Send PUBACK on PUBLISH message validation failure", + "mqtt-send-ack-on-validation-exception-hint": "By default, the platform will close the MQTT session on message validation failure. When enabled, the platform will send publish acknowledgment instead of closing the session.", + "snmp-add-mapping": "Add SNMP mapping", + "snmp-mapping-not-configured": "No mapping for OID to timeseries/telemetry configured", + "snmp-timseries-or-attribute-name": "Timeseries/attribute name for mapping", + "snmp-timseries-or-attribute-type": "Timeseries/attribute type for mapping", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Payload type is required.", + "coap-device-type": "CoAP device type", + "coap-device-payload-type": "CoAP device payload", + "coap-device-type-required": "CoAP device type is required.", + "coap-device-type-default": "Default", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "Single [+] and multi-level [#] wildcards supported.", + "telemetry-topic-filter": "Telemetry topic filter", + "telemetry-topic-filter-required": "Telemetry topic filter is required.", + "attributes-topic-filter": "Attributes publish topic filter", + "attributes-subscribe-topic-filter": "Attributes subscribe topic filter", + "attributes-topic-filter-required": "Attributes publish topic filter is required.", + "attributes-subscribe-topic-filter-required": "Attributes subscribe topic is required", + "telemetry-proto-schema": "Telemetry proto schema", + "telemetry-proto-schema-required": "Telemetry proto schema is required.", + "attributes-proto-schema": "Attributes proto schema", + "attributes-proto-schema-required": "Attributes proto schema is required.", + "rpc-response-proto-schema": "RPC response proto schema", + "rpc-response-proto-schema-required": "RPC response proto schema is required.", + "rpc-response-topic-filter": "RPC response topic filter", + "rpc-response-topic-filter-required": "RPC response topic filter is required.", + "rpc-request-proto-schema": "RPC request proto schema", + "rpc-request-proto-schema-required": "RPC request proto schema is required.", + "rpc-request-proto-schema-hint": "RPC request message should always have fields: string method = 1; int32 requestId = 2; and params = 3 of any data type.", + "not-valid-pattern-topic-filter": "Not valid pattern topic filter", + "not-valid-single-character": "Invalid use of a single-level wildcard character", + "not-valid-multi-character": "Invalid use of a multi-level wildcard character", + "single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/#.", + "alarm-rules": "Alarm rules", + "alarm-rules-with-count": "Alarm rules ({{count}})", + "no-alarm-rules": "No alarm rules configured", + "add-alarm-rule": "Add alarm rule", + "edit-alarm-rule": "Edit alarm rule", + "alarm-type": "Alarm type", + "alarm-type-required": "Alarm type is required.", + "alarm-type-unique": "Alarm type must be unique within the device profile alarm rules.", + "alarm-type-max-length": "Alarm type should be less than 256", + "create-alarm-pattern": "Create {{alarmType}} alarm", + "create-alarm-rules": "Create alarm rules", + "no-create-alarm-rules": "No create conditions configured", + "add-create-alarm-rule-prompt": "Please add create alarm rule", + "clear-alarm-rule": "Clear alarm rule", + "no-clear-alarm-rule": "No clear condition configured", + "add-create-alarm-rule": "Add create condition", + "add-clear-alarm-rule": "Add clear condition", + "select-alarm-severity": "Select alarm severity", + "alarm-severity-required": "Alarm severity is required.", + "condition-duration": "Condition duration", + "condition-duration-value": "Duration value", + "condition-duration-time-unit": "Time unit", + "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", + "condition-duration-value-pattern": "Duration value should be integers.", + "condition-duration-value-required": "Duration value is required.", + "condition-duration-time-unit-required": "Time unit is required.", + "advanced-settings": "Advanced settings", + "alarm-rule-details": "Details", + "alarm-rule-details-hint": "Hint: use ${keyName} to substitute values of the attribute or telemetry keys that are used in alarm rule condition.", + "add-alarm-rule-details": "Add details", + "alarm-rule-mobile-dashboard": "Mobile dashboard", + "alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard", + "alarm-rule-no-mobile-dashboard": "No dashboard selected", + "propagate-alarm": "Propagate alarm to related entities", + "alarm-rule-relation-types-list": "Relation types to propagate", + "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", + "propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)", + "propagate-alarm-to-owner-hierarchy": "Propagate alarm to entity owners hierarchy", + "propagate-alarm-to-tenant": "Propagate alarm to Tenant", + "alarm-details": "Alarm details", + "alarm-rule-condition": "Alarm rule condition", + "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", + "edit-alarm-rule-condition": "Edit alarm rule condition", + "device-provisioning": "Device provisioning", + "provision-strategy": "Provision strategy", + "provision-strategy-required": "Provision strategy is required.", + "provision-strategy-disabled": "Disabled", + "provision-strategy-created-new": "Allow to create new devices", + "provision-strategy-check-pre-provisioned": "Check for pre-provisioned devices", + "provision-device-key": "Provision device key", + "provision-device-key-required": "Provision device key is required.", + "copy-provision-key": "Copy provision key", + "provision-key-copied-message": "Provision key has been copied to clipboard", + "provision-device-secret": "Provision device secret", + "provision-device-secret-required": "Provision device secret is required.", + "copy-provision-secret": "Copy provision secret", + "provision-secret-copied-message": "Provision secret has been copied to clipboard", + "provision-strategy-x509": { + "certificate-chain": "X509 Certificates Chain", + "certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.", + "allow-create-new-devices": "Create new devices", + "allow-create-new-devices-hint": "If selected new devices will be created and client certificate will be used as device credentials.", + "certificate-value": "Certificate in PEM format", + "certificate-value-required": "Certificate in PEM format is required", + "cn-regex-variable": "CN Regular Expression variable", + "cn-regex-variable-required": "CN Regular Expression variable is required", + "cn-regex-variable-hint": "Required to fetch device name from device's X509 certificate's common name." + }, + "condition": "Condition", + "condition-type": "Condition type", + "condition-type-simple": "Simple", + "condition-type-duration": "Duration", + "condition-during": "During {{during}}", + "condition-during-dynamic": "During \"{{ attribute }}\" ({{during}})", + "condition-type-repeating": "Repeating", + "condition-type-required": "Condition type is required.", + "condition-repeating-value": "Count of events", + "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", + "condition-repeating-value-pattern": "Count of events should be integers.", + "condition-repeating-value-required": "Count of events is required.", + "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", + "condition-repeat-times-dynamic": "Repeats \"{ attribute }\" ({ count, plural, =1 {1 time} other {# times} })", + "schedule-type": "Scheduler type", + "schedule-type-required": "Scheduler type is required.", + "schedule": "Schedule", + "edit-schedule": "Edit alarm schedule", + "schedule-any-time": "Active all the time", + "schedule-specific-time": "Active at a specific time", + "schedule-custom": "Custom", + "schedule-day": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "schedule-days": "Days", + "schedule-time": "Time", + "schedule-time-from": "From", + "schedule-time-to": "To", + "schedule-days-of-week-required": "At least one day of week should be selected.", + "create-device-profile": "Create new device profile", + "import": "Import device profile", + "export": "Export device profile", + "export-failed-error": "Unable to export device profile: {{error}}", + "device-profile-file": "Device profile file", + "invalid-device-profile-file-error": "Unable to import device profile: Invalid device profile data structure.", + "power-saving-mode": "Power Saving Mode", + "power-saving-mode-type": { + "default": "Use device profile power saving mode", + "psm": "Power Saving Mode (PSM)", + "drx": "Discontinuous Reception (DRX)", + "edrx": "Extended Discontinuous Reception (eDRX)" + }, + "edrx-cycle": "eDRX cycle", + "edrx-cycle-required": "eDRX cycle is required.", + "edrx-cycle-pattern": "eDRX cycle must be a positive integer.", + "edrx-cycle-min": "Minimum number of eDRX cycle is {{ min }} seconds.", + "paging-transmission-window": "Paging Transmission Window", + "paging-transmission-window-required": "Paging transmission window is required.", + "paging-transmission-window-pattern": "Paging transmission window must be a positive integer.", + "paging-transmission-window-min": "Minimum number ofpPaging transmission window is {{ min }} seconds.", + "psm-activity-timer": "PSM Activity Timer", + "psm-activity-timer-required": "PSM activity timer is required.", + "psm-activity-timer-pattern": "PSM activity timer must be a positive integer.", + "psm-activity-timer-min": "Minimum number of PSM activity timer is {{ min }} seconds.", + "lwm2m": { + "object-list": "Object list", + "object-list-empty": "No objects selected.", + "no-objects-found": "No objects found.", + "no-objects-matching": "No objects matching '{{object}}' were found.", + "model-tab": "LWM2M Model", + "add-new-instances": "Add new instances", + "instances-list": "Instances list", + "instances-list-required": "Instances list is required.", + "instance-id-pattern": "Instance id must be a positive integer.", + "instance-id-max": "Maximum instance id value {{max}}.", + "instance": "Instance", + "resource-label": "#ID Resource name", + "observe-label": "Observe", + "attribute-label": "Attribute", + "telemetry-label": "Telemetry", + "edit-observe-select": "To edit observe select telemetry or attribute", + "edit-attributes-select": "To edit attributes select telemetry or attribute", + "no-attributes-set": "No attributes set", + "key-name": "Key name", + "key-name-required": "Key name is required", + "attribute-name": "Name attribute", + "attribute-name-required": "Name attribute is required.", + "attribute-value": "Attribute value", + "attribute-value-required": "Attribute value is required.", + "attribute-value-pattern": "Attribute value must be a positive integer.", + "edit-attributes": "Edit attributes: {{ name }}", + "view-attributes": "View attributes: {{ name }}", + "add-attribute": "Add attribute", + "edit-attribute": "Edit attribute", + "view-attribute": "View attribute", + "remove-attribute": "Remove attribute", + "delete-server-text": "Be careful, after the confirmation the server configuration will become unrecoverable.", + "delete-server-title": "Are you sure you want to delete the server?", + "mode": "Security config mode", + "bootstrap-tab": "Bootstrap", + "bootstrap-server-legend": "Bootstrap Server (ShortId...)", + "lwm2m-server-legend": "LwM2M Server (ShortId...)", + "server": "Server", + "short-id": "Short server ID", + "short-id-tooltip": "Server short Id. Used as link to associate server Object Instance.\nThis identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.\nResource MUST be set when the Bootstrap-Server Resource has a value of 'false'.\nThe values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", + "short-id-required": "Short server ID is required.", + "short-id-range": "Short server ID should be in a range from 1 to 65534.", + "short-id-pattern": "Short server ID must be a positive integer.", + "lifetime": "Client registration lifetime", + "lifetime-required": "Client registration lifetime is required.", + "lifetime-pattern": "Client registration lifetime must be a positive integer.", + "default-min-period": "Min period between two notifications (s)", + "default-min-period-tooltip": "The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation.", + "default-min-period-required": "Minimum period is required.", + "default-min-period-pattern": "Minimum period must be a positive integer.", + "notification-storing": "Notification storing when disabled or offline", + "binding": "Binding", + "binding-type": { + "u": "U: Client is reachable via the UDP binding at any time.", + "m": "M: Client is reachable via the MQTT binding at any time.", + "h": "H: Client is reachable via the HTTP binding at any time.", + "t": "T: Client is reachable via the TCP binding at any time.", + "s": "S: Client is reachable via the SMS binding at any time.", + "n": "N: Client MUST send the response to such a request over the Non-IP binding (is supported since LWM2M 1.1).", + "uq": "UQ: UDP connection in queue mode (is not supported since LWM2M 1.1)", + "uqs": "UQS: both UDP and SMS connections active; UDP in queue mode, SMS in standard mode (is not supported since LWM2M 1.1)", + "tq": "TQ: TCP connection in queue mode (is not supported since LWM2M 1.1)", + "tqs": "TQS: both TCP and SMS connections active; TCP in queue mode, SMS in standard mode (is not supported since LWM2M 1.1)", + "sq": "SQ: SMS connection in queue mode (is not supported since LWM2M 1.1)" + }, + "binding-tooltip": "This is the list in the\"binding\" resource of the LwM2M server object - /1/x/7.\nIndicates the supported binding modes in the LwM2M Client.\nThis value SHOULD be the same as the value in the “Supported Binding and Modes” resource in the Device Object (/3/0/16).\nWhile multiple transports are supported, only one transport binding can be used during the entire Transport Session.\nAs an example, when UDP and SMS are both supported, the LwM2M Client and the LwM2M Server can choose to communicate either over UDP or SMS during the entire Transport Session.", + "bootstrap-server": "Bootstrap Server", + "lwm2m-server": "LwM2M Server", + "include-bootstrap-server": "Include Bootstrap Server updates", + "bootstrap-update-title": "You already have configured Bootstrap Server. Are you sure you want to exclude the updates?", + "bootstrap-update-text": "Be careful, after the confirmation the Bootstrap Server configuration data will become unrecoverable.", + "server-host": "Host", + "server-host-required": "Host is required.", + "server-port": "Port", + "server-port-required": "Port is required.", + "server-port-pattern": "Port must be a positive integer.", + "server-port-range": "Port should be in a range from 1 to 65535.", + "server-public-key": "Server Public Key", + "server-public-key-required": "Server Public Key is required.", + "client-hold-off-time": "Hold Off Time", + "client-hold-off-time-required": "Hold Off Time is required.", + "client-hold-off-time-pattern": "Hold Off Time must be a positive integer.", + "client-hold-off-time-tooltip": "Client Hold Off Time for use with a Bootstrap-Server only", + "account-after-timeout": "Account after the timeout", + "account-after-timeout-required": "Account after the timeout is required.", + "account-after-timeout-pattern": "Account after the timeout must be a positive integer.", + "account-after-timeout-tooltip": "Bootstrap-Server Account after the timeout value given by this resource.", + "server-type": "Server type", + "add-new-server-title": "Add new server config", + "add-server-config": "Add server config", + "add-lwm2m-server-config": "Add LwM2M server", + "no-config-servers": "No servers configured", + "others-tab": "Other settings", + "client-strategy": "Client strategy when connecting", + "client-strategy-label": "Strategy", + "client-strategy-only-observe": "Only Observe Request to the client after the initial connection", + "client-strategy-read-all": "Read All Resources & Observe Request to the client after registration", + "fw-update": "Firmware update", + "fw-update-strategy": "Firmware update strategy", + "fw-update-strategy-data": "Push firmware update as binary file using Object 19 and Resource 0 (Data)", + "fw-update-strategy-package": "Push firmware update as binary file using Object 5 and Resource 0 (Package)", + "fw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push firmware update as Object 5 and Resource 1 (Package URI)", + "sw-update": "Software update", + "sw-update-strategy": "Software update strategy", + "sw-update-strategy-package": "Push binary file using Object 9 and Resource 2 (Package)", + "sw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push software update using Object 9 and Resource 3 (Package URI)", + "fw-update-resource": "Firmware update CoAP resource", + "fw-update-resource-required": "Firmware update CoAP resource is required.", + "sw-update-resource": "Software update CoAP resource", + "sw-update-resource-required": "Software update CoAP resource is required.", + "config-json-tab": "Json Config Profile Device", + "attributes-name": { + "min-period": "Minimum period", + "max-period": "Maximum period", + "greater-than": "Greater than", + "less-than": "Less than", + "step": "Step", + "min-evaluation-period": "Minimum evaluation period", + "max-evaluation-period": "Maximum evaluation period" + }, + "composite-operations-support": "Supports composite Read/Write/Observe operations" + }, + "snmp": { + "add-communication-config": "Add communication config", + "add-mapping": "Add mapping", + "authentication-passphrase": "Authentication passphrase", + "authentication-passphrase-required": "Authentication passphrase is required.", + "authentication-protocol": "Authentication protocol", + "authentication-protocol-required": "Authentication protocol is required.", + "communication-configs": "Communication configs", + "community": "Community string", + "community-required": "Community string is required.", + "context-name": "Context name", + "data-key": "Data key", + "data-key-required": "Data key is required.", + "data-type": "Data type", + "data-type-required": "Data type is required.", + "engine-id": "Engine ID", + "host": "Host", + "host-required": "Host is required.", + "oid": "OID", + "oid-pattern": "Invalid OID format.", + "oid-required": "OID is required.", + "please-add-communication-config": "Please add communication config", + "please-add-mapping-config": "Please add mapping config", + "port": "Port", + "port-format": "Invalid port format.", + "port-required": "Port is required.", + "privacy-passphrase": "Privacy passphrase", + "privacy-passphrase-required": "Privacy passphrase is required.", + "privacy-protocol": "Privacy protocol", + "privacy-protocol-required": "Privacy protocol is required.", + "protocol-version": "Protocol version", + "protocol-version-required": "Protocol version is required.", + "querying-frequency": "Querying frequency, ms", + "querying-frequency-invalid-format": "Querying frequency must be a positive integer.", + "querying-frequency-required": "Querying frequency is required.", + "retries": "Retries", + "retries-invalid-format": "Retries must be a positive integer.", + "retries-required": "Retries is required.", + "scope": "Scope", + "scope-required": "Scope is required.", + "security-name": "Security name", + "security-name-required": "Security name is required.", + "timeout-ms": "Timeout, ms", + "timeout-ms-invalid-format": "Timeout must be a positive integer.", + "timeout-ms-required": "Timeout is required.", + "user-name": "User name", + "user-name-required": "User name is required." + } + }, + "dialog": { + "close": "Uždaryti dialogo langą", + "error-message-title": "Klaidos pranešimas:", + "error-details-title": "Informacija apie klaidą" + }, + "direction": { + "column": "Stulpelis", + "row": "Eilutė" + }, + "edge": { + "all": "All", + "all-edges": "All edges", + "groups": "Groups", + "shared": "Shared", + "edge": "Edge", + "edge-instances": "Edge instances", + "instances": "Instances", + "edge-file": "Edge file", + "name-max-length": "Name should be less than 256", + "label-max-length": "Label should be less than 256", + "type-max-length": "Type should be less than 256", + "management": "Edge management", + "no-edges-matching": "No edges matching '{{entity}}' were found.", + "add": "Add Edge", + "no-edges-text": "No edges found", + "edge-details": "Edge details", + "add-edge-text": "Add new edge", + "delete": "Delete edge", + "delete-edge-title": "Are you sure you want to delete the edge '{{edgeName}}'?", + "delete-edge-text": "Be careful, after the confirmation the edge and all related data will become unrecoverable.", + "delete-edges-title": "Are you sure you want to edge { count, plural, =1 {1 edge} other {# edges} }?", + "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", + "name": "Name", + "name-starts-with": "Edge name starts with", + "name-required": "Name is required.", + "edge-license-key": "Edge License Key", + "edge-license-key-required": "Edge License Key is required.", + "edge-license-key-max-length": "Edge License Key should be less than 31", + "edge-license-key-hint": "IMPORTANT NOTE: default edge license key provided only for evaluation purposes.
This key is active only for 30 days after the activation. White-labeling feature is disabled.
To obtain your license key please navigate to the pricing page and select the best license option for your case.", + "cloud-endpoint": "Cloud Endpoint", + "cloud-endpoint-required": "Cloud Endpoint is required.", + "cloud-endpoint-max-length": "Cloud Endpoint should be less than 256", + "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.", + "description": "Description", + "details": "Details", + "events": "Events", + "copy-id": "Copy Edge Id", + "id-copied-message": "Edge Id has been copied to clipboard", + "sync": "Sync Edge", + "edge-required": "Edge required", + "edge-type": "Edge type", + "edge-type-required": "Edge type is required.", + "event-action": "Event action", + "entity-id": "Entity ID", + "select-edge-type": "Select edge type", + "assign-to-customer": "Assign to customer", + "assign-to-customer-text": "Please select the customer to assign the edge(s)", + "assign-edge-to-customer": "Assign Edge(s) To Customer", + "assign-edge-to-customer-text": "Please select the edges to assign to the customer", + "assignedToCustomer": "Assigned to customer", + "edge-public": "Edge is public", + "assigned-to-customer": "Assigned to: {{customerTitle}}", + "unassign-from-customer": "Unassign from customer", + "unassign-edge-title": "Are you sure you want to unassign the edge '{{edgeName}}'?", + "unassign-edge-text": "After the confirmation the edge will be unassigned and won't be accessible by the customer.", + "unassign-edges-title": "Are you sure you want to unassign { count, plural, =1 {1 edge} other {# edges} }?", + "unassign-edges-text": "After the confirmation all selected edges will be unassigned and won't be accessible by the customer.", + "make-public": "Make edge public", + "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", + "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", + "make-private": "Make edge private", + "public": "Public", + "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", + "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", + "import": "Import edge", + "install-connect-instructions": "Install & Connect Instructions", + "install-connect-instructions-edge-created": "Edge created! Check Install & Connect Instructions", + "loading-edge-instructions": "Loading edge instructions...", + "label": "Label", + "load-entity-error": "Failed to load data. Entity has been deleted.", + "assign-new-edge": "Assign new edge", + "unassign-from-edge": "Unassign from edge", + "edge-key": "Edge key", + "copy-edge-key": "Copy Edge key", + "edge-key-copied-message": "Edge key has been copied to clipboard", + "edge-secret": "Edge secret", + "copy-edge-secret": "Copy Edge secret", + "edge-secret-copied-message": "Edge secret has been copied to clipboard", + "manage-assets": "Manage assets", + "manage-devices": "Manage devices", + "manage-entity-views": "Manage entity views", + "manage-dashboards": "Manage dashboards", + "manage-rulechains": "Manage rule chains", + "assets": "Edge assets", + "devices": "Edge devices", + "entity-views": "Edge entity views", + "dashboard": "Edge dashboard", + "dashboards": "Edge Dashboards", + "rulechain-templates": "Rule chain templates", + "edge-rulechain-templates": "Edge rule chain templates", + "converter-templates": "Converter templates", + "edge-converter-templates": "Edge converter templates", + "integration-templates": "Integration templates", + "edge-integration-templates": "Edge integration templates", + "rulechains": "Edge rule chains", + "integrations": "Integrations", + "search": "Search edges", + "selected-edges": "{ count, plural, =1 {1 edge} other {# edges} } selected", + "any-edge": "Any edge", + "no-edge-types-matching": "No edge types matching '{{entitySubtype}}' were found.", + "edge-type-list-empty": "No edge types selected.", + "edge-types": "Edge types", + "enter-edge-type": "Enter edge type", + "deployed": "Deployed", + "pending": "Pending", + "downlinks": "Downlinks", + "no-downlinks-prompt": "No downlinks found", + "sync-process-started-successfully": "Sync process started successfully!", + "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", + "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

List of missing rule chain(s):
{{missingRuleChains}}", + "widget-datasource-error": "This widget supports only EDGE entity datasource", + "assign-to-edge": "Assign to edge", + "assign-to-edge-title": "Assign Entity Group(s) to Edge", + "manage-edge-user-groups": "Manage edge user groups", + "manage-edge-asset-groups": "Manage edge asset groups", + "manage-edge-device-groups": "Manage edge device groups", + "manage-edge-entity-view-groups": "Manage edge entity view groups", + "manage-edge-dashboard-groups": "Manage edge dashboard groups", + "manage-edge-rule-chains": "Manage edge rule chains", + "manage-edge-integrations": "Manage edge integrations", + "manage-edge-scheduler-events": "Manage edge scheduler events", + "select-group-to-add": "Select target group to add selected edges", + "select-group-to-move": "Select target group to move selected edges", + "remove-edges-from-group": "Are you sure you want to remove { count, plural, =1 {1 edge} other {# edges} } from group '{entityGroup}'?", + "group": "Group of edges", + "list-of-groups": "{ count, plural, =1 {One edge group} other {List of # edge groups} }", + "group-name-starts-with": "Edge groups whose names start with '{{prefix}}'", + "unassign-entity-group-from-edge-title": "Are you sure you want to unassign the entity group '{{ entityGroupName }}'?", + "unassign-entity-group-from-edge-text": "After the confirmation the entity group will be unassigned and won't be accessible by the edge.", + "unassign-entity-group-from-edge": "Unassign entity group from edge", + "unassign-entity-groups-from-edge-title": "Are you sure you want to unassign {{count}} entity groups?", + "unassign-entity-groups-from-edge-text": "After the confirmation entity groups will be unassigned and won't be accessible by the edge.", + "unassign-entity-groups-from-edge": "Unassign entity groups from edge", + "unassign-scheduler-events-from-edge": "Unassign scheduler events from edge", + "unassign-scheduler-event-from-edge-title": "Are you sure you want to unassign scheduler event '{{schedulerEventName}}'?", + "unassign-scheduler-event-from-edge-text": "After the confirmation the scheduler event will be unassigned and won't be accessible by the edge.", + "unassign-scheduler-events-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 scheduler event} other {# scheduler events} }?", + "unassign-scheduler-events-from-edge-text": "After the confirmation all selected scheduler events will be unassigned and won't be accessible by the edge.", + "manage-user-groups": "Manage user groups", + "manage-asset-groups": "Manage asset groups", + "manage-device-groups": "Manage device groups", + "manage-dashboard-groups": "Manage dashboard groups", + "manage-entity-view-groups": "Manage entity view groups", + "manage-scheduler-events": "Manage scheduler events", + "manage-integrations": "Manage integrations", + "assign-scheduler-event-to-edge-title": "Assign Scheduler Events To Edge", + "assign-scheduler-event-to-edge-text": "Please select the scheduler events to assign to the edge", + "assign-entity-groups-to-edge-text": "Please select the entity groups to assign to the edge", + "assign-integration-to-edge-title": "Assign Integrations To Edge", + "assign-integration-to-edge-text": "Please select the integrations to assign to the edge", + "missing-attributes-title": "Edge has missing attribute(s)", + "missing-attributes-text": "Integration(s), assigned to edge, have attribute placeholders that are not available on this edge.

List of missing edge attributes(s):
{{missingEdgeAttributes}}", + "missing-all-related-attributes-title": "Edge(s) have missing attribute(s)", + "missing-all-related-attributes-text": "Edge(s), related to integration, don't have attributes that are present inside integration configuration as attribute placeholders.

List of missing edge attributes(s):
{{missingEdgeAttributes}}", + "quick-overview-widget-header": "{{edgeName}} Quick Overview" + }, + "edge-event": { + "type-dashboard": "Dashboard", + "type-asset": "Asset", + "type-device": "Device", + "type-device-profile": "Device Profile", + "type-asset-profile": "Asset Profile", + "type-entity-view": "Entity View", + "type-alarm": "Alarm", + "type-entity-group": "Entity Group", + "type-rule-chain": "Rule Chain", + "type-rule-chain-metadata": "Rule Chain Metadata", + "type-edge": "Edge", + "type-user": "User", + "type-tenant": "Tenant", + "type-tenant-profile": "Tenant Profile", + "type-customer": "Customer", + "type-relation": "Relation", + "type-widgets-bundle": "Widgets Bundle", + "type-widgets-type": "Widgets Type", + "type-admin-settings": "Admin Settings", + "type-ota-package": "Ota Package", + "type-queue": "Queue", + "type-scheduler-event": "Scheduler Event", + "type-white-labeling": "White Labeling", + "type-login-white-labeling": "Login White Labeling", + "type-custom-translation": "Custom Translation", + "type-role": "Role", + "type-group-permission": "Group Permission", + "type-integration": "Integration", + "type-converter": "Converter", + "action-type-added": "Added", + "action-type-deleted": "Deleted", + "action-type-updated": "Updated", + "action-type-post-attributes": "Post Attributes", + "action-type-attributes-updated": "Attributes Updated", + "action-type-attributes-deleted": "Attributes Deleted", + "action-type-timeseries-updated": "Timeseries Updated", + "action-type-credentials-updated": "Credentials Updated", + "action-type-assigned-to-customer": "Assigned to Customer", + "action-type-unassigned-from-customer": "Unassigned from Customer", + "action-type-relation-add-or-update": "Relation Add or Update", + "action-type-relation-deleted": "Relation Deleted", + "action-type-rpc-call": "RPC Call", + "action-type-alarm-ack": "Alarm Ack", + "action-type-alarm-clear": "Alarm Clear", + "action-type-alarm-assigned": "Alarm Assigned", + "action-type-alarm-unassigned": "Alarm Unassigned", + "action-type-assigned-to-edge": "Assigned to Edge", + "action-type-unassigned-from-edge": "Unassigned from Edge", + "action-type-credentials-request": "Credentials Request", + "action-type-entity-merge-request": "Entity Merge Request", + "action-type-added-to-entity-group": "Added to Entity Group", + "action-type-removed-from-entity-group": "Removed from Entity Group", + "action-type-change-owner": "Change owner" + }, + "error": { + "unable-to-connect": "Nepavyksta prisijungti prie serverio! Patikrinkite interneto ryšį.", + "unhandled-error-code": "Neapdorotos klaidos kodas: {{errorCode}}", + "unknown-error": "Nežinoma klaida" + }, + "entity": { + "entity": "Subjektas", + "entities": "Subjektai", + "entities-count": "Subjektų kiekis", + "alarms-count": "Įspėjimų kiekis", + "aliases": "Subjektų pseudonimai", + "aliases-short": "Pseudonimai", + "entity-alias": "Subjekto pseudonimas", + "unable-delete-entity-alias-title": "Subjekto pseudonimo panaikinti nepavyko", + "unable-delete-entity-alias-text": "Subjekto pseudonimas '{{entityAlias}}' negali būti panaikintas, nes jis naudojamas valdikliuose:
{{widgetsList}}", + "duplicate-alias-error": "Pseudonimas '{{alias}}' dubliuojasi.
Subjektų pseudonimai skydelyje negali kartotis.", + "missing-entity-filter-error": "Trūksta pseudonimo '{{alias}}' filtro.", + "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", + "alias": "Pseudonimas", + "alias-required": "Subjekto pseudonimas būtinas.", + "remove-alias": "Panaikinti subjekto pseudonimą", + "add-alias": "Pridėti subjekto pseudonimą", + "entity-list": "Subjektų sąrašas", + "entity-type": "Subjekto tipas", + "entity-types": "Subjektų tipai", + "entity-type-list": "Subjektų tipų sąrašas", + "any-entity": "Bet kuris subjektas", + "add-entity-type": "Pridėti subjekto tipą", + "enter-entity-type": "Įveskitesubjekto tipą", + "no-entities-matching": "Subjektų, atitinkančių '{{entity}}' nėra.", + "no-entity-types-matching": "Subjektų tipų, atitinkančių '{{entityType}}' nėra.", + "name-starts-with": "Pavadinimas prasideda", + "help-text": "naudokite simbolį '%' pagal tai, kaip norite ieškoti: '%subjekto_pavadinimo_fragmentas%', '%subjekto_pavadinimo_pabaiga', 'subjekto_pavadinimo_pradžia%'.", + "use-entity-name-filter": "Naudoti filtrą", + "entity-list-empty": "Subjektai nepasirinkti.", + "entity-type-list-required": "Nors vienas subjekto tipas turi būti pasirinktas.", + "entity-name-filter-required": "Subjekto vardo filtras būtinas.", + "entity-name-filter-no-entity-matched": "Subjektų, kurių pavadinimas prasideda '{{entity}}' nėra.", + "all-subtypes": "Visi", + "select-entities": "Pasirinkte subjektus", + "no-aliases-found": "Pseudonimų nėra.", + "no-alias-matching": "'{{alias}}' nėra.", + "create-new-alias": "Sukurti naują!", + "create-new": "Sukurti naują", + "key": "Raktas", + "key-name": "Rakto pavadinimas", + "no-keys-found": "Rakto nėra.", + "no-key-matching": "'{{key}}' nėra.", + "create-new-key": "Sukurti naują!", + "type": "Tipas", + "type-required": "Subjekto tipas būtinas.", + "type-device": "Įrenginys", + "type-devices": "Įrenginiai", + "list-of-devices": "{ count, plural, =1 {Vienas įrenginys} other {# įrenginių sąrašas} }", + "device-name-starts-with": "Įrenginiai, kurių pavadinimas prasideda '{{prefix}}'", + "type-device-profile": "Įrenginio profilis", + "type-device-profiles": "Device profiles", + "clear-selected-profiles": "Clear selected profiles", + "list-of-device-profiles": "{ count, plural, =1 {One device profile} other {List of # device profiles} }", + "device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'", + "type-asset-profile": "Asset profile", + "type-asset-profiles": "Asset profiles", + "list-of-asset-profiles": "{ count, plural, =1 {One asset profile} other {List of # asset profiles} }", + "asset-profile-name-starts-with": "Asset profiles whose names start with '{{prefix}}'", + "type-asset": "Turtas", + "type-assets": "Turtas", + "list-of-assets": "{ count, plural, =1 {Turtas} other {# Turto sąrašas} }", + "asset-name-starts-with": "Turtas, kurio pavadinimas prasideda '{{prefix}}'", + "type-entity-view": "Subjekto rodinys", + "type-entity-views": "Subjektų rodinys", + "list-of-entity-views": "{ count, plural, =1 {Vienas subjekto rodinys} other {# Subjektų rodinių sąrašas} }", + "entity-view-name-starts-with": "Subjektų rodiniai, kurių pavadinimai prasideda '{{prefix}}'", + "type-rule": "Taisyklė", + "type-rules": "Taisyklės", + "list-of-rules": "{ count, plural, =1 {Viena taisyklė} other {# Taisyklių sąrašas} }", + "rule-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", + "type-plugin": "Papildinys", + "type-plugins": "Papildiniai", + "list-of-plugins": "{ count, plural, =1 {Vienas papildinys} other {# Papildinių sąrašas} }", + "plugin-name-starts-with": "Papildiniai, kurių pavadinimai prasideda '{{prefix}}'", + "type-tenant": "Valdytojas", + "type-tenants": "valdytojai", + "list-of-tenants": "{ count, plural, =1 {Vienas valdytojas} other {# Valdytojų sąrašas} }", + "tenant-name-starts-with": "Valdytojai, kurių pavadinimai prasideda '{{prefix}}'", + "type-tenant-profile": "Tenant profile", + "type-tenant-profiles": "Tenant profiles", + "list-of-tenant-profiles": "{ count, plural, =1 {One tenant profile} other {List of # tenant profiles} }", + "tenant-profile-name-starts-with": "Tenant profiles whose names start with '{{prefix}}'", + "type-customer": "Klientas", + "type-customers": "Klientai", + "list-of-customers": "{ count, plural, =1 {Vienas klientas} other {# Klientų sąrašas} }", + "customer-name-starts-with": "Klientai, kurių pavadinimai prasideda '{{prefix}}'", + "type-user": "Vartotojas", + "type-users": "Vartotojai", + "list-of-users": "{ count, plural, =1 {Vienas vartotojas} other {# Vartotojų sąrašas} }", + "user-name-starts-with": "Vartotojai, kurių pavadinimai prasideda '{{prefix}}'", + "type-dashboard": "Skydelis", + "type-dashboards": "Skydeliai", + "list-of-dashboards": "{ count, plural, =1 {Vienas skydelis} other {# Skydelių sąrašas} }", + "dashboard-name-starts-with": "Skydeliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-alarm": "Įspėjimas", + "type-alarms": "Įspėjimai", + "list-of-alarms": "{ count, plural, =1 {Vienas įspėjimas} other {# Įspėjimų sąrašas} }", + "alarm-name-starts-with": "Įspėjimai, kurių pavadinimai prasideda '{{prefix}}'", + "type-rulechain": "Taisyklių grandinė", + "type-rulechains": "Taisyklių grandinės", + "list-of-rulechains": "{ count, plural, =1 {Viena taisyklių grandinė} other {# Taisyklių grandinių sąrašas} }", + "rulechain-name-starts-with": "Taisyklių grandinės, kurų pavadinimai prasideda '{{prefix}}'", + "type-scheduler-event": "Tvarkaraščio įvykis", + "type-scheduler-events": "Tvarkaraščio įvykiai", + "list-of-scheduler-events": "{ count, plural, =1 {Vienas tvarkaraščio įvykis} other {# Tvarkaraščio įvykių sąrašas} }", + "scheduler-event-name-starts-with": "Tvarkaraščio įvykiai, kurių pavadinimai prasideda '{{prefix}}'", + "type-blob-entity": "Blob entity", + "type-blob-entities": "Blob entities", + "list-of-blob-entities": "{ count, plural, =1 {One blob entity} other {List of # blob entities} }", + "blob-entity-name-starts-with": "Blob entities whose names start with '{{prefix}}'", + "type-rulenode": "Tasyklė", + "type-rulenodes": "Taisyklės", + "list-of-rulenodes": "{ count, plural, =1 {Viena taisyklė} other {# Taisyklių sąrašas} }", + "rulenode-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", + "type-current-customer": "Dabartinis klientas", + "type-current-tenant": "Dabartinis valdytojas", + "type-current-user": "Dabartinis vartotojas", + "type-current-user-owner": "Dabartinio vartotojo savininkas", + "type-widgets-bundle": "Valdiklių rinkinys", + "type-widgets-bundles": "Valdiklių rinkiniai", + "list-of-widgets-bundles": "{ count, plural, =1 {Vienas valdiklių rinkinys} other {# Valdiklių rinkinių sąrašas} }", + "type-widget-type": "Valdiklio tipas", + "type-widget-types": "Valdiklių tipai", + "list-of-widget-types": "{ count, plural, =1 {Vienas valdiklio tipas} other {# Valdiklių tipų sąrašas} }", + "search": "Subjektų paieška", + "selected-entities": "Pasirinkta { count, plural, =1 {1 subjektas} other {# subjektai} }", + "entity-name": "Subjekto pavadinimas", + "entity-label": "Subjekto etikelė", + "details": "Informacija apie subjektą", + "no-entities-prompt": "Subjektų nėra", + "no-data": "Duomenų atvaizdavimui nėra", + "columns-to-display": "Rodomi stulpeliai", + "type-api-usage-state": "Api Usage State", + "type-entity-group": "Subjektų grupė", + "type-converter": "Duomenų keitiklis", + "type-converters": "Duomenų keitikliai", + "list-of-converters": "{ count, plural, =1 {Vienas duomenų keitiklis} other {# Duomenų keitiklių sąrašas} }", + "converter-name-starts-with": "Duomenų keitikliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-integration": "Integracija", + "type-integrations": "Integracijos", + "list-of-integrations": "{ count, plural, =1 {Viena integracija} other {# Integracijų sąrašas} }", + "integration-name-starts-with": "Integracijos, kurių pavadinimai prasideda '{{prefix}}'", + "type-role": "Rolė", + "type-roles": "Rolės", + "list-of-roles": "{ count, plural, =1 {Viena rolė} other {# rolių sąrašas} }", + "role-name-starts-with": "Rolės, kurių pavadinimai prasideda '{{prefix}}'", + "type-group-permission": "Grupės leidimai", + "type-edge": "Edge", + "type-edges": "Edges", + "list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }", + "edge-name-starts-with": "Edges whose names start with '{{prefix}}'", + "type-tb-resource": "Resource", + "type-ota-package": "OTA package", + "type-rpc": "RPC", + "type-queue": "Queue", + "type-notification": "Notification", + "type-notification-rule": "Notification rule", + "type-notification-rules": "Notification rules", + "list-of-notification-rules": "{ count, plural, =1 {One notification rule} other {List of # notification rules} }", + "type-notification-target": "Notification recipient", + "type-notification-targets": "Notification recipients", + "list-of-notification-targets": "{ count, plural, =1 {One notification recipient} other {List of # notification recipients} }", + "type-notification-request": "Notification request", + "type-notification-template": "Notification template", + "type-notification-templates": "Notification templates", + "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", + "customer-name": "Klientas", + "sub-customer-name": "Priklausantis klientas", + "parent-customer-name": "Pirminis klientas", + "parent-sub-customer-name": "Pirminis klientas", + "include-customer-entities": "Rodyti klientui priklausančius klientus", + "include-sub-customer-entities": "Rodyti priklausančių klientų subjektus", + "groups": "Grupė" + }, + "entity-group": { + "entity-group": "Subjektų grupė", + "details": "Informacija", + "columns": "Stulpeliai", + "add-column": "Pridėti stulpelį", + "column-value": "Reikšmė", + "column-value-required": "Stulpelio reikšmė būtina.", + "column-title": "Pavadinimas", + "default-sort-order": "Numatytoji rūšiavimo tvarka", + "default-sort-order-required": "Numatytoji rūšiavimo tvarka būtina.", + "hide-in-mobile-view": "Nerodyti mobiliąjame režime", + "use-cell-style-function": "Taikyti langelio stiliaus funkciją", + "use-cell-content-function": "Taikyti langelio turinio funkciją", + "edit-column": "Redaguoti stulpelį", + "column-details": "Stulpelio informacija", + "actions": "Veiksmai", + "settings": "Nustatymai", + "search": "Subjektų grupių paieška", + "delete": "Panaikinti subjektų grupę", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "description": "Aprašymas", + "add": "Pridėti subjektų grupę", + "open-entity-group": "Atverti subjektų grupę", + "add-entity-group-text": "Pridėti naują subjektų grupę", + "no-entity-groups-text": "Subjektų grupių nėra", + "entity-group-details": "Informacija apie subjektų grupę", + "selected-entity-groups": "Pasirinkta { count, plural, =1 {1 subjektų grupė} other {# subjektų grupės} }", + "delete-entity-groups": "Panaikinti subjektų grupes", + "delete-entity-group-title": "Ar tikrai norite panaikinti subjektų grupę '{{entityGroupName}}'?", + "delete-entity-group-text": "Būkite dėmesingi, po patvirtinimo, subjektų grupė ir visa su ja susijusi informacija bus pašalinta ir jų atkurti nebegalėsite.", + "delete-entity-groups-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subjektų grupę} other {# subjektų grupes} }?", + "delete-entity-groups-action-title": "Panaikinti { count, plural, =1 {1 subjektų grupę} other {# subjektų grupes} }", + "delete-entity-groups-text": "Būkite dėmesingi, po patvirtinimo visos subjektų grupės ir su jomis susijusi informacija bus panaikintos ir jų atkurti nebegalėsite.", + "device-groups": "Įrenginių grupės", + "shared-device-groups": "Bendrinamos įrenginių grupės", + "asset-groups": "Turto grupės", + "shared-asset-groups": "Bendrinamos turto grupės", + "customer-groups": "Klientų grupės", + "shared-customer-groups": "Bendrinamos klientų grupės", + "device-group": "Įrenginių grupė", + "asset-group": "Turto grupė", + "user-group": "Vartotojų grupė", + "user-groups": "Vartotojų grupės", + "customer-group": "Klientų grupė", + "entity-view-groups": "Subjektų rodinių grupė", + "shared-entity-view-groups": "Bendrinamos subjektų rodinių grupės", + "entity-view-group": "Subjektų rodinių grupė", + "edge-groups": "Edge groups", + "shared-edge-groups": "Shared edge groups", + "edge-group": "Edge group", + "dashboard-groups": "Skydelių grupės", + "shared-dashboard-groups": "Bendrinamos skydelių grupės", + "dashboard-group": "Skydelių grupė", + "fetch-more": "Gauti daugiau", + "column-type": { + "column-type": "Stulpelio tipas", + "client-attribute": "Kliento atributas", + "shared-attribute": "Bendrinamas atributas", + "server-attribute": "Serverio atributas", + "timeseries": "Telemetrija", + "entity-field": "Subjekto laukas" + }, + "column-type-required": "Stulpelio tipas būtinas.", + "entity-field": { + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "type": "Tipas", + "device_profile": "Device profile", + "asset_profile": "Asset profile", + "assigned_customer": "Priskirtas klientas", + "authority": "Authority", + "first_name": "Vardas", + "last_name": "Pavardė", + "email": "El. paštas", + "title": "Pavadinimas", + "country": "Šalis", + "state": "Apskritis/rajonas", + "city": "Miestas", + "address": "Adresas", + "address2": "Adresas 2", + "zip": "Pašto kodas", + "phone": "Tel, numeris", + "label": "Etiketė" + }, + "sort-order": { + "asc": "Didėjanti", + "desc": "Mažėjanti", + "none": "Nėra" + }, + "details-mode": { + "on-row-click": "Spustelėjus eilutėje", + "on-action-button-click": "Paspaudus informacijos mygtuką", + "disabled": "Išjungta" + }, + "change-owner": "Pakeisti savininką", + "select-target-owner": "Pasirinkite savininką", + "no-owners-matching": "Savinikų atitinkančių '{{owner}}' nėra.", + "target-owner-required": "Savininkas būtinas.", + "confirm-change-owner-title": "Ar tikrai norite pakeisti savininką { count, plural, =1 {1 pasirinktam subjektui} other {# pasirinktiems subjektams} }?", + "confirm-change-owner-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektai iš esamo savininko bus perkelti į naujo savininko grupę 'Visi'.", + "add-to-group": "Pridėti į grupę", + "move-to-group": "Perkelti į grupę", + "select-entity-group": "Pasirinkite subjektų grupę", + "no-entity-groups-matching": "Subjektų grupių, atitinkančių '{{entityGroup}}' nėra.", + "create-new-entity-group": "Sukurti naują!", + "target-entity-group-required": "Subjektų grupė būtina.", + "select-user-group": "Pasirinkite vartotojų grupę", + "no-user-groups-matching": "Vartotojų grupių, atitinkančių '{{entityGroup}}' nėra.", + "target-user-group-required": "Vartotojų grupė būtina.", + "remove-from-group": "Pašalinti iš grupės", + "group-table-title": "Grupių lentelės pavadinimas", + "enable-search": "Leisti subjektų paiešką", + "enable-add": "Leisti pridėti subjektus", + "enable-delete": "Leisti panaikinti subjektus", + "enable-selection": "Leisti subjektų pasirinkimą", + "enable-group-transfer": "Leisti perkėlimus tarp grupių", + "display-pagination": "Rodyti dalinimą puslapiais", + "default-page-size": "Numatytasis puslapio dydis", + "enable-assignment-actions": "Leisti priskirti", + "enable-credentials-management": "Leisti valdyti įgaliojimus", + "enable-login-as-user": "Leisti prisijungti kaip vartotojui", + "enable-users-management": "Leisti valdyti vartotojus", + "enable-customers-management": "Leisti valdyti klientus", + "enable-assets-management": "Leisti valdyti turtą", + "enable-devices-management": "Leisti valdyti įrenginius", + "enable-entity-views-management": "Leisti subjektų rodinių valdymą", + "enable-edges-management": "Enable edges management", + "enable-dashboards-management": "Leisti skydelių valdymą", + "enable-scheduler-events-management": "Leisti tvarkaraščio įvykių valdymą", + "open-details-on": "Atverti subjekto informaciją", + "select-existing": "Pasirinkti egzistuojančią subjektų grupę", + "create-new": "Sukurti naują subjektų grupę", + "new-entity-group-name": "Naujos subjektų grupės pavadinimas", + "entity-group-list": "Subjektų grupių sąrašas", + "entity-group-list-empty": "Nepasirinktos subjektų grupės.", + "name-starts-with": "Subjektų grupės pavadinimas prasideda", + "entity-group-name-filter-required": "Subjektų grupės filtras būtinas.", + "roles": "Rolės", + "permissions": "Leidimai", + "public": "Vieša", + "entity-group-public": "Subjektų grupė yra vieša", + "make-public": "Subjektų grupę padaryti vieša", + "make-private": "Subjektų grupę padaryti privačia", + "make-public-entity-group-title": "Ar tikrai norite subjektų grupę '{{entityGroupName}}' padaryti vieša?", + "make-public-entity-group-text": "Po patvirtinimo subjektų grupė ir visi jai priklausantys subjektai taps vieši ir matomi kitiems vartotojams.", + "make-private-entity-group-title": "Ar tikrai norite subjektų grupę '{{entityGroupName}}' padaryti privačia?", + "make-private-entity-group-text": "Po patvirtinimo, subjektų grupė ir visi jai priklausantys subjektai taps privatūs ir nematomi kitiems vartotojams.", + "share": "Pasdalinti grupe", + "copyId": "Kopijuoti subjektų grupės Id", + "idCopiedMessage": "Subjektų grupės Id nukopijuotas į škarpinę", + "entity-group-name": "Subjektų grupės pavadinimas", + "all-users": "Visi vartotojai", + "owner": "Savininkas", + "owner-required": "Savininkas būtinas.", + "manage-owner-and-groups": "Savininkas ir grupės", + "owner-and-groups": "Savininkas ir grupės" + }, + "entity-field": { + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "type": "Tipas", + "first-name": "Vardas", + "last-name": "Pavardė", + "email": "El. paštas", + "title": "Pavadinimas", + "country": "Šalis", + "state": "Rajonas", + "city": "Miestas", + "address": "Addresas", + "address2": "Addresas 2", + "zip": "Pašto kodas", + "phone": "Telefonas", + "label": "Etikelė", + "configuration": "Konfigūracija", + "schedule": "Grafikas", + "originatorId": "Iniciatoriaus Id", + "originatorType": "Iniciatoriaus tipas" + }, + "entity-view": { + "all": "Visi", + "all-entity-views": "Visi subjektų rodiniai", + "groups": "Grupės", + "shared": "bendrinami", + "entity-view": "Subjektų rodiniai", + "entity-view-required": "Subjeto rodinys būtinas.", + "entity-views": "Subjektų rodiniai", + "management": "Subjektų rodinių valdymas", + "view-entity-views": "Peržiūrėti subjektų rodinius", + "entity-view-alias": "Subjektų rodinio pseudonimas", + "aliases": "Subjektų rodinio pseudonimai", + "no-alias-matching": "'{{alias}}' nerastas.", + "no-aliases-found": "Pseudonimų nėra.", + "no-key-matching": "'{{key}}' nerastas.", + "no-keys-found": "Raktų nėra.", + "create-new-alias": "Sukurti naują!", + "create-new-key": "Sukurti naują!", + "duplicate-alias-error": "'{{alias}}' dubliuojasi.
Įrenginio pseudonimai skydelyje turi būti unikalūs.", + "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", + "no-entity-views-matching": "Subjektų rodinių, atitinkančių '{{entity}}' nėra.", + "public": "Viešas", + "alias": "Pseudonimas", + "alias-required": "Subjekto rodinio pseudonimas būtinas.", + "remove-alias": "Panaikinti subjekto rodinio pseudonimą", + "add-alias": "Pridėti subjekto rodinio pseudonimą", + "name-starts-with": "Subjekto rodinio pavadinimas prasideda", + "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%subjekto_rodinio_pavadinimo_fragmentas%', '%subjekto_rodinio_pavadinimo_pabaiga', 'subjekto_rodinio_pavadinimo_pradžia%'.", + "entity-view-list": "Subjektų rodinių sąrašas", + "use-entity-view-name-filter": "Naudoti filtrą", + "entity-view-list-empty": "Subjektai nepasirinkti.", + "entity-view-name-filter-required": "Subjekto rodinio pavadinimo filtras būtinas.", + "entity-view-name-filter-no-entity-view-matched": "Subjektų rodinių, kurių pavadinimai prasideda '{{entityView}}' nėra.", + "add": "Pridėti subjektų rodinį", + "entity-view-public": "Subjektų rodnys yra viešas", + "assign-to-customer": "Priskirti klientui", + "assign-entity-view-to-customer": "Subjektų rodinį (-ius) priskirti klientui", + "assign-entity-view-to-customer-text": "Pasirinkite subjektų rodinius, kuriuos norite priskirti klientui", + "no-entity-views-text": "Subjektų rodinių nėra", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti subjektų rodinį (-ius)", + "entity-view-details": "Informacija apie subjektų rodinį", + "add-entity-view-text": "Pridėti naują subjektų rodinį", + "delete": "Panikinti subjektų rodinį", + "assign-entity-views": "Priskirti subjektų rodinius", + "assign-entity-views-text": "Priskirti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } klientui", + "delete-entity-views": "Panaikinti subjektų rodinius", + "make-public": "Subjektų rodinį padaryti viešu", + "make-private": "Subjektų rodinį padaryti privačiu", + "unassign-from-customer": "Atsieti nuo kliento", + "unassign-entity-views": "Atsieti subjektų rodinius", + "unassign-entity-views-action-title": "Atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } nuo kliento", + "assign-new-entity-view": "Priskirti naują subjektų rodinį", + "delete-entity-view-title": "Ar tikrai norite panaikinti subjektų rodinį '{{entityViewName}}'?", + "delete-entity-view-text": "Būkite dėmesingi, po patvirtinimo, subjektų rodinys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "delete-entity-views-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subketų rodinį} other {# subjektų rodinius} }?", + "delete-entity-views-action-title": "Panaikinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }", + "delete-entity-views-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektų rodiniai ir visa su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "make-public-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti viešu?", + "make-public-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps vieša ir matoma kitiems vartotojams.", + "make-private-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti privačiu?", + "make-private-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps privati ir kiti vartotojai jų nebematys.", + "unassign-entity-view-title": "Ar tikrai norite atsieti subjektų rodinį '{{entityViewName}}'?", + "unassign-entity-view-text": "Po patvirtinimo subjektų rodinys bus atsietas ir klientas jo nebematys.", + "unassign-entity-view": "Atsieti subjektų rodinį", + "unassign-entity-views-title": "Ar tikrai norite atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", + "unassign-entity-views-text": "Po patvirtinimo visi pasirinkti subjektų rodiniai bus atsieti ir klientas jų nebematys.", + "entity-view-type": "Subjektų rodinio tipas", + "entity-view-type-required": "Subjektų rodinio tipas būtinas.", + "select-entity-view-type": "Pasirinkite subjektų rodinio tipą", + "enter-entity-view-type": "Įveskite subjektų rodinio tipą", + "any-entity-view": "Bet kuris subjektų tipas", + "no-entity-view-types-matching": "Subjektų rodinių, atitinkančių '{{entitySubtype}}' nėra.", + "entity-view-type-list-empty": "Subjektų tipai nepasirinkti.", + "entity-view-types": "Subjektų rodinio tipai", + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "type-max-length": "Rodinio tipas negali viršyti 256 simbolių", + "description": "Aprašymas", + "events": "Įvykiai", + "details": "Informacija", + "copyId": "Kopijuoti subjekto rodinio Id", + "idCopiedMessage": "Subjektų rodinio Id nukopjuotas į iškarpinę", + "assignedToCustomer": "Priskirtas klientui", + "unable-entity-view-device-alias-title": "Subjektų rodinio pseudonimo ištrinti nepavyko", + "unable-entity-view-device-alias-text": "Įrenginio pseudonimas '{{entityViewAlias}}' Negali būti ištrintas, nes jis naudojamas valdikliuose:
{{widgetsList}}", + "select-entity-view": "Pasirinkti subjektų rodinį", + "start-date": "Pradžios data", + "start-ts": "Pradžios laikas", + "end-date": "Pabaigos data", + "end-ts": "Pabaigos laikas", + "date-limits": "Datos limitas", + "client-attributes": "Kliento atributai", + "shared-attributes": "Bendrinami atributai", + "server-attributes": "Serverio atributai", + "timeseries": "Telemetrija", + "client-attributes-placeholder": "Kliento atributai", + "shared-attributes-placeholder": "Bendrinami atributai", + "server-attributes-placeholder": "Serverio atributai", + "timeseries-placeholder": "Telemetrija", + "target-entity": "Tikslinis subjektas", + "attributes-propagation": "Atributų paplitimas", + "attributes-propagation-hint": "Subjektų rodinys automatikškai kopijuos nurodytus atributus iš tikslinio subjekto kiekvieną kartą, kai išsaugosite ar atnaujinsite šį rodinį. Norint išsaugoti sistemos spartą, tikslinio subjekto atributai neperkeliami į subjektų rodinį kiekvieną kartą kai jie pasikeičia. Tačiau jūs galite įjungti automatinį atributų reikšmių perdavimą, taisyklių grandinėje sukonfigūravę \"copy to view\" taisyklę ir \"Post attributes\" bei \"Attributes Updated\" žinutes nukreipę į naują taisyklę.", + "timeseries-data": "Temetrijos duomenys", + "timeseries-data-hint": "Sukonfigūruokite tikslinio objekto telemetrijos duomenų raktus, kurie bus pasiekiami objekto rodinyje. Šios telemetrijos duomenys yra tik skaitomi.", + "select-group-to-add": "Pasirinkite grupę į kurią pridėti pažymėtus subjektų rodinius", + "select-group-to-move": "Pasirinkite grupę į kurią perkelti pažymėtus subjektų rodinius", + "remove-entity-views-from-group": "Ar tikrai norite iš '{{entityGroup}}' grupės pašalinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", + "group": "Subjektų rodinių grupė", + "list-of-groups": "{ count, plural, =1 {Viena subjektų rodinių grupė} other {# subjektų rodinių grupių sąrašas} }", + "group-name-starts-with": "Subjektų rodinių grupės pavadinimas prasideda '{{prefix}}'", + "search": "Subjektų rodinių paieška", + "selected-entity-views": "Pasirinkta { count, plural, =1 {1 subjektų rodinys} other {# subjektų rodiniai} }", + "assign-entity-view-to-edge": "Assign Entity View(s) To Edge", + "assign-entity-view-to-edge-text": "Please select the entity views to assign to the edge", + "unassign-entity-view-from-edge-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "After the confirmation the entity view will be unassigned and won't be accessible by the edge.", + "unassign-entity-views-from-edge-action-title": "Unassign { count, plural, =1 {1 entity view} other {# entity views} } from edge", + "unassign-entity-view-from-edge": "Unassign entity view", + "unassign-entity-views-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 entity view} other {# entity views} }?", + "unassign-entity-views-from-edge-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the edge." + }, + "event": { + "events": "Įvykiai", + "event-type": "Įvykio tipas", + "events-filter": "Įvykių filtras", + "clean-events": "Įšvalyti įvykius", + "type-error": "Klaida", + "type-lc-event": "Gyvavimo ciklo įvykis", + "type-stats": "Statistika", + "type-debug-converter": "Debug", + "type-debug-integration": "Debug", + "type-debug-rule-node": "Debug", + "type-debug-rule-chain": "Debug", + "no-events-prompt": "Įvykių nėra", + "error": "Klaida", + "alarm": "Įspėjimas", + "event-time": "Įvykio laikas", + "server": "Serveris", + "body": "Body", + "method": "Metodas", + "type": "Tipas", + "in": "Į", + "out": "Iš", + "metadata": "Metadata", + "message": "Message", + "entity": "Entity", + "message-id": "Message Id", + "copy-message-id": "Copy message Id", + "message-type": "Message Type", + "data-type": "Data Type", + "relation-type": "Relation Type", + "data": "Data", + "event": "Event", + "status": "Status", + "success": "Success", + "failed": "Failed", + "messages-processed": "Messages processed", + "max-messages-processed": "Maximum messages processed", + "min-messages-processed": "Minimum messages processed", + "errors-occurred": "Errors occurred", + "max-errors-occurred": "Maximum errors occurred", + "min-errors-occurred": "Minimum errors occurred", + "min-value": "Minimum value is 0.", + "all-events": "All", + "has-error": "Has error", + "entity-id": "Entity Id", + "copy-entity-id": "Copy entity Id", + "entity-type": "Entity type", + "clear-filter": "Clear Filter", + "clear-request-title": "Clear all events", + "clear-request-text": "Are you sure you want to clear all events?", + "started": "Started", + "updated": "Updated", + "stopped": "Stopped" + }, + "extension": { + "extensions": "Extensions", + "selected-extensions": "{ count, plural, =1 {1 extension} other {# extensions} } selected", + "type": "Type", + "key": "Key", + "value": "Value", + "id": "Id", + "extension-id": "Extension id", + "extension-type": "Extension type", + "transformer-json": "JSON *", + "unique-id-required": "Current extension id already exists.", + "delete": "Delete extension", + "add": "Add extension", + "edit": "Edit extension", + "view": "View extension", + "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", + "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", + "delete-extensions-title": "Are you sure you want to delete { count, plural, =1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", + "converters": "Converters", + "converter-id": "Converter id", + "configuration": "Configuration", + "converter-configurations": "Converter configurations", + "token": "Security token", + "add-converter": "Add converter", + "add-config": "Add converter configuration", + "device-name-expression": "Device name expression", + "device-type-expression": "Device type expression", + "custom": "Custom", + "to-double": "To Double", + "transformer": "Transformer", + "json-required": "Transformer json is required.", + "json-parse": "Unable to parse transformer json.", + "attributes": "Attributes", + "add-attribute": "Add attribute", + "add-map": "Add mapping element", + "timeseries": "Timeseries", + "add-timeseries": "Add timeseries", + "field-required": "Field is required", + "brokers": "Brokers", + "add-broker": "Add broker", + "host": "Host", + "port": "Port", + "port-range": "Port should be in a range from 1 to 65535.", + "ssl": "Ssl", + "credentials": "Credentials", + "username": "Username", + "password": "Password", + "retry-interval": "Retry interval in milliseconds", + "sas": "Shared Access Signature", + "anonymous": "Anonymous", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "CA certificate file *", + "private-key": "Private key file *", + "cert": "Certificate file *", + "no-file": "No file selected.", + "drop-file": "Drop a file or click to select a file to upload.", + "mapping": "Mapping", + "topic-filter": "Topic filter", + "converter-type": "Converter type", + "converter-json": "Json", + "json-name-expression": "Device name json expression", + "topic-name-expression": "Device name topic expression", + "json-type-expression": "Device type json expression", + "topic-type-expression": "Device type topic expression", + "attribute-key-expression": "Attribute key expression", + "attr-json-key-expression": "Attribute key json expression", + "attr-topic-key-expression": "Attribute key topic expression", + "request-id-expression": "Request id expression", + "request-id-json-expression": "Request id json expression", + "request-id-topic-expression": "Request id topic expression", + "response-topic-expression": "Response topic expression", + "value-expression": "Value expression", + "topic": "Topic", + "timeout": "Timeout in milliseconds", + "converter-json-required": "Converter json is required.", + "converter-json-parse": "Unable to parse converter json.", + "filter-expression": "Filter expression", + "connect-requests": "Connect requests", + "add-connect-request": "Add connect request", + "disconnect-requests": "Disconnect requests", + "add-disconnect-request": "Add disconnect request", + "attribute-requests": "Attribute requests", + "add-attribute-request": "Add attribute request", + "attribute-updates": "Attribute updates", + "add-attribute-update": "Add attribute update", + "server-side-rpc": "Server side RPC", + "add-server-side-rpc-request": "Add server-side RPC request", + "device-name-filter": "Device name filter", + "attribute-filter": "Attribute filter", + "method-filter": "Method filter", + "request-topic-expression": "Request topic expression", + "response-timeout": "Response timeout in milliseconds", + "topic-expression": "Topic expression", + "client-scope": "Client scope", + "add-device": "Add device", + "opc-server": "Servers", + "opc-add-server": "Add server", + "opc-add-server-prompt": "Please add server", + "opc-application-name": "Application name", + "opc-application-uri": "Application uri", + "opc-scan-period-in-seconds": "Scan period in seconds", + "opc-security": "Security", + "opc-identity": "Identity", + "opc-keystore": "Keystore", + "opc-type": "Type", + "opc-keystore-type": "Type", + "opc-keystore-location": "Location *", + "opc-keystore-password": "Password", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Key password", + "opc-device-node-pattern": "Device node pattern", + "opc-device-name-pattern": "Device name pattern", + "modbus-server": "Servers/slaves", + "modbus-add-server": "Add server/slave", + "modbus-add-server-prompt": "Please add server/slave", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Automatically reconnect", + "modbus-rtu-over-tcp": "RTU over TCP", + "modbus-port-name": "Serial port name", + "modbus-encoding": "Encoding", + "modbus-parity": "Parity", + "modbus-baudrate": "Baud rate", + "modbus-databits": "Data bits", + "modbus-stopbits": "Stop bits", + "modbus-databits-range": "Data bits should be in a range from 7 to 8.", + "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", + "modbus-unit-id": "Unit ID", + "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", + "modbus-device-name": "Device name", + "modbus-poll-period": "Poll period (ms)", + "modbus-attributes-poll-period": "Attributes poll period (ms)", + "modbus-timeseries-poll-period": "Timeseries poll period (ms)", + "modbus-poll-period-range": "Poll period should be positive value.", + "modbus-tag": "Tag", + "modbus-function": "Function", + "modbus-register-address": "Register address", + "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", + "modbus-register-count": "Register count", + "modbus-register-count-range": "Register count should be a positive value.", + "modbus-byte-order": "Byte order", + "sync": { + "status": "Status", + "sync": "Sync", + "not-sync": "Not sync", + "last-sync-time": "Last sync time", + "not-available": "Not available" + }, + "export-extensions-configuration": "Export extensions configuration", + "import-extensions-configuration": "Import extensions configuration", + "import-extensions": "Import extensions", + "import-extension": "Import extension", + "export-extension": "Export extension", + "file": "Extensions file", + "invalid-file-error": "Invalid extension file", + "text": "TEXT", + "json": "JSON", + "binary": "BINARY", + "hex": "HEX" + }, + "feature": { + "advanced-features": "Advanced features" + }, + "filter": { + "add": "Add filter", + "edit": "Edit filter", + "name": "Filter name", + "name-required": "Filter name is required.", + "duplicate-filter": "Filter with same name is already exists.", + "filters": "Filters", + "unable-delete-filter-title": "Unable to delete filter", + "unable-delete-filter-text": "Filter '{{filter}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", + "duplicate-filter-error": "Duplicate filter found '{{filter}}'.
Filters must be unique within the dashboard.", + "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.", + "filter": "Filter", + "editable": "Editable", + "no-filters-found": "No filters found.", + "no-filter-text": "No filter specified", + "add-filter-prompt": "Please add filter", + "no-filter-matching": "'{{filter}}' not found.", + "create-new-filter": "Create a new one!", + "create-new": "Create new", + "filter-required": "Filter is required.", + "operation": { + "operation": "Operation", + "equal": "equal", + "not-equal": "not equal", + "starts-with": "starts with", + "ends-with": "ends with", + "contains": "contains", + "not-contains": "not contains", + "greater": "greater than", + "less": "less than", + "greater-or-equal": "greater or equal", + "less-or-equal": "less or equal", + "and": "and", + "or": "or", + "in": "in", + "not-in": "not in" + }, + "ignore-case": "ignore case", + "value": "Value", + "remove-filter": "Remove filter", + "duplicate-filter-action": "Duplicate filter", + "preview": "Filter preview", + "no-filters": "No filters configured", + "add-filter": "Add filter", + "add-complex-filter": "Add complex filter", + "add-complex": "Add complex", + "complex-filter": "Complex filter", + "edit-complex-filter": "Edit complex filter", + "edit-filter-user-params": "Edit filter predicate user parameters", + "filter-user-params": "Filter predicate user parameters", + "user-parameters": "User parameters", + "display-label": "Label to display", + "autogenerated-label": "Auto generate label", + "order-priority": "Field order priority", + "key-filter": "Key filter", + "key-filters": "Key filters", + "key-name": "Key name", + "key-name-required": "Key name is required.", + "key-type": { + "key-type": "Key type", + "attribute": "Attribute", + "timeseries": "Timeseries", + "entity-field": "Entity field", + "constant": "Constant" + }, + "value-type": { + "value-type": "Value type", + "string": "String", + "numeric": "Numeric", + "boolean": "Boolean", + "date-time": "Datetime" + }, + "value-type-required": "Key value type is required.", + "key-value-type-change-title": "Are you sure you want to change key value type?", + "key-value-type-change-message": "If you confirm new value type all entered key filters will be removed.", + "no-key-filters": "No key filters configured", + "add-key-filter": "Add key filter", + "remove-key-filter": "Remove key filter", + "edit-key-filter": "Edit key filter", + "date": "Date", + "time": "Time", + "current-tenant": "Current tenant", + "current-customer": "Current customer", + "current-user": "Current user", + "current-device": "Current device", + "default-value": "Default value", + "dynamic-source-type": "Dynamic source type", + "dynamic-value": "Dynamic value", + "no-dynamic-value": "No dynamic value", + "source-attribute": "Source attribute", + "switch-to-dynamic-value": "Switch to dynamic value", + "switch-to-default-value": "Switch to default value", + "inherit-owner": "Inherit from owner", + "source-attribute-not-set": "If source attribute isn't set" + }, + "fullscreen": { + "expand": "Rodyti per visą ekraną", + "exit": "Išjungti rodymą per visą ekraną", + "toggle": "Rodyti per visą ekraną", + "fullscreen": "Rodyti per visą ekraną" + }, + "function": { + "function": "Funkcija" + }, + "gateway": { + "add-entry": "Add configuration", + "advanced": "Advanced", + "checking-device-activity": "Checking device activity", + "command": "Docker commands", + "command-copied-message": "Docker command has been copied to clipboard", + "configuration": "Configuration", + "connector-add": "Add new connector", + "connector-enabled": "Enable connector", + "connector-name": "Connector name", + "connector-name-required": "Connector name is required.", + "connector-type": "Connector type", + "connector-type-required": "Connector type is required.", + "connectors": "Connectors", + "connectors-config": "Connectors configuration", + "connectors-table-enabled": "Enabled", + "connectors-table-name": "Name", + "connectors-table-type": "Type", + "connectors-table-status": "Status", + "connectors-table-actions": "Actions", + "connectors-table-key": "Key", + "connectors-table-class": "Class", + "rpc-command-send": "Send", + "rpc-command-result": "Result", + "rpc-command-edit-params": "Edit parameters", + "gateway-configuration": "General Configuration", + "docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.", + "create-new-gateway": "Create a new gateway", + "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", + "created-time": "Created time", + "configuration-delete-dialog-header": "Configurations will be deleted", + "configuration-delete-dialog-body": "Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below", + "configuration-delete-dialog-input": "Gateway name", + "configuration-delete-dialog-input-required": "Gateway name is mandatory", + "configuration-delete-dialog-confirm": "Turn Off", + "delete": "Delete configuration", + "download-tip": "Download configuration file", + "drop-file": "Drop file here or", + "gateway": "Gateway", + "gateway-exists": "Device with same name is already exists.", + "gateway-name": "Gateway name", + "gateway-name-required": "Gateway name is required.", + "gateway-saved": "Gateway configuration successfully saved.", + "grpc": "GRPC", + "grpc-keep-alive-timeout": "Keep alive timeout (in ms)", + "grpc-keep-alive-timeout-required": "Keep alive timeout is required", + "grpc-keep-alive-timeout-min": "Keep alive timeout can not be less then 1", + "grpc-keep-alive-timeout-pattern": "Keep alive timeout is not valid", + "grpc-keep-alive": "Keep alive (in ms)", + "grpc-keep-alive-required": "Keep alive is required", + "grpc-keep-alive-min": "Keep alive can not be less then 1", + "grpc-keep-alive-pattern": "Keep alive is not valid", + "grpc-min-time-between-pings": "Min time between pings (in ms)", + "grpc-min-time-between-pings-required": "Min time between pings is required", + "grpc-min-time-between-pings-min": "Min time between pings can not be less then 1", + "grpc-min-time-between-pings-pattern": "Min time between pings is not valid", + "grpc-min-ping-interval-without-data": "Min ping interval without data (in ms)", + "grpc-min-ping-interval-without-data-required": "Min ping interval without data is required", + "grpc-min-ping-interval-without-data-min": "Min ping interval without data can not be less then 1", + "grpc-min-ping-interval-without-data-pattern": "Min ping interval without data is not valid", + "grpc-max-pings-without-data": "Max pings without data", + "grpc-max-pings-without-data-required": "Max pings without data is required", + "grpc-max-pings-without-data-min": "Max pings without data can not be less then 1", + "grpc-max-pings-without-data-pattern": "Max pings without data is not valid", + "inactivity-check-period-seconds": "Inactivity check period (in sec)", + "inactivity-check-period-seconds-required": "Inactivity check period is required", + "inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1", + "inactivity-check-period-seconds-pattern": "Inactivity check period is not valid", + "inactivity-timeout-seconds": "Inactivity timeout (in sec)", + "inactivity-timeout-seconds-required": "Inactivity timeout is required", + "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1", + "inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid", + "json-parse": "Not valid JSON.", + "json-required": "Field cannot be empty.", + "logs": { + "logs": "Logs", + "days": "days", + "hours": "hours", + "minutes": "minutes", + "seconds": "seconds", + "date-format": "Date format", + "date-format-required": "Date format required", + "log-format": "Log format", + "log-type": "Log type", + "log-format-required": "Log format required", + "remote": "Remote logging", + "remote-logs": "Remote logs", + "local": "Local logging", + "level": "Log level", + "file-path": "File path", + "file-path-required": "File path required", + "saving-period": "Log saving period", + "saving-period-min": "Log saving period can not be less then 1", + "saving-period-required": "Log saving period required", + "backup-count": "Backup count", + "backup-count-min": "Backup count can not be less then 1", + "backup-count-required": "Backup count required" + }, + "min-pack-send-delay": "Min pack send delay (in ms)", + "min-pack-send-delay-required": "Min pack send delay is required", + "min-pack-send-delay-min": "Min pack send delay can not be less then 0", + "no-connectors": "No connectors", + "no-data": "No configurations", + "no-gateway-found": "No gateway found.", + "no-gateway-matching": " '{{item}}' not found.", + "path-logs": "Path to log files", + "path-logs-required": "Path is required.", + "permit-without-calls": "Keep alive permit without calls", + "remote": "Remote configuration", + "remote-logging-level": "Logging level", + "remove-entry": "Remove configuration", + "remote-shell": "Remote shell", + "remote-configuration": "Remote Configuration", + "other": "Other", + "save-tip": "Save configuration file", + "security-type": "Security type", + "security-types": { + "access-token": "Access Token", + "username-password": "Username and Password", + "tls": "TLS", + "tls-access-token": "TLS + Access Token", + "tls-private-key": "TLS + Private Key" + }, + "server-port": "Server port", + "statistics": { + "statistic": "Statistic", + "statistics": "Statistics", + "statistic-commands-empty": "No statistics available", + "commands": "Commands", + "send-period": "Statistic send period (in sec)", + "send-period-required": "Statistic send period is required", + "send-period-min": "Statistic send period can not be less then 60", + "send-period-pattern": "Statistic send period is not valid", + "check-connectors-configuration": "Check connectors configuration (in sec)", + "check-connectors-configuration-required": "Check connectors configuration is required", + "check-connectors-configuration-min": "Check connectors configuration can not be less then 1", + "check-connectors-configuration-pattern": "Check connectors configuration is not valid", + "add": "Add command", + "timeout": "Timeout", + "timeout-ms": "Timeout (in ms)", + "timeout-required": "Timeout is required", + "timeout-min": "Timeout can not be less then 1", + "timeout-pattern": "Timeout is not valid", + "attribute-name": "Attribute name", + "attribute-name-required": "Attribute name is required", + "command": "Command", + "command-required": "Command is required", + "remove": "Remove command" + }, + "storage": "Storage", + "storage-max-file-records": "Maximum records in file", + "storage-max-files": "Maximum number of files", + "storage-max-files-min": "Minimum number is 1.", + "storage-max-files-pattern": "Number is not valid.", + "storage-max-files-required": "Number is required.", + "storage-max-records": "Maximum records in storage", + "storage-max-records-min": "Minimum number of records is 1.", + "storage-max-records-pattern": "Number is not valid.", + "storage-max-records-required": "Maximum records is required.", + "storage-read-record-count": "Read record count in storage", + "storage-read-record-count-min": "Minimum number of records is 1.", + "storage-read-record-count-pattern": "Number is not valid.", + "storage-read-record-count-required": "Read record count is required.", + "storage-max-read-record-count": "Max read record count in storage", + "storage-max-read-record-count-min": "Minimum number of records is 1.", + "storage-max-read-record-count-pattern": "Number is not valid.", + "storage-max-read-record-count-required": "Max Read record count is required.", + "storage-data-folder-path": "Data folder path", + "storage-data-folder-path-required": "Data folder path is required.", + "storage-pack-size": "Maximum event pack size", + "storage-pack-size-min": "Minimum number is 1.", + "storage-pack-size-pattern": "Number is not valid.", + "storage-pack-size-required": "Maximum event pack size is required.", + "storage-path": "Storage path", + "storage-path-required": "Storage path is required.", + "storage-type": "Storage type", + "storage-types": { + "file-storage": "File storage", + "memory-storage": "Memory storage", + "sqlite": "SQLITE" + }, + "thingsboard": "ThingsBoard", + "general": "General", + "thingsboard-host": "ThingsBoard host", + "thingsboard-host-required": "Host is required.", + "thingsboard-port": "ThingsBoard port", + "thingsboard-port-max": "Maximum port number is 65535.", + "thingsboard-port-min": "Minimum port number is 1.", + "thingsboard-port-pattern": "Port is not valid.", + "thingsboard-port-required": "Port is required.", + "tidy": "Tidy", + "tidy-tip": "Tidy config JSON", + "title-connectors-json": "Connector {{typeName}} configuration", + "tls-path-ca-certificate": "Path to CA certificate on gateway", + "tls-path-client-certificate": "Path to client certificate on gateway", + "messages-ttl-check-in-hours": "Messages TTL check in hours", + "messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.", + "messages-ttl-check-in-hours-min": "Min number is 1.", + "messages-ttl-check-in-hours-pattern": "Number is not valid.", + "messages-ttl-in-days": "Messages TTL in days", + "messages-ttl-in-days-required": "Messages TTL in days is required.", + "messages-ttl-in-days-min": "Min number is 1.", + "messages-ttl-in-days-pattern": "Number is not valid.", + "mqtt-qos": "QoS", + "mqtt-qos-required": "QoS is required", + "mqtt-qos-range": "QoS values range is from 0 to 1", + "tls-path-private-key": "Path to private key on gateway", + "toggle-fullscreen": "Toggle fullscreen", + "transformer-json-config": "Configuration JSON*", + "update-config": "Add/update configuration JSON", + "hints": { + "remote-configuration": "Enables remote configuration and management of the gateway", + "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", + "host": "Hostname or IP address of ThingsBoard server", + "port": "Port of MQTT service on ThingsBoard server", + "token": "Access token for the gateway from ThingsBoard server", + "client-id": "MQTT client id for the gateway form ThingsBoard server", + "username": "MQTT username for the gateway form ThingsBoard server", + "password": "MQTT password for the gateway form ThingsBoard server", + "ca-cert": "Path to CA certificate file", + "date-form": "Date format in log message", + "data-folder": "Path to folder, that will contains data (Relative or Absolute)", + "log-format": "Log message format", + "remote-log": "Enables remote logging and logs reading from the gateway", + "backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted", + "storage": "Provides configuration for saving incoming data before it is sent to the platform", + "max-file-count": "Maximum count of file that will be created", + "max-read-count": "Count of messages to get from storage and send to ThingsBoard", + "max-records": "Maximum count of records that will be stored in one file", + "read-record-count": "Count of messages to get from storage and send to ThingsBoard", + "max-records-count": "Maximum count of data in storage before send to ThingsBoard", + "ttl-check-hour": "How often will Gateway check data for obsolescence", + "ttl-messages-day": "Maximum days that storage will save data", + "commands": "Commands for collecting additional statistic", + "attribute": "Statistic telemetry key", + "timeout": "Timeout for command executing", + "command": "The result of the command execution, will be used as the value for telemetry", + "check-device-activity": "Enables monitor the activity of each connected device", + "inactivity-timeout": "Time after whose the gateway will disconnect device", + "inactivity-period": "Periodicity of device activity check", + "minimal-pack-delay": "Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)", + "qos": "Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)", + "server-port": "Network port on which GRPC server will listen for incoming connections.", + "grpc-keep-alive-timeout": "Maximum time the server should wait for a keepalive ping response before considering the connection dead.", + "grpc-keep-alive": "Duration between two successive keepalive ping messages when there is no active RPC call.", + "grpc-min-time-between-pings": "Minimum amount of time the server should wait between sending keepalive ping messages", + "grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.", + "grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.", + "permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls." + } + }, + "grid": { + "delete-item-title": "Ar tikrai norite pašalinti šį elementą?", + "delete-item-text": "Būkite dėmesingi, po patvirtinimo šio elemento ir visos su juo susijusios informacijos atkurti nebegalėsite.", + "delete-items-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 elementą} other {# elementus} }?", + "delete-items-action-title": "Panaikinti { count, plural, =1 {1 elementą} other {# elementus} }", + "delete-items-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti elementai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", + "add-item-text": "Pridėti naują elementą", + "no-items-text": "Elementų nėra", + "item-details": "Informacija apie elementą", + "delete-item": "Panaikinti elementą", + "delete-items": "Panaikinti elmentus", + "scroll-to-top": "Slinkti į viršų" + }, + "help": { + "goto-help-page": "Pagalba", + "show-help": "Pagalba" + }, + "home": { + "home": "Pagrindinis", + "profile": "Profilis", + "logout": "Atsijungti", + "menu": "Meniu", + "avatar": "Paveiksliukas", + "open-user-menu": "Atverti vartoto meniu" + }, + "file-input": { + "browse-file": "Pasirinkti failą", + "browse-files": "Pasirinkti failus" + }, + "image-input": { + "drop-images-or": "Užvilkite paveikslėlį arba", + "drag-and-drop": "Užvilkite", + "or": "arba", + "browse": "Pasirinkite", + "no-images": "Paveikslėlis nepasirinktas", + "images": "paveikslėliai" + }, + "import": { + "no-file": "Nepasirinktas failas", + "drop-file": "Nuvilkite JSON failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-csv-file": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-json-file-or": "Nuvilkite JSON failą arba", + "drop-file-csv": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-file-csv-or": "Nuvilkite CSV failą arba", + "column-value": "Reikšmė", + "column-title": "Pavadinimas", + "column-example": "Duomenų pavyzdys", + "column-key": "Atributo/telemetrijos raktas", + "credentials": "Įgaliojimai", + "csv-delimiter": "CSV skyriklis", + "csv-first-line-header": "Pirmoje eilutėje stulpelių pavadinimai", + "csv-update-data": "Atnaujinti artibutą/telemetriją", + "details": "Informacija", + "import-csv-number-columns-error": "Faile turi būti bent du stulpeliai", + "import-csv-invalid-format-error": "Neteisingas failo formatas. Elutė: '{{line}}'", + "column-type": { + "name": "Pavadinimas", + "type": "Tipas", + "label": "Etiketė", + "column-type": "Stulpelio tipas", + "client-attribute": "Kliento atributas", + "shared-attribute": "Bendrinamas atributas", + "server-attribute": "Serverio atributas", + "timeseries": "Telemetrija", + "entity-field": "Subjekto laukas", + "access-token": "Prieigos raktas", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT client ID", + "user-name": "MQTT user name", + "password": "MQTT password" + }, + "lwm2m": { + "client-endpoint": "LwM2M endpoint client name", + "security-config-mode": "LwM2M security config mode", + "client-identity": "LwM2M client identity", + "client-key": "LwM2M client key", + "client-cert": "LwM2M client public key", + "bootstrap-server-security-mode": "LwM2M bootstrap server security mode", + "bootstrap-server-secret-key": "LwM2M bootstrap server secret key", + "bootstrap-server-public-key-id": "LwM2M bootstrap server public key or id", + "lwm2m-server-security-mode": "LwM2M server security mode", + "lwm2m-server-secret-key": "LwM2M server secret key", + "lwm2m-server-public-key-id": "LwM2M server public key or id" + }, + "snmp": { + "host": "SNMP host", + "port": "SNMP port", + "version": "SNMP version (v1, v2c or v3)", + "community-string": "SNMP community string" + }, + "isgateway": "Is Gateway", + "activity-time-from-gateway-device": "Activity time from gateway device", + "description": "Description", + "edge-license-key": "License Key", + "cloud-endpoint": "Cloud Endpoint", + "routing-key": "Edge key", + "secret": "Edge secret" + }, + "stepper-text": { + "select-file": "Pasirinkite failą", + "configuration": "Importo konfigūracija", + "column-type": "Pasirinkite stulpelių tipus", + "creat-entities": "Sukurti naujus subjektus" + }, + "message": { + "create-entities": "Sukurti {{count}} nauji subjektai.", + "update-entities": "Atnaujinti {{count}} subjektai.", + "error-entities": "Kuriant {{count}} subjektų įvyko klaidų." + } + }, + "integration": { + "integration": "Integration", + "integrations": "Integrations", + "integrations-center": "Integrations center", + "select-integration": "Select integration", + "no-integrations-matching": "No integrations matching '{{entity}}' were found.", + "integration-required": "Integration is required", + "delete": "Delete integration", + "management": "Integrations management", + "add-integration-text": "Add new integration", + "no-integrations-text": "No integrations found", + "data-convertor-name": "{{convertorType}} data converter for {{integrationName}}", + "integration-name": "{{integrationType}} integration", + "selected-integrations": "{ count, plural, =1 {1 integration} other {# integrations} } selected", + "delete-integration-title": "Are you sure you want to delete the integration '{{integrationName}}'?", + "delete-integration-text": "Be careful, after the confirmation the integration and all related data will become unrecoverable.", + "delete-integrations-title": "Are you sure you want to delete { count, plural, =1 {1 integration} other {# integrations} }?", + "delete-integrations-action-title": "Delete { count, plural, =1 {1 integration} other {# integrations} }", + "delete-integrations-text": "Be careful, after the confirmation all selected integrations will be removed and all related data will become unrecoverable.", + "events": "Events", + "enabled": "Enable integration", + "allow-create-devices-or-assets": "Allow create devices or assets", + "all-types": "All integration types", + "add": "Add Integration", + "search": "Search integrations", + "integration-details": "Integration details", + "details": "Details", + "copyId": "Copy integration Id", + "idCopiedMessage": "Integration Id has been copied to clipboard", + "debug-mode": "Debug mode", + "enable-security": "Enable security", + "enable-security-new": "Enable security for automatic token updates", + "headers-filter": "Headers filter", + "header": "Header", + "no-headers-filter": "No headers filter", + "downlink-url": "Downlink URL", + "downlink-url-required": "Downlink URL is required", + "create-loriot-output": "Create Loriot Application output", + "send-downlink": "Send downlink", + "allow-downlink": "Allow downlink", + "server": "Server", + "server-required": "Server is required", + "domain": "Domain", + "app-id": "Application ID", + "app-id-required": "Application ID is required", + "app-token": "Application Access Token", + "app-token-required": "Application Access Token is required", + "email": "Email", + "email-required": "Email is required", + "application-uri": "Application URI", + "as-id": "AS ID", + "as-id-required": "AS ID is required.", + "as-key": "AS Key", + "as-key-required": "AS Key is required.", + "client-id-new": "Client Id", + "client-id-new-required": "Client Id (login) is required (login).", + "client-secret": "Client Secret", + "client-secret-required": "Client Secret (password) is required (password).", + "max-time-diff-in-seconds": "Maximum time difference (seconds)", + "max-time-diff-in-seconds-required": "Maximum time difference is required.", + "created-time": "Created time", + "name": "Name", + "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", + "description": "Description", + "base-url": "Base URL", + "base-url-required": "Base URL is required", + "security-key": "Security key", + "http-endpoint": "HTTP endpoint URL", + "replace-no-content-to-ok": "Replace response status from 'No-Content' to 'OK'", + "copy-http-endpoint-url": "Copy HTTP endpoint URL", + "http-endpoint-url-copied-message": "HTTP endpoint URL has been copied to clipboard", + "host": "Host", + "host-required": "Host is required.", + "host-private": "Host must be in the public network.", + "url-private": "URL must be in the public network.", + "host-type": "Host type", + "host-type-required": "Host type is required.", + "api-version": "Use API v3", + "custom-host": "Custom host", + "custom-host-required": "Custom host is required.", + "port": "Port", + "port-required": "Port is required.", + "port-range": "Port should be in a range from 1 to 65535.", + "connect-timeout": "Connection timeout (sec)", + "connect-timeout-required": "Connection timeout is required.", + "connect-timeout-range": "Connection timeout should be in a range from 1 to 200.", + "client-id": "Client ID", + "client-id-required": "Client ID is required.", + "client-id-hint": "Hint: Optional. Leave empty for auto-generated client ID. Be careful when specifying the Client ID. Majority of the MQTT brokers will not allow multiple connections with the same client ID. To connect to such brokers, your MQTT client ID must be unique.", + "max-bytes-in-message": "Max bytes in message", + "max-bytes-in-message-range": "Max bytes in message should be in a range from 1 to 256000000.", + "device-id": "Device ID", + "device-id-required": "Device ID is required.", + "device-id-range": "Device ID should be in a range from 1 to 65535 characters.", + "device-id-pattern": "Device ID should consist of numbers, upper and lower case letters. [MQTT-3.1.3-5]", + "group-id": "Group ID", + "group-id-required": "Group ID is required.", + "topics": "Topics", + "topics-required": "Topic is required.", + "routing-keys": "Routing keys", + "routing-keys-required": "Routing key is required.", + "queues": "Queues", + "queues-required": "Queue name is required.", + "durable": "Durable", + "exclusive": "Exclusive", + "autoDelete": "Auto delete", + "exchange-name": "Exchange name", + "exchange-name-required": "Exchange name is required.", + "downlink-topic": "Downlink topic", + "connection-timeout": "Connection timeout, ms", + "connection-timeout-min": "Invalid connection timeout value.", + "handshake-timeout": "Handshake timeout, ms", + "handshake-timeout-min": "Invalid handshake timeout value.", + "virtual-host": "Virtual host", + "rabbit-mq-poll-interval": "Poll interval, ms", + "rabbit-mq-poll-interval-min": "Invalid poll interval value.", + "application-server-url": "Application server URL", + "application-server-url-required": "Application server URL is required.", + "application-server-api-token": "Application server API Token", + "application-server-api-token-required": "Application server API Token is required.", + "use-api-four-plus": "Use API for ChirpStack 4+", + "bootstrap-servers": "Bootstrap servers", + "bootstrap-servers-required": "Bootstrap servers is required.", + "poll-interval": "Poll interval", + "poll-interval-required": "Poll interval is required.", + "poll-interval-min-value": "Poll interval value can't be less then 1", + "auto-create-topics": "Auto create topics", + "clean-session": "Clean session", + "enable-ssl": "Enable SSL", + "credentials": "Credentials", + "credentials-type": "Credentials type", + "credentials-type-required": "Credentials type is required.", + "username": "Username", + "username-required": "Username is required.", + "password": "Password", + "password-required": "Password is required.", + "azure-ca-cert": "CA certificate file", + "ca-cert": "CA certificate file", + "private-key": "Private key file", + "private-key-password": "Private key password", + "cert": "Certificate file", + "no-file": "No file selected.", + "drop-file": "Drop a file or click to select a file to upload.", + "drop-file-or": "Drag and drop a file or", + "check-connection": "Check connection", + "check-success": "Connection established.", + "topic-filters": "Topic filters", + "remove-topic-filter": "Remove topic filter", + "add-topic-filter": "Add topic filter", + "add-topic-filter-prompt": "Please add topic filter", + "topic": "Topic", + "mqtt-qos": "QoS", + "mqtt-qos-required": "QoS is required", + "mqtt-qos-range": "QoS values range is from 0 to 2", + "mqtt-qos-at-most-once": "At most once", + "mqtt-qos-at-least-once": "At least once", + "mqtt-qos-exactly-once": "Exactly once", + "downlink-topic-pattern": "Downlink topic pattern", + "downlink-topic-pattern-required": "Downlink topic pattern is required.", + "retained-message": "Retained", + "aws-access-key-id": "AWS access key Id", + "aws-secret-access-key": "AWS secret access key", + "aws-region": "Region", + "aws-iot-endpoint": "AWS IoT Endpoint", + "aws-iot-endpoint-required": "AWS IoT Endpoint is required.", + "aws-iot-credentials": "AWS IoT Credentials", + "aws-sqs-polling-period-in-seconds": "Polling period in seconds", + "aws-sqs-queue-url": "SQS Queue URL", + "aws-sqs-queue-url-required": "SQS Queue URL is required", + "aws-sqs-access-key-id-required": "Access Key Id is required", + "aws-sqs-secret-access-key-required": "Secret Access Key is required", + "application-credentials": "Application Credentials", + "api-key": "API Key", + "api-key-required": "API Key is required.", + "api-key-format": "Invalid API Key format.", + "auth-token": "Authentication Token", + "auth-token-required": "Authentication Token is required.", + "region": "Region", + "region-required": "Region is required.", + "application-id": "Application ID", + "application-id-required": "Application ID is required.", + "access-id": "Access Id", + "access-id-required": "Access Id is required.", + "access-key": "Access Key", + "access-key-required": "Access Key is required.", + "access-key-min-length": "Access Key is too short.", + "access-key-max-length": "Access Key is too long.", + "connection-parameters": "Connection parameters", + "service-bus-namespace-name": "Service Bus Namespace Name", + "service-bus-namespace-name-required": "Service Bus Namespace Name is required.", + "connection-string": "Connection string", + "consumer-group": "Consumer group", + "connection-string-required": "Connection string required!", + "event-hub-name": "Event Hub Name", + "event-hub-name-required": "Event Hub Name is required.", + "event-iot-hub-name-required": "Iot Hub Name is required for downlink", + "sas-key-name": "SAS Key Name", + "sas-key-name-required": "SAS Key Name is required.", + "sas-key": "SAS Key", + "sas-key-required": "SAS Key is required.", + "iot-hub-name": "IoT Hub Name (required for downlink)", + "hostname": "Hostname", + "hostname-required": "Hostname is required", + "integration-clazz": "Integration class", + "integration-clazz-required": "Integration class is required", + "integration-configuration": "Integration JSON configuration", + "metadata": "Metadata", + "type": "Integration type", + "select-integration-type": "Select integration type", + "type-required": "Integration type is required.", + "type-not-found": "Integration type not found", + "uplink-converter": "Uplink data converter", + "uplink-converter-required": "Uplink data converter is required.", + "downlink-converter": "Downlink data converter", + "type-http": "HTTP", + "type-http-description": "Transferring text or multimedia files protocol.", + "type-ocean-connect": "OceanConnect", + "type-ocean-connect-description": "Integration of devices with different capabilities with the IoT platform.", + "type-sigfox": "SigFox", + "type-sigfox-description": "Lightweight protocol to handle small data transmissions.", + "type-thingpark": "ThingPark", + "type-thingpark-description": "Message-oriented transport layer protocol.", + "type-loriot": "Loriot", + "type-loriot-description": "Distributed LoRaWAN infrastructure.", + "type-thingpark-enterprise": "ThingParkEnterprise", + "type-thingpark-enterprise-description": "Messaging platform for the LPWA network.", + "type-tmobile-iot-cdp": "iotcreators.com (T-Mobile – IoT CDP)", + "type-tmobile-iot-cdp-description": "Mobile IoT connectivity service.", + "type-mqtt": "MQTT", + "type-mqtt-description": "Machine to machine network protocol.", + "type-aws-iot": "AWS IoT", + "type-aws-iot-description": "Management, storage, and analytics service.", + "type-aws-sqs": "AWS SQS", + "type-aws-sqs-description": "Fully managed message queuing service.", + "type-aws-kinesis": "AWS Kinesis", + "type-aws-kinesis-description": "Service for real-time video and data streams.", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ibm-watson-iot-description": "Fully managed, cloud-hosted service.", + "type-ttn": "The Things Stack Community", + "type-ttn-description": "Open and decentralized LoRaWAN network.", + "type-tti": "The Things Stack Industries", + "type-tti-description": "Software to operate private LoRaWAN networks at scale.", + "type-chirpstack": "ChirpStack", + "type-chirpstack-description": "LoRaWAN network configuration server.", + "type-particle": "Particle", + "type-particle-description": "IoT enablement platform-as-a-service.", + "type-azure-event-hub": "Azure Event Hub", + "type-azure-event-hub-description": "Big data streaming platform and event ingestion service.", + "type-azure-iot-hub": "Azure IoT Hub", + "type-azure-iot-hub-description": "Cloud-hosted messaging service.", + "type-opc-ua": "OPC-UA", + "type-opc-ua-description": "Cross-platform for data exchange from sensors to cloud apps.", + "type-custom": "Custom", + "type-coap": "CoAP", + "type-coap-description": "Machine to machine network protocol for constrained devices.", + "type-udp": "UDP", + "type-udp-description": "Message-oriented transport layer protocol.", + "type-tcp": "TCP", + "type-tcp-description": "Connection-oriented protocol.", + "type-kafka": "Kafka", + "type-kafka-description": "Streaming analytics, data integration streaming platform.", + "type-rabbitmq": "RabbitMQ", + "type-rabbitmq-description": "Message broker that supports multiple messaging protocols.", + "type-pubsub": "Pub/Sub", + "type-pubsub-description": "Service for asynchronous messaging between apps.", + "type-apache-pulsar": "Apache Pulsar", + "type-apache-pulsar-description": "Distributed pub-sub messaging and streaming platform.", + "type-tuya": "Tuya", + "type-tuya-description": "Cloud-hosted messaging. device platform.", + "type-azure-service-bus": "Azure Service Bus", + "type-azure-service-bus-description": "Fully managed enterprise message broker with message queues and publish-subscribe topics.", + "opc-ua-application-name": "Application name", + "opc-ua-application-uri": "Application uri", + "opc-ua-scan-period-in-seconds": "Scan period in seconds", + "opc-ua-scan-period-in-seconds-required": "Scan period is required", + "opc-ua-timeout": "Timeout in milliseconds", + "opc-ua-timeout-required": "Timeout is required", + "opc-ua-security": "Security", + "opc-ua-security-required": "Security is required", + "opc-ua-identity": "Identity", + "opc-ua-identity-required": "Identity is required", + "opc-ua-keystore": "Keystore", + "add-opc-ua-keystore-prompt": "Please add keystore file", + "opc-ua-keystore-required": "Keystore is required", + "opc-ua-type": "Type", + "opc-ua-keystore-type": "Type", + "opc-ua-keystore-type-required": "Type is required", + "opc-ua-keystore-location": "Location *", + "opc-ua-keystore-password": "Password", + "opc-ua-keystore-password-required": "Password is required", + "opc-ua-keystore-alias": "Alias", + "opc-ua-keystore-alias-required": "Alias is required", + "opc-ua-keystore-key-password": "Key password", + "opc-ua-keystore-key-password-required": "Key password is required", + "opc-ua-mapping": "Mapping", + "add-opc-ua-mapping-prompt": "Please add mapping", + "opc-ua-mapping-type": "Mapping type", + "opc-ua-mapping-type-required": "Mapping type is required", + "opc-ua-device-node-pattern": "Device Node Pattern", + "opc-ua-device-node-pattern-required": "Device Node Pattern is required", + "opc-ua-namespace": "Namespace", + "opc-ua-add-map": "Add mapping element", + "kinesis-stream-name": "Stream name", + "kinesis-stream-name-required": "Stream name is required", + "kinesis-region": "Region", + "kinesis-region-required": "Region is required", + "kinesis-access-key-id": "Access Key Id", + "kinesis-access-key-id-required": "Access Key Id is required", + "kinesis-secret-access-key": "Secret Access Key", + "kinesis-secret-access-key-required": "Secret Access Key is required", + "kinesis-use-consumers-with-enhanced-fan-out": "Use Consumers with Enhanced Fan-Out", + "kinesis-use-credentials-from-instance-metadata": "Use credentials from the Amazon EC2 Instance Metadata Service", + "kinesis-application-name": "Application name (by default equals Stream name)", + "kinesis-initial-position-in-stream": "Initial position in stream", + "kinesis-initial-position-in-stream-required": "Initial position in stream required", + "kinesis-max-records": "Max records", + "kinesis-max-records-required": "Max records is required", + "kinesis-max-records-length-range": "Max records length should be in a range from 1 to 10000", + "kinesis-request-timeout": "Request timeout in seconds", + "kinesis-request-timeout-required": "Request timeout is required", + "other-properties": "Other properties", + "subscription-tags": "Subscription tags", + "remove-subscription-tag": "Remove subscription tag", + "add-subscription-tag": "Add subscription tag", + "add-subscription-tag-prompt": "Please add subscription tag", + "key": "Key", + "path": "Path", + "required": "Required", + "integration-key": "Integration key", + "copy-integration-key": "Copy integration key", + "integration-key-copied-message": "Integration key has been copied to clipboard", + "integration-secret": "Integration secret", + "copy-integration-secret": "Copy integration secret", + "integration-secret-copied-message": "Integration secret has been copied to clipboard", + "execute-remotely": "Execute remotely", + "remote": "Remote", + "handler-configuration": "Handler Configuration", + "handler-configuration-type": "Handler Type", + "so-broadcast": "Enable broadcast - integration will accepts broadcast address packets", + "so-keepalive-option": "Enable sending of keep-alive messages on connection-oriented sockets", + "so-reuse-addr": "Bind the process to a port", + "tcp-no-delay": "Forces a socket to send the data without buffering (disable Nagle's buffering algorithm)", + "fail-fast": "Thrown exception soon as the decoder notices the length of the frame will exceed max size", + "strip-delimiter": "Strip Delimiter", + "length-field-offset": "Length Field Offset", + "length-field-offset-required": "Length Field Offset is required.", + "length-field-offset-range": "Length Field Offset should be in a range from 0 to 8.", + "length-field-length": "Length Field Length", + "length-field-length-required": "Length Field Length is required.", + "length-field-length-range": "Length Field Length should be in a range from 0 to 8.", + "length-adjustment": "Length Adjustment (the compensation value to add to the value of the length field)", + "length-adjustment-required": "Length Adjustment is required.", + "length-adjustment-range": "Length Adjustment should be in a range from 0 to 8.", + "byte-order": "Byte Order of the length field", + "initial-bytes-to-strip": "Number of first bytes to strip out from the decoded frame", + "initial-bytes-to-strip-required": "Number of first bytes to strip out from the decoded frame is required.", + "initial-bytes-to-strip-range": "Number of first bytes to strip out from the decoded frame should be in a range from 0 to 8.", + "so-backlog-option": "Max number of pending connects on the socket", + "so-backlog-option-required": "The max number of pending connects on the socket is required.", + "so-backlog-option-range": "The max number of pending connects on the socket should be in a range from 1 to 65535.", + "so-rcv-buf": "Size of the buffer for inbound socket (in KB)", + "so-rcv-buf-required": "Size of the buffer for inbound socket (in KB) is required.", + "so-rcv-buf-range": "Size of the buffer for inbound socket (in KB) should be in a range from 1 to 65535.", + "so-snd-buf": "Size of the buffer for outbound socket (in KB)", + "so-snd-buf-required": "Size of the buffer for outbound socket (in KB) is required.", + "so-snd-buf-range": "Size of the buffer for outbound socket (in KB) should be in a range from 1 to 65535.", + "charset-name": "Charset Name", + "charset-name-required": "Charset Name is required.", + "message-separator": "Message Separator", + "message-separator-required": "Message Separator is required.", + "character-sequence": "Character Sequence", + "character-sequence-required": "Character Sequence is required.", + "max-frame-length": "Max Frame Length (in bytes)", + "max-frame-length-required": "Max Frame Length (in bytes) is required.", + "max-frame-length-range": "Max Frame Length (in bytes) should be in a range from 1 to 65535.", + "handler-type": "Handler Type", + "message-size": "Message Size", + "message-size-required": "Message Size is required.", + "service-url": "Service URL", + "service-url-required": "Service URL is required.", + "subscription-name": "Subscription name", + "subscription-name-required": "Subscription name is required.", + "max-num-messages": "Max number of messages", + "max-num-messages-required": "Max number of messages is required.", + "max-num-bytes": "Max number of bytes", + "max-num-bytes-required": "Max number of bytes required.", + "timeout-in-ms": "Timeout in milliseconds", + "timeout-in-ms-required": "Timeout in milliseconds is required.", + "user-id": "User ID", + "user-id-required": "User ID is required.", + "token": "Token", + "token-required": "Token is required.", + "project-id": "Project ID", + "project-id-required": "Project ID is required.", + "subscription-id": "Subscription ID", + "subscription-id-required": "Subscription ID is required.", + "service-account-key": "Service Account key file", + "service-account-key-required": "Service Account key file is required.", + "tcp": { + "system-line-separator": "System Line Separator", + "nul-delimiter": "Nul Delimiter", + "byte-order-little-endian": "Little Endian", + "byte-order-big-endian": "Big Endian" + }, + "cache-size": "Cache Size", + "cache-time-to-live": "Cache time to live in minutes", + "min-cache-size": "Cache size can't be lower 0", + "min-cache-time-to-live": "Cache time to live can't be lower 0", + "max-cache-time-to-live": "Invalid time of cache live, select between 0 and 525600", + "coap-security-mode": "Security mode", + "coap-security-mode-required": "CoAP security mode is required", + "coap-security-mode-no-secure": "NO SECURE", + "coap-security-mode-dtls": "DTLS", + "coap-security-mode-mixed": "MIXED", + "coap-endpoint": "CoAP endpoint URL", + "coap-endpoint-url-copied-message": "CoAP endpoint URL has been copied to clipboard", + "copy-coap-endpoint-url": "Copy CoAP endpoint URL", + "copy-coap-dtls-endpoint-url": "Copy CoAP DTLS endpoint URL", + "coap-dtls-base-url": "DTLS Base URL", + "coap-dtls-base-url-required": "DTLS Base URL is required", + "coap-dtls-endpoint": "CoAP DTLS endpoint URL", + "coap-dtls-endpoint-url-copied-message": "CoAP DTLS endpoint URL has been copied to clipboard", + "unassign-integration-title": "Are you sure you want to unassign the integration '{{integrationName}}'?", + "unassign-integration-from-edge-text": "After the confirmation the integration will be unassigned and won't be accessible by the edge.", + "unassign-integrations-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 integration} other {# integrations} }?", + "unassign-integrations-from-edge-text": "After the confirmation all selected integrations will be unassigned and won't be accessible by the edge.", + "unassign-integrations": "Unassign integrations", + "edge-placeholder-hint": "You can use placeholder ${{ATTRIBUTE_KEY}} to substitute integration field with attribute value from specific Edge entity. For example, 'Edge A' has attribute 'baseUrl' that equals to 'http://localhost:9999'. You can set ${{baseUrl}} as one of the integration fields, and it is going to be replaced to 'http://localhost:9999' during the assignment of this integration to 'Edge A'. Additionally, if 'Edge A' attribute 'baseUrl' is going to be updated - integration with new updated value is going to be automatically provisioned to 'Edge A'.", + "status": { + "status": "Status", + "active": "Active", + "disabled": "Disabled", + "failed": "Failed", + "pending": "Pending" + }, + "daily-activity": "Daily activity", + "enable-debug-mode": "Enable debug mode", + "disable-debug-mode": "Disable debug mode", + "advanced-settings": "Advanced settings", + "basic-settings": "Basic settings", + "existing-converter": "Select existing", + "new-converter": "Create new", + "connection": "Connection", + "connected": "Connected", + "not-connected": "Not connected", + "environment": "Environment", + "environment-required": "Environment is required.", + "region-cn": "China", + "region-us": "United States", + "region-eu": "Europe", + "region-in": "India", + "topic-name": "Topic name", + "topic-name-required": "Topic name is required", + "sub-name": "Subscription name", + "sub-name-required": "Subscription name is required", + "downlink-connection-string": "Downlink connection string", + "downlink-connection-string-required": "Downlink connection string is required", + "downlink-topic-name": "Downlink topic name", + "downlink-topic-name-required": "Downlink topic name is required" + }, + "item": { + "selected": "Pasirinkta" + }, + "js-func": { + "no-return-error": "Funkcija privalo grąžinti reikšmę!", + "return-type-mismatch": "Funkcijos grąžinama reikšmė privalo būti '{{type}}' tipo!", + "tidy": "Tidy", + "mini": "Mini" + }, + "key-val": { + "key": "Raktas", + "value": "Reikšmė", + "remove-entry": "Panaiknti įrašą", + "add-entry": "Pridėti įrašą", + "no-data": "Įrašų nėra" + }, + "layout": { + "layout": "Maketas", + "layouts": "Maketai", + "manage": "Valdyti maketus", + "settings": "Maketų nustatymai", + "color": "Spalva", + "main": "Pagrindinis", + "right": "Į dešinę", + "left": "Į kairę", + "select": "Pasirinkti maketą", + "percentage-width": "Procentinis plotis (%)", + "fixed-width": "Fiksuotas plotis (px)", + "left-width": "Kairysis stulpelis (%)", + "right-width": "Dešinysis stulpelis (%)", + "pick-fixed-side": "Fiksuota pusė: ", + "layout-fixed-width": "Fiksuotas plotis (px)", + "value-min-error": "Reikšmė turi būti dedesnė nei {{min}}{{unit}}", + "value-max-error": "Reikšmė turi būti mažesnė nei {{max}}{{unit}}", + "layout-fixed-width-required": "Fiksuotas plotis būtinas", + "right-width-percentage-required": "Dešinės pusės procentas būtinas", + "left-width-percentage-required": "kairės pusės procentas būtinas", + "divider": "Daliklis", + "right-side": "Dešinės pusės maketas", + "left-side": "Kairės pusės maketas" + }, + "legend": { + "direction": "Elementų išdėstymo kryptis", + "position": "Legendos pozicija", + "show-values": "Rodyti reikšmes", + "min-option": "Min", + "max-option": "Max", + "average-option": "Vidurkis", + "total-option": "Viso", + "latest-option": "Naujausi", + "sort-legend": "Legendoje rodyti duomenų raktus", + "show-max": "Rodyti didžiausią reišmę", + "show-min": "Rodyti mažiausią reikšmę", + "show-avg": "Rodyti vidutinę reikšmę", + "show-total": "Rodyti suminę rekšmę", + "show-latest": "Rodyti naujausią reikšmę", + "settings": "Legendos nustatymai", + "min": "Min", + "max": "Max", + "avg": "Vidurkis", + "total": "Viso", + "latest": "Naujausia", + "comparison-time-ago": { + "previousInterval": "(previous interval)", + "customInterval": "(custom interval)", + "days": "(day ago)", + "weeks": "(week ago)", + "months": "(month ago)", + "years": "(year ago)" + } + }, + "login": { + "login": "Prisijungti", + "request-password-reset": "Prašyti atstatyti slaptažodį", + "reset-password": "Atstatyti slaptažodį", + "create-password": "Nustatyti slaptažodį", + "two-factor-authentication": "Dviejų veiksnių autentifikavimas", + "passwords-mismatch-error": "Įvesti slaptažodžiai turi sutapti!", + "password-again": "Slaptažodį įveskite dar kartą", + "sign-in": "Prisijunkite", + "username": "Vartoto vardas (el. paštas)", + "remember-me": "Prisiminti mane", + "forgot-password": "Pamiršote slaptažodį?", + "password-reset": "Slaptažodis atstatytas", + "expired-password-reset-message": "Jūsų slaptažodio galiojimas baigėsi! Nustatykite naują slaptažodį.", + "new-password": "Naujas slaptažodis", + "new-password-again": "Pakartokite naują slaptažodį", + "password-link-sent-message": "Atstatymo nuoroda išsiųsta", + "email": "El. paštas", + "no-account": "Neturi paskyros?", + "create-account": "Sukurti paskyrą", + "login-with": "Prisijungti kaip {{name}}", + "or": "arba", + "error": "Login error", + "verify-your-identity": "Verify your identity", + "select-way-to-verify": "Select a way to verify", + "resend-code": "Resend code", + "resend-code-wait": "Resend code in { time, plural, =1 {1 second} other {# seconds} }", + "try-another-way": "Try another way", + "totp-auth-description": "Please enter the security code from your authenticator app.", + "totp-auth-placeholder": "Code", + "sms-auth-description": "A security code has been sent to your phone at {{contact}}.", + "sms-auth-placeholder": "SMS code", + "email-auth-description": "A security code has been sent to your email address at {{contact}}.", + "email-auth-placeholder": "Email code", + "backup-code-auth-description": "Please enter one of your backup codes.", + "backup-code-auth-placeholder": "Backup code" + }, + "signup": { + "firstname": "Vardas", + "lastname": "Pavardė", + "email": "El. paštas", + "signup": "Registracija", + "create-password": "nustatyti slaptažodį", + "repeat-password": "Pakartokite slaptažodį", + "have-account": "Jau turite paskyrą?", + "signin": "Prisijungti", + "no-captcha-message": "Turite patvirtinti, jog nesate robotas", + "password-length-message": "Jūsų slaptažodis turi būti ne nei 6 simboliai", + "email-verification": "El. pašto adreso patvirtinimas", + "email-verification-message": "El. laiškas su patvirtinimo informacija išsiųstas nurodytu el. pašto adresu.
Sekite laiške pateiktas intrukcijas ir atlikite prisijungimui reikalingus veiksmus.
Pastaba: Jei nematote el. laiško, patikrinkite 'Šlamšto' katalogą arba bandykite dar kartą siųsti laišką paspausdami 'Siųsti dar kartą' mygtuką", + "account-activation-title": "Paskyros aktyvavimas", + "account-activated": "Paskyra aktyvuota!", + "account-activated-text": "Sveikiname!
Jūsų paskyra aktyvuota.", + "resend": "Siųsti dar kartą", + "inactive-user-exists-title": "Neaktyvuotas vartotojas sistemoje jau yra", + "inactive-user-exists-text": "Vartotojas su tokiu el. pašto adresu jau užregistruotas, tačiau šis adresas nepatvirtintas.
Paspauskite 'Siųsti dar kartą' mygtuką jei norite išsiųsti patvirtinimo laišką.", + "activating-account": "Paskyra aktyvuojama...", + "activating-account-text": "Jūsų paskyra aktyvuojama. Palaukite...", + "accept-privacy-policy": "Sutikti su privatumo politika", + "accept": "Sutikti", + "privacy-policy": "Privatumo politika", + "accept-privacy-policy-message": "You must accept our Privacy Policy", + "recaptcha-title": "reCAPTCHA", + "terms-of-use": "Terms of Use", + "accept-terms-of-use-message": "You must accept our Terms Of Use" + }, + "notification": { + "action-button": "Action button", + "action-type": "Action type", + "active": "Active", + "add-notification-recipients-group": "Add notification recipients group", + "add-notification-template": "Add notification template", + "add-recipient": "Add recipient", + "add-recipients": "Add recipients", + "add-rule": "Add rule", + "add-stage": "Add stage", + "add-template": "Add template", + "after": "After", + "alarm-assignment-trigger-settings": "Alarm assignment trigger settings", + "alarm-comment-trigger-settings": "Alarm comment trigger settings", + "alarm-trigger-settings": "Alarm trigger settings", + "all": "All", + "api-feature-hint": "If the field is empty, the trigger will be applied to all api features", + "api-usage-trigger-settings": "API usage trigger settings", + "new-platform-version-trigger-settings": "New platform version trigger settings", + "rate-limits-trigger-settings": "Exceeded rate limits trigger settings", + "at-least-one-should-be-selected": "At least one should be selected", + "basic-settings": "Basic settings", + "button-text": "Button text", + "button-text-required": "Button text is required", + "button-text-max-length": "Button text should be less than or equal to {{ length }} characters", + "compose": "Compose", + "conversation": "Conversation", + "conversation-required": "Conversation is required", + "copy-notification-template": "Copy notification template", + "copy-rule": "Copy rule", + "copy-template": "Copy template", + "create-new": "Create new", + "created": "Created", + "delete-notification-text": "Be careful, after the confirmation the notification will become unrecoverable.", + "delete-notification-title": "Are you sure you want to delete the notification?", + "delete-notifications-text": "Be careful, after the confirmation notifications will become unrecoverable.", + "delete-notifications-title": "Are you sure you want to delete { count, plural, =1 {1 notification} other {# notifications} }?", + "delete-recipient-text": "Be careful, after the confirmation the recipient will become unrecoverable.", + "delete-recipient-title": "Are you sure you want to delete recipient '{{recipientName}}'?", + "delete-recipients-text": "Be careful, after the confirmation recipients will become unrecoverable.", + "delete-recipients-title": "Are you sure you want to delete { count, plural, =1 {1 recipient} other {# recipients} }?", + "delete-request-text": "Be careful, after the confirmation request will become unrecoverable.", + "delete-request-title": "Are you sure you want to delete request?", + "delete-requests-text": "Be careful, after the confirmation requests will become unrecoverable.", + "delete-requests-title": "Are you sure you want to delete { count, plural, =1 {1 request} other {# requests} }?", + "delete-rule-text": "Be careful, after the confirmation rule will become unrecoverable.", + "delete-rule-title": "Are you sure you want to delete rule '{{ruleName}}'?", + "delete-rules-text": "Be careful, after the confirmation rules will become unrecoverable.", + "delete-rules-title": "Are you sure you want to delete { count, plural, =1 {1 rule} other {# rules} }?", + "delete-template-text": "Be careful, after the confirmation template will become unrecoverable.", + "delete-template-title": "Are you sure you want to delete template '{{templateName}}'?", + "delete-templates-text": "Be careful, after the confirmation templates will become unrecoverable.", + "delete-templates-title": "Are you sure you want to delete { count, plural, =1 {1 template} other {# templates} }?", + "deleted": "Deleted", + "delivery-method": { + "delivery-method": "Delivery method", + "email": "Email", + "email-preview": "Email notification preview", + "slack": "Slack", + "slack-preview": "Slack notification preview", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams notification preview", + "sms": "SMS", + "sms-preview": "SMS notification preview", + "web": "Web", + "web-preview": "Web notification preview" + }, + "delivery-method-not-configure-click": "Delivery method is not configured. Click to setup.", + "delivery-method-not-configure-contact": "Delivery method is not configured. Contact your system administrator.", + "delivery-methods": "Delivery methods", + "description": "Description", + "device-activity-trigger-settings": "Device active trigger settings", + "device-list-rule-hint": "If the field is empty, the trigger will be applied to all devices", + "device-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all device profiles", + "disabled": "Disabled", + "edit-notification-recipients-group": "Edit notification recipients group", + "edit-notification-template": "Edit notification template", + "edit-rule": "Edit rule", + "edit-template": "Edit template", + "enabled": "Enabled", + "entities-limit-trigger-settings": "Entities limit trigger settings", + "entity-action-trigger-settings": "Entity action trigger settings", + "entity-type": "Entity type", + "escalation-chain": "Escalation chain", + "failed-send": "Sending failures", + "fails": "{ count, plural, =1 {1 fail} other {# fails} }", + "filter": "Filter", + "first-recipient": "First recipient", + "inactive": "Inactive", + "inbox": "Inbox", + "notification-inbox": "Notifications / Inbox", + "input-field-support-templatization": "Input field support templatization.", + "input-fields-support-templatization": "Input fields support templatization.", + "integration-action-trigger-settings": "Integration events trigger settings", + "integration-list-rule-hint": "If the field is empty, the trigger will be applied to all integrations", + "link": "Link", + "link-required": "Link is required", + "link-type": { + "dashboard": "Open dashboard", + "link": "Open URL link" + }, + "loading-notifications": "Loading notifications...", + "management": "Notification management", + "mark-all-as-read": "Mark all as read", + "mark-as-read": "Mark as read", + "message": "Message", + "message-required": "Message is required", + "message-max-length": "Message should be less than or equal to {{ length }} characters", + "name": "Name", + "name-required": "Name is required", + "new-notification": "New notification", + "no-inbox-notification": "No notification found", + "no-notification-request": "No notification request", + "no-notification-templates": "No notification templates found", + "no-notifications-yet": "No notifications yet", + "no-recipients-notification": "No recipients notification", + "no-rule": "No rule configured", + "no-rules-notification": "No rules notification", + "no-severity-found": "No severity found", + "no-severity-matching": "'{{severity}}' not found.", + "no-template-matching": "No resource matching '{{template}}' were found.", + "not-found-slack-recipient": "Slack recipient not found", + "notification": "Notification", + "notification-center": "Notification center", + "notify": "notify", + "notify-again": "Notify again", + "notify-alarm-action": { + "acknowledged": "Alarm acknowledged", + "assigned": "Alarm assigned", + "cleared": "Alarm cleared", + "created": "Alarm created", + "severity-changed": "Alarm severity changed", + "unassigned": "Alarm unassigned" + }, + "notify-on": "Notify on", + "notify-on-comment-update": "Notify on comment update", + "notify-on-required": "Notify on is required", + "notify-on-unassign": "Notify on unassign", + "notify-only-integrations-errors": "Only notify on error", + "notify-only-user-comments": "Notify only user comments", + "only-rule-chain-lifecycle-failures": "Only rule chain lifecycle failures", + "only-rule-node-lifecycle-failures": "Only rule node lifecycle failures", + "platform-users": "Platform users", + "rate-limits": "Rate limits", + "rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits", + "recipient": "Recipient", + "recipient-group": "Recipient group", + "recipient-type": { + "affected-tenant-administrators": "Affected tenant administrators", + "affected-user": "Affected user", + "affected-user-hint": "Affected user hint", + "all-users": "All users", + "customer-users": "Customer users", + "system-administrators": "System administrators", + "tenant-administrators": "Tenant administrators", + "user-filters": "User filter", + "user-group-list": "User group list", + "user-list": "User list", + "user-role": "User role", + "users-entity-owner": "Users of the entity owner", + "users-entity-owner-hint": "Users of the entity owner hint" + }, + "recipients": "Recipients", + "notification-recipients": "Notifications / Recipients", + "recipients-count": "{ count, plural, =1 {1 recipient} other {# recipients} }", + "recipients-required": "Recipients are required", + "refresh-allow-delivery-method": "Refresh allow delivery method", + "request-search": "Request search", + "request-status": { + "processing": "Processing", + "scheduled": "Scheduled", + "sent": "Sent" + }, + "review": "Review", + "rule": "Rule", + "rule-chain-list-rule-hint": "If the field is empty, the trigger will be applied to all rule chains", + "rule-engine-events-trigger-settings": "Rule engine events trigger settings", + "rule-engine-filter": "Rule engine filter", + "rule-name": "Rule name", + "rule-name-required": "Name is required", + "rule-disable": "Disable notification rule", + "rule-enable": "Enable notification rule", + "rule-node-filter": "Rule node filter", + "rules": "Rules", + "notification-rules": "Notifications / Rules", + "scheduler-later": "Schedule for later", + "search-notification": "Search notifications", + "search-recipients": "Search recipients", + "search-rules": "Search rules", + "search-templates": "Search templates", + "see-documentation": "See documentation", + "selected-notifications": "{ count, plural, =1 {1 notification} other {# notifications} } selected", + "selected-recipients": "{ count, plural, =1 {1 recipient} other {# recipients} } selected", + "selected-requests": "{ count, plural, =1 {1 request} other {# requests} } selected", + "selected-rules": "{ count, plural, =1 {1 rule} other {# rules} } selected", + "selected-template": "{ count, plural, =1 {1 template} other {# templates} } selected", + "send-notification": "Send notification", + "sent": "Sent", + "notification-sent": "Notifications / Sent", + "set-entity-from-notification": "Set entity from notification to dashboard state", + "slack-chanel-type": "Slack channel type", + "slack-chanel-types": { + "direct": "Direct message", + "private-channel": "Private channel", + "public-channel": "Public channel" + }, + "start-from-scratch": "Start from scratch", + "status": "Status", + "stop-escalation-alarm-status-become": "Stop the escalation on the alarm status become:", + "subject": "Subject", + "subject-required": "Subject is required", + "template": "Template", + "template-name": "Template name", + "template-required": "Template is required", + "template-type": { + "alarm": "Alarm", + "alarm-assignment": "Alarm assignment", + "alarm-comment": "Alarm comment", + "api-usage-limit": "API usage limit", + "device-activity": "Device activity", + "entities-limit": "Entities limit", + "entity-action": "Entity action", + "general": "General", + "integration-lifecycle-event": "Integration lifecycle event", + "rule-engine-lifecycle-event": "Rule engine lifecycle event", + "rule-node": "Rule node", + "new-platform-version": "New platform version", + "rate-limits": "Exceeded rate limits" + }, + "templates": "Templates", + "notification-templates": "Notifications / Templates", + "tenant-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all tenant profiles", + "tenants-list-rule-hint": "If the field is empty, the trigger will be applied to all tenants", + "threshold": "Threshold", + "theme-color": "Theme color", + "time": "Time", + "track-rule-node-events": "Track rule node events", + "trigger": { + "alarm": "Alarm", + "alarm-assignment": "Alarm assignment", + "alarm-comment": "Alarm comment", + "api-usage-limit": "API usage limit", + "device-activity": "Device activity", + "entities-limit": "Entities limit", + "entity-action": "Entity action", + "integration-lifecycle-event": "Integration lifecycle event", + "rule-engine-lifecycle-event": "Rule engine lifecycle event", + "new-platform-version": "New platform version", + "rate-limits": "Exceeded rate limits", + "trigger": "Trigger", + "trigger-required": "Trigger is required" + }, + "type": "Type", + "unread": "Unread", + "updated": "Updated", + "use-template": "Use template", + "view-all": "View all", + "view-notification-recipients-group": "View notification recipients group", + "view-notification-template": "View notification template", + "view-rule": "View rule", + "warning": "Warning", + "webhook-url": "Webhook URL", + "webhook-url-required": "Webhook URL is required", + "channel-name": "Channel name", + "channel-name-required": "Channel name is required", + "settings": { + "notification-settings": "Notification settings", + "reset-all": "Reset all settings", + "reset-all-title": "Are you sure you want to reset form?", + "reset-all-text": "After the confirmation, the settings form will reset to the default value and save.", + "type": "Type", + "enable-all": "Enable all", + "disable-all": "Disable all", + "delivery-not-configured": "Delivery method is not configured" + } + }, + "ota-update": { + "add": "Add package", + "assign-firmware": "Assigned firmware", + "assign-firmware-required": "Assigned firmware is required", + "assign-software": "Assigned software", + "assign-software-required": "Assigned software is required", + "auto-generate-checksum": "Auto-generate checksum", + "cant-applied-group-all": "Can't be applied for group All", + "checksum": "Checksum", + "checksum-hint": "If checksum is empty, it will be generated automatically", + "checksum-algorithm": "Checksum algorithm", + "checksum-copied-message": "Package checksum has been copied to clipboard", + "change-firmware": "Change of the firmware may cause update of { count, plural, =1 {1 device} other {# devices} }.", + "change-software": "Change of the software may cause update of { count, plural, =1 {1 device} other {# devices} }.", + "chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.", + "chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices", + "chose-software-distributed-device": "Choose software that will be distributed to the devices", + "content-type": "Content type", + "copy-checksum": "Copy checksum", + "copy-direct-url": "Copy direct URL", + "copyId": "Copy package Id", + "copied": "Copied!", + "delete": "Delete package", + "delete-ota-update-text": "Be careful, after the confirmation the OTA update will become unrecoverable.", + "delete-ota-update-title": "Are you sure you want to delete the OTA update '{{title}}'?", + "delete-ota-updates-text": "Be careful, after the confirmation all selected OTA updates will be removed.", + "delete-ota-updates-title": "Are you sure you want to delete { count, plural, =1 {1 OTA update} other {# OTA updates} }?", + "description": "Description", + "direct-url": "Direct URL", + "direct-url-copied-message": "Package direct URL has been copied to clipboard", + "direct-url-required": "Direct URL is required", + "download": "Download package", + "drop-file": "Drop a package file or click to select a file to upload.", + "drop-package-file-or": "Drag and drop a package file or", + "file-name": "File name", + "file-size": "File size", + "file-size-bytes": "File size in bytes", + "idCopiedMessage": "Package Id has been copied to clipboard", + "no-firmware-matching": "No compatible Firmware OTA Update packages matching '{{entity}}' were found.", + "no-firmware-text": "No compatible Firmware OTA Update packages provisioned.", + "no-packages-text": "No packages found", + "no-software-matching": "No compatible Software OTA Update packages matching '{{entity}}' were found.", + "no-software-text": "No compatible Software OTA Update packages provisioned.", + "ota-update": "OTA update", + "ota-update-details": "OTA update details", + "ota-updates": "OTA updates", + "package-type": "Package type", + "packages-repository": "Packages repository", + "search": "Search packages", + "selected-package": "{ count, plural, =1 {1 package} other {# packages} } selected", + "title": "Title", + "title-required": "Title is required.", + "title-max-length": "Title should be less than 256", + "types": { + "firmware": "Firmware", + "software": "Software" + }, + "upload-binary-file": "Upload binary file", + "use-external-url": "Use external URL", + "version": "Version", + "version-required": "Version is required.", + "version-tag": "Version Tag", + "version-tag-hint": "Custom tag should match the package version reported by your device.", + "version-max-length": "Version should be less than 256", + "warning-after-save-no-edit": "Once the package is uploaded, you will not be able to modify title, version, device profile and package type." + }, + "position": { + "top": "Viršus", + "bottom": "Apačia", + "left": "Kairioji pusė", + "right": "Dešinioji pusė" + }, + "profile": { + "profile": "Profilis", + "last-login-time": "Paskutinio prisijungimo laikas", + "change-password": "Pakeisti slaptažodį", + "current-password": "Dabartinis slaptažodis", + "copy-jwt-token": "Copy JWT token", + "jwt-token": "JWT token", + "token-valid-till": "Token is valid till", + "tokenCopiedSuccessMessage": "JWT token has been copied to clipboard", + "tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page." + }, + "profiles": { + "profiles": "Profiles" + }, + "security": { + "security": "Security", + "general-settings": "General security settings", + "access-token": "Access token", + "access-token-required": "Access token is required", + "clientId": "Client ID", + "clientId-required": "Client ID is required", + "username": "Username", + "username-required": "Username is required", + "ca-cert": "CA certificate", + "2fa": { + "2fa": "Two-factor authentication", + "2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.", + "authenticate-with": "You can authenticate with:", + "disable-2fa-provider-text": "Disabling {{name}} will make your account less secure", + "disable-2fa-provider-title": "Are you sure you want to disable {{name}}?", + "get-new-code": "Get new code", + "main-2fa-method": "Use as main two-factor authentication method", + "dialog": { + "activation-step-description-email": "The next time you login in, you will be prompted to enter the security code that will be sent to your email address.", + "activation-step-description-sms": "The next time you login in, you will be prompted to enter the security code that will be sent to the phone number.", + "activation-step-description-totp": "The next time you login in, you will need to provide a two-factor authentication code.", + "activation-step-label": "Activation", + "backup-code-description": "Print out the codes so you have them handy when you need to use them to log in to your account. You can use each backup code once.", + "backup-code-warn": "Once you leave this page, these codes cannot be shown again. Store them safely using the options below.", + "download-txt": "Download (txt)", + "email-step-description": "Enter an email to use as your authenticator.", + "email-step-label": "Email", + "enable-email-title": "Enable email authenticator", + "enable-sms-title": "Enable SMS authenticator", + "enable-totp-title": "Enable authenticator app", + "enter-verification-code": "Enter the 6-digit code here", + "get-backup-code-title": "Get backup code", + "next": "Next", + "scan-qr-code": "Scan this QR code with your verification app", + "send-code": "Send code", + "sms-step-description": "Enter a phone number to use as your authenticator.", + "sms-step-label": "Phone Number", + "success": "Success!", + "totp-step-description-install": "You can install apps like Google Authenticator, Authy, or Duo.", + "totp-step-description-open": "Open the authenticator app on your mobile phone.", + "totp-step-label": "Get app", + "verification-code": "6-digit code", + "verification-code-invalid": "Invalid verification code format", + "verification-code-incorrect": "Verification code is incorrect", + "verification-code-many-request": "Too many requests check verification code", + "verification-step-description": "Enter a 6-digit code we just sent to {{address}}", + "verification-step-label": "Verification" + }, + "provider": { + "email": "Email", + "email-description": "Use a security code sent to your email address to authenticate.", + "email-hint": "Authentication codes are sent via email to {{ info }}", + "sms": "SMS", + "sms-description": "Use your phone to authenticate. We'll send you a security code via SMS message when you log in.", + "sms-hint": "Authentication codes are sent by text message to {{ info }}", + "totp": "Authenticator app", + "totp-description": "Use apps like Google Authenticator, Authy, or Duo on your phone to authenticate. It will generate a security code for logging in.", + "totp-hint": "Authenticator app is set up for your account", + "backup_code": "Backup code", + "backup-code-description": "These printable one-time passcodes allow you to sign in when away from your phone, like when you’re traveling.", + "backup-code-hint": "{{ info }} single-use codes are active at this time" + } + }, + "password-requirement": { + "at-least": "At least:", + "character": "{ count, plural, =1 {1 character} other {# characters} }", + "digit": "{ count, plural, =1 {1 digit} other {# digits} }", + "incorrect-password-try-again": "Incorrect password. Try again", + "lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }", + "new-passwords-not-match": "New password didn't match", + "password-should-not-contain-spaces": "Your password should not contain spaces", + "password-not-meet-requirements": "Password didn't meet requirements", + "password-requirements": "Password requirements", + "password-should-difference": "New password should be different from current", + "special-character": "{ count, plural, =1 {1 special character} other {# special characters} }", + "uppercase-letter": "{ count, plural, =1 {1 uppercase letter} other {# uppercase letters} }" + } + }, + "relation": { + "relations": "Ryšiai", + "direction": "Direction", + "clear-relation-type": "Clear relation type", + "search-direction": { + "FROM": "Iš", + "TO": "Į" + }, + "direction-type": { + "FROM": "iš", + "TO": "į" + }, + "from-relations": "Išeinantys ryšiai", + "to-relations": "įeinantys ryšiai", + "selected-relations": "Pasirinkta { count, plural, =1 {1 ryšys} other {# ryšiai} } selected", + "type": "Tipas", + "to-entity-type": "Į subjekto tipą", + "to-entity-name": "Į subjektą", + "from-entity-type": "iš subjektų tipo", + "from-entity-name": "Iš subjekto", + "to-entity": "į subjektą", + "from-entity": "Iš subjekto", + "delete": "panaikinti ryšį", + "relation-type": "Ryšio tipas", + "relation-type-required": "Ryšio tipas būtinas.", + "relation-type-max-length": "Ryšio tipas negali viršyti 256 simbolių", + "any-relation-type": "Bet kuris tipas", + "add": "pridėti ryšį", + "edit": "Redaguoti ryšį", + "view": "peržiūrėti ryšį", + "delete-to-relation-title": "Ar tikrai norite panaikinti ryšį su '{{entityName}}' subjektu?", + "delete-to-relation-text": "Būkite dėmesingi, po patvirtinimo '{{entityName}}' subjektas neteks ryšio su dabartiniu subjektu.", + "delete-to-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", + "delete-to-relations-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti ryšiai bus panaikinti ir atitinkami subjektai neteks ryšių su dabartiniu subjektu.", + "delete-from-relation-title": "Ar tikrai norite panaikinti ryšį iš '{{entityName}}' subjekto?", + "delete-from-relation-text": "Būkite dėmesingi, po patvirtinimo, dabartinis subjektas neteks ryšio su '{{entityName}}' subjektu.", + "delete-from-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", + "delete-from-relations-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti ryšiai bus panaikinti ir dabartinis subjektas neteks ryšių su atitinkamais subjektais.", + "remove-relation-filter": "Panaikinti ryšių filtrą", + "remove-filter": "Panakinti filtrą", + "add-relation-filter": "Pridėti ryšių filtrą", + "any-relation": "Bet kuris ryšys", + "relation-filters": "Ryšių filtrai", + "additional-info": "Papildoma informacija (JSON)", + "invalid-additional-info": "Papildomos JSON informacijos išanalizuoti nepavyko.", + "no-relations-text": "Ryšių nėra" + }, + "resource": { + "add": "Add Resource", + "all-types": "All", + "copyId": "Copy resource Id", + "delete": "Delete resource", + "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.", + "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?", + "delete-resources-action-title": "Delete { count, plural, =1 {1 resource} other {# resources} }", + "delete-resources-text": "Please note that the selected resources, even if they are used in device profiles, will be deleted.", + "delete-resources-title": "Are you sure you want to delete { count, plural, =1 {1 resource} other {# resources} }?", + "download": "Download resource", + "drop-file": "Drop a resource file or click to select a file to upload.", + "drop-resource-file-or": "Drag and drop a resource file or", + "empty": "Resource is empty", + "file-name": "File name", + "idCopiedMessage": "Resource Id has been copied to clipboard", + "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.", + "no-resource-text": "No resources found", + "open-widgets-bundle": "Open widgets bundle", + "resource": "Resource", + "resource-library-details": "Resource details", + "resource-type": "Resource type", + "resources-library": "Resources library", + "search": "Search resources", + "selected-resources": "{ count, plural, =1 {1 resource} other {# resources} } selected", + "system": "System", + "title": "Title", + "title-required": "Title is required.", + "title-max-length": "Title should be less than 256", + "type": { + "jks": "JKS", + "js-module": "JS module", + "lwm2m-model": "LWM2M model", + "pkcs-12": "PKCS #12" + } + }, + "rulechain": { + "rulechain": "Rule chain", + "rulechain-events": "Rule chain events", + "rulechains": "Rule chains", + "root": "Root", + "delete": "Delete rule chain", + "name": "Name", + "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", + "description": "Description", + "add": "Add Rule Chain", + "set-root": "Make rule chain root", + "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", + "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", + "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?", + "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.", + "delete-rulechains-title": "Are you sure you want to delete { count, plural, =1 {1 rule chain} other {# rule chains} }?", + "delete-rulechains-action-title": "Delete { count, plural, =1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", + "add-rulechain-text": "Add new rule chain", + "no-rulechains-text": "No rule chains found", + "rulechain-details": "Rule chain details", + "details": "Details", + "events": "Events", + "system": "System", + "import": "Import rule chain", + "export": "Export rule chain", + "export-failed-error": "Unable to export rule chain: {{error}}", + "create-new-rulechain": "Create new rule chain", + "rulechain-file": "Rule chain file", + "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", + "copyId": "Copy rule chain Id", + "idCopiedMessage": "Rule chain Id has been copied to clipboard", + "select-rulechain": "Select rule chain", + "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.", + "rulechain-required": "Rule chain is required", + "management": "Rules management", + "debug-mode": "Debug mode", + "search": "Search rule chains", + "selected-rulechains": "{ count, plural, =1 {1 rule chain} other {# rule chains} } selected", + "open-rulechain": "Open rule chain", + "edge-template-root": "Template Root", + "assign-to-edge": "Assign to Edge", + "edge-rulechain": "Edge rule chain", + "unassign-rulechain-from-edge-text": "After the confirmation the rulechain will be unassigned and won't be accessible by the edge.", + "unassign-rulechains-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 rulechain} other {# rulechains} }?", + "unassign-rulechains-from-edge-text": "After the confirmation all selected rulechains will be unassigned and won't be accessible by the edge.", + "assign-rulechain-to-edge-title": "Assign Rule Chain(s) To Edge", + "assign-rulechain-to-edge-text": "Please select the rulechains to assign to the edge", + "set-edge-template-root-rulechain": "Make rule chain as edge template root", + "set-edge-tscheduler-eventemplate-root-rulechain": "Make rule chain as edge template root", + "set-edge-template-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' edge template root?", + "set-edge-template-root-rulechain-text": "After the confirmation the rule chain will become edge template root and will be root rule chain for a newly created edges.", + "invalid-rulechain-type-error": "Unable to import rule chain: Invalid rule chain type. Expected type is {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Assign rule chain to edge(s) on creation", + "set-auto-assign-to-edge-title": "Are you sure you want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", + "set-auto-assign-to-edge-text": "After the confirmation the edge rule chain will be automatically assigned to edge(s) on creation.", + "unset-auto-assign-to-edge": "Do not assign rule chain to edge(s) on creation", + "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", + "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", + "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", + "unassign-rulechains": "Unassign rulechains" + }, + "rulenode": { + "rule-node-events": "Rule node events", + "details": "Details", + "events": "Events", + "search": "Search nodes", + "open-node-library": "Open node library", + "close-node-library": "Close node library", + "add": "Add rule node", + "name": "Name", + "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", + "type": "Type", + "rule-node-description": "Rule node description", + "delete": "Delete rule node", + "select-all-objects": "Select all nodes and connections", + "deselect-all-objects": "Deselect all nodes and connections", + "delete-selected-objects": "Delete selected nodes and connections", + "delete-selected": "Delete selected", + "create-nested-rulechain": "Create nested rule chain", + "select-all": "Select all", + "copy-selected": "Copy selected", + "deselect-all": "Deselect all", + "rulenode-details": "Rule node details", + "debug-mode": "Debug mode", + "singleton-mode": "Singleton mode", + "configuration": "Configuration", + "link": "Link", + "link-details": "Rule node link details", + "add-link": "Add link", + "link-label": "Link label", + "link-label-required": "Link label is required.", + "custom-link-label": "Custom link label", + "custom-link-label-required": "Custom link label is required.", + "link-labels": "Link labels", + "link-labels-required": "Link labels is required.", + "no-link-labels-found": "No link labels found", + "no-link-label-matching": "'{{label}}' not found.", + "create-new-link-label": "Create a new one!", + "type-filter": "Filter", + "type-filter-details": "Filter incoming messages with configured conditions", + "type-enrichment": "Enrichment", + "type-enrichment-details": "Add additional information into Message Metadata", + "type-transformation": "Transformation", + "type-transformation-details": "Change Message payload and Metadata", + "type-action": "Action", + "type-action-details": "Perform special action", + "type-analytics": "Analytics", + "type-analytics-details": "Perform analysis of streamed or persisted data", + "type-external": "External", + "type-external-details": "Interacts with external system", + "type-rule-chain": "Rule Chain", + "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "type-flow": "Flow", + "type-flow-details": "Organizes message flow", + "type-input": "Input", + "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", + "type-unknown": "Unknown", + "type-unknown-details": "Unresolved Rule Node", + "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", + "ui-resources-load-error": "Failed to load configuration ui resources.", + "invalid-target-rulechain": "Unable to resolve target rule chain!", + "test-script-function": "Test script function", + "script-lang-java-script": "Java Script", + "script-lang-tbel": "TBEL", + "message": "Message", + "message-type": "Message type", + "select-message-type": "Select message type", + "message-type-required": "Message type is required", + "metadata": "Metadata", + "metadata-required": "Metadata entries can't be empty.", + "output": "Output", + "test": "Test", + "help": "Help", + "reset-debug-mode": "Reset debug mode in all nodes", + "test-with-this-message": "{{test}} with this message" + }, + "role": { + "role": "Rolė", + "roles": "Rolės", + "management": "Rolių valdymas", + "view-roles": "Peržiūrėti roles", + "no-roles-matching": "Rolių, atitinkančių '{{entity}}' nėra.", + "role-list": "Rolių sąrašas", + "role-list-required": "Rolių sąrašas būtinas", + "add": "Pridėti rolę", + "view": "Peržiūrėti rolę", + "search": "Rolių paieška", + "selected-roles": "Pasirinkta { count, plural, =1 {1 rolė} other {# rolės} }", + "no-roles-text": "Rolių nėra", + "role-details": "Informacija apie rolę", + "add-role-text": "Pridėti naują rolę", + "delete": "Panaikinti rolę", + "delete-roles": "Panaikinti roles", + "delete-role-title": "Ar tikrai norite panaikinti '{{roleName}}' rolę?", + "delete-role-text": "Būkite dėmesingi, po patvirtinimo, rolė ir visa su ja susijusi informacija bus pašalinta.", + "delete-roles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 rolę} other {# roles} }?", + "delete-roles-action-title": "Panaikinti { count, plural, =1 {1 rolę} other {# roles} }", + "delete-roles-text": "Būkite dėmesingi, po patvirtinimo, visos pasirinktos rolės ir su jomis susijusi informacija bus pašalintos.", + "role-type": "Tipas", + "role-type-required": "Rolės tipas būtinas.", + "select-role-type": "Pasirinkite rolės tipą", + "enter-role-type": "Įveskite rolės tipą", + "any-role": "Bet kuri rolė", + "no-role-types-matching": "Rolių tipų, atitinkančių '{{entitySubtype}}' nėra.", + "role-type-list-empty": "Nepasirinktas rolės tipas.", + "role-types": "Rolių tipais", + "created-time": "Sukųrimo laikas", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "description": "Aprašymas", + "events": "Įvykiai", + "details": "Informacija", + "copyId": "Kopijuoti rolės Id", + "idCopiedMessage": "Rolės Id nukopijuotas į iškarpinę", + "permissions": "Leidimai", + "role-required": "Rolė būtina", + "roles-required": "Rolės būtinos", + "display-type": { + "GENERIC": "Bendroji", + "GROUP": "Grupinė" + } + }, + "group-permission": { + "user-group-roles": "Vartotojų grupės rolės", + "entity-group-permissions": "Subjektų grupės leidimai", + "role-type": "Rolės tipas", + "role-name": "Rolės pavadinimas", + "group-type": "Grupės tipas", + "group-name": "Grupės pavadinimas", + "group-owner": "Grupės savininkas", + "user-group-name": "Vartotojų grupės pavadinimas", + "user-group-owner": "Vartotojų grupės savininkas", + "edit": "Redaguoti leidimus", + "delete": "Naikinti leidimus", + "selected-group-permissions": "Pasirinkta { count, plural, =1 {1 grupinis leidimas} other {# grupiniai leidimai} }", + "delete-group-permission-title": "Ar tikrai norite panaikinti '{{roleName}}' grupinį leidimą?", + "delete-group-permission-text": "Būkite dėmesingi, po patvirtinimo, grupinis leidimas ir visa su juo sisijusi informacija bus pašalinta.", + "delete-group-permission": "Panaikinti grupinį leidima", + "delete-group-permissions-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 grupinį leidimą} other {# grupinius leidimus} }?", + "delete-group-permissions-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti grupiniai leidimai ir visa su jais susijusi informacija bus panaikinta ir atitinkami vartotojai neteks prieigos prie tam tikrų resursų.", + "delete-group-permissions": "Panaikinti grupinius leidimus", + "add-group-permission": "Pridėti grupinį leidimą", + "edit-group-permission": "Redaguoti grupinį leidimą", + "entity-group": "Subjektų grupė", + "user-group": "Vartotojų grupė", + "no-owners-matching": "Savininkų, atitinkančių '{{owner}}' nėra.", + "target-owner-required": "Subjektų grupės savininkas būtinas.", + "target-user-group-owner-required": "Vartotojų grupės savininkas būtinas.", + "no-group-permissions-text": "Grupinių leidimų nėra" + }, + "permission": { + "permissions-required": "Turi būti nurodytas nors vienas leidimas.", + "remove-permission": "Panaikinti leidimą", + "add-permission": "Pridėti leidimą", + "other": "Kita", + "resource": { + "resource": "Resursas", + "select-resource": "Pasirinkite resursą", + "resource-required": "Resursas būtinas", + "no-resources-matching": "Resursų, atitinkančių '{{resource}}' nėra.", + "display-type": { + "ALL": "Visi", + "PROFILE": "Profilis", + "ADMIN_SETTINGS": "Administratoriaus nustatymai", + "ALARM": "Įspėjimai", + "DEVICE": "Įrenginys", + "DEVICE_PROFILE": "Įrenginio profilis", + "ASSET": "Turtas", + "CUSTOMER": "Klientas", + "DASHBOARD": "Skydelis", + "ENTITY_VIEW": "Subjekto rodinys", + "EDGE": "Edge", + "TENANT": "Valdytojas", + "TENANT_PROFILE": "Valdytojo profilis", + "RULE_CHAIN": "Taisyklių grandinė", + "USER": "Vartotojas", + "WIDGETS_BUNDLE": "Valdiklių rinkinys", + "WIDGET_TYPE": "Valdiklio tipas", + "CONVERTER": "Keitiklis", + "INTEGRATION": "Integracija", + "SCHEDULER_EVENT": "Tvarkaraščio įvykis", + "BLOB_ENTITY": "Blob subjektas", + "CUSTOMER_GROUP": "Klientų grupė", + "DEVICE_GROUP": "Įrenginių grupė", + "ASSET_GROUP": "Turto grupė", + "USER_GROUP": "Vartotojų grupė", + "ENTITY_VIEW_GROUP": "Subjektų rodinių grupė", + "DASHBOARD_GROUP": "Skydelių grupė", + "ROLE": "Rolė", + "GROUP_PERMISSION": "Grupinis leidimas", + "WHITE_LABELING": "Tinkinimas", + "AUDIT_LOG": "Audito žurnalas", + "API_USAGE_STATE": "API Usage State", + "TB_RESOURCE": "Resource", + "EDGE_GROUP": "Edge Group", + "OTA_PACKAGE": "Ota package", + "QUEUE": "Queue", + "VERSION_CONTROL": "Version control", + "ASSET_PROFILE": "Asset Profile", + "NOTIFICATION": "Notification" + } + }, + "operation": { + "operation": "Operacija", + "operations": "Operacijos", + "operations-required": "Turi būti nurodyta nors viena operacija.", + "enter-operation": "Įveskite operaciją", + "no-operations-matching": "Operacijų, atitinkančių '{{operation}}' nėra.", + "display-type": { + "ALL": "Visi", + "CREATE": "Sukurti", + "READ": "Skaityti", + "WRITE": "Rašyti", + "DELETE": "Panaikinti", + "ASSIGN_TO_CUSTOMER": "Priskirti klientui", + "UNASSIGN_FROM_CUSTOMER": "Atsieti nuo kliento", + "RPC_CALL": "RPC kvietimas", + "READ_CREDENTIALS": "Skaityti įgaliojimus", + "WRITE_CREDENTIALS": "Rašyti įgaliojimus", + "READ_ATTRIBUTES": "Skaityti atributus", + "WRITE_ATTRIBUTES": "Rašyti atributus", + "READ_TELEMETRY": "Skaityti telemetriją", + "WRITE_TELEMETRY": "Rašyti telemetriją", + "CLAIM_DEVICES": "Patvirtinti įrenginius", + "IMPERSONATE": "Įeiti kaip vartotojui", + "CHANGE_OWNER": "Pakeisti savininką", + "ADD_TO_GROUP": "Įtraukti į grupę", + "REMOVE_FROM_GROUP": "Pašalinti iš grupės", + "SHARE_GROUP": "Bendrinti grupę", + "ASSIGN_TO_TENANT": "Priskirti valdytojui" + } + } + }, + "scheduler": { + "scheduler": "Tvarkaraštis", + "scheduler-event": "Tvarkaraščio įvykis", + "select-scheduler-event": "Pasirinkite tvarkaraščio įvykį", + "no-scheduler-events-matching": "Tvarkaraščio įvykių, atitinkančių '{{entity}}' nėra.", + "scheduler-event-required": "Tvarkaraščio įvykis būtinas", + "management": "Tvarkaraščio valdymas", + "scheduler-events": "Tvarkaraščio įvykiai", + "add-scheduler-event": "Pridėti tvarkaraščio įvykį", + "search-scheduler-events": "Tvarkaraščio įvykių paieška", + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "type": "Tipas", + "created_customer": "Sukūrė klientas", + "edit-scheduler-event": "Redaguoti tvarkaraščio įvykį", + "view-scheduler-event": "Peržiūrėti tvarkaraščio įvykį", + "delete-scheduler-event": "Panaikinti tvarkaraščio įvykį", + "no-scheduler-events": "Tvarkaraščio įvykių nėra", + "selected-scheduler-events": "Pasirinkta { count, plural, =1 {1 tvarkaraščio įvykis} other {# tvarkaraščio įvykiai} }", + "delete-scheduler-event-title": "Ar tikrai norite panaikinti tvarkaraščio įvykį '{{schedulerEventName}}'?", + "delete-scheduler-event-text": "`", + "delete-scheduler-events-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 tvarkaraščio įvykį} other {# tvarkaraščio įvykius} }?", + "delete-scheduler-events-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti tvarkaraščio įvykiai ir visa su jais susijusi informacija bus panaikinta.", + "create": "Sukurti tvarkaraščio įvykį", + "edit": "Redaguoti tvarkaraščio įvykį", + "view": "Peržiūrėti tvarkaraščio įvykį", + "configuration": "Konfigūracija", + "schedule": "Tvarkaraštis", + "start-time": "Pradžios laikas", + "repeat": "Pakartoti", + "repeats": "Pakartojimai", + "daily": "Kasdieną", + "every-n-days": "Kas N dienas", + "every-n-days-text": "Every { days, plural, =1 {day} other {# days} }", + "weekly": "Kas savaitę", + "every-n-weeks": "Kas N savaites", + "every-n-weeks-text": "Every { weeks, plural, =1 {week} other {# weeks} }", + "monthly": "Kas mėnesį", + "yearly": "Kas metus", + "timer": "Nurodytu laiku", + "repeats-required": "Pakartojimai būtini.", + "repeat-on": "Kartoti", + "repeat-every": "Kartoti kiekvieną", + "repeat-every-n-days": "Kartoti kas N dienas", + "repeat-days-required": "Pakartojim dienų skaičius būtinas.", + "invalid-repeat-days-value": "Pakartojimo dienų skaičius turi būti sveikas teigiamas skaičius.", + "repeat-every-n-weeks": "Kartoti kas N savaites", + "repeat-weeks-required": "Pakartojimo savaičių skaičius būtinas.", + "invalid-repeat-weeks-value": "Pakartojimo savaičių skaičius turi būti sveikas teigiamas skaičius.", + "ends-on": "Pabaiga", + "sunday-label": "Sk", + "monday-label": "Pr", + "tuesday-label": "An", + "wednesday-label": "Tr", + "thursday-label": "Kt", + "friday-label": "Pn", + "saturday-label": "Št", + "repeat-on-sunday": "Kartoti sekmadieniais", + "repeat-on-monday": "Kartoti pirmadieniais", + "repeat-on-tuesday": "Kartoti antradieniais", + "repeat-on-wednesday": "Kartoti trečiadieniais", + "repeat-on-thursday": "Kartoti ketvirtadieniais", + "repeat-on-friday": "Kartoti penktadieniais", + "repeat-on-saturday": "Kartoti šeštadieniais", + "event-type": "Įvykio tipas", + "select-event-type": "Pasirinkite įvykio tiipą", + "event-type-required": "Įvykio tipas būtinas.", + "event-type-max-length": "Įvykio tipas negali viršyti 256 simbolių.", + "list-mode": "Sąrašas", + "calendar-mode": "Kalendorius", + "calendar-view-type": "Kalendoriaus peržiūros tipas", + "month": "Mėnuo", + "week": "Savaitė", + "day": "Diena", + "agenda-week": "Savaitinis tvarkaraštis", + "agenda-day": "Dienų tvarkaraštis", + "list-year": "Sąrašas metams", + "list-month": "Sąrašas mėnesiui", + "list-week": "Sąrašas savaitei", + "list-day": "Sąrašas dienai", + "today": "Šiandien", + "navigate-before": "Ankstesnis", + "navigate-next": "Sekantis", + "starting-from": "Pradėti nuo", + "until": "iki", + "on": "į", + "sunday": "Sekmadienis", + "monday": "Pirmadienis", + "tuesday": "Antradienis", + "wednesday": "Trečiadienis", + "thursday": "Ketvirtadienis", + "friday": "Penktadienis", + "saturday": "Šesštadienis", + "originator": "Iniciatorius", + "single-entity": "Vienas subjektas", + "group-of-entities": "Subjektų grupė", + "entities-group-owner": "Subjektų grupės savininkas", + "single-device": "Vienas įrenginys", + "group-of-devices": "Įrenginių grupė", + "devices-group-owner": "Įrenginių grupės savininkas", + "message-body": "Pranešimo tekstas", + "target": "Tikslas", + "rpc-method": "Method", + "rpc-method-required": "Method is required", + "rpc-method-white-space": "White space is not allowed.", + "rpc-params": "Params", + "select-dashboard-state": "Pasirinkite skydelio būseną", + "hours": "Valandos", + "minutes": "Minutės", + "seconds": "Sekundės", + "time-interval-required": "Laiko intervalas būtinas", + "time-unit-required": "Laiko vienetas būtinas", + "every-hour": "kas { count, plural, =1 {valandą} other {# valandas} }", + "every-minute": "kas { count, plural, =1 {minutę} other {# minutes} }", + "every-second": "kas { count, plural, =1 {sekundę} other {# sekundes} }", + "invalid-time": "Neteisingas laikas" + }, + "report": { + "report-config": "Ataskaitos konfigūracija", + "email-config": "Elektroninio pašto konfigūracija", + "dashboard-state-param": "Skydelio būsenos parametro reikšmė", + "base-url": "Pagrindinis URL", + "base-url-required": "Pagrindinis URL būtinas.", + "use-dashboard-timewindow": "Naudoti skydelio laiko langą", + "timewindow": "Laiko langas", + "name-pattern": "Ataskaitos pavadinimo šablonas", + "name-pattern-required": "Ataskaitos pavadinimo šablonas būtinas", + "type": "Ataskaitos tipas", + "use-current-user-credentials": "Naudoti dabartinio vartotojo įgaliojimus", + "customer-user-credentials": "Kliento vartotojo įgaliojimai", + "customer-user-credentials-required": "Kliento vartotojo įgaliojimai būtini", + "generate-test-report": "Generuoti bandomąją ataskaitą", + "send-email": "Siųsti laišką", + "from": "Nuo", + "from-required": "Būtina nurodyti nuo ko siunčiamas laiškas.", + "to": "Kam", + "to-required": "Būtina nurodyti kam siunčiamas laiškas.", + "cc": "Cc", + "bcc": "Bcc", + "subject": "Tema", + "subject-required": "Būtina nurodyti laiško temą.", + "body": "Tekstas", + "body-required": "Būtina įrašyti laiško tekstą." + }, + "blob-entity": { + "blob-entity": "Blob subjektas", + "select-blob-entity": "Pasirinkite blob subjektą", + "no-blob-entities-matching": "Blob subjektų, atitinkančių '{{entity}}' nėra.", + "blob-entity-required": "Blob subjektas būtinas", + "files": "Failai", + "search": "Failų paieška", + "clear-search": "Išvalyti paiešką", + "no-blob-entities-prompt": "Failų nėra", + "report": "Ataskaita", + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "type": "Tipas", + "created_customer": "Skūrė klientas", + "selected-blob-entities": "Pasirinkta{ count, plural, =1 {1 failas} other {# failai} }", + "download-blob-entity": "Atsisiųsti failą", + "delete-blob-entity": "Ištrinti failą", + "delete-blob-entity-title": "Ar tikrai norite ištrinti '{{blobEntityName}}' failą?", + "delete-blob-entity-text": "Būkite dėmesingi, po patvirtinimo failas bus ištrintas.", + "delete-blob-entities-title": "Artikrai norite ištrinti { count, plural, =1 {1 failą} other {# failus} }?", + "delete-blob-entities-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti failai ir su jais susijusi informacija bus ištrinta." + }, + "timezone": { + "timezone": "Laiko juosta", + "select-timezone": "Pasirinkti laiko juostą", + "no-timezones-matching": "Laiko juostų, atitinkančių '{{timezone}}' nėra.", + "timezone-required": "Laiko juosta būtina.", + "browser-time": "Browser Time" + }, + "queue": { + "queue-name": "Queue", + "no-queues-found": "No queues found.", + "no-queues-matching": "No queues matching '{{queue}}' were found.", + "select-name": "Select queue name", + "name": "Name", + "name-required": "Queue name is required!", + "name-unique": "Queue name is not unique!", + "name-pattern": "Queue name contains a character other than ASCII alphanumerics, '.', '_' and '-'!", + "queue-required": "Queue is required!", + "topic-required": "Queue topic is required!", + "poll-interval-required": "Poll interval is required!", + "poll-interval-min-value": "Poll interval value can't be less then 1", + "partitions-required": "Partitions is required!", + "partitions-min-value": "Partitions value can't be less then 1", + "pack-processing-timeout-required": "Processing timeout is required", + "pack-processing-timeout-min-value": "Processing timeout value can't be less then 1", + "batch-size-required": "Batch size is required!", + "batch-size-min-value": "Batch size value can't be less then 1", + "retries-required": "Retries is required!", + "retries-min-value": "Retries value can't be negative", + "failure-percentage-required": "Failure percentage is required!", + "failure-percentage-min-value": "Failure percentage value can't be less then 0", + "failure-percentage-max-value": "Failure percentage value can't be more then 100", + "pause-between-retries-required": "Pause between retries is required!", + "pause-between-retries-min-value": "Pause between retries value can't be less then 1", + "max-pause-between-retries-required": "Max pause between retries is required!", + "max-pause-between-retries-min-value": "Max pause between retries value can't be less then 1", + "submit-strategy-type-required": "Submit strategy type is required!", + "processing-strategy-type-required": "Processing strategy type is required!", + "queues": "Queues", + "selected-queues": "{ count, plural, =1 {1 queue} other {# queues} } selected", + "delete-queue-title": "Are you sure you want to delete the queue '{{queueName}}'?", + "delete-queues-title": "Are you sure you want to delete { count, plural, =1 {1 queue} other {# queues} }?", + "delete-queue-text": "Be careful, after the confirmation the queue and all related data will become unrecoverable.", + "delete-queues-text": "After the confirmation all selected queues will be deleted and won't be accessible.", + "search": "Search queue", + "add": "Add queue", + "details": "Queue details", + "topic": "Topic", + "submit-settings": "Submit settings", + "submit-strategy": "Strategy type *", + "grouping-parameter": "Grouping parameter", + "processing-settings": "Retries processing settings", + "processing-strategy": "Processing type *", + "retries-settings": "Retries settings", + "polling-settings": "Polling settings", + "batch-processing": "Batch processing", + "poll-interval": "Poll interval", + "partitions": "Partitions", + "immediate-processing": "Immediate processing", + "consumer-per-partition": "Send message poll for each consumer", + "consumer-per-partition-hint": "Enable separate consumer(s) per each partition", + "processing-timeout": "Processing within, ms", + "batch-size": "Batch size", + "retries": "Number of retries (0 – unlimited)", + "failure-percentage": "Percentage of failure messages for skipping retries", + "pause-between-retries": "Retry within, sec", + "max-pause-between-retries": "Additional retry within, sec", + "delete": "Delete queue", + "copyId": "Copy queue Id", + "idCopiedMessage": "Queue Id has been copied to clipboard", + "description": "Description", + "description-hint": "This text will be displayed in the Queue description instead of the selected strategy", + "alt-description": "Submit Strategy: {{submitStrategy}}, Processing Strategy: {{processingStrategy}}", + "custom-properties": "Custom properties", + "custom-properties-hint": "Custom queue (topic) creation properties, e.g. 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "Sequential by originator", + "sequential-by-originator-hint": "New message for e.g. device A is not submitted until previous message for device A is acknowledged", + "sequential-by-tenant-label": "Sequential by tenant", + "sequential-by-tenant-hint": "New message for e.g tenant A is not submitted until previous message for tenant A is acknowledged", + "sequential-label": "Sequential", + "sequential-hint": "New message is not submitted until previous message is acknowledged", + "burst-label": "Burst", + "burst-hint": "All messages are submitted to the rule chains in the order they arrive", + "batch-label": "Batch", + "batch-hint": "New batch is not submitted until previous batch is acknowledged", + "skip-all-failures-label": "Skip all failures", + "skip-all-failures-hint": "Ignore all failures", + "skip-all-failures-and-timeouts-label": "Skip all failures and timeouts", + "skip-all-failures-and-timeouts-hint": "Ignore all failures and timeouts", + "retry-all-label": "Retry all", + "retry-all-hint": "Retry all messages from processing pack", + "retry-failed-label": "Retry failed", + "retry-failed-hint": "Retry all failed messages from processing pack", + "retry-timeout-label": "Retry timeout", + "retry-timeout-hint": "Retry all timed-out messages from processing pack", + "retry-failed-and-timeout-label": "Retry failed and timeout", + "retry-failed-and-timeout-hint": "Retry all failed and timed-out messages from processing pack" + } + }, + "server-error": { + "general": "General server error", + "authentication": "Authentication error", + "jwt-token-expired": "JWT token expired", + "tenant-trial-expired": "Tenant trial expired", + "credentials-expired": "Credentials expired", + "permission-denied": "Permission denied", + "invalid-arguments": "Invalid arguments", + "bad-request-params": "Bad request params", + "item-not-found": "Item not found", + "too-many-requests": "Too many requests", + "too-many-updates": "Too many updates" + }, + "tenant": { + "tenant": "Valdytojas", + "tenants": "Valdytojai", + "management": "Valdytojų valdymas", + "add": "Pridėti valdytoją", + "admins": "Administratoriai", + "manage-tenant-admins": "Valdyti valdytojo administratorius", + "delete": "Ištrinti valdytoją", + "add-tenant-text": "Pridėti naują valdytoją", + "no-tenants-text": "Valdytojų nėra", + "tenant-details": "Informacija apie valdytoją", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "delete-tenant-title": "Ar tikrai norite ištrinti valdytoją '{{tenantTitle}}'?", + "delete-tenant-text": "Būkite dėmesingi, po patvirtinimo valdytojas ir visa su juo susijusi informacija bus pašalinta.", + "delete-tenants-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }?", + "delete-tenants-action-title": "Pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }", + "delete-tenants-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojai ir su jais susijusi informacija bus pašalinta.", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas.", + "description": "Aprašymas", + "details": "Informacija", + "events": "Įvykiai", + "copyId": "Kopijuoti valdytojo Id", + "idCopiedMessage": "Valdytojo Id nukopijuotas į iškarpinę", + "select-tenant": "Pasirinkti valdytoją", + "no-tenants-matching": "Valdytojų, atitinkančių '{{entity}}' nėra.", + "tenant-required": "Valdytojas būtinas", + "allow-white-labeling": "Leisti personalizavimą", + "allow-customer-white-labeling": "Leisti kliento personalizavimą", + "search": "Valdytojų paieška", + "selected-tenants": "Pasirinkta { count, plural, =1 {1 valdytojas} other {# valdutojai} }", + "isolated-tb-rule-engine": "Use isolated ThingsBoard Rule Engine queues", + "isolated-tb-rule-engine-details": "Each tenant will have dedicated Rule Engine queues" + }, + "tenant-profile": { + "tenant-profile": "Valdytojų profilis", + "tenant-profiles": "Valdytojų profiliai", + "add": "Pridėti valdytojų profilį", + "add-profile": "Pridėti profilį", + "edit": "Redaguoti valdytojų profilį", + "tenant-profile-details": "Informacija apie valdytojų profilį", + "no-tenant-profiles-text": "Valdytojų profilių nėra", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "search": "Valdytojų profilių paieška", + "selected-tenant-profiles": "Pasirinkta { count, plural, =1 {1 valdytojų profilis} other {# valdytojų profiliai} }", + "no-tenant-profiles-matching": "Valdytojų profilių, atitinkančių '{{entity}}' nėra.", + "tenant-profile-required": "Valdytojų profilis būtinas", + "idCopiedMessage": "Valdytojų profilio Id nukopijuotas į iškarpinę", + "set-default": "Valdytojų profilį nustatyti kaip pagrndinį", + "delete": "Panaikinti valdytojų profilį", + "copyId": "Kopijuoti valdytojų profilio Id", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "data": "Profilio duomenys", + "profile-configuration": "Profilio konfigūracija", + "description": "Aprašymas", + "default": "Pagrindinis", + "delete-tenant-profile-title": "Ar tikrai norite panaikinti valdytojų profilį '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Būkite dėmesingi, po patvirtinimo, valdytojų profilis ir visa su juo susijusi informacija bus pašalinta.", + "delete-tenant-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytojų profilį} other {# valdytojų profilius} }?", + "delete-tenant-profiles-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojų profiliai ir su jais susijusi informacija bus pašalinta.", + "set-default-tenant-profile-title": "Ar tikrai norite valdytojų profilį '{{tenantProfileName}}' nustatyti kaip pagrindinį?", + "set-default-tenant-profile-text": "Po patvirtinimo, valdytojų profilis bus nustatytas kaip pagrinfinis ir bus priskiriamas naujiems valdytojams, kuriems valdytojų profilis nenurodytas.", + "no-tenant-profiles-found": "Valdytojų profilių nėra.", + "create-new-tenant-profile": "Sukurti naują!", + "create-tenant-profile": "Sukuri naują valdytojo profilį", + "import": "Importuoti valdytojo profilį", + "export": "Eksportuoti valdytojo profilį", + "export-failed-error": "Valdytojo profilio importuoti nepavyko: {{error}}", + "tenant-profile-file": "Valdytojo profilio failas", + "invalid-tenant-profile-file-error": "Valdytojo profilio importuoti nepavyko: Neteisinga valdytojo profilio duomenų struktūra.", + "advanced-settings": "Pažangūs nustatymai", + "entities": "Subjektai", + "rule-engine": "Rule Engine", + "time-to-live": "Time-to-live", + "alarms-and-notifications": "Alarms and notifications", + "ota-files-in-bytes": "OTA files in bytes", + "ws-title": "WS", + "unlimited": "(0 - unlimited)", + "maximum-devices": "Devices maximum number", + "maximum-devices-required": "Devices maximum number is required.", + "maximum-devices-range": "Devices maximum number can't be negative", + "maximum-assets": "Assets maximum number", + "maximum-assets-required": "Assets maximum number is required.", + "maximum-assets-range": "Assets maximum number can't be negative", + "maximum-customers": "Customers maximum number", + "maximum-customers-required": "Customers maximum number is required.", + "maximum-customers-range": "Customers maximum number can't be negative", + "maximum-users": "Users maximum number", + "maximum-users-required": "Users maximum number is required.", + "maximum-users-range": "Users maximum number can't be negative", + "maximum-dashboards": "Dashboards maximum number", + "maximum-dashboards-required": "Dashboards maximum number is required.", + "maximum-dashboards-range": "Dashboards maximum number can't be negative", + "maximum-rule-chains": "Rule chains maximum number", + "maximum-rule-chains-required": "Rule chains maximum number is required.", + "maximum-rule-chains-range": "Rule chains maximum number can't be negative", + "maximum-integrations": "Integrations maximum number", + "maximum-integrations-required": "Integrations maximum number is required.", + "maximum-integrations-range": "Integrations maximum number can't be negative", + "maximum-converters": "Converters maximum number", + "maximum-converters-required": "Converters maximum number is required.", + "maximum-converters-range": "Converters maximum number can't be negative", + "maximum-scheduler-events": "Scheduler events maximum number", + "maximum-scheduler-events-required": "Scheduler events maximum number is required.", + "maximum-scheduler-events-range": "Scheduler events maximum number can't be negative", + "maximum-resources-sum-data-size": "Resource files sum size", + "maximum-resources-sum-data-size-required": "Resource files sum size is required.", + "maximum-resources-sum-data-size-range": "Resource files sum size can`t be negative", + "maximum-ota-packages-sum-data-size": "OTA package files sum size", + "maximum-ota-package-sum-data-size-required": "OTA package files sum size is required.", + "maximum-ota-package-sum-data-size-range": "OTA package files sum size can`t be negative", + "rest-requests-for-tenant": "REST requests for tenant", + "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", + "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", + "transport-device-msg-rate-limit": "Transport device messages", + "transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages", + "transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points", + "tenant-entity-export-rate-limit": "Entity version creation", + "tenant-entity-import-rate-limit": "Entity version load", + "tenant-notification-request-rate-limit": "Notification requests", + "tenant-notification-requests-per-rule-rate-limit": "Notification requests per notification rule", + "integration-msgs-per-tenant-rate-limit": "Tenant integration messages", + "integration-msgs-per-device-rate-limit": "Device integration messages", + "max-transport-messages": "Transport messages maximum number", + "max-transport-messages-required": "Transport messages maximum number is required.", + "max-transport-messages-range": "Transport messages maximum number can't be negative", + "max-transport-data-points": "Transport data points maximum number ", + "max-transport-data-points-required": "Transport data points maximum number is required.", + "max-transport-data-points-range": "Transport data points maximum number can't be negative", + "max-r-e-executions": "Rule Engine executions maximum number", + "max-r-e-executions-required": "Rule Engine executions maximum number is required.", + "max-r-e-executions-range": "Rule Engine executions maximum number can't be negative", + "max-j-s-executions": "JavaScript executions maximum number ", + "max-j-s-executions-required": "JavaScript executions maximum number is required.", + "max-j-s-executions-range": "JavaScript executions maximum number can't be negative", + "max-d-p-storage-days": "Data points storage days maximum number", + "max-d-p-storage-days-required": "Data points storage days maximum number is required.", + "max-d-p-storage-days-range": "Data points storage days maximum number can't be negative", + "default-storage-ttl-days": "Storage TTL days by default", + "default-storage-ttl-days-required": "Storage TTL days by default is required.", + "default-storage-ttl-days-range": "Storage TTL days by default can't be negative", + "alarms-ttl-days": "Alarms TTL days", + "alarms-ttl-days-required": "Alarms TTL days required", + "alarms-ttl-days-days-range": "Alarms TTL days can't be negative", + "rpc-ttl-days": "RPC TTL days", + "rpc-ttl-days-required": "RPC TTL days required", + "rpc-ttl-days-days-range": "RPC TTL days can't be negative", + "max-rule-node-executions-per-message": "Rule node per message executions maximum number", + "max-rule-node-executions-per-message-required": "MRule node per message executions maximum number is required.", + "max-rule-node-executions-per-message-range": "Rule node per message executions maximum number can't be negative", + "max-emails": "Emails sent maximum number", + "max-emails-required": "Emails sent maximum number is required.", + "max-emails-range": "Emails sent maximum number can't be negative", + "sms-enabled": "SMS enabled", + "max-sms": "SMS sent maximum number", + "max-sms-required": "SMS sent maximum number is required.", + "max-sms-range": "SMS sent maximum number can't be negative", + "max-created-alarms": "Alarms created maximum number", + "max-created-alarms-required": "Alarms created maximum number is required.", + "max-created-alarms-range": "Alarms created maximum number be negative", + "no-queue": "No Queue configured", + "add-queue": "Add Queue", + "queues-with-count": "Queues ({{count}})", + "tenant-rest-limits": "REST requests for tenant", + "customer-rest-limits": "REST requests for customer", + "incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60", + "too-small-value-zero": "The value must be bigger than 0", + "too-small-value-one": "The value must be bigger than 1", + "queue-size-is-limited-by-system-configuration": "The size of the queue is also limited by the system configuration.", + "cassandra-tenant-limits-configuration": "Cassandra query for tenant", + "ws-limit-max-sessions-per-tenant": "Sessions per tenant maximum number", + "ws-limit-max-sessions-per-customer": "Sessions per customer maximum number", + "ws-limit-max-sessions-per-regular-user": "Sessions per regular user maximum number", + "ws-limit-max-sessions-per-public-user": "Sessions per public user maximum number", + "ws-limit-queue-per-session": "Message queue per session maximum size", + "ws-limit-max-subscriptions-per-tenant": "Subscriptions per tenant maximum number", + "ws-limit-max-subscriptions-per-customer": "Subscriptions per customer maximum number", + "ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number", + "ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number", + "ws-limit-updates-per-session": "WS updates per session", + "rate-limits": { + "add-limit": "Add limit", + "advanced-settings": "Advanced settings", + "edit-limit": "Edit limit", + "but-less-than": "but less than", + "edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits", + "edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits", + "edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits", + "edit-transport-device-msg-title": "Edit transport device messages rate limits", + "edit-transport-device-telemetry-msg-title": "Edit transport device telemetry messages rate limits", + "edit-transport-device-telemetry-data-points-title": "Edit transport device telemetry data points rate limits", + "edit-tenant-rest-limits-title": "Edit REST requests for tenant rate limits", + "edit-customer-rest-limits-title": "Edit REST requests for customer rate limits", + "edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits", + "edit-cassandra-tenant-limits-configuration-title": "Edit Cassandra query for tenant rate limits", + "edit-tenant-entity-export-rate-limit-title": "Edit entity version creation rate limits", + "edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits", + "edit-tenant-notification-request-rate-limit-title": "Edit notification requests rate limits", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Edit notification requests per notification rule rate limits", + "edit-integration-msgs-per-tenant-rate-limit-title": "Edit integration messages per tenant rate limits", + "edit-integration-msgs-per-device-rate-limit-title": "Edit integration messages per device rate limits", + "messages-per": "messages per", + "not-set": "Not set", + "number-of-messages": "Number of messages", + "number-of-messages-required": "Number of messages is required.", + "number-of-messages-min": "Minimum value is 1.", + "preview": "Preview", + "per-seconds": "Per seconds", + "per-seconds-required": "Time rate is required.", + "per-seconds-min": "Minimum value is 1.", + "rate-limits": "Rate limits", + "remove-limit": "Remove limit", + "transport-tenant-msg": "Transport tenant messages", + "transport-tenant-telemetry-msg": "Transport tenant telemetry messages", + "transport-tenant-telemetry-data-points": "Transport tenant telemetry data points", + "transport-device-msg": "Transport device messages", + "transport-device-telemetry-msg": "Transport device telemetry messages", + "transport-device-telemetry-data-points": "Transport device telemetry data points", + "sec": "sec" + } + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, =1 {1 sekundė} other {# sekundės} }", + "minutes-interval": "{ minutes, plural, =1 {1 minutė} other {# minutės} }", + "hours-interval": "{ hours, plural, =1 {1 valanda} other {# valandos} }", + "days-interval": "{ days, plural, =1 {1 diena} other {# dienos} }", + "days": "Dienos", + "hours": "Valandos", + "minutes": "Minutės", + "seconds": "Sekundės", + "advanced": "Patyręs vartotojas", + "predefined": { + "yesterday": "Yesterday", + "day-before-yesterday": "Day before yesterday", + "this-day-last-week": "This day last week", + "previous-week": "Previous week (Sun - Sat)", + "previous-week-iso": "Previous week (Mon - Sun)", + "previous-month": "Previous month", + "previous-quarter": "Previous quarter", + "previous-half-year": "Previous half year", + "previous-year": "Previous year", + "current-hour": "Current hour", + "current-day": "Current day", + "current-day-so-far": "Current day so far", + "current-week": "Current week (Sun - Sat)", + "current-week-iso": "Current week (Mon - Sun)", + "current-week-so-far": "Current week so far (Sun - Sat)", + "current-week-iso-so-far": "Current week so far (Mon - Sun)", + "current-month": "Current month", + "current-month-so-far": "Current month so far", + "current-quarter": "Current quarter", + "current-quarter-so-far": "Current quarter so far", + "current-half-year": "Current half year", + "current-half-year-so-far": "Current half year so far", + "current-year": "Current year", + "current-year-so-far": "Current year so far" + } + }, + "timeunit": { + "milliseconds": "Milisekundės", + "seconds": "Sekundės", + "minutes": "Minutės", + "hours": "Valandos", + "days": "Dienos" + }, + "timewindow": { + "timewindow": "Laikotarpio vaizdas", + "years": "{ years, plural, =1 { metai } other {# metai } }", + "years-short": "{{ years }}m", + "months": "{ months, plural, =1 { mėnuo } other {# mėnesiai } }", + "months-short": "{{ months }}mėn", + "weeks": "{ weeks, plural, =1 { savaitė } other {# savaitės } }", + "weeks-short": "{{ weeks }}sav", + "days": "{ days, plural, =1 { diena } other {# dienos } }", + "days-short": "{{ days }}d", + "hours": "{ hours, plural, =0 { valandų } =1 {1 valanda } other {# valandos } }", + "hr": "{{ hr }} val", + "hr-short": "{{ hr }}val", + "minutes": "{ minutes, plural, =0 { minučių } =1 {1 minutė } other {# minutės } }", + "min": "{{ min }} min", + "min-short": "{{ min }}min", + "seconds": "{ seconds, plural, =0 { sekundžių } =1 {1 sekundė } other {# sekundės } }", + "sec": "{{ sec }} sek", + "sec-short": "{{ sec }}s", + "short": { + "days": "{ days, plural, =1 {1 diena } other {# dienos } }", + "hours": "{ hours, plural, =1 {1 valanda } other {# valandos } }", + "minutes": "{{minutes}} min ", + "seconds": "{{seconds}} sek " + }, + "realtime": "Realiu laiku", + "history": "istorija", + "last-prefix": "Paskutinės", + "period": "nuo {{ startTime }} iki {{ endTime }}", + "edit": "Redaguoti laikotarpio vaizdą", + "date-range": "Datų intervalas", + "for-all-time": "Viskas", + "last": "Paskutinės", + "time-period": "Laikotarpis", + "hide": "Slėpti", + "interval": "Interval", + "just-now": "Just now", + "just-now-lower": "just now", + "ago": "ago", + "style": "Timewindow style", + "icon": "Icon", + "icon-position": "Icon position", + "icon-position-left": "Left", + "icon-position-right": "Right", + "font": "Font", + "color": "Color", + "displayTypePrefix": "Display Realtime/History prefix", + "preview": "Preview" + }, + "unit": { + "millimeter": "Millimeter", + "centimeter": "Centimeter", + "angstrom": "Angstrom", + "nanometer": "Nanometer", + "micrometer": "Micrometer", + "meter": "Meter", + "kilometer": "Kilometer", + "inch": "Inch", + "foot": "Foot", + "yard": "Yard", + "mile": "Mile", + "nautical-mile": "Nautical Mile", + "astronomical-unit": "Astronomical Unit", + "reciprocal-metre": "Reciprocal Metre", + "meter-per-meter": "Meter per meter", + "steradian": "Steradian", + "thou": "Thou", + "barleycorn": "Barleycorn", + "hand": "Hand", + "chain": "Chain", + "furlong": "Furlong", + "league": "League", + "fathom": "Fathom", + "cable": "Cable", + "link": "Link", + "rod": "Rod", + "nanogram": "Nanogram", + "microgram": "Microgram", + "milligram": "Milligram", + "gram": "Gram", + "kilogram": "Kilogram", + "tonne": "Tonne", + "ounce": "Ounce", + "pound": "Pound", + "stone": "Stone", + "hundredweight-count": "Hundredweight count", + "short-tons": "Short tons", + "dalton": "Dalton", + "grain": "Grain", + "drachm": "Drachm", + "quarter": "Quarter", + "slug": "Slug", + "carat": "Carat", + "cubic-millimeter": "Cubic Millimeter", + "cubic-centimeter": "Cubic Centimeter", + "cubic-meter": "Cubic Meter/s", + "cubic-kilometer": "Cubic Kilometers", + "microliter": "Microliter", + "milliliter": "Milliliter", + "liter": "Liter", + "hectoliter": "Hectolitre", + "cubic-inch": "Cubic Inch", + "cubic-foot": "Cubic Foot", + "cubic-yard": "Cubic Yards", + "fluid-ounce": "Fluid Ounce", + "pint": "Pint", + "quart": "Quart", + "gallon": "Gallon", + "oil-barrels": "Oil Barrels", + "cubic-meter-per-kilogram": "Cubic Meter per Kilogram", + "gill": "Gill", + "hogshead": "Hogshead", + "teaspoon": "Teaspoon", + "tablespoon": "Tablespoon", + "cup": "Cup", + "celsius": "Celsius", + "kelvin": "Kelvin", + "rankine": "Rankine", + "fahrenheit": "Fahrenheit", + "percent": "Percent", + "meter-per-second": "Meter per Second", + "kilometer-per-hour": "Kilometer per Hour", + "foot-per-second": "Foot per Second", + "mile-per-hour": "Mile per Hour", + "knot": "Knot", + "millimeters-per-minute": "Millimeters per minute", + "kilometer-per-hour-squared": "Kilometer per hour squared", + "foot-per-second-squared": "Foot per second squared", + "pascal": "Pascal", + "kilopascal": "Kilopascal", + "megapascal": "Megapascal", + "gigapascal": "Gigapascal", + "millibar": "Millibar", + "bar": "Bar", + "kilobar": "Kilobar", + "newton": "Newton", + "newton-meter": "Newton meter", + "foot-pounds": "Foot-pounds", + "inch-pounds": "Inch-pounds", + "newton-per-meter": "Newton per meter", + "atmospheres": "Atmospheres", + "pounds-per-square-inch": "Pounds per Square Inch", + "torr": "Torr", + "inches-of-mercury": "Inches of Mercury", + "pascal-per-square-meter": "Pascal per Square Meter", + "pound-per-square-inch": "Pound per Square Inch", + "newton-per-square-meter": "Newton per Square Meter", + "kilogram-force-per-square-meter": "Kilogram-force per Square Meter", + "pascal-per-square-centimeter": "Pascal per Square Centimeter", + "ton-force-per-square-inch": "Ton-force per Square Inch", + "kilonewton-per-square-meter": "Kilonewton per Square Meter", + "newton-per-square-millimeter": "Newton per Square Millimeter", + "microjoule": "Microjoule", + "millijoule": "Millijoule", + "joule": "Joule", + "kilojoule": "Kilojoule", + "megajoule": "Megajoule", + "gigajoule": "Gigajoule", + "watt-hour": "Watt-hour", + "kilowatt-hour": "Kilowatt-hour", + "electron-volts": "Electron volts", + "joules-per-coulomb": "Joules per Coulomb", + "british-thermal-unit": "British Thermal Units", + "foot-pound": "Foot-pound", + "calorie": "Calorie", + "small-calorie": "Small Calorie", + "kilocalorie": "Kilocalorie", + "joule-per-kelvin": "Joule per Kelvin", + "joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin", + "joule-per-kilogram": "Joule per Kilogram", + "watt-per-meter-kelvin": "Watt per Meter-Kelvin", + "joule-per-cubic-meter": "Joule per Cubic Meter", + "therm": "Therm", + "electric-dipole-moment": "Electric Dipole Moment", + "magnetic-dipole-moment": "Magnetic Dipole Moment", + "debye": "Debye", + "coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt", + "milliwatt": "Milliwatt", + "microwatt": "Microwatt", + "watt": "Watt", + "kilowatt": "Kilowatt", + "megawatt": "Megawatt", + "gigawatt": "Gigawatt", + "metric-horsepower": "Metric Horsepower", + "milliwatt-per-square-centimeter": "Milliwatts per square centimeter", + "watt-per-square-centimeter": "Watts per square centimeter", + "kilowatt-per-square-centimeter": "Kilowatts per square centimeter", + "milliwatt-per-square-meter": "Milliwatts per square meter", + "watt-per-square-meter": "Watts per square meter", + "kilowatt-per-square-meter": "Kilowatts per square meter", + "watt-per-square-inch": "Watts per square inch", + "kilowatt-per-square-inch": "Kilowatts per square inch", + "horsepower": "Horsepower", + "btu-per-hour": "British thermal units/hour", + "coulomb": "Coulomb", + "millicoulomb": "Millicoulombs", + "microcoulomb": "Microcoulomb", + "picocoulomb": "Picocoulomb", + "coulomb-per-meter": "Coulomb per meter", + "coulomb-per-cubic-meter": "Coulomb per Cubic Meter", + "coulomb-per-square-meter": "Coulomb per Square Meter", + "square-millimeter": "Square Millimeter", + "square-centimeter": "Square Centimeter", + "square-meter": "Square Meter", + "hectare": "Hectare", + "square-kilometer": "Square Kilometer", + "square-inch": "Square Inch", + "square-foot": "Square Foot", + "square-yard": "Square Yard", + "acre": "Acre", + "square-mile": "Square Mile", + "are": "Are", + "barn": "Barn", + "circular-inch": "Circular Inch", + "milliampere-hour": "Milliampere-hour", + "milliampere-hour-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, milliampere-hour, milliampere-hours, mAh", + "ampere-hours": "Ampere-hours", + "ampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, ampere, ampere-hours, Ah", + "kiloampere-hours": "Kiloampere-hours", + "kiloampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, kiloampere-hours, kiloampere-hour, kAh", + "nanoampere": "Nanoampere", + "nanoampere-tags": "current, amperes, nanoampere, nA", + "picoampere": "Picoampere", + "picoampere-tags": "current, amperes, picoampere, pA", + "microampere": "Microampere", + "microampere-tags": "electric current, microampere, microamperes, μA", + "milliampere": "Milliampere", + "milliampere-tags": "electric current, milliampere, milliamperes, mA", + "ampere": "Ampere", + "ampere-tags": "electric current, current flow, flow of electricity, electrical flow, ampere, amperes, amperage, A", + "kiloamperes": "Kiloamperes", + "kiloamperes-tags": "electric current, current flow, kiloamperes, kA", + "microampere-per-square-centimeter": "Microampere per square centimeter", + "microampere-per-square-centimeter-tags": "Current density, microampere per square centimeter, µA/cm²", + "ampere-per-square-meter": "Ampere per Square Meter", + "ampere-per-square-meter-tags": "current density, current per unit area, ampere per square meter, A/m²", + "ampere-per-meter": "Ampere per Meter", + "ampere-per-meter-tags": "magnetic field strength, magnetic field intensity, ampere per meter, A/m", + "oersted": "Oersted", + "oersted-tags": "magnetic field, oersted, Oe", + "bohr-magneton": "Bohr Magneton", + "bohr-magneton-tags": "atomic physics, magnetic moment, bohr magneton, μB", + "ampere-meter-squared": "Ampere-Meter Squared", + "ampere-meter-squared-tags": "magnetic moment, dipole moment, ampere-meter squared, A·m²", + "ampere-meter": "Ampere-Meter", + "ampere-meter-tags": "magnetic field, current loop, ampere-meter, A·m", + "nanovolt": "Nanovolt", + "picovolt": "Picovolt", + "millivolts": "Millivolts", + "microvolts": "Microvolts", + "volt": "Volt", + "kilovolts": "Kilovolts", + "dbmV": "dBmV", + "volt-meter": "Volt-Meter", + "kilovolt-meter": "Kilovolt-Meter", + "megavolt-meter": "Megavolt-Meter", + "microvolt-meter": "Microvolt-Meter", + "millivolt-meter": "Millivolt-Meter", + "nanovolt-meter": "Nanovolt-Meter", + "ohm": "Ohm", + "microohm": "Microohm", + "milliohm": "Milliohm", + "kilohm": "Kilohm", + "megohm": "Megohm", + "gigohm": "Gigohm", + "hertz": "Hertz", + "kilohertz": "Kilohertz", + "megahertz": "Megahertz", + "gigahertz": "Gigahertz", + "rpm": "Revolutions Per Minute", + "candela-per-square-meter": "Candela per square meter", + "candela": "Candela", + "lumen": "Lumen", + "lux": "Lux", + "foot-candle": "Foot-candle", + "lumen-per-square-meter": "Lumen per square meter", + "lux-second": "Lux second", + "lumen-second": "Lumen second", + "lumens-per-watt": "Lumens per watt", + "absorbance": "Absorbance", + "mole": "Mole", + "nanomole": "Nanomole", + "micromole": "MicroMole", + "millimole": "Millimole", + "kilomole": "Kilomole", + "mole-per-cubic-meter": "Mole per Cubic Meter", + "rssi": "RSSI", + "ppm": "Parts Per Million", + "ppb": "Parts Per Billion", + "micrograms-per-cubic-meter": "Micrograms per Cubic Meter", + "aqi": "AQI", + "gram-per-cubic-meter": "Gram per cubic meter", + "gram-per-kilogram": "Specific Humidity", + "millimeters-per-second": "Millimeters per second", + "neper": "Neper", + "bel": "Bel", + "decibel": "Decibel", + "meters-per-second-squared": "Meters per second squared", + "becquerel": "Becquerel", + "curie": "Curie", + "gray": "Gray", + "sievert": "Sievert", + "roentgen": "Roentgen", + "cps": "Counts per Second", + "rad": "Rad", + "rem": "Rem", + "dps": "Disintegrations per second", + "rutherford": "Rutherford", + "coulombs-per-kilogram": "Coulombs per kilogram", + "becquerels-per-cubic-meter": "Becquerels per cubic meter", + "curies-per-liter": "Curies per liter", + "becquerels-per-second": "Becquerels per second", + "curies-per-second": "Curies per second", + "gy-per-second": "Gray per Second", + "watt-per-steradian": "Watt per Steradian", + "watt-per-square-metre-steradian": "Watt per Square Metre-Steradian", + "ph-level": "pH Level", + "turbidity": "Turbidity", + "mg-per-liter": "Milligrams per liter", + "microsiemens-per-centimeter": "Microsiemens per centimeter", + "millisiemens-per-meter": "Millisiemens per meter", + "siemens-per-meter": "Siemens per meter", + "kilogram-per-cubic-meter": "Kilogram per cubic meter", + "gram-per-cubic-centimeter": "Gram per cubic centimeter", + "kilogram-per-square-meter": "Kilogram per square metre", + "milligram-per-milliliter": "Milligram per milliliter", + "pound-per-cubic-foot": "Pound per cubic foot", + "ounces-per-cubic-inch": "Ounces per cubic inch", + "tons-per-cubic-yard": "Tons per cubic yard", + "particle-density": "Particle density", + "kilometers-per-liter": "Kilometers per liter", + "miles-per-gallon": "Miles per gallon", + "liters-per-100-km": "Liters per 100 km", + "gallons-per-mile": "Gallons per mile", + "liters-per-hour": "Liters per hour", + "gallons-per-hour": "Gallons per hour", + "beats-per-minute": "Beats per minute", + "millimeters-of-mercury": "Millimeters of mercury", + "milligrams-per-deciliter": "Milligrams per deciliter", + "g-force": "G-force", + "kilonewton": "Kilonewton", + "kilogram-force": "Kilogram-Force", + "pound-force": "Pound-Force", + "kilopound-force": "Kilopound-Force", + "dyne": "Dyne", + "poundal": "Poundal", + "kip": "Kip", + "gal": "Gal", + "gravity": "Gravity", + "hectopascal": "Hectopascal", + "atmosphere": "Atmosphere", + "millibars": "Millibars", + "inch-of-mercury": "One inch of mercury", + "richter-scale": "Richter Scale", + "second": "Second", + "minute": "Minute", + "hour": "Hour", + "day": "Day", + "week": "Week", + "month": "Month", + "year": "Year", + "cubic-foot-per-minute": "Cubic Foot Per Minute", + "cubic-meters-per-hour": "Cubic Meters Per Hour", + "cubic-meters-per-second": "Cubic Meters Per Second", + "liter-per-second": "Liter Per Second", + "liter-per-minute": "Liter Per Minute", + "gallons-per-minute": "Gallons Per Minute", + "cubic-foot-per-second": "Cubic foot per second", + "milliliters-per-minute": "Milliliters per minute", + "bit": "Bit", + "byte": "Byte", + "kilobyte": "Kilobyte", + "megabyte": "Megabyte", + "gigabyte": "Gigabyte", + "terabyte": "Terabyte", + "petabyte": "Petabyte", + "exabyte": "Exabyte", + "zettabyte": "Zettabyte", + "yottabyte": "Yottabyte", + "bit-per-second": "Bit per second", + "kilobit-per-second": "Kilobit per second", + "megabit-per-second": "Megabit per second", + "gigabit-per-second": "Gigabit per second", + "terabit-per-second": "Terabit per second", + "byte-per-second": "Byte per second", + "kilobyte-per-second": "Kilobyte per second", + "megabyte-per-second": "Megabyte per second", + "gigabyte-per-second": "Gigabyte per second", + "degree": "Degree", + "radian": "Radian", + "gradian": "Gradian", + "mil": "Mil", + "revolution": "Revolution", + "siemens": "Siemens", + "millisiemens": "Millisiemens", + "microsiemens": "Microsiemens", + "kilosiemens": "Kilosiemens", + "megasiemens": "Megasiemens", + "gigasiemens": "Gigasiemens", + "farad": "Farad", + "millifarad": "Millifarad", + "microfarad": "Microfarad", + "nanofarad": "Nanofarad", + "picofarad": "Picofarad", + "kilofarad": "Kilofarad", + "megafarad": "Megafarad", + "gigafarad": "Gigafarad", + "terfarad": "Terfarad", + "farad-per-meter": "Farad per Meter", + "tesla": "Tesla", + "gauss": "Gauss", + "kilogauss": "Kilogauss", + "millitesla": "Millitesla", + "microtesla": "Microtesla", + "nanotesla": "Nanotesla", + "kilotesla": "Kilotesla", + "megatesla": "Megatesla", + "millitesla-square-meters": "millitesla square meters", + "gamma": "Gamma", + "lambda": "Lambda", + "square-meter-per-second": "Square meter per second", + "square-centimeter-per-second": "Square centimeter per second", + "stoke": "Stoke", + "centistokes": "Centistokes", + "square-foot-per-second": "Square foot per second", + "square-inch-per-second": "Square inch per second", + "pascal-second": "Pascal-second", + "centipoise": "Centipoise", + "poise": "Poise", + "reynolds": "Reynolds", + "pound-per-foot-hour": "Pound per foot-hour", + "newton-second-per-square-meter": "Newton second per square meter", + "dyne-second-per-square-centimeter": "Dyne second per square centimeter", + "kilogram-per-meter-second": "Kilogram per meter-second", + "tesla-square-meters": "Tesla square meters", + "maxwell": "Maxwell", + "tesla-per-meter": "Tesla per Meter", + "gauss-per-centimeter": "Gauss per Centimeter", + "weber": "Weber", + "microweber": "Microweber", + "milliweber": "Milliweber", + "gauss-square-centimeter": "Gauss-Square Centimeter", + "kilogauss-square-centimeter": "Kilogauss-Square Centimeter", + "henry": "Henry", + "millihenry": "Millihenry", + "microhenry": "Microhenry", + "nanohenry": "Nanohenry", + "henry-per-meter": "Henry per Meter", + "tesla-meter-per-ampere": "Tesla Meter per Ampere", + "gauss-per-oersted": "Gauss per Oersted", + "kilogram-per-mole": "Kilogram per mole", + "gram-per-mole": "Gram per mole", + "milligram-per-mole": "Milligram per mole", + "joule-per-mole": "Joule per Mole", + "joule-per-mole-kelvin": "Joule per Mole-Kelvin", + "millivolts-per-meter": "Millivolts per meter", + "volts-per-meter": "Volts per meter", + "kilovolts-per-meter": "Kilovolts per meter", + "radian-per-second": "Radian per second", + "radian-per-second-squared": "Radian per second squared", + "revolutions-per-minute-per-second": "Angular acceleration", + "revolutions-per-minute-per-second-squared": "Angular Acceleration", + "deg-per-second": "deg/s", + "degrees-brix": "Degrees Brix", + "katal": "Katal", + "katal-per-cubic-metre": "Katal per Cubic Metre" + }, + "user": { + "all": "Visi", + "all-users": "Visi vartotojai", + "groups": "Grupės", + "user": "Vartotojas", + "users": "Vartotojai", + "management": "Vartotojų valdymas", + "customer-users": "Kliento vartotojai", + "tenant-admins": "Valdytojų administratoriai", + "sys-admin": "Sistemos administratoriai", + "tenant-admin": "Valdytojo administratoriai", + "customer": "Klientas", + "anonymous": "Anonimas", + "add": "Pridėti vartotoją", + "delete": "Panaikinti vartotoją", + "add-user-text": "Pridėti naują vartotoją", + "no-users-text": "Vartotojų nėra", + "user-details": "Informacija apie vartotoją", + "delete-users": "Panaikinti vartotojus", + "delete-user-title": "Ar tikrai norite panaikinti vartotoją '{{userEmail}}'?", + "delete-user-text": "Būkite dėmesingi, po patvirtinimo vartotojas ir visa su juo susijusi informacija bus panaikinta.", + "delete-users-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 vatotoją} other {# vartotojus} }?", + "delete-users-action-title": "Panaikinti { count, plural, =1 {1 vartotoją} other {# vartotojus} }", + "delete-users-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti vartotojai ir su jis susijusi informacija bus panaikinta.", + "activation-email-sent-message": "Aktyvinimo el. laiškas sėkmingai išsiųstas!", + "resend-activation": "Iš naujo siųsti aktyvinimą", + "email": "El. pašto adresas", + "email-required": "El. pašto adresas būtinas.", + "invalid-email-format": "Neteisingas el. pašto adreso formatas.", + "first-name": "Vardas", + "last-name": "Pavardė", + "description": "Aprašymas", + "default-dashboard": "Pagrindinis skydelis", + "always-fullscreen": "Visada per visą ekraną", + "select-user": "Pasirinkti vartotoją", + "no-users-matching": "Vartotojų, atitinkančių'{{entity}}' nėra.", + "user-required": "Vartotojas būtinas", + "activation-method": "Aktyvavimo būdas", + "display-activation-link": "Rodyti aktyvavimo nuorodą", + "send-activation-mail": "Siųsti aktyvavimo el. laišką", + "activation-link": "Vartotojo aktyvavimo nuoroda", + "activation-link-text": "Vartotojui aktyvuoti, paspauskite nuorodą :", + "copy-activation-link": "Kopijuoti aktyvavimo nuorodą", + "activation-link-copied-message": "Vartotojo aktyvavimo nuoroda nukopijuota į iškarpinę", + "details": "Informacija", + "login-as-tenant-admin": "Prisijunkti kaip valdytojo administratorius", + "login-as-customer-user": "Prisijunkti kaip kliento vartotojas", + "select-group-to-add": "Pasirinkite grupę, kuriai priskirti pažymėtus vartotojus", + "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pažymėtus vartotojus", + "remove-users-from-group": "Ar tikrai norite pašalinti { count, plural, =1 {1 vartotoją} other {# vartotojus} } iš grupės '{{entityGroup}}'?", + "group": "Vartotojų grupė", + "list-of-groups": "{ count, plural, =1 {Viena vartotojų grupė} other {# Vartotojų grupių sąrašas} }", + "group-name-starts-with": "Vartotojų grupės, kurios prasideda '{{prefix}}'", + "search": "Vartotojų paieška", + "selected-users": "Pasirinkta { count, plural, =1 {1 vartotojas} other {# vartotojai} }", + "disable-account": "Išjungti vartotojo paskyrą", + "enable-account": "Įjungti vartotojo paskyrą", + "enable-account-message": "Vartotojo paskyra sėkmingai įjungta!", + "disable-account-message": "Vartotojo paskyra sėkmingai išjungta!", + "copyId": "Kopijuoti vartotojo Id", + "idCopiedMessage": "Vartotojo Id nukopiuotas į iškarpinę", + "user-list": "Vartotojų sąrašas", + "user-list-required": "Vartotojų sąrašas būtinas" + }, + "value": { + "type": "Reikšmės tipas", + "string": "Tekstas", + "string-value": "Tekstinė informacija", + "string-value-required": "Tekstinė informacija būtina", + "integer": "Sveikasis skaičius", + "integer-value": "Sveikojo skaičiaus reikšmė", + "integer-value-required": "Sveikojo skaičiaus reikšmė būtina", + "invalid-integer-value": "Sveikojo skaičiaus reikšmė neteisinga", + "double": "Realusis skaičius", + "double-value": "Realiojo skaičiaus reikšmė", + "double-value-required": "Realiojo skaičiaus reikšmė būtina", + "boolean": "Loginis", + "boolean-value": "Loginė reikšmė", + "false": "Netiesa", + "true": "Tiesa", + "long": "Sveikas skaičius", + "json": "JSON", + "json-value": "JSON reikšmė", + "json-value-invalid": "Neteisingas JSON reikšmės formatas", + "json-value-required": "JSON reikšmė būtina." + }, + "version-control": { + "version-control": "Version control", + "management": "Version control management", + "search": "Search versions", + "branch": "Branch", + "default": "Default", + "select-branch": "Select branch", + "branch-required": "Branch is required", + "create-entity-version": "Create entity version", + "version-name": "Version name", + "version-name-required": "Version name is required", + "author": "Author", + "export-relations": "Export relations", + "export-attributes": "Export attributes", + "export-credentials": "Export credentials", + "export-group-entities": "Export group entities", + "export-roles": "Export roles", + "entity-versions": "Entity versions", + "versions": "Versions", + "created-time": "Created time", + "version-id": "Version ID", + "no-entity-versions-text": "No entity versions found", + "no-versions-text": "No versions found", + "copy-full-version-id": "Copy full version id", + "create-version": "Create version", + "creating-version": "Creating version... Please wait", + "nothing-to-commit": "No changes to commit", + "restore-version": "Restore version", + "restore-entity-from-version": "Restore entity from version '{{versionName}}'", + "restoring-entity-version": "Restoring entity version... Please wait", + "load-relations": "Load relations", + "load-attributes": "Load attributes", + "load-credentials": "Load credentials", + "load-group-entities": "Load group entities", + "load-roles": "Load roles", + "compare-with-current": "Compare with current", + "diff-entity-with-version": "Diff with entity version '{{versionName}}'", + "previous-difference": "Previous Difference", + "next-difference": "Next Difference", + "current": "Current", + "differences": "{ count, plural, =1 {1 difference} other {# differences} }", + "create-entities-version": "Create entities version", + "default-sync-strategy": "Default sync strategy", + "sync-strategy-merge": "Merge", + "sync-strategy-overwrite": "Overwrite", + "entities-to-export": "Entities to export", + "entities-to-restore": "Entities to restore", + "sync-strategy": "Sync strategy", + "all-entities": "All entities", + "no-entities-to-export-prompt": "Please specify entities to export", + "no-entities-to-restore-prompt": "Please specify entities to restore", + "add-entity-type": "Add entity type", + "remove-all": "Remove all", + "version-create-result": "{ added, plural, =0 {No entities} =1 {1 entity} other {# entities} } added.
{ modified, plural, =0 {No entities} =1 {1 entity} other {# entities} } modified.
{ removed, plural, =0 {No entities} =1 {1 entity} other {# entities} } removed.", + "remove-other-entities": "Remove other entities", + "find-existing-entity-by-name": "Find existing entity by name", + "restore-entities-from-version": "Restore entities from version '{{versionName}}'", + "restoring-entities-from-version": "Restoring entities... Please wait", + "no-entities-restored": "No entities restored", + "created": "{{created}} created", + "updated": "{{updated}} updated", + "deleted": "{{deleted}} deleted", + "groups-created": "{ created, plural, =1 {1 group} other {# groups} } created", + "groups-updated": "{ updated, plural, =1 {1 group} other {# groups} } updated", + "groups-deleted": "{ deleted, plural, =1 {1 group} other {# groups} } deleted", + "remove-other-entities-confirm-text": "Be careful! This will permanently delete all current entities
not present in the version you want to restore.

Please type \"remove other entities\" to confirm.", + "auto-commit-to-branch": "auto-commit to {{ branch }} branch", + "default-create-entity-version-name": "{{entityName}} update", + "sync-strategy-merge-hint": "Creates or updates selected entities in the repository. All other repository entities are not modified.", + "sync-strategy-overwrite-hint": "Creates or updates selected entities in the repository. All other repository entities are deleted.", + "device-credentials-conflict": "Failed to load the device with external id {{entityId}}
due to the same credentials are already present in the database for another device.
Please consider disabling the load credentials setting in the restore form.", + "missing-referenced-entity": "Failed to load the {{sourceEntityTypeName}} with external id {{sourceEntityId}}
because it references missing {{targetEntityTypeName}} with id {{targetEntityId}}.", + "add-entity-groups": "Add entity groups", + "entity-groups": "Entity groups", + "integration-routing-key-conflict": "Failed to load the integration with external id {{entityId}}
due to the same integration key is already present in the database for another integration.
Please consider enabling the auto-generate integration key setting in the restore form.", + "auto-generate-integration-key": "Auto-generate integration key", + "runtime-failed": "Failed: {{message}}", + "auto-commit-settings-read-only-hint": "Auto-commit feature doesn't work with enabled read-only option in Repository settings." + }, + "widget": { + "widget-library": "Valdiklių galerija", + "widget-bundle": "Valdiklių rinkinys", + "all-bundles": "Visi rinkiniai", + "select-widgets-bundle": "Pasirinkti valdiklių rinkinį", + "widgets": "Valdikliai", + "all-widgets": "Visi valdikliai", + "widget": "Valdiklis", + "select-widget": "Pasirinkti valdiklį", + "no-widgets-matching": "Valdiklių atitinkančių '{{entity}}' nėra.", + "no-widgets": "Valdiklių dar nėra", + "no-widgets-text": "Valdiklių nėra", + "management": "Valdiklių valdymas", + "editor": "Valdiklių redaktorius", + "widget-type-not-found": "Įkeliant valdiklio konfigūraciją kilo klaidų.
Gali būti, kad\n toks valdiklio tipas panaikintas.", + "widget-type-load-error": "Valdiklis neįkeltas, nes iškilo klaidų:", + "remove": "Panaikinti valdiklį", + "delete": "Panaikinti valdiklį", + "edit": "Redaguoti valdiklį", + "edit-widget-type": "Redaguoti valdikliį", + "remove-widget-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTitle}}'?", + "remove-widget-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", + "timeseries": "Telemetrija", + "search-data": "Duomenų paieška", + "no-data-found": "Duomenų nėra", + "latest": "Naujausios reikšmės", + "rpc": "Valdymo valdiklis", + "alarm": "Įspėjimų valdiklis", + "static": "Statinis valdiklis", + "select-widget-type": "Pasirinkite valdiklio tipą", + "missing-widget-title-error": "Valdiklio pavadinimas būtinas!", + "widget-saved": "Valdiklis išsaugotas", + "unable-to-save-widget-error": "Valdiklio išsaugoti negalima! Valdiklyje yra klaidų!", + "save": "Valdiklis išsaugotas", + "saveAs": "valdiklį išsaugoti kaip", + "move": "Perkelti valdiklį", + "save-widget-type-as": "Valdiklį išsaugoti kaip", + "save-widget-type-as-text": "Įrašykite naują valdiklio pavadinimą", + "toggle-fullscreen": "Perjungti į viso ekrano režimą", + "run": "Paleisti valdiklį", + "widget-title": "Pavadinimas", + "title": "Pavadinimas", + "title-required": "Valdiklio pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "system": "Sisteminis", + "type": "Tipas", + "resources": "Resursai", + "resource-url": "JavaScript/CSS URL", + "resource-is-module": "Is module", + "remove-resource": "Panaikinti resursą", + "add-resource": "Pridėti resursą", + "html": "HTML", + "tidy": "Tidy", + "css": "CSS", + "settings-schema": "Nustatymų schema", + "datakey-settings-schema": "Duomenų raktų nustatymų schema", + "latest-datakey-settings-schema": "Latest data key settings schema", + "widget-settings": "Valdiklio nustatymai", + "description": "Aprašymas", + "image-preview": "Paveiksliuko peržiūra", + "settings-form-selector": "Settings form selector", + "data-key-settings-form-selector": "Data key settings form selector", + "latest-data-key-settings-form-selector": "Latest data key settings form selector", + "all": "All", + "actual": "Actual", + "deprecated": "Pasenęs", + "has-basic-mode": "Has basic mode", + "basic-mode-form-selector": "Basic mode form selector", + "basic-mode": "Basic", + "advanced-mode": "Advanced", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "Ar tikrai norite pašalinti valdiklį '{{widgetName}}'?", + "remove-widget-type-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", + "remove-widget-type": "Panaikinti valdiklį", + "widget-types": "Valdikliai", + "delete-widget-type-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTypeName}}'?", + "delete-widget-type-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", + "delete-widget-types-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklį} other {# valdiklius} }?", + "delete-widget-types-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti valdikliai ir su jais susijusi informacija bus pašalinta.", + "delete-widget-type": "Panaikinti valdiklį", + "add-widget-type": "Pridėti naują valdiklį", + "widget-type-load-failed-error": "Valdiklio įkelti nepavyko!", + "widget-template-load-failed-error": "Valdiklio šablono įkelti nepavyko!", + "details": "Informacija", + "widget-type-details": "Informacija apie valdiklį", + "add": "Pridėti valdiklį", + "no-widget-types-text": "Valdiklių nėra", + "search-widget-types": "Valdiklių paieška", + "selected-widget-types": "Pasirinkta { count, plural, =1 {1 valdiklis} other {# valdikliai} }", + "undo": "Atšaukti valdiklio pakeitimus", + "export": "Eksportuoti valdiklį", + "export-data": "Eksportuoti valdiklio duomenis", + "export-to-csv": "Eksportuoti valdiklio duomenis į CSV...", + "export-to-excel": "Eksportuoti valdiklio duomenis į XLS...", + "export-to-excel-xlsx": "Eksportuoti valdiklio duomenis į XLSX...", + "no-data": "Duomenų atvaizdavimui nėra", + "data-overflow": "Valdiklyje rodoma {{count}} iš {{total}} įrašų", + "alarm-data-overflow": "Valdiklyje rodomi {{allowedEntities}} (didižiausias leistinas skaičius) įspėjimų įrašai iš {{totalEntities}}", + "search": "Valdiklių paieška", + "filter": "Valdiklių filtro tipas", + "loading-widgets": "Valdikliai įkeliami...", + "widget-template-error": "Netinkamas valdiklio HTML šablonas." + }, + "widget-action": { + "header-button": "Valdiklio antraštės mygtukas", + "open-dashboard-state": "Eiti į naują skydelio būseną", + "update-dashboard-state": "Atnaujinti dabartinę skydelio būseną", + "open-dashboard": "Eiti į kitą skydelio būseną", + "custom": "Aprašomas veiksmas", + "custom-pretty": "Aprašomas veiksmas (su HTML šablonu)", + "custom-pretty-error-title": "Custom dialog error", + "custom-pretty-template-error": "Invalid custom dialog template.", + "custom-pretty-controller-error": "Error occurred while evaluating custom dialog function.", + "mobile-action": "Mobile action", + "target-dashboard-state": "Tikslinė skydelio būsena", + "target-dashboard-state-required": "Tikslnė skydelio būsena būtina", + "set-entity-from-widget": "Nustatyti subjektą iš valdiklio", + "target-dashboard": "Tikslinis skydelis", + "open-right-layout": "Atidarykiti tinkamą skydelio išdėstymą (vaizdas mobiliesiems)", + "state-display-type": "Skydelio būsenos rodymo parinktis", + "open-normal": "Normal", + "open-in-separate-dialog": "Open in separate dialog", + "open-in-popover": "Open in popover", + "dialog-title": "Dialog title", + "dialog-hide-dashboard-toolbar": "Hide dashboard toolbar in dialog", + "dialog-width": "Dialog width in percents relative to viewport width", + "dialog-height": "Dialog height in percents relative to viewport height", + "dialog-size-range-error": "Dialog size percent value should be in a range from 1 to 100.", + "popover-preferred-placement": "Preferred popover placement", + "popover-placement-top": "Top", + "popover-placement-topLeft": "Top left", + "popover-placement-topRight": "Top right", + "popover-placement-right": "Right", + "popover-placement-rightTop": "Right top", + "popover-placement-rightBottom": "Right bottom", + "popover-placement-bottom": "Bottom", + "popover-placement-bottomLeft": "Bottom left", + "popover-placement-bottomRight": "Bottom right", + "popover-placement-left": "Left", + "popover-placement-leftTop": "Left top", + "popover-placement-leftBottom": "Left bottom", + "popover-hide-on-click-outside": "Hide popover on outside click", + "popover-hide-dashboard-toolbar": "Hide dashboard toolbar in popover", + "popover-width": "Popover width in browser units (ex. 100px, 25vw)", + "popover-height": "Popover height in browser units (ex. 100px, 25vh)", + "popover-style": "Popover style", + "open-new-browser-tab": "Open in a new browser tab", + "mobile": { + "action-type": "Mobile action type", + "action-type-required": "Mobile action type is required", + "take-picture-from-gallery": "Take picture from gallery", + "take-photo": "Take photo", + "map-direction": "Open map directions", + "map-location": "Open map location", + "scan-qr-code": "Scan QR Code", + "make-phone-call": "Make phone call", + "get-location": "Get phone location", + "take-screenshot": "Take screenshot" + } + }, + "widgets-bundle": { + "current": "Dabartinis rinkinys", + "widgets-bundles": "Valdiklių rinkiniai", + "widgets-bundle-widgets": "Valdiklių rinkinio valdikliai", + "add": "Pridėti valdiklių rinkinį", + "delete": "Panaikinti valdiklių rinkinį", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "description": "Aprašymas", + "image-preview": "Paveiksliuko peržiūra", + "add-widgets-bundle-text": "Pridėti naują valdiklių rinkinį", + "no-widgets-bundles-text": "Valdiklių rinkinių nėra", + "empty": "Valdiklių rinkinys tuščias", + "details": "Informacija", + "widgets-bundle-details": "Informacija apie valdiklių rinkinį", + "delete-widgets-bundle-title": "Ar tikrai norite panaikinti valdiklių rinkinį '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Būkite dėmesingi, po patvirtinimo valdiklių rinkinys ir visa su juo susijusi informacija bus panaikinta.", + "delete-widgets-bundles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinkinius} }?", + "delete-widgets-bundles-action-title": "Panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinknius} }", + "delete-widgets-bundles-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti valdiklių rinkiniai ir su jais susijusi informacija bus panaikinta.", + "no-widgets-bundles-matching": "Valdiklių rinkinių, atitinkančių '{{widgetsBundle}}' nėra.", + "widgets-bundle-required": "Valdiklių rinkinys būtinas.", + "system": "Sisteminis", + "import": "Importuoti valdiklių rinknį", + "export": "Eksportuoti valdiklių rinkinį", + "export-widgets-bundle-widgets-prompt": "Valdiklių rinkinių tipus įtraukti į eksportuotus duomenis (kitu atveju bus eksportuojami tik nurodytų valdiklių tipų FQN)", + "export-failed-error": "Valdiklių rinkinio eksportuoti nepavyko: {{error}}", + "create-new-widgets-bundle": "Sukurti naują valdiklių rinkinį", + "widgets-bundle-file": "Valdiklių rinkinio failas", + "invalid-widgets-bundle-file-error": "Valdiklių rinkinio importuoti nepavyko: neteisinga duomenš struktūra.", + "search": "Valdiklių rinkinių paieška", + "selected-widgets-bundles": "Pasirinkta { count, plural, =1 {1 valdiklių rinkinys} other {# valdiklių rinkiniai} }", + "open-widgets-bundle": "Atverti valdiklių rinkinį", + "loading-widgets-bundles": "Įkeliamas valdiklių rinkinys..." + }, + "widget-config": { + "data": "Duomenys", + "settings": "Nustatymai", + "advanced": "Išplėstiniai nustatymai", + "appearance": "Išvaizda", + "widget-card": "Valdiklio kortelė", + "mobile": "Mobilusis vaizdas", + "title": "Pavadinimas", + "title-tooltip": "Pavadinimo paaiškinimas", + "general-settings": "Pagrindiniai nustatymai", + "display-title": "Rodyti valdiklio pavadinimą", + "card-title": "Kortelės pavadinimas", + "drop-shadow": "Šešėlis", + "enable-fullscreen": "Įjungti rodymą per visą ekraną", + "enable-data-export": "Įjungti duomenų eksportą", + "data-export": "Duomenų eksportas", + "background-color": "Fono spalva", + "text-color": "Teksto spalva", + "border-radius": "Rėmelio storis", + "padding": "Padding", + "margin": "Margin", + "widget-style": "Valdiklio stilius", + "widget-css": "Valdiklio CSS", + "title-style": "Antraštės stilius", + "mobile-mode-settings": "Mobiliojo vaizdo režimas", + "order": "Rikiavimo tvarka", + "height": "Aukštis", + "mobile-hide": "Valdiklį paslėpti mobiliojo vaizdo režime", + "desktop-hide": "Slėpti valdiklį darbalaukio režime", + "units": "Papildomas simbolis šalia reikšmės", + "units-by-default": "Numatytieji matavimo vienetai", + "decimals": "Skaimenys po kablelio", + "decimals-by-default": "Dešimtainiai skaičiai pagal numatytuosius nustatymus", + "default-data-key-parameter-hint": "Šis parametras taikomas visoms valdiklių reikšmėms, nebent jis nustatomas duomenų rakto konfigūracijoje", + "units-short": "Vienetai", + "decimals-short": "Dešimtainiai skaičiai", + "decimals-suffix": "dešimtainiai skaičiai", + "timewindow": "Laikotarpio langas", + "use-dashboard-timewindow": "Naudoti skydelio laikotarpio langą", + "use-widget-timewindow": "Naudoti valdiklio laikotarpio langą", + "display-timewindow": "Rodyti laikotarpio langą", + "legend": "Legenda", + "display-legend": "Rodyti legendą", + "datasources": "Duomenų šaltiniai", + "datasource": "Duomenų šaltinis", + "maximum-datasources": "Daugiausiai leidžiama { count, plural, =1 {1 vienas duomenų šaltinis.} other {# duomenų šaltiniai} }", + "timeseries-key-error": "Reikia nurodyti nors vieną telemetrijos rodiklį", + "datasource-type": "Tipas", + "datasource-parameters": "Parametrai", + "remove-datasource": "Pašalinti duomenų šaltinį", + "add-datasource": "Pridėti duomenų šaltinį", + "target-device": "Tikslinis įrenginys", + "alarm-source": "Įspėjimo šaltinis", + "actions": "Veiksmai", + "action": "Veiksmas", + "add-action": "Pridėti veiksmą", + "search-actions": "Veiksmų paieška", + "no-actions-text": "Veiksmų nėra", + "action-source": "Veiksmo šaltinis", + "action-source-required": "Veiksmo šaltinis būtinas.", + "action-name": "Pavadinimas", + "action-name-required": "Veiksmo pavadinimas būtinas.", + "action-name-not-unique": "Veiksmas su tokiu pavadinimu jau yra.
Šaltinyje veiksmų pavadinimai kartotis negali.", + "action-icon": "Piktograma", + "show-hide-action-using-function": "Rodyti/slėpti veiksmą funkcijos pagalba", + "action-type": "Tipas", + "action-type-required": "Veiksmo tipas būtinas.", + "edit-action": "Redaguoti veiksmą", + "delete-action": "Panaikinti veiksmą", + "delete-action-title": "Panaikinti valdiklio veiksmą", + "delete-action-text": "Ar tikrai norite panaikinti valdiklio veiksmą '{{actionName}}'?", + "title-icon": "Antraštės piktograma", + "display-icon": "Rodyti antraštės piktoramą", + "card-icon": "Kortelės piktograma", + "icon-color": "Piktogramos spalva", + "icon-size": "Piktogramos dydis", + "advanced-settings": "Išplėstiniai nustatymai", + "data-settings": "Duomenų nustatymai", + "limits": "Ribos", + "no-data-display-message": "\"Nėra duomenų\" alternatyvus tekstas", + "data-page-size": "Maximum entities per datasource", + "settings-component-not-found": "Settings form component not found for selector '{{selector}}'", + "preview": "Preview", + "set": "Set", + "set-message": "Set message", + "advanced-title-style": "Advanced title style", + "card-style": "Card style", + "text": "Text", + "background": "Background", + "advanced-widget-style": "Advanced widget style", + "card-buttons": "Card buttons", + "show-card-buttons": "Show card buttons", + "card-border-radius": "Card border radius", + "card-appearance": "Card appearance", + "color": "Color" + }, + "widget-type": { + "import": "Importuoti valdiklį", + "export": "Eksportuoti valdiklį", + "export-widget-types": "Eksportuoti valdiklius", + "export-failed-error": "Valdiklio eksportuoti nepavyko: {{error}}", + "create-new-widget-type": "Sukurt naują valdiklį", + "widget-type-file": "Valdiklių failas", + "invalid-widget-type-file-error": "Nepavyko importuoti valdiklio: neteisinga valdiklio duomenų struktūra." + }, + "self-registration": { + "self-registration": "Self Registration", + "self-registration-url": "Self Registration URL", + "captcha-version": "reCAPTCHA version", + "captcha-action": "reCAPTCHA log action name", + "captcha-site-key": "reCAPTCHA site key", + "captcha-secret-key": "reCAPTCHA secret key", + "notification-email": "El. pašto adresas pranešimų siuntimui", + "notification-email-invalid": "Neteisingas el. pašto adreso formatas.", + "notification-email-required": "El. pašto adresas pranešimų siuntimui būtinas.", + "privacy-policy-text": "Privatumo politikos tekstas", + "terms-of-use-text": "Terms of Use text", + "text-message-page": "Tekstinė žinutė registracijos puslapiui", + "enable-mobile-self-registration": "Enable self registration from mobile application", + "mobile-self-registration-title": "Mobile application self registration settings", + "mobile-package": "Application package", + "mobile-package-placeholder": "Ex.: my.example.app", + "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", + "mobile-package-required": "Application package is required", + "mobile-app-secret": "Application secret", + "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", + "copy-mobile-app-secret": "Copy application secret", + "mobile-app-scheme": "Application URL scheme", + "mobile-app-scheme-placeholder": "Ex.: customscheme", + "mobile-app-scheme-required": "Application URL scheme is required", + "mobile-app-host": "Application URL hostname", + "mobile-app-host-placeholder": "Ex.: my.app.host", + "mobile-app-host-required": "Application URL hostname is required", + "show-privacy-policy": "Show Privacy Policy", + "show-terms-of-use": "Show Terms of Use", + "domain-settings": "Domain settings", + "general-settings": "General settings" + }, + "solution-template": { + "solution-template": "Solution template", + "solution-templates": "Solution templates", + "management": "Manage solution templates", + "details": "Details", + "install": "Install", + "level": "Level", + "install-title": "Solution template successfully installed", + "install-failed-title": "Solution template installation failed", + "instructions": "Instructions", + "goto-main-dashboard": "Goto main dashboard", + "delete": "Delete", + "delete-solution-title": "Are you sure you want to delete the solution '{{solutionTitle}}'?", + "delete-solution-text": "Be careful, after the confirmation the solution and all related data will become unrecoverable.", + "installing": "Installing solution template..." + }, + "markdown": { + "edit": "Edit", + "preview": "Preview", + "copy-code": "Click to copy", + "copied": "Copied!" + }, + "white-labeling": { + "white-labeling": "Tinkinimas", + "white-labeling-general": "General White Labeling", + "login-white-labeling": "Prisijungimo puslapio tinkinimas", + "general": "General", + "login": "Prisijungti", + "preview": "Peržiūra", + "app-title": "Aplikaciijios pavadinimas", + "favicon": "Aplikacijos piktograma", + "favicon-description": "*.ico, *.gif ar *.png paveiksliukas, kurio maksimalus dydis {{kbSize}} kilobaitai.", + "favicon-size-error": "Paveiksliukas per didelis. Maksimalus leistinas dydis yra {{kbSize}} kilobaitai.", + "favicon-type-error": "Netinkamas paveiksliuko formatas. Leidžiami tik ICO, GIF ar PNG formatai.", + "drop-favicon-image": "Užvilkite svetainės piktogramos paveiksliuką arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-favicon-image-or": "Drag and drop a website icon image or", + "no-favicon-image": "Piktograma nepasirinkta", + "logo": "Logo", + "logo-description": "Bet koks paveiksliukas, kurio dydis neviršija {{kbSize}} kilobaitų.", + "logo-size-error": "Logotipas per didelis. Maksimalus leistinas dydis yra {{kbSize}} kilobaitai.", + "logo-type-error": "Netinkamas logotipo failo. Only images are accepted.", + "drop-logo-image": "Užvilkite svetainės logotipo paveiksliuką arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-logo-image-or": "Drag and drop a logo image or", + "no-logo-image": "Logotipas nepasirinktas", + "logo-height": "Logotipo aukštis, px", + "primary-palette": "Pirminė paletė", + "accent-palette": "Akcentų paletė", + "customize-palette": "Pritaikyti", + "advanced-css": "Advanced CSS", + "edit-palette": "Redaguoti paletę", + "save-palette": "Išsaugoti paletę", + "primary-background": "Pirminis fonas", + "secondary-background": "Antrinis fonas", + "hue1": "HUE 1", + "hue2": "HUE 2", + "hue3": "HUE 3", + "page-background-color": "Puslapio fono spalva", + "dark-foreground": "Tamsi tema", + "domain-name": "Domeno pavadinimas", + "base-url": "Bazinis URL", + "prohibit-different-url": "Prohibit to use hostname from the client request headers", + "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", + "help-link-base-url": "Bazinis pagalbos puslapio url", + "ui-help-base-url": "UI help base url", + "enable-help-links": "Įjungti pagalbos nuorodas", + "error-verification-url": "Domeno pavadinime negali būti '/' ir ':' simbolių. Pavyzdžiui: thingsboard.io", + "show-platform-name-version": "Rodyti platformos pavadinimą ir versiją", + "platform-name": "Platformas pavadinimas", + "platform-version": "Platformos versija", + "version-mask": "{{name}} v.{{version}}", + "position": { + "label": "Platformos pavadinimo ir versijos išdėstymas", + "under-logo": "Po logotipu", + "bottom": "Prisijungimo formos apačioje" + } + }, + "widgets": { + "background": { + "background": "Background", + "background-settings": "Background settings", + "background-type-image": "Upload image", + "background-type-image-url": "Image URL", + "background-type-color": "Solid color", + "image-url": "Image URL", + "overlay": "Overlay", + "enable-overlay": "Enable overlay", + "blur": "Blur", + "preview": "Preview" + }, + "battery-level": { + "layout": "Layout", + "layout-vertical-solid": "Vertical. Solid", + "layout-horizontal-solid": "Horizontal. Solid", + "layout-vertical-divided": "Vertical. Divided", + "layout-horizontal-divided": "Horizontal. Divided", + "icon": "Icon", + "value": "Value", + "auto-scale": "Auto scale", + "battery-level-color": "Battery level color", + "battery-shape-color": "Battery shape color", + "battery-level-card-style": "Battery level card style" + }, + "chart": { + "common-settings": "Common settings", + "enable-stacking-mode": "Enable stacking mode", + "selection": "Time range selection", + "enable-selection-mode": "Enable selection mode", + "line-shadow-size": "Line shadow size", + "display-smooth-lines": "Display smooth (curved) lines", + "default-bar-width": "Default bar width for non-aggregated data (milliseconds)", + "bar-alignment": "Bar alignment", + "bar-alignment-left": "Left", + "bar-alignment-right": "Right", + "bar-alignment-center": "Center", + "default-font": "Default font", + "default-font-size": "Default font size", + "default-font-color": "Default font color", + "thresholds-line-width": "Default line width for all thresholds", + "tooltip-settings": "Tooltip settings", + "tooltip": "Tooltip", + "show-tooltip": "Show tooltip", + "hover-individual-points": "Hover individual points", + "show-cumulative-values": "Show cumulative values in stacking mode", + "hide-zero-false-values": "Hide zero/false values from tooltip", + "tooltip-value-format-function": "Tooltip value format function", + "grid-settings": "Grid settings", + "show-vertical-lines": "Show vertical lines", + "show-horizontal-lines": "Show horizontal lines", + "grid-outline-border-width": "Grid outline/border width (px)", + "primary-color": "Primary color", + "background-color": "Background color", + "ticks-color": "Ticks color", + "xaxis-settings": "X axis settings", + "axis-title": "Axis title", + "xaxis-tick-labels-settings": "X axis tick labels settings", + "show-tick-labels": "Show axis tick labels", + "yaxis-settings": "Y axis settings", + "min-scale-value": "Minimum value on the scale", + "max-scale-value": "Maximum value on the scale", + "yaxis-tick-labels-settings": "Y axis tick labels settings", + "tick-step-size": "Step size between ticks", + "number-of-decimals": "The number of decimals to display", + "ticks-formatter-function": "Ticks formatter function", + "comparison-settings": "Comparison settings", + "enable-comparison": "Enable comparison", + "time-for-comparison": "Comparison period", + "time-for-comparison-previous-interval": "Previous interval (default)", + "time-for-comparison-days": "Day ago", + "time-for-comparison-weeks": "Week ago", + "time-for-comparison-months": "Month ago", + "time-for-comparison-years": "Year ago", + "time-for-comparison-custom-interval": "Custom interval", + "custom-interval-value": "Custom interval value (ms)", + "comparison-x-axis-settings": "Comparison X axis settings", + "axis-position": "Axis position", + "axis-position-top": "Top (default)", + "axis-position-bottom": "Bottom", + "custom-legend-settings": "Custom legend settings", + "enable-custom-legend": "Enable custom legend (this will allow you to use attribute/timeseries values in key labels)", + "key-name": "Key name", + "key-name-required": "Key name is required", + "key-type": "Key type", + "key-type-attribute": "Attribute", + "key-type-timeseries": "Timeseries", + "label-keys-list": "Keys list to use in labels", + "no-label-keys": "No keys configured", + "add-label-key": "Add new key", + "line-width": "Line width", + "color": "Color", + "data-is-hidden-by-default": "Data is hidden by default", + "disable-data-hiding": "Disable data hiding", + "remove-from-legend": "Remove datakey from legend", + "exclude-from-stacking": "Exclude from stacking(available in \"Stacking\" mode)", + "line-settings": "Line settings", + "show-line": "Show line", + "fill-line": "Fill line", + "fill-line-opacity": "Fill opacity", + "points-settings": "Points settings", + "show-points": "Show points", + "points-line-width": "Line width of points", + "points-radius": "Radius of points", + "point-shape": "Point shape", + "point-shape-circle": "Circle", + "point-shape-cross": "Cross", + "point-shape-diamond": "Diamond", + "point-shape-square": "Square", + "point-shape-triangle": "Triangle", + "point-shape-custom": "Custom function", + "point-shape-draw-function": "Point shape draw function", + "show-separate-axis": "Show separate axis", + "axis-position-left": "Left", + "axis-position-right": "Right", + "thresholds": "Thresholds", + "no-thresholds": "No thresholds configured", + "add-threshold": "Add threshold", + "show-values-for-comparison": "Show historical values for comparison", + "comparison-values-label": "Historical values label", + "comparison-line-color": "Comparison line color", + "threshold-settings": "Threshold settings", + "use-as-threshold": "Use key value as threshold", + "threshold-line-width": "Threshold line width", + "threshold-color": "Threshold color", + "common-pie-settings": "Common pie settings", + "radius": "Radius", + "inner-radius": "Inner radius", + "tilt": "Tilt", + "common-pie-settings-range-error": "Value should be in range from 0 to 1", + "stroke-settings": "Stroke settings", + "width-pixels": "Width (pixels)", + "show-labels": "Show labels", + "animation-settings": "Animation settings", + "animated-pie": "Enable pie animation (experimental)", + "border-settings": "Border settings", + "border-width": "Border width", + "border-color": "Border color", + "legend-settings": "Legend settings", + "display-legend": "Display legend", + "labels-font-color": "Labels font color", + "series": "Series", + "add-series": "Add series", + "series-settings": "Series settings", + "remove-series": "Remove series", + "no-series": "No series configured", + "no-series-error": "At least one series should be specified", + "chart-appearance": "Chart appearance", + "vertical-grid-lines": "Vertical grid lines", + "horizontal-grid-lines": "Horizontal grid lines", + "chart-background": "Chart background", + "grid-lines-color": "Grid lines color", + "border": "Border", + "axis": "Axis", + "vertical-axis": "Vertical axis", + "ticks": "Ticks", + "horizontal-axis": "Horizontal axis" + }, + "color": { + "color-settings": "Color settings", + "color-type-constant": "Constant", + "color-type-range": "Range", + "color-type-function": "Function", + "color": "Color", + "value-range": "Value range", + "from": "From", + "to": "To", + "color-function": "Color function" + }, + "dashboard-state": { + "dashboard-state-settings": "Dashboard state settings", + "dashboard-state": "Dashboard state id", + "autofill-state-layout": "Autofill state layout height by default", + "default-margin": "Default widgets margin", + "default-background-color": "Default background color", + "sync-parent-state-params": "Sync state params with parent dashboard" + }, + "date-range-navigator": { + "date-range-picker-settings": "Date range picker settings", + "hide-date-range-picker": "Hide date range picker", + "picker-one-panel": "Date range picker one panel", + "picker-auto-confirm": "Date range picker auto confirm", + "picker-show-template": "Date range picker show template", + "first-day-of-week": "First day of the week", + "interval-settings": "Interval settings", + "hide-interval": "Hide interval", + "initial-interval": "Initial interval", + "interval-hour": "Hour", + "interval-day": "Day", + "interval-week": "Week", + "interval-two-weeks": "2 weeks", + "interval-month": "Month", + "interval-three-months": "3 months", + "interval-six-months": "6 months", + "step-settings": "Step settings", + "hide-step-size": "Hide step size", + "initial-step-size": "Initial step size", + "hide-labels": "Hide labels", + "use-session-storage": "Use session storage", + "localizationMap": { + "Sun": "Sun", + "Mon": "Mon", + "Tue": "Tue", + "Wed": "Wed", + "Thu": "Thu", + "Fri": "Fri", + "Sat": "Sat", + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "May", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Dec", + "January": "January", + "February": "February", + "March": "March", + "April": "April", + "June": "June", + "July": "July", + "August": "August", + "September": "September", + "October": "October", + "November": "November", + "December": "December", + "Custom Date Range": "Custom Date Range", + "Date Range Template": "Date Range Template", + "Today": "Today", + "Yesterday": "Yesterday", + "This Week": "This Week", + "Last Week": "Last Week", + "This Month": "This Month", + "Last Month": "Last Month", + "Year": "Year", + "This Year": "This Year", + "Last Year": "Last Year", + "Date picker": "Date picker", + "Hour": "Hour", + "Day": "Day", + "Week": "Week", + "2 weeks": "2 Weeks", + "Month": "Month", + "3 months": "3 Months", + "6 months": "6 Months", + "Custom interval": "Custom interval", + "Interval": "Interval", + "Step size": "Step size", + "Ok": "Ok" + } + }, + "entities-hierarchy": { + "hierarchy-data-settings": "Hierarchy data settings", + "relations-query-function": "Node relations query function", + "has-children-function": "Node has children function", + "node-state-settings": "Node state settings", + "node-opened-function": "Default node opened function", + "node-disabled-function": "Node disabled function", + "display-settings": "Display settings", + "node-icon-function": "Node icon function", + "node-text-function": "Node text function", + "sort-settings": "Sort settings", + "nodes-sort-function": "Nodes sort function" + }, + "edge": { + "display-default-title": "Display default title" + }, + "gateway": { + "general-settings": "General settings", + "widget-title": "Widget title", + "default-archive-file-name": "Default archive file name", + "device-type-for-new-gateway": "Device type for new gateway", + "messages-settings": "Messages settings", + "save-config-success-message": "Text message about successfully saved gateway configuration", + "device-name-exists-message": "Text message when device with entered name is already exists", + "gateway-title": "Gateway form", + "read-only": "Read only", + "events-title": "Gateway events form title", + "events-filter": "Events filter", + "event-key-contains": "Event key contains...", + "show-connector": "Show for the connector", + "connector-state-param-key": "Connector state parameter key", + "status": "Status", + "message": "Message", + "created-time": "Created time" + }, + "gauge": { + "default-color": "Default color", + "radial-gauge-settings": "Radial gauge settings", + "ticks-settings": "Ticks settings", + "min-value": "Minimum value", + "max-value": "Maximum value", + "start-ticks-angle": "Start ticks angle", + "ticks-angle": "Ticks angle", + "major-ticks-count": "Major ticks count", + "major-ticks-color": "Major ticks color", + "minor-ticks-count": "Minor ticks count", + "minor-ticks-color": "Minor ticks color", + "tick-numbers-font": "Tick numbers font", + "unit-title-settings": "Unit title settings", + "show-unit-title": "Show unit title", + "unit-title": "Unit title", + "title-font": "Title text font", + "units-settings": "Units settings", + "units-font": "Units text font", + "value-box-settings": "Value box settings", + "show-value-box": "Show value box", + "value-int": "Digits count for integer part of value", + "value-font": "Value text font", + "value-box-rect-stroke-color": "Value box rectangle stroke color", + "value-box-rect-stroke-color-end": "Value box rectangle stroke color - end gradient", + "value-box-background-color": "Value box background color", + "value-box-shadow-color": "Value box shadow color", + "plate-settings": "Plate settings", + "show-plate-border": "Show plate border", + "plate-color": "Plate color", + "needle-settings": "Needle settings", + "needle-circle-size": "Needle circle size", + "needle-color": "Needle color", + "needle-color-end": "Needle color - end gradient", + "needle-color-shadow-up": "Upper half of the needle shadow color", + "needle-color-shadow-down": "Drop shadow needle color", + "highlights-settings": "Highlights settings", + "highlights-width": "Highlights width", + "highlights": "Highlights", + "highlight-from": "From", + "highlight-to": "To", + "highlight-color": "Color", + "no-highlights": "No highlights configured", + "add-highlight": "Add highlight", + "animation-settings": "Animation settings", + "enable-animation": "Enable animation", + "animation-duration": "Animation duration", + "animation-rule": "Animation rule", + "animation-linear": "Linear", + "animation-quad": "Quad", + "animation-quint": "Quint", + "animation-cycle": "Cycle", + "animation-bounce": "Bounce", + "animation-elastic": "Elastic", + "animation-dequad": "Dequad", + "animation-dequint": "Dequint", + "animation-decycle": "Decycle", + "animation-debounce": "Debounce", + "animation-delastic": "Delastic", + "linear-gauge-settings": "Linear gauge settings", + "bar-stroke-width": "Bar stroke width", + "bar-stroke-color": "Bar stroke color", + "bar-background-color": "Gauge bar background color", + "bar-background-color-end": "Bar background color - end gradient", + "progress-bar-color": "Progress bar color", + "progress-bar-color-end": "Progress bar color - end gradient", + "major-ticks-names": "Major ticks names", + "show-stroke-ticks": "Show ticks stroke", + "major-ticks-font": "Major ticks font", + "border-color": "Border color", + "border-width": "Border width", + "needle-circle-color": "Needle circle color", + "animation-target": "Animation target", + "animation-target-needle": "Needle", + "animation-target-plate": "Plate", + "common-settings": "Common gauge settings", + "gauge-type": "Gauge type", + "gauge-type-arc": "Arc", + "gauge-type-donut": "Donut", + "gauge-type-horizontal-bar": "Horizontal bar", + "gauge-type-vertical-bar": "Vertical bar", + "donut-start-angle": "Angle to start from", + "bar-settings": "Gauge bar settings", + "relative-bar-width": "Relative bar width", + "neon-glow-brightness": "Neon glow effect brightness, (0-100), 0 - disable effect", + "stripes-thickness": "Thickness of the stripes, 0 - no stripes", + "rounded-line-cap": "Display rounded line cap", + "bar-color-settings": "Bar color settings", + "use-precise-level-color-values": "Use precise color levels", + "bar-colors": "Bar colors, from lower to upper", + "color": "Color", + "no-bar-colors": "No bar colors configured", + "add-bar-color": "Add bar color", + "from": "From", + "to": "To", + "fixed-level-colors": "Bar colors using boundary values", + "gauge-title-settings": "Gauge title settings", + "show-gauge-title": "Show gauge title", + "gauge-title": "Gauge title", + "gauge-title-font": "Gauge title font", + "unit-title-and-timestamp-settings": "Unit title and timestamp settings", + "show-timestamp": "Show value timestamp", + "timestamp-format": "Timestamp format", + "label-font": "Font of label showing under value", + "value-settings": "Value settings", + "show-value": "Show value text", + "min-max-settings": "Minimum/maximum labels settings", + "show-min-max": "Show min and max values", + "min-max-font": "Font of minimum and maximum labels", + "show-ticks": "Show ticks", + "tick-width": "Tick width", + "tick-color": "Tick color", + "tick-values": "Tick values", + "no-tick-values": "No tick values configured", + "add-tick-value": "Add tick value" + }, + "gpio": { + "pin": "Pin", + "label": "Label", + "row": "Row", + "column": "Column", + "color": "Color", + "panel-settings": "Panel settings", + "background-color": "Background color", + "gpio-switches": "GPIO switches", + "no-gpio-switches": "No GPIO switches configured", + "add-gpio-switch": "Add GPIO switch", + "gpio-status-request": "GPIO status request", + "method-name": "Method name", + "method-body": "Method body", + "gpio-status-change-request": "GPIO status change request", + "parse-gpio-status-function": "Parse gpio status function", + "gpio-leds": "GPIO leds", + "no-gpio-leds": "No GPIO leds configured", + "add-gpio-led": "Add GPIO led" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, + "input-widgets": { + "attribute-not-allowed": "Attribute parameter cannot be used in this widget", + "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", + "device-name": "Device name", + "device-name-required": "Device name is required", + "discard-changes": "Discard changes", + "entity-attribute-required": "Entity attribute is required", + "entity-coordinate-required": "Both fields, latitude and longitude, are required", + "entity-timeseries-required": "Entity timeseries is required", + "get-location": "Get current location", + "invalid-date": "Invalid Date", + "latitude": "Latitude", + "longitude": "Longitude", + "min-value-error": "Min value is {{value}}", + "max-value-error": "Max value is {{value}}", + "not-allowed-entity": "Selected entity cannot have shared attributes", + "no-attribute-selected": "No attribute is selected", + "no-datakey-selected": "No datakey is selected", + "no-coordinate-specified": "Datakey for latitude/longitude doesn't specified", + "no-entity-selected": "No entity selected", + "no-image": "No image", + "no-support-geolocation": "Your browser doesn't support geolocation", + "no-support-web-camera": "Your browser does not support cameras", + "enable-https-use-widget": "Please enable HTTPS to use this widget", + "no-found-your-camera": "Can't find your camera", + "no-permission-camera": "Permission was denied by the user / This site doesn't have permission to use the camera", + "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-camera": "Switch camera", + "switch-timeseries-value": "Switch entity timeseries value", + "take-photo": "Take photo", + "time": "Time", + "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget", + "update-failed": "Update failed", + "update-successful": "Update successful", + "update-attribute": "Update attribute", + "update-timeseries": "Update timeseries", + "value": "Value", + "general-settings": "General settings", + "widget-title": "Widget title", + "claim-button-label": "Claiming button label", + "show-secret-key-field": "Show 'Secret key' input field", + "labels-settings": "Labels settings", + "show-labels": "Show labels", + "device-name-label": "Label for device name input field", + "secret-key-label": "Label for secret key input field", + "messages-settings": "Messages settings", + "claim-device-success-message": "Text message of successful device claiming", + "claim-device-not-found-message": "Text message when device not found", + "claim-device-failed-message": "Text message of failed device claiming", + "claim-device-name-required-message": "'Device name required' error message", + "claim-device-secret-key-required-message": "'Secret key required' error message", + "relations-settings": "Relations settings", + "relate-device-to-state-entity": "Relate device to current state entity", + "relate-direction": "Relate direction", + "relate-direction-from": "From", + "relate-direction-to": "To", + "relation-type": "Relation type", + "show-label": "Show label", + "label": "Label", + "required": "Required", + "required-error-message": "'Required' error message", + "show-result-message": "Show result message", + "integer-field-settings": "Integer field settings", + "min-value": "Min value", + "max-value": "Max value", + "double-field-settings": "Double field settings", + "text-field-settings": "Text field settings", + "min-length": "Min length", + "max-length": "Max length", + "checkbox-settings": "Checkbox settings", + "true-label": "Checked label", + "false-label": "Unchecked label", + "image-input-settings": "Image input settings", + "display-preview": "Display preview", + "display-clear-button": "Display clear button", + "display-apply-button": "Display apply button", + "display-discard-button": "Display discard button", + "datetime-field-settings": "Date/time field settings", + "display-time-input": "Display time input", + "latitude-key-name": "Latitude key name", + "longitude-key-name": "Longitude key name", + "show-get-location-button": "Show button 'Get current location'", + "use-high-accuracy": "Use high accuracy", + "location-fields-settings": "Location fields settings", + "latitude-label": "Label for latitude", + "longitude-label": "Label for longitude", + "input-fields-alignment": "Input fields alignment", + "input-fields-alignment-column": "Column (default)", + "input-fields-alignment-row": "Row", + "layout": "Layout", + "row-gap": "Gap between rows in pixels", + "column-gap": "Gap between columns in pixels", + "latitude-field-required": "Latitude field required", + "longitude-field-required": "Longitude field required", + "attribute-settings": "Attribute settings", + "widget-mode": "Widget mode", + "widget-mode-update-attribute": "Update attribute", + "widget-mode-update-timeseries": "Update timeseries", + "attribute-scope": "Attribute scope", + "attribute-scope-server": "Server attribute", + "attribute-scope-shared": "Shared attribute", + "value-required": "Value required", + "image-settings": "Image settings", + "image-format": "Image format", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "Image quality that use lossy compression such as jpeg and webp", + "max-image-width": "Maximum image width", + "max-image-height": "Maximum image height", + "action-buttons": "Action buttons", + "show-action-buttons": "Show action buttons", + "update-all-values": "Update all values, not only modified", + "save-button-label": "'SAVE' button label", + "reset-button-label": "'UNDO' button label", + "group-settings": "Group settings", + "show-group-title": "Show title for group of fields, related to different entities", + "group-title": "Group title", + "fields-alignment": "Fields alignment", + "fields-alignment-row": "Row (default)", + "fields-alignment-column": "Column", + "fields-in-row": "Number of fields in the row", + "option-value": "Value (write 'null' for create empty option)", + "option-label": "Label", + "hide-input-field": "Hide input field", + "datakey-type": "Datakey type", + "datakey-type-server": "Server attribute (default)", + "datakey-type-shared": "Shared attribute", + "datakey-type-timeseries": "Timeseries", + "datakey-value-type": "Datakey value type", + "datakey-value-type-string": "String", + "datakey-value-type-double": "Double", + "datakey-value-type-integer": "Integer", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "Boolean (Checkbox)", + "datakey-value-type-boolean-switch": "Boolean (Switch)", + "datakey-value-type-date-time": "Date & Time", + "datakey-value-type-date": "Date", + "datakey-value-type-time": "Time", + "datakey-value-type-select": "Select", + "datakey-value-type-color": "Color", + "value-is-required": "Value is required", + "ability-to-edit-attribute": "Ability to edit attribute", + "ability-to-edit-attribute-editable": "Editable (default)", + "ability-to-edit-attribute-disabled": "Disabled", + "ability-to-edit-attribute-readonly": "Read-only", + "disable-on-datakey-name": "Disable on false value of another datakey (specify datakey name)", + "field-appearance": "Field appearance", + "appearance-fill": "Fill", + "appearance-outline": "Outline", + "subscript-sizing": "Subscript sizing", + "subscript-sizing-fixed": "Fixed", + "subscript-sizing-dynamic": "Dynamic", + "slide-toggle-settings": "Slide toggle settings", + "slide-toggle-label-position": "Slide toggle label position", + "slide-toggle-label-position-after": "After", + "slide-toggle-label-position-before": "Before", + "select-options": "Select options", + "no-select-options": "No select options configured", + "add-select-option": "Add select option", + "numeric-field-settings": "Numeric field settings", + "step-interval": "Step interval between values", + "error-messages": "Error messages", + "min-value-error-message": "'Min value' error message", + "max-value-error-message": "'Max value' error message", + "invalid-date-error-message": "'Invalid date' error message", + "invalid-JSON-error-message": "'Invalid JSON' error message", + "icon-settings": "Icon settings", + "dialog-editor-settings": "Dialog editor settings", + "use-custom-icon": "Use custom icon", + "input-cell-icon": "Icon to show before input cell", + "value-conversion-settings": "Value conversion settings", + "get-value-settings": "Get value settings", + "use-get-value-function": "Use getValue function", + "get-value-function": "getValue function", + "set-value-settings": "Set value settings", + "use-set-value-function": "Use setValue function", + "set-value-function": "setValue function", + "json-invalid": "JSON value has an invalid format", + "title": "Title", + "cancel-button-label": "'Cancel' button label" + }, + "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", + "qr-code": { + "use-qr-code-text-function": "Use QR code text function", + "qr-code-text-pattern": "QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')", + "qr-code-text-pattern-hint": "QR code text pattern use the value of the first found key in the entities in the entity alias.", + "qr-code-text-pattern-required": "QR code text pattern is required.", + "qr-code-text-function": "QR code text function" + }, + "label-widget": { + "label-pattern": "Pattern", + "label-pattern-hint": "Hint: for ex. 'Text ${keyName} units.' or ${#<key index>} units'", + "label-pattern-required": "Pattern is required", + "label-position": "Position (Percentage relative to background)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "Background color", + "font-settings": "Font settings", + "background-image": "Background image", + "labels": "Labels", + "no-labels": "No labels configured", + "add-label": "Add label" + }, + "navigation": { + "title": "Title", + "navigation-path": "Navigation path", + "filter-type": "Filter type", + "filter-type-all": "All items", + "filter-type-include": "Include items", + "filter-type-exclude": "Exclude items", + "items": "Items", + "enter-urls-to-filter": "Enter urls to filter..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Message type", + "method": "Method", + "params": "Params", + "created-time": "Created time", + "expiration-time": "Expiration time", + "retries": "Retries", + "status": "Status", + "filter": "Filter", + "refresh": "Refresh", + "add": "Add RPC request", + "details": "Details", + "delete": "Delete", + "delete-request-title": "Delete Persistent RPC request", + "delete-request-text": "Are you sure you want to delete request?", + "details-title": "Details RPC ID: ", + "additional-info": "Additional info", + "response": "Response", + "any-status": "Any status", + "rpc-status-list": "RPC status list", + "no-request-prompt": "No request to display", + "send-request": "Send request", + "add-title": "Create Persistent RPC request", + "method-error": "Method is required.", + "timeout-error": "Min timeout value is 5000 (5 seconds).", + "white-space-error": "White space is not allowed.", + "rpc-status": { + "QUEUED": "QUEUED", + "SENT": "SENT", + "DELIVERED": "DELIVERED", + "SUCCESSFUL": "SUCCESSFUL", + "TIMEOUT": "TIMEOUT", + "EXPIRED": "EXPIRED", + "FAILED": "FAILED" + }, + "rpc-search-status-all": "ALL", + "message-types": { + "false": "Two-way", + "true": "One-way" + }, + "general-settings": "General settings", + "enable-filter": "Enable filter", + "enable-sticky-header": "Display header while scrolling", + "enable-sticky-action": "Display actions column while scrolling", + "display-request-details": "Display request details", + "allow-send-request": "Allow send RPC request", + "allow-delete-request": "Allow delete request", + "columns-settings": "Columns settings", + "display-columns": "Columns to display", + "column": "Column", + "no-columns-found": "No columns found", + "no-columns-matching": "'{{column}}' not found." + }, + "rpc": { + "value-settings": "Value settings", + "initial-value": "Initial value", + "retrieve-value-settings": "Retrieve on/off value settings", + "retrieve-value-method": "Retrieve value using method", + "retrieve-value-method-none": "Don't retrieve", + "retrieve-value-method-rpc": "Call RPC get value method", + "retrieve-value-method-attribute": "Subscribe for attribute", + "retrieve-value-method-timeseries": "Subscribe for timeseries", + "attribute-value-key": "Attribute key", + "timeseries-value-key": "Timeseries key", + "get-value-method": "RPC get value method", + "parse-value-function": "Parse value function", + "update-value-settings": "Update value settings", + "set-value-method": "RPC set value method", + "convert-value-function": "Convert value function", + "rpc-settings": "RPC settings", + "request-timeout": "RPC request timeout (ms)", + "persistent-rpc-settings": "Persistent RPC settings", + "request-persistent": "RPC request persistent", + "persistent-polling-interval": "Polling interval (ms) to get persistent RPC command response", + "common-settings": "Common settings", + "switch-title": "Switch title", + "show-on-off-labels": "Show on/off labels", + "slide-toggle-label": "Slide toggle label", + "label-position": "Label position", + "label-position-before": "Before", + "label-position-after": "After", + "slider-color": "Slider color", + "slider-color-primary": "Primary", + "slider-color-accent": "Accent", + "slider-color-warn": "Warn", + "button-style": "Button style", + "button-raised": "Raised button", + "button-primary": "Primary color", + "button-background-color": "Button background color", + "button-text-color": "Button text color", + "widget-title": "Widget title", + "button-label": "Button label", + "device-attribute-scope": "Device attribute scope", + "server-attribute": "Server attribute", + "shared-attribute": "Shared attribute", + "device-attribute-parameters": "Device attribute parameters", + "is-one-way-command": "Is one way command", + "rpc-method": "RPC method", + "rpc-method-params": "RPC method params", + "show-rpc-error": "Show RPC command execution error", + "led-title": "LED title", + "led-color": "LED color", + "check-status-settings": "Check status settings", + "perform-rpc-status-check": "Perform RPC device status check", + "retrieve-led-status-value-method": "Retrieve led status value using method", + "led-status-value-attribute": "Device attribute containing led status value", + "led-status-value-timeseries": "Device timeseries containing led status value", + "check-status-method": "RPC check device status method", + "parse-led-status-value-function": "Parse led status value function", + "knob-title": "Knob title", + "min-value": "Minimum value", + "max-value": "Maximum value" + }, + "maps": { + "select-entity": "Select entity", + "select-entity-hint": "Hint: after selection click at the map to set position", + "tooltips": { + "placeMarker": "Click to place '{{entityName}}' entity", + "firstVertex": "Polygon for '{{entityName}}': click to place first point", + "firstVertex-cut": "Click to place first point", + "continueLine": "Polygon for '{{entityName}}': click to continue drawing", + "continueLine-cut": "Click to continue drawing", + "finishLine": "Click any existing marker to finish", + "finishPoly": "Polygon for '{{entityName}}': click first marker to finish and save", + "finishPoly-cut": "Click first marker to finish and save", + "finishRect": "Polygon for '{{entityName}}': click to finish and save", + "startCircle": "Circle for '{{entityName}}': click to place circle center", + "finishCircle": "Circle for '{{entityName}}': click to finish circle", + "placeCircleMarker": "Click to place circle marker" + }, + "actions": { + "finish": "Finish", + "cancel": "Cancel", + "removeLastVertex": "Remove last point" + }, + "buttonTitles": { + "drawMarkerButton": "Place entity", + "drawPolyButton": "Create polygon", + "drawLineButton": "Create polyline", + "drawCircleButton": "Create circle", + "drawRectButton": "Create rectangle", + "editButton": "Edit mode", + "dragButton": "Drag-drop mode", + "cutButton": "Cut polygon area", + "deleteButton": "Remove", + "drawCircleMarkerButton": "Create circle marker", + "rotateButton": "Rotate polygon" + }, + "map-provider-settings": "Map provider settings", + "map-provider": "Map provider", + "map-provider-google": "Google maps", + "map-provider-openstreet": "OpenStreet maps", + "map-provider-here": "HERE maps", + "map-provider-image": "Image map", + "map-provider-tencent": "Tencent maps", + "openstreet-provider": "OpenStreet map provider", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Default)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "Use custom provider", + "custom-provider-tile-url": "Custom provider tile URL", + "google-maps-api-key": "Google Maps API Key", + "default-map-type": "Default map type", + "google-map-type-roadmap": "Roadmap", + "google-map-type-satelite": "Satellite", + "google-map-type-hybrid": "Hybrid", + "google-map-type-terrain": "Terrain", + "map-layer": "Map layer", + "here-map-normal-day": "HERE.normalDay (Default)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "Credentials", + "here-app-id": "HERE app id", + "here-app-code": "HERE app code", + "here-api-key": "HERE API key", + "here-use-new-version-api-3": "Use API version 3", + "tencent-maps-api-key": "Tencent Maps API Key", + "tencent-map-type-roadmap": "Roadmap", + "tencent-map-type-satelite": "Satellite", + "tencent-map-type-hybrid": "Hybrid", + "image-map-background": "Image map background", + "image-map-background-from-entity-attribute": "Take image map background from entity attribute", + "image-url-source-entity-alias": "Image URL source entity alias", + "image-url-source-entity-attribute": "Image URL source entity attribute", + "common-map-settings": "Common map settings", + "x-pos-key-name": "X position key name", + "y-pos-key-name": "Y position key name", + "latitude-key-name": "Latitude key name", + "longitude-key-name": "Longitude key name", + "default-map-zoom-level": "Default map zoom level (0 - 20)", + "default-map-center-position": "Default map center position (0,0)", + "disable-scroll-zooming": "Disable scroll zooming", + "disable-double-click-zooming": "Disable double click zooming", + "disable-zoom-control-buttons": "Disable zoom control buttons", + "fit-map-bounds": "Fit map bounds to cover all markers", + "use-default-map-center-position": "Use default map center position", + "entities-limit": "Limit of entities to load", + "markers-settings": "Markers settings", + "marker-offset-x": "Marker X offset relative to position multiplied by marker width", + "marker-offset-y": "Marker Y offset relative to position multiplied by marker height", + "position-function": "Position conversion function, should return x,y coordinates as double from 0 to 1 each", + "draggable-marker": "Draggable marker", + "label": "Label", + "show-label": "Show label", + "use-label-function": "Use label function", + "label-pattern": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", + "label-function": "Label function", + "tooltip": "Tooltip", + "show-tooltip": "Show tooltip", + "show-tooltip-action": "Action for displaying the tooltip", + "show-tooltip-action-click": "Show tooltip on click (Default)", + "show-tooltip-action-hover": "Show tooltip on hover", + "auto-close-tooltips": "Auto-close tooltips", + "use-tooltip-function": "Use tooltip function", + "tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", + "tooltip-function": "Tooltip function", + "tooltip-offset-x": "Tooltip X offset relative to marker anchor multiplied by marker width", + "tooltip-offset-y": "Tooltip Y offset relative to marker anchor multiplied by marker height", + "color": "Color", + "use-color-function": "Use color function", + "color-function": "Color function", + "marker-image": "Marker image", + "use-marker-image-function": "Use marker image function", + "custom-marker-image": "Custom marker image", + "custom-marker-image-size": "Custom marker image size (px)", + "marker-image-function": "Marker image function", + "marker-images": "Marker images", + "polygon-settings": "Polygon settings", + "show-polygon": "Show polygon", + "polygon-key-name": "Polygon key name", + "enable-polygon-edit": "Enable polygon edit", + "polygon-label": "Polygon label", + "show-polygon-label": "Show polygon label", + "use-polygon-label-function": "Use polygon label function", + "polygon-label-pattern": "Polygon label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", + "polygon-label-function": "Polygon label function", + "polygon-tooltip": "Polygon tooltip", + "show-polygon-tooltip": "Show polygon tooltip", + "auto-close-polygon-tooltips": "Auto-close polygon tooltips", + "use-polygon-tooltip-function": "Use polygon tooltip function", + "polygon-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", + "polygon-tooltip-function": "Polygon tooltip function", + "polygon-color": "Polygon color", + "polygon-opacity": "Polygon opacity", + "use-polygon-color-function": "Use polygon color function", + "polygon-color-function": "Polygon color function", + "polygon-stroke": "Polygon stroke", + "stroke-color": "Stroke color", + "stroke-opacity": "Stroke opacity", + "stroke-weight": "Stroke weight", + "use-polygon-stroke-color-function": "Use polygon stroke color function", + "polygon-stroke-color-function": "Polygon stroke color function", + "circle-settings": "Circle settings", + "show-circle": "Show circle", + "circle-key-name": "Circle key name", + "enable-circle-edit": "Enable circle edit", + "circle-label": "Circle label", + "show-circle-label": "Show circle label", + "use-circle-label-function": "Use circle label function", + "circle-label-pattern": "Circle label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", + "circle-label-function": "Circle label function", + "circle-tooltip": "Circle tooltip", + "show-circle-tooltip": "Show circle tooltip", + "auto-close-circle-tooltips": "Auto-close circle tooltips", + "use-circle-tooltip-function": "Use circle tooltip function", + "circle-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", + "circle-tooltip-function": "Circle tooltip function", + "circle-fill-color": "Circle fill color", + "circle-fill-color-opacity": "Circle fill color opacity", + "use-circle-fill-color-function": "Use circle fill color function", + "circle-fill-color-function": "Circle fill color function", + "circle-stroke": "Circle stroke", + "use-circle-stroke-color-function": "Use circle stroke color function", + "circle-stroke-color-function": "Circle stroke color function", + "markers-clustering-settings": "Markers clustering settings", + "use-map-markers-clustering": "Use map markers clustering", + "zoom-on-cluster-click": "Zoom when clicking on a cluster", + "max-cluster-zoom": "The maximum zoom level when a marker can be part of a cluster (0 - 18)", + "max-cluster-radius-pixels": "Maximum radius that a cluster will cover in pixels", + "cluster-zoom-animation": "Show animation on markers when zooming", + "show-markers-bounds-on-cluster-mouse-over": "Show the bounds of markers when mouse over a cluster", + "spiderfy-max-zoom-level": "Spiderfy at the max zoom level (to see all cluster markers)", + "load-optimization": "Load optimization", + "cluster-chunked-loading": "Use chunks for adding markers so that the page does not freeze", + "cluster-markers-lazy-load": "Use lazy load for adding markers", + "editor-settings": "Editor settings", + "enable-snapping": "Enable snapping to other vertices for precision drawing", + "init-draggable-mode": "Initialize map in draggable mode", + "hide-all-edit-buttons": "Hide all edit control buttons", + "hide-draw-buttons": "Hide draw buttons", + "hide-edit-buttons": "Hide edit buttons", + "hide-remove-button": "Hide remove button", + "route-map-settings": "Route map settings", + "trip-animation-settings": "Trip animation settings", + "normalization-step": "Normalization data step (ms)", + "tooltip-background-color": "Tooltip background color", + "tooltip-font-color": "Tooltip font color", + "tooltip-opacity": "Tooltip opacity (0-1)", + "auto-close-tooltip": "Auto-close tooltip", + "rotation-angle": "Set additional rotation angle for marker (deg)", + "path-settings": "Path settings", + "path-color": "Path color", + "use-path-color-function": "Use path color function", + "path-color-function": "Path color function", + "path-decorator": "Path decorator", + "use-path-decorator": "Use path decorator", + "decorator-symbol": "Decorator symbol", + "decorator-symbol-arrow-head": "Arrow", + "decorator-symbol-dash": "Dash", + "decorator-symbol-size": "Decorator symbol size (px)", + "use-path-decorator-custom-color": "Use path decorator custom color", + "decorator-custom-color": "Decorator custom color", + "decorator-offset": "Decorator offset", + "end-decorator-offset": "End decorator offset", + "decorator-repeat": "Decorator repeat", + "points-settings": "Points settings", + "show-points": "Show points", + "point-color": "Point color", + "point-size": "Point size (px)", + "use-point-color-function": "Use point color function", + "point-color-function": "Point color function", + "use-point-as-anchor": "Use point as anchor", + "point-as-anchor-function": "Point as anchor function", + "independent-point-tooltip": "Independent point tooltip", + "clustering-markers": "Clustering markers", + "use-icon-create-function": "Use markers colour function", + "marker-color-function": "Marker color function" + }, + "markdown": { + "use-markdown-text-function": "Use markdown/HTML value function", + "markdown-text-function": "Markdown/HTML value function", + "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", + "apply-default-markdown-style": "Apply default markdown style", + "markdown-css": "Markdown/HTML CSS" + }, + "simple-card": { + "label": "Label", + "label-position": "Label position", + "label-position-left": "Left", + "label-position-top": "Top" + }, + "value-card": { + "layout": "Layout", + "layout-square": "Square", + "layout-vertical": "Vertical", + "layout-centered": "Centered", + "layout-simplified": "Simplified", + "layout-horizontal": "Horizontal", + "layout-horizontal-reversed": "Horizontal reversed", + "label": "Label", + "icon": "Icon", + "value": "Value", + "date": "Date", + "value-card-style": "Value card style" + }, + "aggregated-value-card": { + "subtitle": "Subtitle", + "chart": "Chart", + "values": "Values", + "value-appearance": "Value appearance", + "position": "Position", + "position-center": "Center", + "position-right-top": "Right top", + "position-right-bottom": "Right bottom", + "position-left-top": "Left top", + "position-left-bottom": "Left bottom", + "font": "Font", + "color": "Color", + "arrow": "Arrow", + "display-up-down-arrow": "Display Up/Down arrow", + "add-value": "Add value", + "remove-value": "Remove value", + "no-values": "No values configured", + "aggregation": "Aggregation", + "aggregated-value-card-style": "Aggregated value card style" + }, + "alarm-count": { + "alarm-count-card-style": "Alarm count card style" + }, + "entity-count": { + "entity-count-card-style": "Entity count card style" + }, + "count": { + "layout": "Layout", + "layout-column": "Column", + "layout-row": "Row", + "label": "Label", + "icon": "Icon", + "icon-background": "Icon background", + "value": "Value", + "chevron": "Chevron" + }, + "table": { + "common-table-settings": "Common Table Settings", + "enable-search": "Enable search", + "enable-sticky-header": "Always display header", + "enable-sticky-action": "Always display actions column", + "hidden-cell-button-display-mode": "Hidden cell button actions display mode", + "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", + "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", + "display-timestamp": "Display timestamp column", + "display-milliseconds": "Display timestamp milliseconds", + "display-pagination": "Display pagination", + "default-page-size": "Default page size", + "use-entity-label-tab-name": "Use entity label in tab name", + "hide-empty-lines": "Hide empty lines", + "row-style": "Row style", + "use-row-style-function": "Use row style function", + "row-style-function": "Row style function", + "cell-style": "Cell style", + "use-cell-style-function": "Use cell style function", + "cell-style-function": "Cell style function", + "cell-content": "Cell content", + "use-cell-content-function": "Use cell content function", + "cell-content-function": "Cell content function", + "show-latest-data-column": "Show latest data column", + "latest-data-column-order": "Latest data column order", + "entities-table-title": "Entities table title", + "enable-select-column-display": "Enable select columns to display", + "display-entity-name": "Display entity name column", + "entity-name-column-title": "Entity name column title", + "display-entity-label": "Display entity label column", + "entity-label-column-title": "Entity label column title", + "display-entity-type": "Display entity type column", + "default-sort-order": "Default sort order", + "custom-title": "Custom header title", + "column-width": "Column width (px or %)", + "default-column-visibility": "Default column visibility", + "column-visibility-visible": "Visible", + "column-visibility-hidden": "Hidden", + "column-visibility-hidden-mobile": "Hidden in mobile mode", + "column-selection-to-display": "Column selection in 'Columns to Display'", + "column-selection-to-display-enabled": "Enabled", + "column-selection-to-display-disabled": "Disabled", + "column-export-option": "Include column in export", + "column-export-option-always": "Always", + "column-export-option-only-visible": "Only if column visible", + "column-export-option-never": "Never", + "alarms-table-title": "Alarms table title", + "enable-alarms-selection": "Enable alarms selection", + "enable-alarms-search": "Enable alarms search", + "enable-alarm-filter": "Enable alarm filter", + "display-alarm-details": "Display alarm details", + "allow-alarms-ack": "Allow alarms acknowledgment", + "allow-alarms-clear": "Allow alarms clear", + "display-alarm-activity": "Display alarm activity", + "allow-alarms-assign": "Allow alarms assignment", + "blob-entities-table-title": "Blob entities table title", + "display-created-time": "Display created time column", + "display-type": "Display type column", + "display-customer": "Display customer column", + "no-data-display-message": "\"No data to display\" alternative message", + "force-default-blob-entity-type": "Force default blob entity type", + "scheduler-events-table-title": "Scheduler events table title", + "force-default-event-type": "Force default event type", + "columns": "Columns", + "column-settings": "Column settings", + "remove-column": "Remove column", + "add-column": "Add column", + "no-columns": "No columns configured", + "columns-to-display": "Columns to display", + "table-header": "Table header", + "header-buttons": "Header buttons", + "table-buttons": "Table buttons", + "pagination": "Pagination", + "rows": "Rows", + "timeseries-column-error": "At least one timeseries column should be specified", + "table-tabs": "Table tabs", + "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode" + }, + "scheduler": { + "enabled-scheduler-event-views": "Enabled scheduler events views", + "enabled-scheduler-event-views-both": "Both", + "enabled-scheduler-event-views-list": "List View Only", + "enabled-scheduler-event-views-calendar": "Calendar View Only", + "display-name": "Display name", + "type-name": "Type name", + "display-originator-select": "Display originator entity select", + "display-message-type-select": "Display message type select", + "display-metadata-table": "Display message metadata table", + "configuration-html-template": "Configuration HTML template", + "custom-event-types": "Custom event types", + "no-custom-event-types": "No custom event types configured", + "add-custom-event-type": "Add custom event type" + }, + "value-source": { + "value-source": "Value source", + "predefined-value": "Predefined value", + "entity-attribute": "Entity attribute", + "value": "Value", + "source-entity-alias": "Source entity alias", + "source-entity-attribute": "Source entity attribute" + }, + "widget-font": { + "font-settings": "Font settings", + "font-family": "Font family", + "size": "Size", + "relative-font-size": "Relative font size (percents)", + "font-style": "Style", + "font-style-normal": "Normal", + "font-style-italic": "Italic", + "font-style-oblique": "Oblique", + "font-weight": "Weight", + "font-weight-normal": "Normal", + "font-weight-bold": "Bold", + "font-weight-bolder": "Bolder", + "font-weight-lighter": "Lighter", + "color": "Color", + "shadow-color": "Shadow color", + "preview": "Preview", + "line-height": "Line height", + "auto": "Auto" + }, + "home": { + "no-data-available": "No data available" + }, + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "Disk", + "cpu-warning-text": "Running high on CPU usage. To avoid system failure, optimize system performance.", + "cpu-critical-text": "Critically high CPU usage. To avoid system failure, optimize system performance.", + "ram-warning-text": "Running low on reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", + "ram-critical-text": "Critically low reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", + "disk-warning-text": "Running low on disk space. To avoid data loss, free up or expand the disk space.", + "disk-critical-text": "Critically low disk space. To avoid data loss, free up or expand the disk space." + }, + "cluster-info": { + "service-id": "Service id", + "service-type": "Service type", + "no-data": "No data" + }, + "transport-messages": { + "title": "Transport messages", + "info": "All the messages that came from devices" + }, + "activity": { + "title": "Activity" + }, + "documentation": { + "title": "Documentation", + "add-link": "Add link", + "add-link-title": "Add documentation link", + "name": "Name", + "name-required": "Name is required.", + "link": "Link", + "link-required": "Link is required.", + "columns": "Columns" + }, + "quick-links": { + "title": "Quick links", + "add-link": "Add link", + "add-link-title": "Add quick link", + "quick-link": "Quick link", + "quick-link-required": "Quick link is required.", + "no-links-matching": "No links matching '{{name}}' were found.", + "columns": "Columns" + }, + "recent-dashboards": { + "title": "Dashboards", + "last": "Last viewed", + "starred": "Starred", + "name": "Name", + "last-viewed": "Last viewed", + "no-last-viewed-dashboards": "No last viewed dashboards yet" + }, + "configured-features": { + "title": "Configured features", + "info": "Status of features that require configuration", + "white-labeling-feature": "White labeling", + "email-feature": "Email", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2FA", + "feature-configured": "Feature is configured.\nClick to setup", + "feature-not-configured": "Feature is not configured.\nClick to setup" + }, + "version-info": { + "title": "Version", + "contact-us": "Contact us", + "current-version": "Current version", + "current": "Current", + "available-version": "Available version", + "available": "Available", + "upgrade": "Upgrade", + "version-is-up-to-date": "Version is up to date" + }, + "license-info": { + "title": "License info", + "view-all": "View all", + "license-portal": "License portal", + "devices-info": "{count} / { max, plural, =0 {∞} other {#} } Devices", + "assets-info": "{count} / { max, plural, =0 {∞} other {#} } Assets", + "dashboards-info": "{count} / { max, plural, =0 {∞} other {#} } Dashboards", + "integrations-info": "{count} / { max, plural, =0 {∞} other {#} } Integrations", + "community-support": "Community support", + "white-labeling": "White labeling", + "unlimited-datapoints-and-messages": "Unlimited datapoints and messages", + "unlimited-api-calls": "Unlimited API calls" + }, + "usage-info": { + "title": "Usage", + "entities": "Entities", + "api-calls": "API calls" + }, + "functions": { + "title": "Functions", + "pe-feature-tooltip": "Only on ThingsBoard\nProfessional Edition", + "switch-to-pe": "Switch to PE", + "alarms": "Alarms", + "dashboards": "Dashboards", + "entities-and-relations": "Entities & Relations", + "profiles": "Profiles", + "advanced-features": "Advanced features", + "notification-center": "Notification center", + "api-usage": "API usage", + "customers": "Customers", + "customers-hierarchy": "Customers hierarchy", + "roles-and-permissions": "Roles & Permissions", + "groups": "Groups", + "integrations": "Integrations", + "solution-templates": "Solution templates", + "scheduler": "Scheduler", + "white-labeling": "White-labeling" + }, + "devices": { + "view-docs": "View docs", + "inactive": "Inactive", + "active": "Active", + "total": "Total" + }, + "alarms": { + "critical": "Critical", + "assigned-to-me": "Assigned to me", + "total": "Total" + }, + "solution-templates": { + "title": "Solution templates", + "prototype-plan": "Prototype plan", + "startup-plan": "Startup plan" + }, + "getting-started": { + "get-started": "Get started", + "finish": "Finish", + "done-welcome-title": "Welcome on board", + "done-welcome-text": "You did great with it!", + "sys-admin": { + "step1": { + "title": "Create Tenant & Tenant Administrator", + "content": "

A tenant is an individual or an organization that owns or produces devices and assets. The tenant may have multiple tenant administrator users, customers, devices, and assets.

The Tenant Administrator can create and manage devices, assets, customers, and dashboards within the tenant account.

Follow the documentation on how to do it:

", + "how-to-create-tenant": "How to create Tenant & Tenant Administrator" + }, + "step2": { + "title": "Configure feature: Mail server", + "content": "

Mail server configuration is essential for user activation, password recovery, and alarm notification delivery.

Follow the documentation on how to do it:

", + "how-to-configure-mail-server": "How to configure Mail server" + }, + "step3": { + "title": "Configure feature: SMS provider", + "content": "

Configure SMS providers to notify the customers about the alarms via SMS.

Follow the documentation on how to do it:

", + "how-to-configure-sms-provider": "How to configure SMS provider" + }, + "step4": { + "title": "Configure feature: White-labeling", + "content": "

Easily customize your company's or product's logo and color scheme without coding and without restarting the service.

Follow the documentation on how to do it:

", + "how-to-configure-white-labeling": "How to configure White-labeling" + }, + "step5": { + "title": "Configure feature: 2FA", + "content": "

Improve security of the platform accounts with two-factor authentication.

Follow the documentation on how to do it:

", + "how-to-configure-2fa": "How to configure 2FA" + }, + "step6": { + "title": "Configure feature: OAuth 2", + "content": "

Simplify login for the tenant and customer users with Single Sign-On functionality via OAuth 2.0.

Follow the documentation on how to do it:

", + "how-to-configure-oauth2": "How to configure OAuth 2" + }, + "step7": { + "title": "Configure feature: Slack", + "content": "

Distribute notifications to the tenant and customer users via Slack according to the notification rules you set.

Follow the documentation on how to do it:

", + "how-to-configure-notifications": "How to configure Slack" + } + }, + "tenant-admin": { + "step1": { + "title": "Create device", + "content": "

Let's provision your first device to the platform via UI. Follow the documentation on how to do it:

", + "how-to-create-device": "How to create Device" + }, + "step2": { + "title": "Connect device", + "content-before": "

To connect the device you need to get the device credentials. We recommend using default auto-generated credentials which is access token for this guide.

  • Go to device table
  • Click on the device row to open device details
  • Press the button \"Copy access token\"

Use simple commands to publish data over HTTP. Don't forget to replace $ACCESS_TOKEN with your device access token:

", + "ubuntu": { + "install-curl": "Install cURL for Ubuntu:" + }, + "macos": { + "install-curl": "Install cURL for MacOS:" + }, + "windows": { + "install-curl": "Starting Windows 10 b17063, cURL is available by default." + }, + "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", + "content-after": "

You can also use other protocols such as MQTT, CoAP, etc.

Follow the documentation on how to do it:

", + "how-to-connect-device": "How to connect Device" + }, + "step3": { + "title": "Create dashboard", + "content": "

Create a dashboard to visualize data from entities such as assets, devices, etc.

Follow the documentation on how to do it:

", + "how-to-create-dashboard": "How to create Dashboard" + }, + "step4": { + "title": "Configure alarm rules", + "alarm-rules": "Alarm rules", + "content": "

Let's raise an alarm when the temperature reaches 25°C. Follow the documentation on how to do it:

", + "how-to-configure-alarm-rules": "How to configure Alarm rules" + }, + "step5": { + "title": "Create alarm", + "content-before": "

To trigger the alarm, send a new telemetry value of 26°C or higher.

", + "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", + "content-after": "

Follow the documentation on how to do it:

", + "how-to-create-alarm": "How to create Alarm" + }, + "step6": { + "title": "Create customer and share dashboard", + "content": "

By creating end-user dashboards, a customer user can only see his own devices, and data from another customer will be hidden.

Follow the documentation on how to do it:

", + "how-to-create-customer-and-share-dashboard": "How to create Customer and share Dashboard" + } + } + } + }, + "icon": { + "icon": "Icon", + "icons": "Icons", + "select-icon": "Select icon", + "material-icons": "Material icons", + "show-all": "Show all icons", + "search-icon": "Search icon", + "no-icons-found": "No icons found for '{{iconSearch}}'" + }, + "subscription": { + "entity-limit-text": "However you can upgrade your subscription plan in order to increase your limits.", + "upgrade-your-plan": "Upgrade subscription plan", + "white-labeling-feature": "White labeling feature", + "white-labeling-text-full": "Rebrand ThingsBoard platform web interface with your company or product logo and color scheme in 2 minutes.

Remove “Powered By” on the dashboards footer.
No coding or service restart required. Allow your customers to white-label their interface as well.", + "enable-white-labeling": "Enable white-labeling feature now by upgrading your subscription plan!", + "read-more": "Read more", + "white-labeling-video-text": "See video tutorial below to see how this feature works!" + }, + "subscription-error": { + "title": "Subscription violation", + "warning-title": "Subscription warning", + "upgrade-subscription-plan": "Please upgrade your subscription plan", + "upgrade-subscription-plan-to-install-solution-template": "In order to install {{solutionTemplateName}} solution you must upgrade your subscription at least to the {{planName}} plan!", + "limit-reached": { + "device-count": "You have reached maximum devices ({{value}}) allowed by your subscription plan!", + "asset-count": "You have reached maximum assets ({{value}}) allowed by your subscription plan!" + }, + "feature-disabled": { + "white-labeling": "White Labeling feature is not allowed by your subscription plan!" + } + }, + "phone-input": { + "phone-input-label": "Phone number", + "phone-input-required": "Phone number is required", + "phone-input-validation": "Phone number is invalid or not possible", + "phone-input-pattern": "Invalid phone number. Should be in E.164 format, ex. {{phoneNumber}}", + "phone-input-hint": "Telefono numeris tarptautiniu formatu , pvz. +37060123456" + }, + "custom": { + "widget-action": { + "action-cell-button": "Action cell button", + "row-click": "On row click", + "polygon-click": "On polygon click", + "marker-click": "On marker click", + "circle-click": "On circle click", + "tooltip-tag-action": "Tooltip tag action", + "node-selected": "On node selected", + "element-click": "On HTML element click", + "pie-slice-click": "On slice click", + "row-double-click": "On row double click", + "card-click": "On card click" + } + }, + "paginator": { + "items-per-page": "Eilučių skaičius puslapyje:", + "first-page-label": "Pirmas puslapis", + "last-page-label": "Paskutinis puslapis", + "next-page-label": "Kitas puslapis", + "previous-page-label": "Ankstesnis puslapis", + "items-per-page-separator": "iš" + } +} + From 53301bf641e594a887e7ca1d6686eb3077271fca Mon Sep 17 00:00:00 2001 From: kalytka Date: Wed, 15 May 2024 14:52:59 +0300 Subject: [PATCH 103/121] Include bundle widget checkbox enabled by default --- .../import-export/export-widgets-bundle-dialog.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts index 4794901f98..025a2ddf4c 100644 --- a/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts +++ b/ui-ngx/src/app/shared/import-export/export-widgets-bundle-dialog.component.ts @@ -22,6 +22,7 @@ import { FormControl } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { isDefinedAndNotNull } from '@core/utils'; export interface ExportWidgetsBundleDialogData { widgetsBundle: WidgetsBundle; @@ -43,7 +44,7 @@ export class ExportWidgetsBundleDialogComponent extends DialogComponent, protected router: Router, @@ -51,7 +52,7 @@ export class ExportWidgetsBundleDialogComponent extends DialogComponent) { super(store, router, dialogRef); this.widgetsBundle = data.widgetsBundle; - if (data.includeBundleWidgetsInExport) { + if (isDefinedAndNotNull(data.includeBundleWidgetsInExport)) { this.exportWidgetsFormControl.patchValue(data.includeBundleWidgetsInExport, {emitEvent: false}); } } From 45ed484c2a3d5f930523a712b9e2e80f7b7e7f4f Mon Sep 17 00:00:00 2001 From: rusikv Date: Wed, 15 May 2024 17:26:36 +0300 Subject: [PATCH 104/121] UI: mobile app qr code improvements, cleanup --- .../json/system/widget_bundles/cards.json | 2 +- .../widget_bundles/home_page_widgets.json | 3 +- .../widget_types/home_mobile_app_qr_code.json | 27 ----- .../widget_types/mobile_app_qr_code.json | 4 +- ui-ngx/src/app/core/auth/auth.selectors.ts | 2 +- .../src/app/core/http/mobile-app.service.ts | 12 +-- .../cards/mobile-app-qr-code-widget.models.ts | 17 ++-- .../mobile-app-qrcode-widget.component.html | 6 +- .../mobile-app-qrcode-widget.component.scss | 23 ----- .../lib/mobile-app-qrcode-widget.component.ts | 71 ++++++++------ ...app-qr-code-widget-settings.component.html | 84 +++------------- ...app-qr-code-widget-settings.component.scss | 19 ---- ...e-app-qr-code-widget-settings.component.ts | 32 +----- .../admin/mobile-app-settings.component.html | 12 +-- .../admin/mobile-app-settings.component.scss | 4 - .../admin/mobile-app-settings.component.ts | 6 +- .../home-links/home-links-routing.module.ts | 98 ++++++++++--------- .../app/shared/models/mobile-app.models.ts | 5 +- .../assets/dashboard/sys_admin_home_page.json | 12 ++- .../dashboard/tenant_admin_home_page.json | 13 ++- .../assets/locale/locale.constant-en_US.json | 4 +- ui-ngx/src/form.scss | 6 ++ 22 files changed, 168 insertions(+), 294 deletions(-) delete mode 100644 application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json delete mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 5a839d4c82..e0f4f4c615 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -17,7 +17,7 @@ "cards.label_widget", "cards.dashboard_state_widget", "cards.qr_code", - "cards.mobile_app_qr_code", + "mobile_app_qr_code", "cards.attributes_card", "cards.html_card", "cards.html_value_card", diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index cb8d0ed915..a30d6ee76f 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -13,7 +13,6 @@ "home_page_widgets.quick_links", "home_page_widgets.documentation_links", "home_page_widgets.dashboards", - "home_page_widgets.usage_info", - "home_page_widgets.home_page_mobile_app_qr_code" + "home_page_widgets.usage_info" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json deleted file mode 100644 index 908533b279..0000000000 --- a/application/src/main/data/json/system/widget_types/home_mobile_app_qr_code.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "fqn": "home_page_widgets.home_page_mobile_app_qr_code", - "name": "Home page mobile app QR code", - "deprecated": false, - "image": "tb-image:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0iI2ZmZiIgcng9IjQiLz48cmVjdCB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE1OSIgeD0iLjUiIHk9Ii41IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIuMSIgcng9IjMuNSIvPjxnIGNsaXAtcGF0aD0idXJsKCNhKSI+PHJlY3Qgd2lkdGg9IjEzNyIgaGVpZ2h0PSIxMzciIHg9IjMxLjUiIHk9IjExLjUiIGZpbGw9IiNmZmYiIHJ4PSI4LjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTY4LjUgMTEuNWgtMTM3djEzN2gxMzd2LTEzN1oiLz48bWFzayBpZD0iYiIgd2lkdGg9IjEyNCIgaGVpZ2h0PSIxMjQiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDUyLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTQyLjcgNjAuOWgtNC4zVjY1aDQuM1Y2MVpNNDIuNyA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNNDIuNyA5NC45aC00LjNWOTloNC4zVjk1Wk00NyA1Ni42aC00LjNWNjFoNC4ydi00LjNaTTQ3IDgyLjFoLTQuM3Y0LjNoNC4yVjgyWk00NyA4Ni40aC00LjN2NC4yaDQuMnYtNC4yWk00NyA5NC45aC00LjNWOTloNC4yVjk1Wk00NyAxMDMuNGgtNC4zdjQuMmg0LjJ2LTQuMlpNNTEuMiA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDYwLjloLTQuM1Y2NWg0LjNWNjFaTTUxLjIgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDc3LjloLTQuM1Y4Mmg0LjNWNzhaTTUxLjIgODIuMWgtNC4zdjQuM2g0LjNWODJaTTUxLjIgOTAuNmgtNC4zVjk1aDQuM3YtNC4zWk01MS4yIDk5LjFoLTQuM3Y0LjNoNC4zVjk5Wk01NS40IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk01NS40IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWk01OS43IDY1LjFoLTQuM3Y0LjNoNC4zVjY1Wk01OS43IDY5LjRoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgODYuNGgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTkuNyA5MC42aC00LjNWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNNjMuOSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk02My45IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTYzLjkgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNjMuOSA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNNjguMSA1Mi40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA2MC45SDY0VjY1aDQuMlY2MVpNNjguMSA2OS40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA3Ny45SDY0VjgyaDQuMlY3OFpNNjguMSA4Ni40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA5NC45SDY0Vjk5aDQuMlY5NVpNNjguMSAxMDMuNEg2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcyLjQgNTIuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNjkuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNzcuOUg2OFY4Mmg0LjNWNzhaTTcyLjQgODIuMUg2OHY0LjNoNC4zVjgyWk03Mi40IDg2LjRINjh2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Mi40IDkwLjZINjhWOTVoNC4zdi00LjNaTTcyLjQgOTkuMUg2OHY0LjNoNC4zVjk5Wk03Mi40IDEwMy40SDY4djQuMmg0LjN2LTQuMlpNNzYuNiAyMi43aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDMxLjJoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNzYuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNNzYuNiA0OC4xaC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDYwLjloLTQuMlY2NWg0LjJWNjFaTTc2LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTc2LjYgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNzYuNiA5NC45aC00LjJWOTloNC4yVjk1Wk03Ni42IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDExNi4xaC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyOC44aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk04MC45IDE4LjRoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSAyN2gtNC4zdjQuMkg4MXYtNC4zWk04MC45IDMxLjJoLTQuM3Y0LjJIODF2LTQuMlpNODAuOSAzNS40aC00LjN2NC4zSDgxdi00LjNaTTgwLjkgMzkuN2gtNC4zdjQuMkg4MXYtNC4yWk04MC45IDQ4LjFoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSA2MC45aC00LjNWNjVIODFWNjFaTTgwLjkgNjUuMWgtNC4zdjQuM0g4MVY2NVpNODAuOSA3My42aC00LjNWNzhIODF2LTQuM1pNODAuOSA4Mi4xaC00LjN2NC4zSDgxVjgyWk04MC45IDk0LjloLTQuM1Y5OUg4MVY5NVpNODAuOSA5OS4xaC00LjN2NC4zSDgxVjk5Wk04MC45IDEwMy40aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTExLjloLTQuM3Y0LjJIODF2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODAuOSAxMTYuMWgtNC4zdjQuMkg4MXYtNC4yWk04MC45IDEyNC42aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTM3LjNoLTQuM3Y0LjNIODF2LTQuM1pNODUuMSAxOC40SDgxdjQuM0g4NXYtNC4zWk04NS4xIDMxLjJIODF2NC4ySDg1di00LjJaTTg1LjEgMzUuNEg4MXY0LjNIODV2LTQuM1pNODUuMSA0My45SDgxdjQuMkg4NVY0NFpNODUuMSA1Ni42SDgxVjYxSDg1di00LjNaTTg1LjEgNzMuNkg4MVY3OEg4NXYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik04NS4xIDc3LjlIODFWODJIODVWNzhaTTg1LjEgODIuMUg4MXY0LjNIODVWODJaTTg1LjEgODYuNEg4MXY0LjJIODV2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODUuMSA5MC42SDgxVjk1SDg1di00LjNaTTg1LjEgOTkuMUg4MXY0LjNIODVWOTlaTTg1LjEgMTExLjlIODF2NC4ySDg1di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTg1LjEgMTE2LjFIODF2NC4ySDg1di00LjJaTTg1LjEgMTMzSDgxdjQuM0g4NXYtNC4yWk04OS40IDIyLjdIODV2NC4yaDQuM3YtNC4yWk04OS40IDMxLjJIODV2NC4yaDQuM3YtNC4yWk04OS40IDM1LjRIODV2NC4zaDQuM3YtNC4zWk04OS40IDQ4LjFIODV2NC4zaDQuM3YtNC4zWk04OS40IDUyLjRIODV2NC4yaDQuM3YtNC4yWk04OS40IDU2LjZIODVWNjFoNC4zdi00LjNaTTg5LjQgNjUuMUg4NXY0LjNoNC4zVjY1Wk04OS40IDEwMy40SDg1djQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMDcuNkg4NXY0LjNoNC4zdi00LjNaTTg5LjQgMTExLjlIODV2NC4yaDQuM3YtNC4yWk04OS40IDEyOC44SDg1djQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMzNIODV2NC4zaDQuM3YtNC4yWk05My42IDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTkzLjYgMjIuN2gtNC4ydjQuMmg0LjJ2LTQuMlpNOTMuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNOTMuNiAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk05My42IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk05My42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNOTMuNiA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNOTMuNiA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDczLjZoLTQuMlY3OGg0LjJ2LTQuM1pNOTMuNiA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNOTMuNiA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDkwLjZoLTQuMlY5NWg0LjJ2LTQuM1pNOTMuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOTMuNiAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNOTcuOSAxOC40aC00LjN2NC4zSDk4di00LjNaTTk3LjkgMjIuN2gtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDM1LjRoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSA0OC4xaC00LjN2NC4zSDk4di00LjNaTTk3LjkgNTIuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDYwLjloLTQuM1Y2NUg5OFY2MVpNOTcuOSA2NS4xaC00LjN2NC4zSDk4VjY1Wk05Ny45IDczLjZoLTQuM1Y3OEg5OHYtNC4zWk05Ny45IDgyLjFoLTQuM3Y0LjNIOThWODJaTTk3LjkgODYuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDk0LjloLTQuM1Y5OUg5OFY5NVpNOTcuOSA5OS4xaC00LjN2NC4zSDk4Vjk5Wk05Ny45IDEwMy40aC00LjN2NC4ySDk4di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTk3LjkgMTA3LjZoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSAxMTYuMWgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDEyNC42aC00LjN2NC4ySDk4di00LjJaTTk3LjkgMTMzaC00LjN2NC4zSDk4di00LjJaTTEwMi4xIDIyLjdIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA0My45SDk4djQuMmg0LjJWNDRaTTEwMi4xIDUyLjRIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA2OS40SDk4djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgNzMuNkg5OFY3OGg0LjJ2LTQuM1pNMTAyLjEgOTAuNkg5OFY5NWg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgOTQuOUg5OFY5OWg0LjJWOTVaTTEwMi4xIDEwMy40SDk4djQuMmg0LjJ2LTQuMlpNMTAyLjEgMTMzSDk4djQuM2g0LjJ2LTQuMlpNMTAyLjEgMTM3LjNIOTh2NC4zaDQuMnYtNC4zWk0xMDYuNCAxOC40SDEwMnY0LjNoNC4zdi00LjNaTTEwNi40IDIyLjdIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgNDguMUgxMDJ2NC4zaDQuM3YtNC4zWk0xMDYuNCA2MC45SDEwMlY2NWg0LjNWNjFaTTEwNi40IDczLjZIMTAyVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMDYuNCA3Ny45SDEwMlY4Mmg0LjNWNzhaTTEwNi40IDgyLjFIMTAydjQuM2g0LjNWODJaTTEwNi40IDg2LjRIMTAydjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTA2LjQgOTAuNkgxMDJWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEwNi40IDk0LjlIMTAyVjk5aDQuM1Y5NVpNMTA2LjQgMTAzLjRIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTE2LjFIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTMzSDEwMnY0LjNoNC4zdi00LjJaTTEwNi40IDEzNy4zSDEwMnY0LjNoNC4zdi00LjNaTTExMC42IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMTAuNiAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTAuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTEwLjYgNDguMWgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTEwLjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWk0xMTAuNiA5MC42aC00LjJWOTVoNC4ydi00LjNaTTExMC42IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk0xMTAuNiAxMDcuNmgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTExMC42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTAuNiAxMzNoLTQuMnY0LjNoNC4ydi00LjJaTTExMC42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTQuOSAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA0OC4xaC00LjN2NC4zaDQuM3YtNC4zWk0xMTQuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTE0LjkgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSA3Ny45aC00LjNWODJoNC4zVjc4Wk0xMTQuOSA4Mi4xaC00LjN2NC4zaDQuM1Y4MlpNMTE0LjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgOTkuMWgtNC4zdjQuM2g0LjNWOTlaTTExNC45IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSAxMTYuMWgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgMTMzaC00LjN2NC4zaDQuM3YtNC4yWk0xMTQuOSAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNMTE5LjEgMTguNEgxMTV2NC4zaDQuMnYtNC4zWk0xMTkuMSAzNS40SDExNXY0LjNoNC4ydi00LjNaTTExOS4xIDM5LjdIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNDMuOUgxMTV2NC4yaDQuMlY0NFpNMTE5LjEgNjUuMUgxMTV2NC4zaDQuMlY2NVpNMTE5LjEgNjkuNEgxMTV2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTkuMSA3My42SDExNVY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNzcuOUgxMTVWODJoNC4yVjc4Wk0xMTkuMSA4Mi4xSDExNXY0LjNoNC4yVjgyWk0xMTkuMSA4Ni40SDExNXY0LjJoNC4ydi00LjJaTTExOS4xIDk0LjlIMTE1Vjk5aDQuMlY5NVpNMTE5LjEgMTAzLjRIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgMTA3LjZIMTE1djQuM2g0LjJ2LTQuM1pNMTE5LjEgMTE2LjFIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTI0LjZIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTMzSDExNXY0LjNoNC4ydi00LjJaTTExOS4xIDEzNy4zSDExNXY0LjNoNC4ydi00LjNaTTEyMy40IDMxLjJIMTE5djQuMmg0LjN2LTQuMlpNMTIzLjQgMzUuNEgxMTl2NC4zaDQuM3YtNC4zWk0xMjMuNCA1Ni42SDExOVY2MWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgNjAuOUgxMTlWNjVoNC4zVjYxWk0xMjMuNCA5MC42SDExOVY5NWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgOTQuOUgxMTlWOTloNC4zVjk1Wk0xMjMuNCA5OS4xSDExOXY0LjNoNC4zVjk5Wk0xMjMuNCAxMjAuM0gxMTl2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMjMuNCAxMjQuNkgxMTl2NC4yaDQuM3YtNC4yWk0xMjcuNiAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNMTI3LjYgMzEuMmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTI3LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgNDMuOWgtNC4ydjQuMmg0LjJWNDRaTTEyNy42IDQ4LjFoLTQuMnY0LjNoNC4ydi00LjNaTTEyNy42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTI3LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTEyNy42IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDc3LjloLTQuMlY4Mmg0LjJWNzhaTTEyNy42IDgyLjFoLTQuMnY0LjNoNC4yVjgyWk0xMjcuNiA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTI3LjYgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyNy42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTMzaC00LjJ2NC4zaDQuMnYtNC4yWk0xMzEuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMzEuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTMxLjkgNzcuOWgtNC4zVjgyaDQuM1Y3OFpNMTMxLjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTMxLjkgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNMTMxLjkgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTEzMS45IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzEuOSAxMzNoLTQuM3Y0LjNoNC4zdi00LjJaTTEzMS45IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzYuMSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA1Ni42aC00LjJWNjFoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDYwLjloLTQuMlY2NWg0LjJWNjFaTTEzNi4xIDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk0xMzYuMSA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA5NC45aC00LjJWOTloNC4yVjk1Wk0xMzYuMSAxMDMuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTExLjloLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSAxMjQuNmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTE0MC4zIDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTQwLjMgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTE0MC4zIDczLjZoLTQuMlY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQwLjMgNzcuOWgtNC4yVjgyaDQuMlY3OFpNMTQwLjMgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQwLjMgOTQuOWgtNC4yVjk5aDQuMlY5NVpNMTQwLjMgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xNDAuMyAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ0LjYgNjAuOWgtNC4zVjY1aDQuM1Y2MVoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ0LjYgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDEwNy42aC00LjN2NC4zaDQuM3YtNC4zWk0xNDQuNiAxMTEuOWgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMTE2LjFoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNDQuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ4LjggNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggNjAuOWgtNC4yVjY1aDQuMlY2MVpNMTQ4LjggNzMuNmgtNC4yVjc4aDQuMnYtNC4zWk0xNDguOCA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNMTQ4LjggODYuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ4LjggOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNDguOCA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTQ4LjggMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ4LjggMTM3LjNoLTQuMnY0LjNoNC4ydi00LjNaTTE1MyA1Ni42aC00LjJWNjFoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE1MyA2MC45aC00LjJWNjVoNC4zVjYxWk0xNTMgNjUuMWgtNC4ydjQuM2g0LjNWNjVaTTE1MyA5NC45aC00LjJWOTloNC4zVjk1Wk0xNTMgOTkuMWgtNC4ydjQuM2g0LjNWOTlaTTE1MyAxMDMuNGgtNC4ydjQuMmg0LjN2LTQuMlpNMTUzIDEyMC4zaC00LjJ2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTMgMTI0LjZoLTQuMnY0LjJoNC4zdi00LjJaTTE1MyAxMzcuM2gtNC4ydjQuM2g0LjN2LTQuM1pNMTU3LjMgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTcuMyA2MC45aC00LjJWNjVoNC4yVjYxWk0xNTcuMyA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNMTU3LjMgOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNTcuMyAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTE1Ny4zIDEzM2gtNC4ydjQuM2g0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTYxLjYgNTYuNmgtNC4zVjYxaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA2MC45aC00LjNWNjVoNC4zVjYxWk0xNjEuNiA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTYxLjYgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWk0xNjEuNiA5MC42aC00LjNWOTVoNC4zdi00LjNaTTE2MS42IDEwMy40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMDcuNmgtNC4zdjQuM2g0LjN2LTQuM1pNMTYxLjYgMTExLjloLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTYxLjYgMTMzaC00LjN2NC4zaDQuM3YtNC4yWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2IpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTYxLjYgMTguNEgzOC40djEyMy4yaDEyMy4yVjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJjIiB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMjIuN2gtNC4zdjQuMmg0LjN2LTQuMlpNNDIuNyAyN2gtNC4zdjQuMmg0LjN2LTQuM1pNNDIuNyAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMzkuN2gtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDIuNyA0My45aC00LjN2NC4yaDQuM1Y0NFpNNDcgMTguNGgtNC4zdjQuM2g0LjJ2LTQuM1pNNDcgNDMuOWgtNC4zdjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTguNGgtNC4zdjQuM2g0LjN2LTQuM1pNNTEuMiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk01NS40IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk01OS43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTU5LjcgNDMuOWgtNC4zdjQuMmg0LjNWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYzLjkgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNNjMuOSA0My45aC00LjJ2NC4yaDQuMlY0NFpNNjguMSAxOC40SDY0djQuM2g0LjJ2LTQuM1pNNjguMSAyMi43SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAyN0g2NHY0LjJoNC4ydi00LjNaTTY4LjEgMzEuMkg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMzUuNEg2NHY0LjNoNC4ydi00LjNaTTY4LjEgMzkuN0g2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgNDMuOUg2NHY0LjJoNC4yVjQ0WiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2MpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNjguMSAxOC40SDM4LjR2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJkIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjQ2IiB5PSIyNiIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDI3aC00LjN2NC4yaDQuM3YtNC4zWk01MS4yIDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMzUuNGgtNC4zdjQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNNTUuNCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDM1LjRoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTU5LjcgMzEuMmgtNC4zdjQuMmg0LjN2LTQuMlpNNTkuNyAzNS40aC00LjN2NC4zaDQuM3YtNC4zWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2QpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNTkuNyAyN0g0Ni45djEyLjdoMTIuOFYyNi45WiIvPjwvZz48bWFzayBpZD0iZSIgd2lkdGg9IjMxIiBoZWlnaHQ9IjMxIiB4PSIxMzEiIHk9IjE4IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTEzNi4xIDIyLjdoLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMzYuMSAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMzYuMSAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTQwLjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQwLjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0NC42IDQzLjloLTQuM3Y0LjJoNC4zVjQ0Wk0xNDguOCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTUzIDE4LjRoLTQuMnY0LjNoNC4zdi00LjNaTTE1MyA0My45aC00LjJ2NC4yaDQuM1Y0NFpNMTU3LjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTU3LjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2MS42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE2MS42IDIyLjdoLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDI3aC00LjN2NC4yaDQuM3YtNC4zWk0xNjEuNiAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xNjEuNiAzNS40aC00LjN2NC4zaDQuM3YtNC4zWk0xNjEuNiAzOS43aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48L21hc2s+PGcgbWFzaz0idXJsKCNlKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE2MS42IDE4LjRoLTI5Ljd2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJmIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjE0MCIgeT0iMjYiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHN0eWxlPSJtYXNrLXR5cGU6bHVtaW5hbmNlIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTE0NC42IDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0OC44IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xNDguOCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNTMgMjdoLTQuMnY0LjJoNC4zdi00LjNaTTE1MyAzMS4yaC00LjJ2NC4yaDQuM3YtNC4yWk0xNTMgMzUuNGgtNC4ydjQuM2g0LjN2LTQuM1oiLz48L21hc2s+PGcgbWFzaz0idXJsKCNmKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE1MyAyN2gtMTIuN3YxMi43aDEyLjhWMjYuOVoiLz48L2c+PG1hc2sgaWQ9ImciIHdpZHRoPSIzMSIgaGVpZ2h0PSIzMSIgeD0iMzgiIHk9IjExMSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExNi4xaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEyNC42aC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyOC44aC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEzM2gtNC4zdjQuM2g0LjN2LTQuMlpNNDIuNyAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNNDcgMTExLjloLTQuM3Y0LjJoNC4ydi00LjJaTTQ3IDEzNy4zaC00LjN2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01MS4yIDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01NS40IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk01OS43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01OS43IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02My45IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk02OC4xIDExMS45SDY0djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNjguMSAxMTYuMUg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMTIwLjNINjR2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02OC4xIDEyNC42SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAxMjguOEg2NHY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgMTMzSDY0djQuM2g0LjJ2LTQuMlpNNjguMSAxMzcuM0g2NHY0LjNoNC4ydi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjZykiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik02OC4xIDExMS45SDM4LjR2MjkuN2gyOS43di0yOS43WiIvPjwvZz48bWFzayBpZD0iaCIgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiB4PSI0NiIgeT0iMTIwIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjaCkiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik01OS43IDEyMC4zSDQ2Ljl2MTIuOGgxMi44di0xMi44WiIvPjwvZz48L2c+PGRlZnM+PGNsaXBQYXRoIGlkPSJhIj48cmVjdCB3aWR0aD0iMTM3IiBoZWlnaHQ9IjEzNyIgeD0iMzEuNSIgeT0iMTEuNSIgZmlsbD0iI2ZmZiIgcng9IjguMiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg==", - "description": "Enables users to open the ThingsBoard mobile app by scanning a QR code from Home page.", - "descriptor": { - "type": "static", - "sizeX": 6, - "sizeY": 3, - "resources": [], - "templateHtml": "\n", - "templateCss": "", - "controllerScript": "self.onInit = function() {\n}", - "settingsSchema": "", - "dataKeySettingsSchema": "", - "settingsDirective": "", - "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Home page mobile app QR code\",\"dropShadow\":true}" - }, - "tags": [ - "ios", - "android", - "qr code", - "mobile application", - "home page" - ] -} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json index ccb9bea73f..37f7df0ae6 100644 --- a/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json +++ b/application/src/main/data/json/system/widget_types/mobile_app_qr_code.json @@ -1,5 +1,5 @@ { - "fqn": "cards.mobile_app_qr_code", + "fqn": "mobile_app_qr_code", "name": "Mobile app QR code", "deprecated": false, "image": "tb-image:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=:bW9iaWxlX2FwcF9xcl9jb2RlX3dpZGdldC5zdmc=;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0iI2ZmZiIgcng9IjQiLz48cmVjdCB3aWR0aD0iMTk5IiBoZWlnaHQ9IjE1OSIgeD0iLjUiIHk9Ii41IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIuMSIgcng9IjMuNSIvPjxnIGNsaXAtcGF0aD0idXJsKCNhKSI+PHJlY3Qgd2lkdGg9IjEzNyIgaGVpZ2h0PSIxMzciIHg9IjMxLjUiIHk9IjExLjUiIGZpbGw9IiNmZmYiIHJ4PSI4LjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTY4LjUgMTEuNWgtMTM3djEzN2gxMzd2LTEzN1oiLz48bWFzayBpZD0iYiIgd2lkdGg9IjEyNCIgaGVpZ2h0PSIxMjQiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDUyLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTQyLjcgNjAuOWgtNC4zVjY1aDQuM1Y2MVpNNDIuNyA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNNDIuNyA5NC45aC00LjNWOTloNC4zVjk1Wk00NyA1Ni42aC00LjNWNjFoNC4ydi00LjNaTTQ3IDgyLjFoLTQuM3Y0LjNoNC4yVjgyWk00NyA4Ni40aC00LjN2NC4yaDQuMnYtNC4yWk00NyA5NC45aC00LjNWOTloNC4yVjk1Wk00NyAxMDMuNGgtNC4zdjQuMmg0LjJ2LTQuMlpNNTEuMiA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDYwLjloLTQuM1Y2NWg0LjNWNjFaTTUxLjIgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDc3LjloLTQuM1Y4Mmg0LjNWNzhaTTUxLjIgODIuMWgtNC4zdjQuM2g0LjNWODJaTTUxLjIgOTAuNmgtNC4zVjk1aDQuM3YtNC4zWk01MS4yIDk5LjFoLTQuM3Y0LjNoNC4zVjk5Wk01NS40IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk01NS40IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWk01OS43IDY1LjFoLTQuM3Y0LjNoNC4zVjY1Wk01OS43IDY5LjRoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgODYuNGgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTkuNyA5MC42aC00LjNWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNNjMuOSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk02My45IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTYzLjkgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNjMuOSA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNNjguMSA1Mi40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA2MC45SDY0VjY1aDQuMlY2MVpNNjguMSA2OS40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA3Ny45SDY0VjgyaDQuMlY3OFpNNjguMSA4Ni40SDY0djQuMmg0LjJ2LTQuMlpNNjguMSA5NC45SDY0Vjk5aDQuMlY5NVpNNjguMSAxMDMuNEg2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcyLjQgNTIuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNjkuNEg2OHY0LjJoNC4zdi00LjJaTTcyLjQgNzcuOUg2OFY4Mmg0LjNWNzhaTTcyLjQgODIuMUg2OHY0LjNoNC4zVjgyWk03Mi40IDg2LjRINjh2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Mi40IDkwLjZINjhWOTVoNC4zdi00LjNaTTcyLjQgOTkuMUg2OHY0LjNoNC4zVjk5Wk03Mi40IDEwMy40SDY4djQuMmg0LjN2LTQuMlpNNzYuNiAyMi43aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDMxLjJoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNzYuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNNzYuNiA0OC4xaC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTc2LjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDYwLjloLTQuMlY2NWg0LjJWNjFaTTc2LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTc2LjYgNjkuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNNzYuNiA5NC45aC00LjJWOTloNC4yVjk1Wk03Ni42IDEwMy40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik03Ni42IDExNi4xaC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk03Ni42IDEyOC44aC00LjJ2NC4zaDQuMnYtNC4zWk03Ni42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk04MC45IDE4LjRoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSAyN2gtNC4zdjQuMkg4MXYtNC4zWk04MC45IDMxLjJoLTQuM3Y0LjJIODF2LTQuMlpNODAuOSAzNS40aC00LjN2NC4zSDgxdi00LjNaTTgwLjkgMzkuN2gtNC4zdjQuMkg4MXYtNC4yWk04MC45IDQ4LjFoLTQuM3Y0LjNIODF2LTQuM1pNODAuOSA2MC45aC00LjNWNjVIODFWNjFaTTgwLjkgNjUuMWgtNC4zdjQuM0g4MVY2NVpNODAuOSA3My42aC00LjNWNzhIODF2LTQuM1pNODAuOSA4Mi4xaC00LjN2NC4zSDgxVjgyWk04MC45IDk0LjloLTQuM1Y5OUg4MVY5NVpNODAuOSA5OS4xaC00LjN2NC4zSDgxVjk5Wk04MC45IDEwMy40aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTExLjloLTQuM3Y0LjJIODF2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODAuOSAxMTYuMWgtNC4zdjQuMkg4MXYtNC4yWk04MC45IDEyNC42aC00LjN2NC4ySDgxdi00LjJaTTgwLjkgMTM3LjNoLTQuM3Y0LjNIODF2LTQuM1pNODUuMSAxOC40SDgxdjQuM0g4NXYtNC4zWk04NS4xIDMxLjJIODF2NC4ySDg1di00LjJaTTg1LjEgMzUuNEg4MXY0LjNIODV2LTQuM1pNODUuMSA0My45SDgxdjQuMkg4NVY0NFpNODUuMSA1Ni42SDgxVjYxSDg1di00LjNaTTg1LjEgNzMuNkg4MVY3OEg4NXYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik04NS4xIDc3LjlIODFWODJIODVWNzhaTTg1LjEgODIuMUg4MXY0LjNIODVWODJaTTg1LjEgODYuNEg4MXY0LjJIODV2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODUuMSA5MC42SDgxVjk1SDg1di00LjNaTTg1LjEgOTkuMUg4MXY0LjNIODVWOTlaTTg1LjEgMTExLjlIODF2NC4ySDg1di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTg1LjEgMTE2LjFIODF2NC4ySDg1di00LjJaTTg1LjEgMTMzSDgxdjQuM0g4NXYtNC4yWk04OS40IDIyLjdIODV2NC4yaDQuM3YtNC4yWk04OS40IDMxLjJIODV2NC4yaDQuM3YtNC4yWk04OS40IDM1LjRIODV2NC4zaDQuM3YtNC4zWk04OS40IDQ4LjFIODV2NC4zaDQuM3YtNC4zWk04OS40IDUyLjRIODV2NC4yaDQuM3YtNC4yWk04OS40IDU2LjZIODVWNjFoNC4zdi00LjNaTTg5LjQgNjUuMUg4NXY0LjNoNC4zVjY1Wk04OS40IDEwMy40SDg1djQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMDcuNkg4NXY0LjNoNC4zdi00LjNaTTg5LjQgMTExLjlIODV2NC4yaDQuM3YtNC4yWk04OS40IDEyOC44SDg1djQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNODkuNCAxMzNIODV2NC4zaDQuM3YtNC4yWk05My42IDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTkzLjYgMjIuN2gtNC4ydjQuMmg0LjJ2LTQuMlpNOTMuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNOTMuNiAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk05My42IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk05My42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNOTMuNiA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNOTMuNiA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDczLjZoLTQuMlY3OGg0LjJ2LTQuM1pNOTMuNiA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNOTMuNiA4Ni40aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik05My42IDkwLjZoLTQuMlY5NWg0LjJ2LTQuM1pNOTMuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOTMuNiAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNOTcuOSAxOC40aC00LjN2NC4zSDk4di00LjNaTTk3LjkgMjIuN2gtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDM1LjRoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSA0OC4xaC00LjN2NC4zSDk4di00LjNaTTk3LjkgNTIuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDYwLjloLTQuM1Y2NUg5OFY2MVpNOTcuOSA2NS4xaC00LjN2NC4zSDk4VjY1Wk05Ny45IDczLjZoLTQuM1Y3OEg5OHYtNC4zWk05Ny45IDgyLjFoLTQuM3Y0LjNIOThWODJaTTk3LjkgODYuNGgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDk0LjloLTQuM1Y5OUg5OFY5NVpNOTcuOSA5OS4xaC00LjN2NC4zSDk4Vjk5Wk05Ny45IDEwMy40aC00LjN2NC4ySDk4di00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTk3LjkgMTA3LjZoLTQuM3Y0LjNIOTh2LTQuM1pNOTcuOSAxMTYuMWgtNC4zdjQuMkg5OHYtNC4yWk05Ny45IDEyNC42aC00LjN2NC4ySDk4di00LjJaTTk3LjkgMTMzaC00LjN2NC4zSDk4di00LjJaTTEwMi4xIDIyLjdIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA0My45SDk4djQuMmg0LjJWNDRaTTEwMi4xIDUyLjRIOTh2NC4yaDQuMnYtNC4yWk0xMDIuMSA2OS40SDk4djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgNzMuNkg5OFY3OGg0LjJ2LTQuM1pNMTAyLjEgOTAuNkg5OFY5NWg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAyLjEgOTQuOUg5OFY5OWg0LjJWOTVaTTEwMi4xIDEwMy40SDk4djQuMmg0LjJ2LTQuMlpNMTAyLjEgMTMzSDk4djQuM2g0LjJ2LTQuMlpNMTAyLjEgMTM3LjNIOTh2NC4zaDQuMnYtNC4zWk0xMDYuNCAxOC40SDEwMnY0LjNoNC4zdi00LjNaTTEwNi40IDIyLjdIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgNDguMUgxMDJ2NC4zaDQuM3YtNC4zWk0xMDYuNCA2MC45SDEwMlY2NWg0LjNWNjFaTTEwNi40IDczLjZIMTAyVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMDYuNCA3Ny45SDEwMlY4Mmg0LjNWNzhaTTEwNi40IDgyLjFIMTAydjQuM2g0LjNWODJaTTEwNi40IDg2LjRIMTAydjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTA2LjQgOTAuNkgxMDJWOTVoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEwNi40IDk0LjlIMTAyVjk5aDQuM1Y5NVpNMTA2LjQgMTAzLjRIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTE2LjFIMTAydjQuMmg0LjN2LTQuMlpNMTA2LjQgMTMzSDEwMnY0LjNoNC4zdi00LjJaTTEwNi40IDEzNy4zSDEwMnY0LjNoNC4zdi00LjNaTTExMC42IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMTAuNiAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTAuNiA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTEwLjYgNDguMWgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTEwLjYgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWk0xMTAuNiA5MC42aC00LjJWOTVoNC4ydi00LjNaTTExMC42IDk5LjFoLTQuMnY0LjNoNC4yVjk5Wk0xMTAuNiAxMDcuNmgtNC4ydjQuM2g0LjJ2LTQuM1pNMTEwLjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTExMC42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTAuNiAxMzNoLTQuMnY0LjNoNC4ydi00LjJaTTExMC42IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMTQuOSAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA0OC4xaC00LjN2NC4zaDQuM3YtNC4zWk0xMTQuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMTQuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTE0LjkgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSA3Ny45aC00LjNWODJoNC4zVjc4Wk0xMTQuOSA4Mi4xaC00LjN2NC4zaDQuM1Y4MlpNMTE0LjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgOTkuMWgtNC4zdjQuM2g0LjNWOTlaTTExNC45IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTQuOSAxMTYuMWgtNC4zdjQuMmg0LjN2LTQuMlpNMTE0LjkgMTMzaC00LjN2NC4zaDQuM3YtNC4yWk0xMTQuOSAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNMTE5LjEgMTguNEgxMTV2NC4zaDQuMnYtNC4zWk0xMTkuMSAzNS40SDExNXY0LjNoNC4ydi00LjNaTTExOS4xIDM5LjdIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNDMuOUgxMTV2NC4yaDQuMlY0NFpNMTE5LjEgNjUuMUgxMTV2NC4zaDQuMlY2NVpNMTE5LjEgNjkuNEgxMTV2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMTkuMSA3My42SDExNVY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgNzcuOUgxMTVWODJoNC4yVjc4Wk0xMTkuMSA4Mi4xSDExNXY0LjNoNC4yVjgyWk0xMTkuMSA4Ni40SDExNXY0LjJoNC4ydi00LjJaTTExOS4xIDk0LjlIMTE1Vjk5aDQuMlY5NVpNMTE5LjEgMTAzLjRIMTE1djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTE5LjEgMTA3LjZIMTE1djQuM2g0LjJ2LTQuM1pNMTE5LjEgMTE2LjFIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTI0LjZIMTE1djQuMmg0LjJ2LTQuMlpNMTE5LjEgMTMzSDExNXY0LjNoNC4ydi00LjJaTTExOS4xIDEzNy4zSDExNXY0LjNoNC4ydi00LjNaTTEyMy40IDMxLjJIMTE5djQuMmg0LjN2LTQuMlpNMTIzLjQgMzUuNEgxMTl2NC4zaDQuM3YtNC4zWk0xMjMuNCA1Ni42SDExOVY2MWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgNjAuOUgxMTlWNjVoNC4zVjYxWk0xMjMuNCA5MC42SDExOVY5NWg0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIzLjQgOTQuOUgxMTlWOTloNC4zVjk1Wk0xMjMuNCA5OS4xSDExOXY0LjNoNC4zVjk5Wk0xMjMuNCAxMjAuM0gxMTl2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMjMuNCAxMjQuNkgxMTl2NC4yaDQuM3YtNC4yWk0xMjcuNiAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNMTI3LjYgMzEuMmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTI3LjYgMzkuN2gtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgNDMuOWgtNC4ydjQuMmg0LjJWNDRaTTEyNy42IDQ4LjFoLTQuMnY0LjNoNC4ydi00LjNaTTEyNy42IDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTI3LjYgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTEyNy42IDY5LjRoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDc3LjloLTQuMlY4Mmg0LjJWNzhaTTEyNy42IDgyLjFoLTQuMnY0LjNoNC4yVjgyWk0xMjcuNiA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTI3LjYgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEyNy42IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTE2LjFoLTQuMnY0LjJoNC4ydi00LjJaTTEyNy42IDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xMjcuNiAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTI3LjYgMTMzaC00LjJ2NC4zaDQuMnYtNC4yWk0xMzEuOSA1Mi40aC00LjN2NC4yaDQuM3YtNC4yWk0xMzEuOSA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTMxLjkgNzcuOWgtNC4zVjgyaDQuM1Y3OFpNMTMxLjkgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTMxLjkgOTQuOWgtNC4zVjk5aDQuM1Y5NVpNMTMxLjkgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaTTEzMS45IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzEuOSAxMzNoLTQuM3Y0LjNoNC4zdi00LjJaTTEzMS45IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWk0xMzYuMSA1Mi40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA1Ni42aC00LjJWNjFoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDYwLjloLTQuMlY2NWg0LjJWNjFaTTEzNi4xIDY1LjFoLTQuMnY0LjNoNC4yVjY1Wk0xMzYuMSA2OS40aC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSA5NC45aC00LjJWOTloNC4yVjk1Wk0xMzYuMSAxMDMuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTExLjloLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSAxMjQuNmgtNC4ydjQuMmg0LjJ2LTQuMlpNMTM2LjEgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTE0MC4zIDUyLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDU2LjZoLTQuMlY2MWg0LjJ2LTQuM1pNMTQwLjMgNjUuMWgtNC4ydjQuM2g0LjJWNjVaTTE0MC4zIDczLjZoLTQuMlY3OGg0LjJ2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQwLjMgNzcuOWgtNC4yVjgyaDQuMlY3OFpNMTQwLjMgODYuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQwLjMgOTQuOWgtNC4yVjk5aDQuMlY5NVpNMTQwLjMgMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaTTE0MC4zIDEyMC4zaC00LjJ2NC4zaDQuMnYtNC4zWk0xNDAuMyAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ0LjYgNjAuOWgtNC4zVjY1aDQuM1Y2MVoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgODYuNGgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ0LjYgMTAzLjRoLTQuM3Y0LjJoNC4zdi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDEwNy42aC00LjN2NC4zaDQuM3YtNC4zWk0xNDQuNiAxMTEuOWgtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMTE2LjFoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNDQuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTQ4LjggNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggNjAuOWgtNC4yVjY1aDQuMlY2MVpNMTQ4LjggNzMuNmgtNC4yVjc4aDQuMnYtNC4zWk0xNDguOCA4Mi4xaC00LjJ2NC4zaDQuMlY4MlpNMTQ4LjggODYuNGgtNC4ydjQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ4LjggOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNDguOCA5OS4xaC00LjJ2NC4zaDQuMlY5OVpNMTQ4LjggMTAzLjRoLTQuMnY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEwNy42aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCAxMTEuOWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTQ4LjggMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0OC44IDEyNC42aC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAxMjguOGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQ4LjggMTM3LjNoLTQuMnY0LjNoNC4ydi00LjNaTTE1MyA1Ni42aC00LjJWNjFoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE1MyA2MC45aC00LjJWNjVoNC4zVjYxWk0xNTMgNjUuMWgtNC4ydjQuM2g0LjNWNjVaTTE1MyA5NC45aC00LjJWOTloNC4zVjk1Wk0xNTMgOTkuMWgtNC4ydjQuM2g0LjNWOTlaTTE1MyAxMDMuNGgtNC4ydjQuMmg0LjN2LTQuMlpNMTUzIDEyMC4zaC00LjJ2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTMgMTI0LjZoLTQuMnY0LjJoNC4zdi00LjJaTTE1MyAxMzcuM2gtNC4ydjQuM2g0LjN2LTQuM1pNMTU3LjMgNTIuNGgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgNTYuNmgtNC4yVjYxaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNTcuMyA2MC45aC00LjJWNjVoNC4yVjYxWk0xNTcuMyA2NS4xaC00LjJ2NC4zaDQuMlY2NVpNMTU3LjMgOTAuNmgtNC4yVjk1aDQuMnYtNC4zWk0xNTcuMyAxMTYuMWgtNC4ydjQuMmg0LjJ2LTQuMlpNMTU3LjMgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTE1Ny4zIDEzM2gtNC4ydjQuM2g0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTYxLjYgNTYuNmgtNC4zVjYxaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA2MC45aC00LjNWNjVoNC4zVjYxWk0xNjEuNiA2NS4xaC00LjN2NC4zaDQuM1Y2NVpNMTYxLjYgNzMuNmgtNC4zVjc4aDQuM3YtNC4zWk0xNjEuNiA5MC42aC00LjNWOTVoNC4zdi00LjNaTTE2MS42IDEwMy40aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMDcuNmgtNC4zdjQuM2g0LjN2LTQuM1pNMTYxLjYgMTExLjloLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiAxMjQuNmgtNC4zdjQuMmg0LjN2LTQuMlpNMTYxLjYgMTMzaC00LjN2NC4zaDQuM3YtNC4yWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2IpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTYxLjYgMTguNEgzOC40djEyMy4yaDEyMy4yVjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJjIiB3aWR0aD0iMzEiIGhlaWdodD0iMzEiIHg9IjM4IiB5PSIxOCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMjIuN2gtNC4zdjQuMmg0LjN2LTQuMlpNNDIuNyAyN2gtNC4zdjQuMmg0LjN2LTQuM1pNNDIuNyAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTQyLjcgMzkuN2gtNC4zdjQuMmg0LjN2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDIuNyA0My45aC00LjN2NC4yaDQuM1Y0NFpNNDcgMTguNGgtNC4zdjQuM2g0LjJ2LTQuM1pNNDcgNDMuOWgtNC4zdjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTguNGgtNC4zdjQuM2g0LjN2LTQuM1pNNTEuMiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk01NS40IDQzLjloLTQuMnY0LjJoNC4yVjQ0Wk01OS43IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTU5LjcgNDMuOWgtNC4zdjQuMmg0LjNWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTYzLjkgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNNjMuOSA0My45aC00LjJ2NC4yaDQuMlY0NFpNNjguMSAxOC40SDY0djQuM2g0LjJ2LTQuM1pNNjguMSAyMi43SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAyN0g2NHY0LjJoNC4ydi00LjNaTTY4LjEgMzEuMkg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMzUuNEg2NHY0LjNoNC4ydi00LjNaTTY4LjEgMzkuN0g2NHY0LjJoNC4ydi00LjJaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgNDMuOUg2NHY0LjJoNC4yVjQ0WiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2MpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNjguMSAxOC40SDM4LjR2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJkIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjQ2IiB5PSIyNiIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDI3aC00LjN2NC4yaDQuM3YtNC4zWk01MS4yIDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMzUuNGgtNC4zdjQuM2g0LjN2LTQuM1oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTUuNCAyN2gtNC4ydjQuMmg0LjJ2LTQuM1pNNTUuNCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDM1LjRoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTU5LjcgMzEuMmgtNC4zdjQuMmg0LjN2LTQuMlpNNTkuNyAzNS40aC00LjN2NC4zaDQuM3YtNC4zWiIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2QpIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNTkuNyAyN0g0Ni45djEyLjdoMTIuOFYyNi45WiIvPjwvZz48bWFzayBpZD0iZSIgd2lkdGg9IjMxIiBoZWlnaHQ9IjMxIiB4PSIxMzEiIHk9IjE4IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTEzNi4xIDE4LjRoLTQuMnY0LjNoNC4ydi00LjNaTTEzNi4xIDIyLjdoLTQuMnY0LjJoNC4ydi00LjJaTTEzNi4xIDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xMzYuMSAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xMzYuMSAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xMzYuMSAzOS43aC00LjJ2NC4yaDQuMnYtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMzYuMSA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTQwLjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTQwLjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0NC42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0NC42IDQzLjloLTQuM3Y0LjJoNC4zVjQ0Wk0xNDguOCAxOC40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNDguOCA0My45aC00LjJ2NC4yaDQuMlY0NFpNMTUzIDE4LjRoLTQuMnY0LjNoNC4zdi00LjNaTTE1MyA0My45aC00LjJ2NC4yaDQuM1Y0NFpNMTU3LjMgMTguNGgtNC4ydjQuM2g0LjJ2LTQuM1pNMTU3LjMgNDMuOWgtNC4ydjQuMmg0LjJWNDRaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2MS42IDE4LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE2MS42IDIyLjdoLTQuM3Y0LjJoNC4zdi00LjJaTTE2MS42IDI3aC00LjN2NC4yaDQuM3YtNC4zWk0xNjEuNiAzMS4yaC00LjN2NC4yaDQuM3YtNC4yWk0xNjEuNiAzNS40aC00LjN2NC4zaDQuM3YtNC4zWk0xNjEuNiAzOS43aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xNjEuNiA0My45aC00LjN2NC4yaDQuM1Y0NFoiLz48L21hc2s+PGcgbWFzaz0idXJsKCNlKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE2MS42IDE4LjRoLTI5Ljd2MjkuN2gyOS43VjE4LjRaIi8+PC9nPjxtYXNrIGlkPSJmIiB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHg9IjE0MCIgeT0iMjYiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHN0eWxlPSJtYXNrLXR5cGU6bHVtaW5hbmNlIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQ0LjYgMjdoLTQuM3Y0LjJoNC4zdi00LjNaTTE0NC42IDMxLjJoLTQuM3Y0LjJoNC4zdi00LjJaTTE0NC42IDM1LjRoLTQuM3Y0LjNoNC4zdi00LjNaTTE0OC44IDI3aC00LjJ2NC4yaDQuMnYtNC4zWk0xNDguOCAzMS4yaC00LjJ2NC4yaDQuMnYtNC4yWk0xNDguOCAzNS40aC00LjJ2NC4zaDQuMnYtNC4zWk0xNTMgMjdoLTQuMnY0LjJoNC4zdi00LjNaTTE1MyAzMS4yaC00LjJ2NC4yaDQuM3YtNC4yWk0xNTMgMzUuNGgtNC4ydjQuM2g0LjN2LTQuM1oiLz48L21hc2s+PGcgbWFzaz0idXJsKCNmKSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE1MyAyN2gtMTIuN3YxMi43aDEyLjhWMjYuOVoiLz48L2c+PG1hc2sgaWQ9ImciIHdpZHRoPSIzMSIgaGVpZ2h0PSIzMSIgeD0iMzgiIHk9IjExMSIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDExNi4xaC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyMC4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEyNC42aC00LjN2NC4yaDQuM3YtNC4yWk00Mi43IDEyOC44aC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00Mi43IDEzM2gtNC4zdjQuM2g0LjN2LTQuMlpNNDIuNyAxMzcuM2gtNC4zdjQuM2g0LjN2LTQuM1pNNDcgMTExLjloLTQuM3Y0LjJoNC4ydi00LjJaTTQ3IDEzNy4zaC00LjN2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01MS4yIDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01MS4yIDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01NS40IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk01NS40IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk01OS43IDExMS45aC00LjN2NC4yaDQuM3YtNC4yWk01OS43IDEzNy4zaC00LjN2NC4zaDQuM3YtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02My45IDExMS45aC00LjJ2NC4yaDQuMnYtNC4yWk02My45IDEzNy4zaC00LjJ2NC4zaDQuMnYtNC4zWk02OC4xIDExMS45SDY0djQuMmg0LjJ2LTQuMloiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNjguMSAxMTYuMUg2NHY0LjJoNC4ydi00LjJaTTY4LjEgMTIwLjNINjR2NC4zaDQuMnYtNC4zWiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik02OC4xIDEyNC42SDY0djQuMmg0LjJ2LTQuMlpNNjguMSAxMjguOEg2NHY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTY4LjEgMTMzSDY0djQuM2g0LjJ2LTQuMlpNNjguMSAxMzcuM0g2NHY0LjNoNC4ydi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjZykiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik02OC4xIDExMS45SDM4LjR2MjkuN2gyOS43di0yOS43WiIvPjwvZz48bWFzayBpZD0iaCIgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiB4PSI0NiIgeT0iMTIwIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBzdHlsZT0ibWFzay10eXBlOmx1bWluYW5jZSI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTUxLjIgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTUxLjIgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTIwLjNoLTQuMnY0LjNoNC4ydi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU1LjQgMTI0LjZoLTQuMnY0LjJoNC4ydi00LjJaTTU1LjQgMTI4LjhoLTQuMnY0LjNoNC4ydi00LjNaTTU5LjcgMTIwLjNoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTU5LjcgMTI0LjZoLTQuM3Y0LjJoNC4zdi00LjJaTTU5LjcgMTI4LjhoLTQuM3Y0LjNoNC4zdi00LjNaIi8+PC9tYXNrPjxnIG1hc2s9InVybCgjaCkiPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik01OS43IDEyMC4zSDQ2Ljl2MTIuOGgxMi44di0xMi44WiIvPjwvZz48L2c+PGRlZnM+PGNsaXBQYXRoIGlkPSJhIj48cmVjdCB3aWR0aD0iMTM3IiBoZWlnaHQ9IjEzNyIgeD0iMzEuNSIgeT0iMTEuNSIgZmlsbD0iI2ZmZiIgcng9IjguMiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg==", @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "", "settingsDirective": "tb-mobile-app-qr-code-widget-settings", - "defaultConfig": "{\n \"showTitle\": true,\n \"backgroundColor\": \"rgb(255, 255, 255)\",\n \"color\": \"rgba(0, 0, 0, 0.87)\",\n \"padding\": \"8px\",\n \"settings\": {\n \"useDefaultApp\": true,\n \"androidConfig\": {\n \"enabled\": true,\n \"appPackage\": \"\",\n \"sha256CertFingerprints\": \"\"\n },\n \"iosConfig\": {\n \"enabled\": true,\n \"appId\": \"\"\n },\n \"qrCodeConfig\": {\n \"badgeEnabled\": true,\n \"badgeStyle\": \"ORIGINAL\",\n \"badgePosition\": \"RIGHT\",\n \"qrCodeLabelEnabled\": true,\n \"qrCodeLabel\": \"Scan to connect or download mobile app\"\n }\n },\n \"title\": \"Mobile app QR code\",\n \"dropShadow\": true,\n \"enableFullscreen\": false,\n \"widgetStyle\": {},\n \"widgetCss\": \"\",\n \"pageSize\": 1024,\n \"noDataDisplayMessage\": \"\",\n \"showTitleIcon\": false,\n \"titleTooltip\": \"\",\n \"titleStyle\": {\n \"fontSize\": \"16px\",\n \"fontWeight\": 400\n }\n}" + "defaultConfig": "{\n \"showTitle\": true,\n \"backgroundColor\": \"rgb(255, 255, 255)\",\n \"color\": \"rgba(0, 0, 0, 0.87)\",\n \"padding\": \"8px\",\n \"settings\": {\n \"useSystemSettings\": true,\n \"androidConfig\": {\n \"enabled\": true\n },\n \"iosConfig\": {\n \"enabled\": true\n },\n \"qrCodeConfig\": {\n \"badgeEnabled\": true,\n \"badgeStyle\": \"ORIGINAL\",\n \"badgePosition\": \"RIGHT\",\n \"qrCodeLabelEnabled\": true,\n \"qrCodeLabel\": \"Scan to connect or download mobile app\"\n }\n },\n \"title\": \"Mobile app QR code\",\n \"dropShadow\": true,\n \"enableFullscreen\": false,\n \"widgetStyle\": {},\n \"widgetCss\": \"\",\n \"pageSize\": 1024,\n \"noDataDisplayMessage\": \"\",\n \"showTitleIcon\": false,\n \"titleTooltip\": \"\",\n \"titleStyle\": {\n \"fontSize\": \"16px\",\n \"fontWeight\": 400\n }\n}\n" }, "tags": [ "ios", diff --git a/ui-ngx/src/app/core/auth/auth.selectors.ts b/ui-ngx/src/app/core/auth/auth.selectors.ts index 0a97ef5665..2d84955936 100644 --- a/ui-ngx/src/app/core/auth/auth.selectors.ts +++ b/ui-ngx/src/app/core/auth/auth.selectors.ts @@ -83,7 +83,7 @@ export const selectMobileQrEnabled = createSelector( (state: AuthState) => state.mobileQrEnabled ); -export const selectPersistDeviceStateToTelemetryAndMobileQrEnabled = createSelector( +export const selectHomeDashboardParams = createSelector( selectPersistDeviceStateToTelemetry, selectMobileQrEnabled, (persistDeviceStateToTelemetry, mobileQrEnabled) => ({persistDeviceStateToTelemetry, mobileQrEnabled}) diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index bb4dc6c863..7b108e487c 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -18,7 +18,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; +import { MobileAppSettings } from '@shared/models/mobile-app.models'; @Injectable({ providedIn: 'root' @@ -30,15 +30,15 @@ export class MobileAppService { ) { } - public getMobileAppSettings(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + public getMobileAppSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); } - public saveMobileAppSettings(mobileAppSettings: MobileAppQRCodeSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); } - public getMobileAppDeepLink( config?: RequestConfig): Observable { + public getMobileAppDeepLink(config?: RequestConfig): Observable { return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts index 4027e0445b..1b280d68c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/mobile-app-qr-code-widget.models.ts @@ -22,23 +22,20 @@ import { QRCodeConfig } from '@shared/models/mobile-app.models'; -export type MobileAppQrCodeWidgetSettings = { - useDefaultApp: boolean; - androidConfig: AndroidConfig; - iosConfig: IosConfig; +export interface MobileAppQrCodeWidgetSettings { + useSystemSettings: boolean; + androidConfig: Pick; + iosConfig: Pick; qrCodeConfig: Omit; } export const mobileAppQrCodeWidgetDefaultSettings: MobileAppQrCodeWidgetSettings = { - useDefaultApp: true, + useSystemSettings: true, androidConfig: { - enabled: true, - appPackage: '', - sha256CertFingerprints: '' + enabled: true }, iosConfig: { - enabled: true, - appId: '' + enabled: true }, qrCodeConfig: { badgeEnabled: true, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html index 35092807dc..2c2623cc0c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.html @@ -15,16 +15,12 @@ limitations under the License. --> -
admin.mobile-app.connect-mobile-app
- -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss index d00baa119e..ddb8870d42 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.scss @@ -23,24 +23,6 @@ background-color: transparent; } -.tb-title { - padding-bottom: 12px; - align-self: start; - font-weight: 600; - font-size: 20px; - line-height: 24px; - letter-spacing: 0.1px; - color: rgba(0, 0, 0, 0.76); - - @media #{$mat-md-lg} { - padding-bottom: 0; - font-weight: 500; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.25px; - } -} - .tb-qrcode-label { font-size: 14px; color: rgba(0, 0, 0, 0.54); @@ -56,8 +38,3 @@ border-radius: 6px; } -.tb-badge-container { - @media #{$mat-md} { - display: none; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index 5085d43aec..0f450d47f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -14,97 +14,108 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; -import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppQRCodeSettings } from '@shared/models/mobile-app.models'; +import { BadgePosition, BadgeStyle, badgeStyleURLMap, MobileAppSettings } from '@shared/models/mobile-app.models'; import { MobileAppService } from '@core/http/mobile-app.service'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; -import { interval, mergeMap, Observable, Subject, takeUntil } from 'rxjs'; +import { Subject } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; -import { coerceBoolean } from '@shared/decorators/coercion'; import { MobileAppQrCodeWidgetSettings } from '@home/components/widget/lib/cards/mobile-app-qr-code-widget.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { ResizeObserver } from '@juggle/resize-observer'; @Component({ selector: 'tb-mobile-app-qrcode-widget', templateUrl: './mobile-app-qrcode-widget.component.html', styleUrls: ['./mobile-app-qrcode-widget.component.scss'] }) -export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { +export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnInit, OnDestroy { @Input() ctx: WidgetContext; @Input() - @coerceBoolean() - previewMode: boolean; - - @Input() - set mobileAppSettings(settings: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings) { + set mobileAppSettings(settings: MobileAppSettings | MobileAppQrCodeWidgetSettings) { if (settings) { this.mobileAppSettingsValue = settings; } }; - get mobileAppSettings() { + get mobileAppSettings(): MobileAppSettings | MobileAppQrCodeWidgetSettings { return this.mobileAppSettingsValue; } - @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + @ViewChild('canvas', {static: true}) canvasRef: ElementRef; private readonly destroy$ = new Subject(); + private widgetResize$: ResizeObserver; badgeStyle = BadgeStyle; badgePosition = BadgePosition; badgeStyleURLMap = badgeStyleURLMap; + showBadgeContainer = true; - private mobileAppSettingsValue: MobileAppQRCodeSettings | MobileAppQrCodeWidgetSettings; + private mobileAppSettingsValue: MobileAppSettings | MobileAppQrCodeWidgetSettings; private deepLinkTTL: number; + private deepLinkTTLTimeoutID: NodeJS.Timeout; constructor(protected store: Store, protected cd: ChangeDetectorRef, private mobileAppService: MobileAppService, - private utilsService: UtilsService) { + private utilsService: UtilsService, + private elementRef: ElementRef) { super(store); } ngOnInit(): void { - if (!this.previewMode) { - if (this.ctx) { + if (!this.mobileAppSettings) { + if (isDefinedAndNotNull(this.ctx.settings.useSystemSettings) && !this.ctx.settings.useSystemSettings) { this.mobileAppSettings = this.ctx.settings; } else { this.mobileAppService.getMobileAppSettings().subscribe((settings => { this.mobileAppSettings = settings; - this.cd.detectChanges(); + this.cd.markForCheck(); })); } } - } - - ngAfterViewInit(): void { - this.getMobileAppDeepLink().subscribe(link => { - this.deepLinkTTL = Number(this.utilsService.getQueryParam('ttl', link)) * MINUTE; - this.updateQRCode(link); - interval(this.deepLinkTTL).pipe( - takeUntil(this.destroy$), - mergeMap(() => this.getMobileAppDeepLink()) - ).subscribe(link => this.updateQRCode(link)); + this.initMobileAppQRCode(); + this.widgetResize$ = new ResizeObserver(() => { + const showHideBadgeContainer = this.elementRef.nativeElement.offsetWidth > 250; + if (showHideBadgeContainer !== this.showBadgeContainer) { + this.showBadgeContainer = showHideBadgeContainer; + this.cd.markForCheck(); + } }); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } super.ngOnDestroy(); this.destroy$.next(); this.destroy$.complete(); + clearTimeout(this.deepLinkTTLTimeoutID); } - getMobileAppDeepLink(): Observable { - return this.mobileAppService.getMobileAppDeepLink(); + private initMobileAppQRCode() { + if (this.deepLinkTTLTimeoutID) { + clearTimeout(this.deepLinkTTLTimeoutID); + this.deepLinkTTLTimeoutID = null; + } + this.mobileAppService.getMobileAppDeepLink().subscribe(link => { + this.deepLinkTTL = Number(this.utilsService.getQueryParam('ttl', link)) * MINUTE; + this.updateQRCode(link); + this.deepLinkTTLTimeoutID = setTimeout(() => this.initMobileAppQRCode(), this.deepLinkTTL); + }); } - updateQRCode(link: string) { + private updateQRCode(link: string) { import('qrcode').then((QRCode) => { QRCode.toCanvas(this.canvasRef.nativeElement, link, { width: 100 }); }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html index d474714216..4d930347c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.html @@ -15,88 +15,36 @@ limitations under the License. --> -
-
-
-
admin.mobile-app.applications
- - {{ 'admin.mobile-app.default' | translate }} - {{ 'admin.mobile-app.custom' | translate }} - +
+
+
+ + {{ 'admin.mobile-app.use-system-settings' | translate }} +
-
+
+
+
admin.mobile-app.applications
+
- + {{ 'admin.mobile-app.android' | translate }}
-
-
-
{{ 'admin.mobile-app.app-package-name' | translate }}
- - - - warning - - -
-
-
-
-
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
- - - - warning - - -
-
- + {{ 'admin.mobile-app.ios' | translate }}
-
-
-
{{ 'admin.mobile-app.app-id' | translate }}
- - - - warning - - -
-
-
+
+
admin.mobile-app.appearance
- {{ 'admin.mobile-app.badges' | translate }} @@ -118,7 +66,7 @@
- + {{ 'admin.mobile-app.label' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss deleted file mode 100644 index d0a2c9ac16..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.scss +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -.tb-fixed-width { - min-width: 230px; -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts index 664dbe3327..bcb70518f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/mobile-app-qr-code-widget-settings.component.ts @@ -25,7 +25,7 @@ import { mobileAppQrCodeWidgetDefaultSettings } from '@home/components/widget/li @Component({ selector: 'tb-mobile-app-qr-code-widget-settings', templateUrl: './mobile-app-qr-code-widget-settings.component.html', - styleUrls: ['/mobile-app-qr-code-widget-settings.component.scss', './../widget-settings.scss'] + styleUrls: ['./../widget-settings.scss'] }) export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsComponent { @@ -49,15 +49,12 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon protected onSettingsSet(settings: WidgetSettings) { this.mobileAppQRCodeWidgetSettingsForm = this.fb.group({ - useDefaultApp: [settings.useDefaultApp], + useSystemSettings: [settings.useSystemSettings], androidConfig: this.fb.group({ enabled: [settings.androidConfig.enabled], - appPackage: [settings.androidConfig.appPackage, [Validators.required]], - sha256CertFingerprints: [settings.androidConfig.sha256CertFingerprints, [Validators.required]] }), iosConfig: this.fb.group({ enabled: [settings.iosConfig.enabled], - appId: [settings.iosConfig.appId, [Validators.required]] }), qrCodeConfig: this.fb.group({ badgeEnabled: [settings.qrCodeConfig.badgeEnabled], @@ -70,35 +67,16 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon } protected validatorTriggers(): string[] { - return ['useDefaultApp', 'androidConfig.enabled', 'iosConfig.enabled', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled']; + return ['useSystemSettings', 'androidConfig.enabled', 'iosConfig.enabled', 'qrCodeConfig.badgeEnabled', 'qrCodeConfig.qrCodeLabelEnabled']; } protected updateValidators(emitEvent: boolean) { - const useDefaultApp = this.mobileAppQRCodeWidgetSettingsForm.get('useDefaultApp').value; + const useSystemSettings = this.mobileAppQRCodeWidgetSettingsForm.get('useSystemSettings').value; const androidEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.enabled').value; const iosEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.enabled').value; const badgeEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').value; const qrCodeLabelEnabled = this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabelEnabled').value; - if (useDefaultApp) { - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); - } else { - if (androidEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').enable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').enable({emitEvent: false}); - } else { - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.appPackage').disable({emitEvent: false}); - this.mobileAppQRCodeWidgetSettingsForm.get('androidConfig.sha256CertFingerprints').disable({emitEvent: false}); - } - if (iosEnabled) { - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').enable({emitEvent: false}); - } else { - this.mobileAppQRCodeWidgetSettingsForm.get('iosConfig.appId').disable({emitEvent: false}); - } - } - if (!androidEnabled && !iosEnabled) { this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeEnabled').disable({emitEvent: false}); this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.badgeStyle').disable({emitEvent: false}); @@ -116,7 +94,7 @@ export class MobileAppQrCodeWidgetSettingsComponent extends WidgetSettingsCompon } } - if (qrCodeLabelEnabled) { + if (qrCodeLabelEnabled && !useSystemSettings) { this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').enable({emitEvent: false}); } else { this.mobileAppQRCodeWidgetSettingsForm.get('qrCodeConfig.qrCodeLabel').disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html index 88c6c287d5..0d5de08de0 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.html @@ -43,7 +43,7 @@
-
{{ 'admin.mobile-app.app-package-name' | translate }}
+
{{ 'admin.mobile-app.app-package-name' | translate }}
-
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
+
{{ 'admin.mobile-app.sha256-certificate-fingerprints' | translate }}
-
{{ 'admin.mobile-app.app-id' | translate }}
+
{{ 'admin.mobile-app.app-id' | translate }}
- {{ 'admin.mobile-app.badges' | translate }} @@ -135,7 +135,7 @@
- + {{ 'admin.mobile-app.label' | translate }} @@ -154,7 +154,7 @@
admin.mobile-app.preview
-
diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss index 5f04c04e8f..40bffb8ca6 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.scss @@ -14,10 +14,6 @@ * limitations under the License. */ -.tb-fixed-width { - min-width: 230px; -} - .tb-qrcode-preview { background-color: rgba(0, 0, 0, 0.04); box-shadow: none; diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index 96e8e89e6a..0423ce989f 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -27,7 +27,7 @@ import { badgePositionTranslationsMap, BadgeStyle, badgeStyleTranslationsMap, - MobileAppQRCodeSettings + MobileAppSettings } from '@shared/models/mobile-app.models'; @Component({ @@ -39,7 +39,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf mobileAppSettingsForm: FormGroup; - mobileAppSettings: MobileAppQRCodeSettings; + mobileAppSettings: MobileAppSettings; private readonly destroy$ = new Subject(); @@ -133,7 +133,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf }); } - private processMobileAppSettings(mobileAppSettings: MobileAppQRCodeSettings): void { + private processMobileAppSettings(mobileAppSettings: MobileAppSettings): void { this.mobileAppSettings = {...mobileAppSettings}; this.mobileAppSettingsForm.reset(this.mobileAppSettings); } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index 9c6adf91ca..b141600732 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -27,8 +27,9 @@ import { AppState } from '@core/core.state'; import { map } from 'rxjs/operators'; import { getCurrentAuthUser, + selectHomeDashboardParams, selectMobileQrEnabled, - selectPersistDeviceStateToTelemetryAndMobileQrEnabled + selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors'; import { EntityKeyType } from '@shared/models/query/query.models'; import { ResourcesService } from '@core/services/resources.service'; @@ -37,9 +38,42 @@ const sysAdminHomePageJson = '/assets/dashboard/sys_admin_home_page.json'; const tenantAdminHomePageJson = '/assets/dashboard/tenant_admin_home_page.json'; const customerUserHomePageJson = '/assets/dashboard/customer_user_home_page.json'; -const updateDeviceActivityKeyFilterIfNeeded = (store: Store, - dashboard$: Observable): Observable => - store.pipe(select(selectPersistDeviceStateToTelemetryAndMobileQrEnabled)).pipe( +const getHomeDashboard = (store: Store, resourcesService: ResourcesService) => { + const authority = getCurrentAuthUser(store).authority; + switch (authority) { + case Authority.SYS_ADMIN: + return applySystemParametersToHomeDashboard(store, resourcesService.loadJsonResource(sysAdminHomePageJson), authority); + case Authority.TENANT_ADMIN: + return applySystemParametersToHomeDashboard(store, resourcesService.loadJsonResource(tenantAdminHomePageJson), authority); + case Authority.CUSTOMER_USER: + return applySystemParametersToHomeDashboard(store, resourcesService.loadJsonResource(customerUserHomePageJson), authority); + default: + return of(null); + } +}; + +const applySystemParametersToHomeDashboard = (store: Store, + dashboard$: Observable, + authority: Authority): Observable => { + let selectParams$: Observable<{persistDeviceStateToTelemetry?: boolean, mobileQrEnabled?: boolean}>; + switch (authority) { + case Authority.SYS_ADMIN: + selectParams$ = store.pipe( + select(selectMobileQrEnabled), + map(mobileQrEnabled => ({mobileQrEnabled})) + ); + break; + case Authority.TENANT_ADMIN: + selectParams$ = store.pipe(select(selectHomeDashboardParams)); + break; + case Authority.CUSTOMER_USER: + selectParams$ = store.pipe( + select(selectPersistDeviceStateToTelemetry), + map(persistDeviceStateToTelemetry => ({persistDeviceStateToTelemetry})) + ); + break; + } + return selectParams$.pipe( mergeMap((params) => dashboard$.pipe( map((dashboard) => { if (params.persistDeviceStateToTelemetry) { @@ -49,31 +83,21 @@ const updateDeviceActivityKeyFilterIfNeeded = (store: Store, } } } - return params.mobileQrEnabled ? toggleMobileQRCodeDisplay(dashboard) : dashboard; - }) - )) - ); - -const toggleMobileQRCodeDisplayIfNeeded = (store: Store, - dashboard$: Observable): Observable => - store.pipe(select(selectMobileQrEnabled)).pipe( - mergeMap((mobileQrEnabled) => dashboard$.pipe( - map((dashboard) => { - return mobileQrEnabled ? toggleMobileQRCodeDisplay(dashboard) : dashboard; + if (params.mobileQrEnabled) { + for (const widgetId of Object.keys(dashboard.configuration.widgets)) { + if (dashboard.configuration.widgets[widgetId].config.title === 'Select show mobile QR code') { + dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction = + (dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction as string) + .replace('\'${mobileQrEnabled}\'', String(true)); + } + } + } + dashboard.hideDashboardToolbar = true; + return dashboard; }) )) ); - -const toggleMobileQRCodeDisplay = (dashboard: HomeDashboard) => { - for (const widgetId of Object.keys(dashboard.configuration.widgets)) { - if (dashboard.configuration.widgets[widgetId].config.title === 'Select show mobile QR code') { - dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction = - (dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction as string) - .replace('\'${mobileQrEnabled}\'', String(true)); - } - } - return dashboard; -} +}; export const homeDashboardResolver: ResolveFn = ( route: ActivatedRouteSnapshot, @@ -85,27 +109,7 @@ export const homeDashboardResolver: ResolveFn = ( dashboardService.getHomeDashboard().pipe( mergeMap((dashboard) => { if (!dashboard) { - let dashboard$: Observable; - const authority = getCurrentAuthUser(store).authority; - switch (authority) { - case Authority.SYS_ADMIN: - dashboard$ = toggleMobileQRCodeDisplayIfNeeded(store, resourcesService.loadJsonResource(sysAdminHomePageJson)); - break; - case Authority.TENANT_ADMIN: - dashboard$ = updateDeviceActivityKeyFilterIfNeeded(store, resourcesService.loadJsonResource(tenantAdminHomePageJson)); - break; - case Authority.CUSTOMER_USER: - dashboard$ = updateDeviceActivityKeyFilterIfNeeded(store, resourcesService.loadJsonResource(customerUserHomePageJson)); - break; - } - if (dashboard$) { - return dashboard$.pipe( - map((homeDashboard) => { - homeDashboard.hideDashboardToolbar = true; - return homeDashboard; - }) - ); - } + return getHomeDashboard(store, resourcesService); } return of(dashboard); }) diff --git a/ui-ngx/src/app/shared/models/mobile-app.models.ts b/ui-ngx/src/app/shared/models/mobile-app.models.ts index 02139bd913..262cc34f9e 100644 --- a/ui-ngx/src/app/shared/models/mobile-app.models.ts +++ b/ui-ngx/src/app/shared/models/mobile-app.models.ts @@ -14,10 +14,9 @@ /// limitations under the License. /// -import { TenantId } from '@shared/models/id/tenant-id'; +import { HasTenantId } from '@shared/models/entity.models'; -export interface MobileAppQRCodeSettings { - tenantId: TenantId; +export interface MobileAppSettings extends HasTenantId { useDefaultApp: boolean; androidConfig: AndroidConfig; iosConfig: IosConfig; diff --git a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json index 4984de6e29..24bac97279 100644 --- a/ui-ngx/src/assets/dashboard/sys_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/sys_admin_home_page.json @@ -2634,22 +2634,24 @@ "typeFullFqn": "system.home_page_widgets.getting_started" }, "53d5f2dd-d432-5362-745f-63bf4487dc96": { - "typeFullFqn": "system.home_page_widgets.home_page_mobile_app_qr_code", + "typeFullFqn": "system.mobile_app_qr_code", "type": "static", "sizeX": 6, "sizeY": 3, "config": { - "showTitle": false, + "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", "padding": "8px", "settings": {}, - "title": "Mobile app QR code", + "title": "{i18n:admin.mobile-app.connect-mobile-app}", "dropShadow": false, "enableFullscreen": false, "widgetStyle": {}, - "widgetCss": "", - "noDataDisplayMessage": "" + "widgetCss": ".tb-widget-container > .tb-widget > .tb-widget-header > .tb-widget-title {\n padding: 0;\n}\n\n.tb-widget-container > .tb-widget > .tb-widget-header > .tb-widget-title > .title-row > .title {\n padding-bottom: 12px;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-header > .tb-widget-title > .title-row > .title {\n padding-bottom: 0;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n }\n}\n", + "showTitleIcon": false, + "titleTooltip": "", + "titleStyle": null }, "row": 0, "col": 0, diff --git a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json index 6c1bed741e..40792caf09 100644 --- a/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json +++ b/ui-ngx/src/assets/dashboard/tenant_admin_home_page.json @@ -1177,19 +1177,24 @@ "typeFullFqn": "system.home_page_widgets.getting_started" }, "8c5207d8-7103-4dc2-db48-d61c0c8c61fd": { - "typeFullFqn": "system.home_page_widgets.home_page_mobile_app_qr_code", + "typeFullFqn": "system.mobile_app_qr_code", "type": "static", "sizeX": 6, "sizeY": 3, "config": { - "showTitle": false, + "showTitle": true, "backgroundColor": "rgb(255, 255, 255)", "color": "rgba(0, 0, 0, 0.87)", "padding": "8px", "settings": {}, - "title": "Mobile app QR code", + "title": "{i18n:admin.mobile-app.connect-mobile-app}", "dropShadow": false, - "enableFullscreen": false + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": ".tb-widget-container > .tb-widget > .tb-widget-header > .tb-widget-title {\n padding: 0;\n}\n\n.tb-widget-container > .tb-widget > .tb-widget-header > .tb-widget-title > .title-row > .title {\n padding-bottom: 12px;\n font-weight: 600;\n font-size: 20px;\n line-height: 24px;\n letter-spacing: 0.1px;\n color: rgba(0, 0, 0, 0.76);\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-widget-container > .tb-widget > .tb-widget-header > .tb-widget-title > .title-row > .title {\n padding-bottom: 0;\n font-weight: 500;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.25px;\n }\n}\n", + "showTitleIcon": false, + "titleTooltip": "", + "titleStyle": null }, "row": 0, "col": 0, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d79b468a8e..c2fb5e2a1c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -433,6 +433,7 @@ "ios": "iOS", "app-id": "App ID", "app-id-required": "App ID is required", + "appearance": "Appearance", "appearance-on-home-page": "Appearance on Home page", "enabled": "Enabled", "disabled": "Disabled", @@ -445,7 +446,8 @@ "left": "Left", "set": "Set", "preview": "Preview", - "connect-mobile-app": "Connect mobile app" + "connect-mobile-app": "Connect mobile app", + "use-system-settings": "Use system settings" }, "2fa": { "2fa": "Two-factor authentication", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index df9211720c..f38d459b97 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -234,6 +234,12 @@ } .fixed-title-width { min-width: 200px; + + &-230 { + min-width: 230px; + } + } + [class^="fixed-title-width"] { @media #{$mat-xs} { min-width: fit-content; } From 4daf53bf25af545789b83ffbe8c98f0aa03e911b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 16 May 2024 11:52:41 +0300 Subject: [PATCH 105/121] Lazy Jpa bootstrap mode --- .../thingsboard/server/dao/JpaDaoConfig.java | 3 +- .../server/dao/SqlTimeseriesDaoConfig.java | 4 +- .../server/dao/SqlTsDaoConfig.java | 3 +- .../server/dao/SqlTsLatestDaoConfig.java | 3 +- .../server/dao/TimescaleDaoConfig.java | 3 +- .../dao/TimescaleTsLatestDaoConfig.java | 3 +- .../dao/service/JpaRepositoriesTest.java | 42 +++++++++++++++++++ 7 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java index 05becc8bde..4dd0633420 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/JpaDaoConfig.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.thingsboard.server.dao.util.TbAutoConfiguration; @@ -28,7 +29,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration; @Configuration @TbAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sql", "org.thingsboard.server.dao.attributes", "org.thingsboard.server.dao.cache", "org.thingsboard.server.cache"}) -@EnableJpaRepositories("org.thingsboard.server.dao.sql") +@EnableJpaRepositories(value = "org.thingsboard.server.dao.sql", bootstrapMode = BootstrapMode.LAZY) @EntityScan("org.thingsboard.server.dao.model.sql") @EnableTransactionManagement public class JpaDaoConfig { diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java index 9cf9532aea..09553fb973 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTimeseriesDaoConfig.java @@ -19,14 +19,14 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.thingsboard.server.dao.util.SqlTsOrTsLatestAnyDao; import org.thingsboard.server.dao.util.TbAutoConfiguration; @Configuration @TbAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.dictionary"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.dictionary"}) +@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.dictionary"}, bootstrapMode = BootstrapMode.LAZY) @EntityScan({"org.thingsboard.server.dao.model.sqlts.dictionary"}) @EnableTransactionManagement public class SqlTimeseriesDaoConfig { diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java index 6bdf7c2170..bbd31846db 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTsDaoConfig.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.thingsboard.server.dao.util.SqlTsDao; import org.thingsboard.server.dao.util.TbAutoConfiguration; @@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration; @Configuration @TbAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.sql", "org.thingsboard.server.dao.sqlts.insert.sql"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.sql"}) +@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.ts", "org.thingsboard.server.dao.sqlts.insert.sql"}, bootstrapMode = BootstrapMode.LAZY) @EntityScan({"org.thingsboard.server.dao.model.sqlts.ts"}) @EnableTransactionManagement @SqlTsDao diff --git a/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java index 1793c4ed76..6fcf21a0a8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/SqlTsLatestDaoConfig.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.thingsboard.server.dao.util.SqlTsLatestDao; import org.thingsboard.server.dao.util.TbAutoConfiguration; @@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TbAutoConfiguration; @Configuration @TbAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.sql"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}) +@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}, bootstrapMode = BootstrapMode.LAZY) @EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"}) @EnableTransactionManagement @SqlTsLatestDao diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java index 16617c12a8..c134b88897 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/TimescaleDaoConfig.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.thingsboard.server.dao.util.TbAutoConfiguration; import org.thingsboard.server.dao.util.TimescaleDBTsDao; @@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsDao; @Configuration @TbAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.timescale", "org.thingsboard.server.dao.sqlts.insert.timescale"}) +@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.timescale", "org.thingsboard.server.dao.sqlts.insert.timescale"}, bootstrapMode = BootstrapMode.LAZY) @EntityScan({"org.thingsboard.server.dao.model.sqlts.timescale"}) @EnableTransactionManagement @TimescaleDBTsDao diff --git a/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java b/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java index f0e6fc362b..f0e74f1c3d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/TimescaleTsLatestDaoConfig.java @@ -19,6 +19,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.thingsboard.server.dao.util.TbAutoConfiguration; import org.thingsboard.server.dao.util.TimescaleDBTsLatestDao; @@ -26,7 +27,7 @@ import org.thingsboard.server.dao.util.TimescaleDBTsLatestDao; @Configuration @TbAutoConfiguration @ComponentScan({"org.thingsboard.server.dao.sqlts.timescale"}) -@EnableJpaRepositories({"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}) +@EnableJpaRepositories(value = {"org.thingsboard.server.dao.sqlts.insert.latest.sql", "org.thingsboard.server.dao.sqlts.latest"}, bootstrapMode = BootstrapMode.LAZY) @EntityScan({"org.thingsboard.server.dao.model.sqlts.latest"}) @EnableTransactionManagement @TimescaleDBTsLatestDao diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java new file mode 100644 index 0000000000..a822401aeb --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +@Slf4j +@DaoSqlTest +public class JpaRepositoriesTest extends AbstractServiceTest { + + @Autowired + List> repositories; + + /* + * Verifying that all the repositories are successfully bootstrapped, when using Lazy Jpa bootstrap mode + * */ + @Test + public void testRepositories() { + for (var repository : repositories) { + repository.count(); + } + } + +} From 45672d99885220f69a65b61cd7e67589a6798db5 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 16 May 2024 11:57:28 +0300 Subject: [PATCH 106/121] Move repositories test to EntityDaoRegistryTest --- .../dao/service/EntityDaoRegistryTest.java | 15 +++++++ .../dao/service/JpaRepositoriesTest.java | 42 ------------------- 2 files changed, 15 insertions(+), 42 deletions(-) delete mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java index 8524efc4a0..30110a2741 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityDaoRegistryTest.java @@ -19,11 +19,13 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.entity.EntityDaoRegistry; +import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +39,9 @@ public class EntityDaoRegistryTest extends AbstractServiceTest { @Autowired EntityDaoRegistry entityDaoRegistry; + @Autowired + List> repositories; + @Test public void givenAllEntityTypes_whenGetDao_thenAllPresent() { for (EntityType entityType : EntityType.values()) { @@ -73,4 +78,14 @@ public class EntityDaoRegistryTest extends AbstractServiceTest { } } + /* + * Verifying that all the repositories are successfully bootstrapped, when using Lazy Jpa bootstrap mode + * */ + @Test + public void testJpaRepositories() { + for (var repository : repositories) { + repository.count(); + } + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java deleted file mode 100644 index a822401aeb..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/JpaRepositoriesTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2024 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service; - -import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -@Slf4j -@DaoSqlTest -public class JpaRepositoriesTest extends AbstractServiceTest { - - @Autowired - List> repositories; - - /* - * Verifying that all the repositories are successfully bootstrapped, when using Lazy Jpa bootstrap mode - * */ - @Test - public void testRepositories() { - for (var repository : repositories) { - repository.count(); - } - } - -} From e8d14f37cb012243ebe5033dde274674c7d33435 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 16 May 2024 13:24:49 +0300 Subject: [PATCH 107/121] Improvements for Kafka admin creation --- .../server/queue/kafka/TbKafkaAdmin.java | 45 +++++++++++-------- .../kafka/TbKafkaConsumerStatsService.java | 12 ++--- .../server/queue/kafka/TbKafkaSettings.java | 35 ++++++++++++--- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index bf5555817d..a5a0427606 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -16,7 +16,6 @@ package org.thingsboard.server.queue.kafka; import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.CreateTopicsResult; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.common.errors.TopicExistsException; @@ -35,23 +34,17 @@ import java.util.concurrent.ExecutionException; @Slf4j public class TbKafkaAdmin implements TbQueueAdmin { - private final AdminClient client; + private final TbKafkaSettings settings; private final Map topicConfigs; - private final Set topics = ConcurrentHashMap.newKeySet(); private final int numPartitions; + private volatile Set topics; private final short replicationFactor; public TbKafkaAdmin(TbKafkaSettings settings, Map topicConfigs) { - client = AdminClient.create(settings.toAdminProps()); + this.settings = settings; this.topicConfigs = topicConfigs; - try { - topics.addAll(client.listTopics().names().get()); - } catch (InterruptedException | ExecutionException e) { - log.error("Failed to get all topics.", e); - } - String numPartitionsStr = topicConfigs.get(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING); if (numPartitionsStr != null) { numPartitions = Integer.parseInt(numPartitionsStr); @@ -64,6 +57,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { @Override public void createTopicIfNotExists(String topic, String properties) { + Set topics = getTopics(); if (topics.contains(topic)) { return; } @@ -86,12 +80,13 @@ public class TbKafkaAdmin implements TbQueueAdmin { @Override public void deleteTopic(String topic) { + Set topics = getTopics(); if (topics.contains(topic)) { - client.deleteTopics(Collections.singletonList(topic)); + settings.getAdminClient().deleteTopics(Collections.singletonList(topic)); } else { try { - if (client.listTopics().names().get().contains(topic)) { - client.deleteTopics(Collections.singletonList(topic)); + if (settings.getAdminClient().listTopics().names().get().contains(topic)) { + settings.getAdminClient().deleteTopics(Collections.singletonList(topic)); } else { log.warn("Kafka topic [{}] does not exist.", topic); } @@ -101,14 +96,28 @@ public class TbKafkaAdmin implements TbQueueAdmin { } } - @Override - public void destroy() { - if (client != null) { - client.close(); + private Set getTopics() { + if (topics == null) { + synchronized (this) { + if (topics == null) { + topics = ConcurrentHashMap.newKeySet(); + try { + topics.addAll(settings.getAdminClient().listTopics().names().get()); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to get all topics.", e); + } + } + } } + return topics; } public CreateTopicsResult createTopic(NewTopic topic) { - return client.createTopics(Collections.singletonList(topic)); + return settings.getAdminClient().createTopics(Collections.singletonList(topic)); + } + + @Override + public void destroy() { } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java index dbc1b7b7c4..56440cf4a5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java @@ -15,11 +15,12 @@ */ package org.thingsboard.server.queue.kafka; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; @@ -35,8 +36,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.queue.discovery.PartitionService; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -62,7 +61,6 @@ public class TbKafkaConsumerStatsService { @Autowired private PartitionService partitionService; - private AdminClient adminClient; private Consumer consumer; private ScheduledExecutorService statsPrintScheduler; @@ -71,7 +69,6 @@ public class TbKafkaConsumerStatsService { if (!statsConfig.getEnabled()) { return; } - this.adminClient = AdminClient.create(kafkaSettings.toAdminProps()); this.statsPrintScheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("kafka-consumer-stats")); Properties consumerProps = kafkaSettings.toConsumerProps(null); @@ -90,7 +87,7 @@ public class TbKafkaConsumerStatsService { } for (String groupId : monitoredGroups) { try { - Map groupOffsets = adminClient.listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata() + Map groupOffsets = kafkaSettings.getAdminClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata() .get(statsConfig.getKafkaResponseTimeoutMs(), TimeUnit.MILLISECONDS); Map endOffsets = consumer.endOffsets(groupOffsets.keySet(), timeoutDuration); @@ -157,9 +154,6 @@ public class TbKafkaConsumerStatsService { if (statsPrintScheduler != null) { statsPrintScheduler.shutdownNow(); } - if (adminClient != null) { - adminClient.close(); - } if (consumer != null) { consumer.close(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 96fa7afb7f..ebbc6fac7e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -19,6 +19,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; @@ -34,6 +35,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.TbProperty; import org.thingsboard.server.queue.util.PropertyUtils; +import javax.annotation.PreDestroy; import java.util.Collections; import java.util.List; import java.util.Map; @@ -143,13 +145,7 @@ public class TbKafkaSettings { @Setter private Map> consumerPropertiesPerTopic = Collections.emptyMap(); - public Properties toAdminProps() { - Properties props = toProps(); - props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers); - props.put(AdminClientConfig.RETRIES_CONFIG, retries); - - return props; - } + private volatile AdminClient adminClient; public Properties toConsumerProps(String topic) { Properties props = toProps(); @@ -221,4 +217,29 @@ public class TbKafkaSettings { } } + public AdminClient getAdminClient() { + if (adminClient == null) { + synchronized (this) { + if (adminClient == null) { + adminClient = AdminClient.create(toAdminProps()); + } + } + } + return adminClient; + } + + private Properties toAdminProps() { + Properties props = toProps(); + props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers); + props.put(AdminClientConfig.RETRIES_CONFIG, retries); + return props; + } + + @PreDestroy + private void destroy() { + if (adminClient != null) { + adminClient.close(); + } + } + } From 1a264dafa70a31210795a9b45a9be151034e2002 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 16 May 2024 14:34:50 +0300 Subject: [PATCH 108/121] Fix TbKafkaSettingsTest --- .../org/thingsboard/server/queue/kafka/TbKafkaSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index ebbc6fac7e..760487c61e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -228,7 +228,7 @@ public class TbKafkaSettings { return adminClient; } - private Properties toAdminProps() { + protected Properties toAdminProps() { Properties props = toProps(); props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(AdminClientConfig.RETRIES_CONFIG, retries); From 5c6b4122d355c4bf16bf9dfc8d345231d1c1dcaa Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 16 May 2024 18:02:23 +0300 Subject: [PATCH 109/121] Add SSL support for Kafka queue connection in JS executor --- msa/js-executor/api/utils.ts | 4 +++ .../config/custom-environment-variables.yml | 11 +++++++ msa/js-executor/config/default.yml | 2 ++ msa/js-executor/queue/kafkaTemplate.ts | 32 ++++++++++++++++++- 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/msa/js-executor/api/utils.ts b/msa/js-executor/api/utils.ts index 963ea1ddae..c228d98315 100644 --- a/msa/js-executor/api/utils.ts +++ b/msa/js-executor/api/utils.ts @@ -62,3 +62,7 @@ export function parseJsErrorDetails(err: any): string | undefined { export function isNotUUID(candidate: string) { return candidate.length != 36 || !candidate.includes('-'); } + +export function isNotEmptyStr(value: any): boolean { + return typeof value === 'string' && value.trim().length > 0; +} diff --git a/msa/js-executor/config/custom-environment-variables.yml b/msa/js-executor/config/custom-environment-variables.yml index 1f5e5d711a..7c79eda0d1 100644 --- a/msa/js-executor/config/custom-environment-variables.yml +++ b/msa/js-executor/config/custom-environment-variables.yml @@ -38,6 +38,17 @@ kafka: topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES" use_confluent_cloud: "TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD" client_id: "KAFKA_CLIENT_ID" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh + ssl: + # Enable or disable SSL for Kafka communication. + enabled: "TB_KAFKA_SSL_ENABLED" + # Path to the server certificate file. This file can hold the server certificate or a certificate chain and may include the server private key. + cert_file: "TB_KAFKA_SSL_PEM_CERTIFICATE" + # Optional: Path to the server certificate private key file. Required if the private key is not included in the server certificate file. + key_file: "TB_KAFKA_SSL_PEM_KEY" + # Optional: Password for the server certificate private key, if applicable. + key_password: "TB_KAFKA_SSL_PEM_KEY_PASSWORD" + # Optional: Path to a custom CA certificate file. Defaults to trusting well-known CAs curated by Mozilla. + ca_file: "TB_KAFKA_SSL_PEM_TRUSTS_CERTIFICATE" confluent: sasl: mechanism: "TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM" diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml index b340425a31..b2e84d0d46 100644 --- a/msa/js-executor/config/default.yml +++ b/msa/js-executor/config/default.yml @@ -38,6 +38,8 @@ kafka: topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1" use_confluent_cloud: false client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh + ssl: + enabled: false confluent: sasl: mechanism: "PLAIN" diff --git a/msa/js-executor/queue/kafkaTemplate.ts b/msa/js-executor/queue/kafkaTemplate.ts index 4ba3a1aee1..659d1fb8b2 100644 --- a/msa/js-executor/queue/kafkaTemplate.ts +++ b/msa/js-executor/queue/kafkaTemplate.ts @@ -15,6 +15,7 @@ /// import config from 'config'; +import fs from 'node:fs'; import { _logger, KafkaJsWinstonLogCreator } from '../config/logger'; import { JsInvokeMessageProcessor } from '../api/jsInvokeMessageProcessor' import { IQueue } from './queue.models'; @@ -29,8 +30,10 @@ import { Producer, TopicMessages } from 'kafkajs'; +import { isNotEmptyStr } from '../api/utils'; +import { KeyObject } from 'tls'; -import process, { kill, exit } from 'process'; +import process, { exit, kill } from 'process'; export class KafkaTemplate implements IQueue { @@ -64,6 +67,7 @@ export class KafkaTemplate implements IQueue { const queuePrefix: string = config.get('queue_prefix'); const requestTopic: string = queuePrefix ? queuePrefix + "." + config.get('request_topic') : config.get('request_topic'); const useConfluent = config.get('kafka.use_confluent_cloud'); + const enabledSsl = Boolean(config.get('kafka.ssl.enabled')); const groupId:string = queuePrefix ? queuePrefix + ".js-executor-group" : "js-executor-group"; this.logger.info('Kafka Bootstrap Servers: %s', kafkaBootstrapServers); this.logger.info('Kafka Requests Topic: %s', requestTopic); @@ -93,6 +97,31 @@ export class KafkaTemplate implements IQueue { kafkaConfig['ssl'] = true; } + if (enabledSsl) { + const certFilePath: string = config.has('kafka.ssl.cert_file') ? config.get('kafka.ssl.cert_file') : ''; + const keyFilePath: string = config.has('kafka.ssl.key_file') ? config.get('kafka.ssl.key_file') : ''; + const keyPassword: string = config.has('kafka.ssl.key_password') ? config.get('kafka.ssl.key_password') : ''; + const caFilePath: string = config.has('kafka.ssl.ca_file') ? config.get('kafka.ssl.ca_file') : ''; + + kafkaConfig.ssl = {}; + + if (isNotEmptyStr(certFilePath)) { + kafkaConfig.ssl.cert = fs.readFileSync(certFilePath, 'utf-8'); + } + + if (isNotEmptyStr(keyFilePath)) { + const keyConfig: KeyObject = {pem: fs.readFileSync(keyFilePath, 'utf-8')}; + if (isNotEmptyStr(keyPassword)) { + keyConfig.passphrase = keyPassword; + } + kafkaConfig.ssl.key = [keyConfig]; + } + + if (isNotEmptyStr(caFilePath)) { + kafkaConfig.ssl.ca = fs.readFileSync(caFilePath, 'utf-8'); + } + } + this.parseTopicProperties(); this.kafkaClient = new Kafka(kafkaConfig); @@ -213,6 +242,7 @@ export class KafkaTemplate implements IQueue { private createTopic(topic: string, partitions: number): Promise { return this.kafkaAdmin.createTopics({ + timeout: this.requestTimeout, topics: [{ topic: topic, numPartitions: partitions, From a4e5b940f73598ebef88d14eabba47efa2a9bbcb Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 17 May 2024 12:34:51 +0300 Subject: [PATCH 110/121] Update home-links-routing.module.ts --- .../modules/home/pages/home-links/home-links-routing.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index b141600732..a455bac64d 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -89,6 +89,7 @@ const applySystemParametersToHomeDashboard = (store: Store, dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction = (dashboard.configuration.widgets[widgetId].config.settings.markdownTextFunction as string) .replace('\'${mobileQrEnabled}\'', String(true)); + break; } } } From 9a5e3c81fc0ea5f7b9cfa34f23ffa098917eeceb Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 17 May 2024 13:11:33 +0300 Subject: [PATCH 111/121] Log startup time --- .../server/ThingsboardServerApplication.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java index 60f69cd8a7..6ec00b5661 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java @@ -15,24 +15,32 @@ */ package org.thingsboard.server; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; +import org.thingsboard.server.queue.util.AfterStartUp; import java.util.Arrays; +import java.util.concurrent.TimeUnit; @SpringBootConfiguration @EnableAsync @EnableScheduling @ComponentScan({"org.thingsboard.server", "org.thingsboard.script"}) +@Slf4j public class ThingsboardServerApplication { private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "thingsboard"; + private static long startTs; + public static void main(String[] args) { + startTs = System.currentTimeMillis(); SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args)); } @@ -45,4 +53,11 @@ public class ThingsboardServerApplication { } return args; } + + @AfterStartUp(order = Ordered.LOWEST_PRECEDENCE) + public void afterStartUp() { + long startupTimeMs = System.currentTimeMillis() - startTs; + log.info("Started ThingsBoard in {} seconds", TimeUnit.MILLISECONDS.toSeconds(startupTimeMs)); + } + } From 5467903c0cdc2b3e331cede6ad77ae203a2945e8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 17 May 2024 13:59:00 +0300 Subject: [PATCH 112/121] Fix MobileSecretRedisCache --- .../server/service/mobile/secret/MobileSecretRedisCache.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java index 81fa8672f8..1ff6be61a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java +++ b/application/src/main/java/org/thingsboard/server/service/mobile/secret/MobileSecretRedisCache.java @@ -23,12 +23,11 @@ import org.thingsboard.server.cache.RedisTbTransactionalCache; import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbJsonRedisSerializer; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.model.JwtPair; @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @Service("MobileSecretCache") -public class MobileSecretRedisCache extends RedisTbTransactionalCache { +public class MobileSecretRedisCache extends RedisTbTransactionalCache { public MobileSecretRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { super(CacheConstants.MOBILE_SECRET_KEY_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(JwtPair.class)); From 161e10a686e03ab05b8f7a19dbba7cf57127bef8 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Sun, 19 May 2024 22:35:21 +0200 Subject: [PATCH 113/121] added rate limits for the gateway --- .../controller/TenantProfileController.java | 3 + .../server/common/data/limit/LimitedApi.java | 1 + .../DefaultTenantProfileConfiguration.java | 3 + .../server/common/data/util/TbPair.java | 4 + common/proto/src/main/proto/queue.proto | 2 + .../AbstractGatewayDeviceSessionContext.java | 2 + .../DefaultTransportRateLimitService.java | 90 ++++++++++++++----- .../transport/limits/TransportLimitsType.java | 20 +++++ .../limits/TransportRateLimitService.java | 3 +- .../service/DefaultTransportService.java | 16 +++- ...enant-profile-configuration.component.html | 18 ++++ ...-tenant-profile-configuration.component.ts | 3 + .../tenant/rate-limits/rate-limits.models.ts | 9 ++ .../assets/locale/locale.constant-en_US.json | 9 ++ 14 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 8796b8c8a2..097aec6306 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -138,6 +138,9 @@ public class TenantProfileController extends BaseController { " \"transportDeviceMsgRateLimit\": \"20:1,600:60\",\n" + " \"transportDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" + " \"transportDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" + + " \"transportGatewayMsgRateLimit\": \"20:1,600:60\",\n" + + " \"transportGatewayTelemetryMsgRateLimit\": \"20:1,600:60\",\n" + + " \"transportGatewayTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" + " \"maxTransportMessages\": 10000000,\n" + " \"maxTransportDataPoints\": 10000000,\n" + " \"maxREExecutions\": 4000000,\n" + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 4afead3435..11702100d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -40,6 +40,7 @@ public enum LimitedApi { TWO_FA_VERIFICATION_CODE_CHECK(false, true), TRANSPORT_MESSAGES_PER_TENANT("transport messages", true), TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), + TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false), EMAILS("emails sending", true); private Function configExtractor; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index a0cfbfe1e1..d990c300da 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -47,6 +47,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private String transportDeviceMsgRateLimit; private String transportDeviceTelemetryMsgRateLimit; private String transportDeviceTelemetryDataPointsRateLimit; + private String transportGatewayMsgRateLimit; + private String transportGatewayTelemetryMsgRateLimit; + private String transportGatewayTelemetryDataPointsRateLimit; private String tenantEntityExportRateLimit; private String tenantEntityImportRateLimit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java index bdca8b2e77..6e8b2e5696 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbPair.java @@ -23,4 +23,8 @@ import lombok.Data; public class TbPair { private S first; private T second; + + public static TbPair of(S first, T second) { + return new TbPair<>(first, second); + } } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 7b2aa2fe1b..53a3026424 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -96,6 +96,8 @@ message SessionInfoProto { int64 deviceProfileIdLSB = 13; int64 customerIdMSB = 14; int64 customerIdLSB = 15; + optional int64 gatewayIdMSB = 16; + optional int64 gatewayIdLSB = 17; } enum SessionEvent { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java index f803f0fe3a..e174ad86bd 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java @@ -59,6 +59,8 @@ public abstract class AbstractGatewayDeviceSessionContext tenantAllowed = new ConcurrentHashMap<>(); private final ConcurrentMap> tenantDevices = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantGateways = new ConcurrentHashMap<>(); private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap perGatewayLimits = new ConcurrentHashMap<>(); private final Map ipMap = new ConcurrentHashMap<>(); private final TransportTenantProfileCache tenantProfileCache; @@ -65,15 +72,20 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi } @Override - public EntityType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints) { + public TbPair checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints) { if (!tenantAllowed.getOrDefault(tenantId, Boolean.TRUE)) { - return EntityType.API_USAGE_STATE; + return TbPair.of(EntityType.API_USAGE_STATE, false); } if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) { - return EntityType.TENANT; + return TbPair.of(EntityType.TENANT, false); + } + + if (gatewayId != null && !checkEntityRateLimit(dataPoints, getGatewayRateLimits(tenantId, gatewayId))) { + return TbPair.of(EntityType.DEVICE, true); } + if (!checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) { - return EntityType.DEVICE; + return TbPair.of(EntityType.DEVICE, false); } return null; } @@ -89,24 +101,27 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi @Override public void update(TenantProfileUpdateResult update) { log.info("Received tenant profile update: {}", update.getProfile()); - EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(update.getProfile(), true); - EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(update.getProfile(), false); + EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(update.getProfile(), TENANT_LIMITS); + EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(update.getProfile(), DEVICE_LIMITS); + EntityTransportRateLimits gatewayRateLimitPrototype = createRateLimits(update.getProfile(), GATEWAY_LIMITS); for (TenantId tenantId : update.getAffectedTenants()) { - mergeLimits(tenantId, tenantRateLimitPrototype, perTenantLimits::get, perTenantLimits::put); - getTenantDevices(tenantId).forEach(deviceId -> { - mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put); - }); + update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype); } } @Override public void update(TenantId tenantId) { - EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), true); - EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), false); + EntityTransportRateLimits tenantRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), TENANT_LIMITS); + EntityTransportRateLimits deviceRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), DEVICE_LIMITS); + EntityTransportRateLimits gatewayRateLimitPrototype = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_LIMITS); + update(tenantId, tenantRateLimitPrototype, deviceRateLimitPrototype, gatewayRateLimitPrototype); + } + + private void update(TenantId tenantId, EntityTransportRateLimits tenantRateLimitPrototype, + EntityTransportRateLimits deviceRateLimitPrototype, EntityTransportRateLimits gatewayRateLimitPrototype) { mergeLimits(tenantId, tenantRateLimitPrototype, perTenantLimits::get, perTenantLimits::put); - getTenantDevices(tenantId).forEach(deviceId -> { - mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put); - }); + getTenantDevices(tenantId).forEach(deviceId -> mergeLimits(deviceId, deviceRateLimitPrototype, perDeviceLimits::get, perDeviceLimits::put)); + getTenantGateways(tenantId).forEach(deviceId -> mergeLimits(deviceId, gatewayRateLimitPrototype, perGatewayLimits::get, perGatewayLimits::put)); } @Override @@ -233,15 +248,34 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi } } - private EntityTransportRateLimits createRateLimits(TenantProfile tenantProfile, boolean tenant) { + private EntityTransportRateLimits createRateLimits(TenantProfile tenantProfile, TransportLimitsType limitsType) { TenantProfileData profileData = tenantProfile.getProfileData(); DefaultTenantProfileConfiguration profile = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); if (profile == null) { return new EntityTransportRateLimits(ALLOW, ALLOW, ALLOW); } else { - TransportRateLimit regularMsgRateLimit = newLimit(tenant ? profile.getTransportTenantMsgRateLimit() : profile.getTransportDeviceMsgRateLimit()); - TransportRateLimit telemetryMsgRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryMsgRateLimit() : profile.getTransportDeviceTelemetryMsgRateLimit()); - TransportRateLimit telemetryDpRateLimit = newLimit(tenant ? profile.getTransportTenantTelemetryDataPointsRateLimit() : profile.getTransportDeviceTelemetryDataPointsRateLimit()); + TransportRateLimit regularMsgRateLimit; + TransportRateLimit telemetryMsgRateLimit; + TransportRateLimit telemetryDpRateLimit; + switch (limitsType) { + case TENANT_LIMITS -> { + regularMsgRateLimit = newLimit(profile.getTransportTenantMsgRateLimit()); + telemetryMsgRateLimit = newLimit(profile.getTransportTenantTelemetryMsgRateLimit()); + telemetryDpRateLimit = newLimit(profile.getTransportTenantTelemetryDataPointsRateLimit()); + } + case DEVICE_LIMITS -> { + regularMsgRateLimit = newLimit(profile.getTransportDeviceMsgRateLimit()); + telemetryMsgRateLimit = newLimit(profile.getTransportDeviceTelemetryMsgRateLimit()); + telemetryDpRateLimit = newLimit(profile.getTransportDeviceTelemetryDataPointsRateLimit()); + } + case GATEWAY_LIMITS -> { + regularMsgRateLimit = newLimit(profile.getTransportGatewayMsgRateLimit()); + telemetryMsgRateLimit = newLimit(profile.getTransportGatewayTelemetryMsgRateLimit()); + telemetryDpRateLimit = newLimit(profile.getTransportGatewayTelemetryDataPointsRateLimit()); + } + default -> throw new IllegalStateException("Unknown limits type: " + limitsType); + } + return new EntityTransportRateLimits(regularMsgRateLimit, telemetryMsgRateLimit, telemetryDpRateLimit); } } @@ -251,21 +285,31 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi } private EntityTransportRateLimits getTenantRateLimits(TenantId tenantId) { - return perTenantLimits.computeIfAbsent(tenantId, k -> { - return createRateLimits(tenantProfileCache.get(tenantId), true); - }); + return perTenantLimits.computeIfAbsent(tenantId, k -> createRateLimits(tenantProfileCache.get(tenantId), TENANT_LIMITS)); } private EntityTransportRateLimits getDeviceRateLimits(TenantId tenantId, DeviceId deviceId) { return perDeviceLimits.computeIfAbsent(deviceId, k -> { - EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), false); + EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), DEVICE_LIMITS); getTenantDevices(tenantId).add(deviceId); return limits; }); } + private EntityTransportRateLimits getGatewayRateLimits(TenantId tenantId, DeviceId deviceId) { + return perGatewayLimits.computeIfAbsent(deviceId, k -> { + EntityTransportRateLimits limits = createRateLimits(tenantProfileCache.get(tenantId), GATEWAY_LIMITS); + getTenantGateways(tenantId).add(deviceId); + return limits; + }); + } + private Set getTenantDevices(TenantId tenantId) { return tenantDevices.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet()); } + private Set getTenantGateways(TenantId tenantId) { + return tenantGateways.computeIfAbsent(tenantId, id -> ConcurrentHashMap.newKeySet()); + } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java new file mode 100644 index 0000000000..ab65b07414 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportLimitsType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public enum TransportLimitsType { + TENANT_LIMITS, DEVICE_LIMITS, GATEWAY_LIMITS +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java index 975602c5da..e33f9db240 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java @@ -18,13 +18,14 @@ package org.thingsboard.server.common.transport.limits; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; import java.net.InetSocketAddress; public interface TransportRateLimitService { - EntityType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints); + TbPair checkLimits(TenantId tenantId, DeviceId gatewayId, DeviceId deviceId, int dataPoints); void update(TenantProfileUpdateResult update); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 8b7b88c261..9312370c34 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -852,18 +852,27 @@ public class DefaultTransportService extends TransportActivityManager implements } TenantId tenantId = TenantId.fromUUID(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); + DeviceId gatewayId = null; + if (sessionInfo.hasGatewayIdMSB() && sessionInfo.hasGatewayIdLSB()) { + gatewayId = new DeviceId(new UUID(sessionInfo.getGatewayIdMSB(), sessionInfo.getGatewayIdLSB())); + } - EntityType rateLimitedEntityType = rateLimitService.checkLimits(tenantId, deviceId, dataPoints); - if (rateLimitedEntityType == null) { + var rateLimitedPair = rateLimitService.checkLimits(tenantId, gatewayId, deviceId, dataPoints); + if (rateLimitedPair == null) { return true; } else { + var rateLimitedEntityType = rateLimitedPair.getFirst(); if (callback != null) { callback.onError(new TbRateLimitsException(rateLimitedEntityType)); } + if (rateLimitedEntityType == EntityType.DEVICE || rateLimitedEntityType == EntityType.TENANT) { + LimitedApi limitedApi = + rateLimitedEntityType == EntityType.TENANT ? LimitedApi.TRANSPORT_MESSAGES_PER_TENANT : + rateLimitedPair.getSecond() ? LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY : LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE; notificationRuleProcessor.process(RateLimitsTrigger.builder() .tenantId(tenantId) - .api(rateLimitedEntityType == EntityType.DEVICE ? LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE : LimitedApi.TRANSPORT_MESSAGES_PER_TENANT) + .api(limitedApi) .limitLevel(rateLimitedEntityType == EntityType.DEVICE ? deviceId : tenantId) .limitLevelEntityName(rateLimitedEntityType == EntityType.DEVICE ? sessionInfo.getDeviceName() : null) .build()); @@ -933,7 +942,6 @@ public class DefaultTransportService extends TransportActivityManager implements log.trace("ResourceUpdate - [{}] [{}]", id, mdRez); transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceUpdate(msg)); }); - } else if (toSessionMsg.hasResourceDeleteMsg()) { TransportProtos.ResourceDeleteMsg msg = toSessionMsg.getResourceDeleteMsg(); TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index ef024fe0a2..91f96c7aba 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -516,6 +516,18 @@ [type]="rateLimitsType.DEVICE_TELEMETRY_MESSAGES">
+
+ + +
+
+
+ + +
+
@@ -531,6 +543,12 @@ [type]="rateLimitsType.DEVICE_TELEMETRY_DATA_POINTS">
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index c13eed9c03..2b32504839 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -74,6 +74,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA transportDeviceMsgRateLimit: [null, []], transportDeviceTelemetryMsgRateLimit: [null, []], transportDeviceTelemetryDataPointsRateLimit: [null, []], + transportGatewayMsgRateLimit: [null, []], + transportGatewayTelemetryMsgRateLimit: [null, []], + transportGatewayTelemetryDataPointsRateLimit: [null, []], tenantEntityExportRateLimit: [null, []], tenantEntityImportRateLimit: [null, []], tenantNotificationRequestsRateLimit: [null, []], diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts index ee13ea9b23..a04336e15e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts @@ -26,6 +26,9 @@ export enum RateLimitsType { DEVICE_TELEMETRY_MESSAGES = 'DEVICE_TELEMETRY_MESSAGES', DEVICE_TELEMETRY_DATA_POINTS = 'DEVICE_TELEMETRY_DATA_POINTS', TENANT_MESSAGES = 'TENANT_MESSAGES', + GATEWAY_MESSAGES = 'GATEWAY_MESSAGES', + GATEWAY_TELEMETRY_MESSAGES = 'GATEWAY_TELEMETRY_MESSAGES', + GATEWAY_TELEMETRY_DATA_POINTS = 'GATEWAY_TELEMETRY_DATA_POINTS', TENANT_TELEMETRY_MESSAGES = 'TENANT_TELEMETRY_MESSAGES', TENANT_TELEMETRY_DATA_POINTS = 'TENANT_TELEMETRY_DATA_POINTS', TENANT_SERVER_REST_LIMITS_CONFIGURATION = 'TENANT_SERVER_REST_LIMITS_CONFIGURATION', @@ -50,6 +53,9 @@ export const rateLimitsLabelTranslationMap = new Map( [RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.transport-device-msg'], [RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-device-telemetry-msg'], [RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-device-telemetry-data-points'], + [RateLimitsType.GATEWAY_MESSAGES, 'tenant-profile.rate-limits.transport-gateway-msg'], + [RateLimitsType.GATEWAY_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-gateway-telemetry-msg'], + [RateLimitsType.GATEWAY_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-gateway-telemetry-data-points'], [RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rest-requests-for-tenant'], [RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.customer-rest-limits'], [RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.ws-limit-updates-per-session'], @@ -74,6 +80,9 @@ export const rateLimitsDialogTitleTranslationMap = new Map Date: Mon, 20 May 2024 14:27:16 +0200 Subject: [PATCH 114/121] added tests for gateway rate limits --- .../mqtt/MqttGatewayRateLimitsTest.java | 171 ++++++++++++++++++ .../session/GatewaySessionHandlerTest.java | 4 + 2 files changed, 175 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java new file mode 100644 index 0000000000..794d93a44a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; + +import java.util.function.Consumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY; + +@DaoSqlTest +@TestPropertySource(properties = { + "service.integrations.supported=ALL", + "transport.mqtt.enabled=true", +}) +public class MqttGatewayRateLimitsTest extends AbstractControllerTest { + + private static final String TOPIC = "v1/gateway/telemetry"; + private static final String DEVICE_A = "DeviceA"; + private static final String DEVICE_B = "DeviceB"; + + private DeviceId gatewayId; + private String gatewayAccessToken; + + @SpyBean + private NotificationRuleProcessor notificationRuleProcessor; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class); + Assert.assertNotNull(tenantProfile); + + DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration(); + + profileConfiguration.setTransportGatewayMsgRateLimit(null); + profileConfiguration.setTransportGatewayTelemetryMsgRateLimit(null); + profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit(null); + + doPost("/api/tenantProfile", tenantProfile); + + loginTenantAdmin(); + createGateway(); + + Mockito.reset(notificationRuleProcessor); + } + + @Test + public void transportGatewayMsgRateLimitTest() throws Exception { + transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayMsgRateLimit("1:600")); + } + + @Test + public void transportGatewayTelemetryMsgRateLimitTest() throws Exception { + transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryMsgRateLimit("1:600")); + } + + @Test + public void transportGatewayTelemetryDataPointsRateLimitTest() throws Exception { + transportGatewayRateLimitTest(profileConfiguration -> profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit("1:600")); + } + + private void transportGatewayRateLimitTest(Consumer profileConfiguration) throws Exception { + loginSysAdmin(); + + TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class); + Assert.assertNotNull(tenantProfile); + + profileConfiguration.accept((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration()); + + doPost("/api/tenantProfile", tenantProfile); + + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_A)); + + loginTenantAdmin(); + + Device deviceA = getDeviceByName(DEVICE_A); + + var deviceATrigger = createRateLimitsTrigger(deviceA); + + Mockito.verify(notificationRuleProcessor, Mockito.never()).process(eq(deviceATrigger)); + + try { + client.publishAndWait(TOPIC, getGatewayPayload(DEVICE_B)); + } catch (Exception t) { + } + + Device deviceB = getDeviceByName(DEVICE_B); + + var deviceBTrigger = createRateLimitsTrigger(deviceB); + + Mockito.verify(notificationRuleProcessor, Mockito.times(1)).process(deviceBTrigger); + + if (client.isConnected()) { + client.disconnect(); + } + } + + private void createGateway() throws Exception { + Device device = new Device(); + device.setName("gateway"); + ObjectNode additionalInfo = JacksonUtil.newObjectNode(); + additionalInfo.put("gateway", true); + device.setAdditionalInfo(additionalInfo); + device = doPost("/api/device", device, Device.class); + assertNotNull(device); + gatewayId = device.getId(); + assertNotNull(gatewayId); + + DeviceCredentials deviceCredentials = doGet("/api/device/" + gatewayId + "/credentials", DeviceCredentials.class); + assertNotNull(deviceCredentials); + assertEquals(gatewayId, deviceCredentials.getDeviceId()); + gatewayAccessToken = deviceCredentials.getCredentialsId(); + assertNotNull(gatewayAccessToken); + } + + private Device getDeviceByName(String deviceName) throws Exception { + Device device = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); + assertNotNull(device); + return device; + } + + private byte[] getGatewayPayload(String deviceName) { + return String.format("{\"%s\": [{\"values\": {\"temperature\": 42}}]}", deviceName).getBytes(); + } + + private RateLimitsTrigger createRateLimitsTrigger(Device device) { + return RateLimitsTrigger.builder() + .tenantId(tenantId) + .api(TRANSPORT_MESSAGES_PER_GATEWAY) + .limitLevel(device.getId()) + .limitLevelEntityName(device.getName()) + .build(); + } +} diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java index 158acc2ce2..670bd98af1 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandlerTest.java @@ -64,6 +64,10 @@ public class GatewaySessionHandlerTest { lenient().doNothing().when(transportService).recordActivity(any()); lenient().when(transportContext.getTransportService()).thenReturn(transportService); lenient().when(deviceSessionCtx.getContext()).thenReturn(transportContext); + + var deviceInfo = new TransportDeviceInfo(); + deviceInfo.setDeviceId(new DeviceId(UUID.randomUUID())); + lenient().when(deviceSessionCtx.getDeviceInfo()).thenReturn(deviceInfo); handler = new GatewaySessionHandler(deviceSessionCtx, UUID.randomUUID(), true); lenient().when(handler.getNodeId()).thenReturn("nodeId"); } From dd573426a3d2be449017b206b318d0962130c055 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 20 May 2024 17:42:53 +0300 Subject: [PATCH 115/121] updated haproxy configuration for /.well-known/... apis --- docker/haproxy/config/haproxy.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/haproxy/config/haproxy.cfg b/docker/haproxy/config/haproxy.cfg index 757efe692f..b76fe29276 100644 --- a/docker/haproxy/config/haproxy.cfg +++ b/docker/haproxy/config/haproxy.cfg @@ -96,7 +96,7 @@ frontend http-in acl transport_http_acl path_beg /api/v1/ acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ acl tb_images_api_acl path_beg /api/images/ - acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ + acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ /.well-known/assetlinks.json /.well-known/apple-app-site-association redirect scheme https if !letsencrypt_http_acl !transport_http_acl { env(FORCE_HTTPS_REDIRECT) -m str true } @@ -124,7 +124,7 @@ frontend https_in acl transport_http_acl path_beg /api/v1/ acl tb_images_api_acl path_beg /api/images/ - acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ + acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ /.well-known/assetlinks.json /.well-known/apple-app-site-association use_backend tb-http-backend if transport_http_acl use_backend tb-images-api-backend if tb_images_api_acl From ba02898f0b841c1b07b6660d69e913fd1aaefc27 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 20 May 2024 19:27:24 +0200 Subject: [PATCH 116/121] fixed race condition during adding new gateway connector with default config --- .../lib/gateway/dialog/add-connector-dialog.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index 78a22cd267..98261a8e57 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts @@ -93,8 +93,7 @@ export class AddConnectorDialogComponent extends DialogComponent Date: Tue, 21 May 2024 12:05:06 +0200 Subject: [PATCH 117/121] added upgrade for the gateway rate limits --- .../update/DefaultDataUpdateService.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 9626190f1b..b355ac32e9 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -92,12 +92,42 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Updating data from version 3.6.4 to 3.7.0 ..."); updateCustomersWithTheSameTitle(); updateMaxRuleNodeExecsPerMessage(); + updateGatewayRateLimits(); break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } } + private void updateGatewayRateLimits() { + var tenantProfiles = new PageDataIterable<>(link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE); + tenantProfiles.forEach(tenantProfile -> { + var configurationOpt = tenantProfile.getProfileConfiguration(); + configurationOpt.ifPresent(configuration -> { + boolean updated = false; + if (configuration.getTransportDeviceMsgRateLimit() != null && configuration.getTransportGatewayMsgRateLimit() == null) { + configuration.setTransportGatewayMsgRateLimit(configuration.getTransportDeviceMsgRateLimit()); + updated = true; + } + if (configuration.getTransportDeviceTelemetryMsgRateLimit() != null && configuration.getTransportGatewayTelemetryMsgRateLimit() == null) { + configuration.setTransportGatewayTelemetryMsgRateLimit(configuration.getTransportDeviceTelemetryMsgRateLimit()); + updated = true; + } + if (configuration.getTransportDeviceTelemetryDataPointsRateLimit() != null && configuration.getTransportGatewayTelemetryDataPointsRateLimit() == null) { + configuration.setTransportGatewayTelemetryDataPointsRateLimit(configuration.getTransportDeviceTelemetryDataPointsRateLimit()); + updated = true; + } + if (updated) { + try { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + } catch (Exception e) { + log.error("Failed to update tenant profile with id: {} due to: ", tenantProfile.getId(), e); + } + } + }); + }); + } + private void updateMaxRuleNodeExecsPerMessage() { var tenantProfiles = new PageDataIterable<>( link -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, link), DEFAULT_PAGE_SIZE); From 811f7d7206ebcbb4a0ce7b5fd6d2803fa5f9e7c3 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 15 May 2024 17:42:37 +0300 Subject: [PATCH 118/121] Additional logging of WS commands processing --- .../DefaultTbEntityDataSubscriptionService.java | 12 ++++++++++-- .../DefaultTbLocalSubscriptionService.java | 6 +++++- .../server/service/ws/DefaultWebSocketService.java | 3 ++- .../server/service/ws/WebSocketSessionRef.java | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 10e342b1e8..984af019b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -95,7 +95,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc private static final int DEFAULT_LIMIT = 100; private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); - @Autowired @Lazy + @Autowired + @Lazy private WebSocketService wsService; @Autowired @@ -728,7 +729,14 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc public void cancelAllSessionSubscriptions(String sessionId) { Map sessionSubs = subscriptionsBySessionId.remove(sessionId); if (sessionSubs != null) { - sessionSubs.values().forEach(this::cleanupAndCancel); + sessionSubs.values().forEach(sub -> { + try { + cleanupAndCancel(sub); + } catch (Exception e) { + log.warn("[{}] Failed to remove subscription {} due to ", sub.getTenantId(), sub, e); + } + } + ); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index bcb900c3f0..a280ae6754 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -217,7 +217,11 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); if (sessionSubscriptions != null) { for (TbSubscription subscription : sessionSubscriptions.values()) { - modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); + try { + modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); + } catch (Exception e) { + log.warn("[{}][{}] Failed to remove subscription {} due to ", subscription.getTenantId(), subscription.getEntityId(), subscription, e); + } } } else { log.debug("[{}] No session subscriptions found!", sessionId); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index e41d1821c1..78b78d8b8c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -197,7 +197,8 @@ public class DefaultWebSocketService implements WebSocketService { wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); break; case ERROR: - log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null)); + log.debug("[{}] Unknown websocket session error: ", sessionId, + event.getError().orElse(new RuntimeException("No error specified"))); break; case CLOSED: wsSessionsMap.remove(sessionId); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java index 190183cce2..44919b138c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java @@ -33,7 +33,7 @@ public class WebSocketSessionRef { private static final long serialVersionUID = 1L; private final String sessionId; - private SecurityUser securityCtx; + private volatile SecurityUser securityCtx; private final InetSocketAddress localAddress; private final InetSocketAddress remoteAddress; private final WebSocketSessionType sessionType; From a1cbd8815bd746ff46fbaad52ced7d43494b105f Mon Sep 17 00:00:00 2001 From: Daria Shevchenko <116559345+dashevchenko@users.noreply.github.com> Date: Fri, 17 May 2024 17:54:23 +0300 Subject: [PATCH 119/121] Added scheduled session cleanup (#10818) * added scheduled session cleanup for cases when session was closed before last session command processed * updated logging to include user info, updated isOpen method to include native check * updated cleanup to go through subscriptionsByEntityId map values * made modification of subscriptionsBySessionId and subscriptionsByEntityId maps atomic * fixed cancelAllSessionSubscriptions * added try-catch for pushSubscriptionEvent, modifySubscription * refactored logging * deleted redundant logging * refactoring: updated isEmpty to hasEvent --- .../controller/plugin/TbWebSocketHandler.java | 12 ++ .../DefaultTbLocalSubscriptionService.java | 107 ++++++++++++------ .../ModifySubscriptionResult.java | 39 +++++++ .../service/ws/DefaultWebSocketService.java | 10 ++ .../service/ws/WebSocketMsgEndpoint.java | 2 + .../server/service/ws/WebSocketService.java | 3 + 6 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/subscription/ModifySubscriptionResult.java diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 71350bb1fd..69728b20c3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -539,6 +539,18 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } } + @Override + public boolean isOpen(String externalId) { + String internalId = externalSessionMap.get(externalId); + if (internalId != null) { + SessionMetaData sessionMd = getSessionMd(internalId); + if (sessionMd != null) { + return sessionMd.session.isOpen(); + } + } + return false; + } + private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws IOException { var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); if (tenantProfileConfiguration == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index a280ae6754..d1addf8b0d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -45,6 +45,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; @@ -62,6 +63,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -84,18 +87,21 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer private final PartitionService partitionService; private final TbClusterService clusterService; private final SubscriptionManagerService subscriptionManagerService; + private final WebSocketService webSocketService; private ExecutorService tsCallBackExecutor; + private ScheduledExecutorService staleSessionCleanupExecutor; public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService, TbClusterService clusterService, - @Lazy SubscriptionManagerService subscriptionManagerService) { + @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService) { this.attrService = attrService; this.tsService = tsService; this.serviceInfoProvider = serviceInfoProvider; this.partitionService = partitionService; this.clusterService = clusterService; this.subscriptionManagerService = subscriptionManagerService; + this.webSocketService = webSocketService; } private String serviceId; @@ -108,6 +114,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer subscriptionUpdateExecutor = ThingsBoardExecutors.newWorkStealingPool(20, getClass()); tsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-sub-callback")); serviceId = serviceInfoProvider.getServiceId(); + staleSessionCleanupExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("stale-session-cleanup")); + staleSessionCleanupExecutor.scheduleWithFixedDelay(this::cleanupStaleSessions, 60, 60, TimeUnit.SECONDS); } @PreDestroy @@ -118,6 +126,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer if (tsCallBackExecutor != null) { tsCallBackExecutor.shutdownNow(); } + if (staleSessionCleanupExecutor != null) { + staleSessionCleanupExecutor.shutdownNow(); + } } @Override @@ -157,9 +168,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer TenantId tenantId = subscription.getTenantId(); EntityId entityId = subscription.getEntityId(); log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription); - Map> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); - sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); - modifySubscription(tenantId, entityId, subscription, true); + ModifySubscriptionResult modifySubscriptionResult; + subsLock.lock(); + try { + Map> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); + sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); + modifySubscriptionResult = modifySubscription(tenantId, entityId, subscription, true); + } finally { + subsLock.unlock(); + } + if (modifySubscriptionResult.hasEvent()) { + pushSubscriptionEvent(modifySubscriptionResult); + } } @Override @@ -195,37 +215,49 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @Override public void cancelSubscription(String sessionId, int subscriptionId) { log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); - Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); - if (sessionSubscriptions != null) { - TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); - if (subscription != null) { - if (sessionSubscriptions.isEmpty()) { - subscriptionsBySessionId.remove(sessionId); + ModifySubscriptionResult modifySubscriptionResult = null; + subsLock.lock(); + try { + Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); + if (sessionSubscriptions != null) { + TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); + if (subscription != null) { + if (sessionSubscriptions.isEmpty()) { + subscriptionsBySessionId.remove(sessionId); + } + modifySubscriptionResult = modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); + } else { + log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); } - modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); } else { - log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); + log.debug("[{}] No session subscriptions found!", sessionId); } - } else { - log.debug("[{}] No session subscriptions found!", sessionId); + } finally { + subsLock.unlock(); + } + if (modifySubscriptionResult != null && modifySubscriptionResult.hasEvent()) { + pushSubscriptionEvent(modifySubscriptionResult); } } @Override public void cancelAllSessionSubscriptions(String sessionId) { log.debug("[{}] Going to remove session subscriptions.", sessionId); - Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); - if (sessionSubscriptions != null) { - for (TbSubscription subscription : sessionSubscriptions.values()) { - try { - modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); - } catch (Exception e) { - log.warn("[{}][{}] Failed to remove subscription {} due to ", subscription.getTenantId(), subscription.getEntityId(), subscription, e); + List result = new ArrayList<>(); + subsLock.lock(); + try { + Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); + if (sessionSubscriptions != null) { + for (TbSubscription subscription : sessionSubscriptions.values()) { + result.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false)); } + } else { + log.debug("[{}] No session subscriptions found!", sessionId); } - } else { - log.debug("[{}] No session subscriptions found!", sessionId); + } finally { + subsLock.unlock(); } + result.stream().filter(ModifySubscriptionResult::hasEvent).forEach(this::pushSubscriptionEvent); } @Override @@ -388,10 +420,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer callback.onSuccess(); } - private void modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription subscription, boolean add) { + private ModifySubscriptionResult modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription subscription, boolean add) { TbSubscription missedUpdatesCandidate = null; - TbEntitySubEvent event; - subsLock.lock(); + TbEntitySubEvent event = null; try { TbEntityLocalSubsInfo entitySubs = subscriptionsByEntityId.computeIfAbsent(entityId.getId(), id -> new TbEntityLocalSubsInfo(tenantId, entityId)); event = add ? entitySubs.add(subscription) : entitySubs.remove(subscription); @@ -401,17 +432,23 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } else if (add) { missedUpdatesCandidate = entitySubs.registerPendingSubscription(subscription, event); } - } finally { - subsLock.unlock(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to {} subscription {} due to ", tenantId, entityId, add ? "add" : "remove", subscription, e); } - if (event != null) { - log.trace("[{}][{}][{}] Event: {}", tenantId, entityId, subscription.getSubscriptionId(), event); - pushSubEventToManagerService(tenantId, entityId, event); + return new ModifySubscriptionResult(tenantId, entityId, subscription, missedUpdatesCandidate, event); + } + + private void pushSubscriptionEvent(ModifySubscriptionResult modifySubResult) { + try { + TbEntitySubEvent event = modifySubResult.getEvent(); + log.trace("[{}][{}][{}] Event: {}", modifySubResult.getTenantId(), modifySubResult.getEntityId(), modifySubResult.getSubscription().getSubscriptionId(), event); + pushSubEventToManagerService(modifySubResult.getTenantId(), modifySubResult.getEntityId(), event); + TbSubscription missedUpdatesCandidate = modifySubResult.getMissedUpdatesCandidate(); if (missedUpdatesCandidate != null) { checkMissedUpdates(missedUpdatesCandidate); } - } else { - log.trace("[{}][{}][{}] No changes detected.", tenantId, entityId, subscription.getSubscriptionId()); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push subscription event {} due to ", modifySubResult.getTenantId(), modifySubResult.getEntityId(), modifySubResult.getEvent(), e); } } @@ -522,4 +559,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } } + private void cleanupStaleSessions() { + subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/ModifySubscriptionResult.java b/application/src/main/java/org/thingsboard/server/service/subscription/ModifySubscriptionResult.java new file mode 100644 index 0000000000..483c250065 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/ModifySubscriptionResult.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +/** + * The modification result of entity subscription + */ +@Builder +@Data +public class ModifySubscriptionResult { + + private TenantId tenantId; + private EntityId entityId; + private TbSubscription subscription; + private TbSubscription missedUpdatesCandidate; + private TbEntitySubEvent event; + + public boolean hasEvent() { + return event != null; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 78b78d8b8c..2a53e9a772 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -295,6 +295,16 @@ public class DefaultWebSocketService implements WebSocketService { } } + @Override + public void cleanupIfStale(String sessionId) { + if (!msgEndpoint.isOpen(sessionId)) { + log.info("[{}] Cleaning up stale session ", sessionId); + wsSessionsMap.remove(sessionId); + oldSubService.cancelAllSessionSubscriptions(sessionId); + entityDataSubService.cancelAllSessionSubscriptions(sessionId); + } + } + private void processSessionClose(WebSocketSessionRef sessionRef) { var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); if (tenantProfileConfiguration != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java index 41a68c98ca..416af080f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java @@ -29,4 +29,6 @@ public interface WebSocketMsgEndpoint { void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException; void close(WebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException; + + boolean isOpen(String sessionId); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java index 8f586f264e..284431ff84 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java @@ -36,4 +36,7 @@ public interface WebSocketService { void sendError(WebSocketSessionRef sessionRef, int subId, SubscriptionErrorCode errorCode, String errorMsg); void close(String sessionId, CloseStatus status); + + void cleanupIfStale(String sessionId); + } From cdc9bda92c94c22fc0b8b7bd822ec0ff33af74a1 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 17 May 2024 18:12:32 +0300 Subject: [PATCH 120/121] Minor refactoring for subscription service --- .../DefaultTbLocalSubscriptionService.java | 38 +++++++++---------- ...va => SubscriptionModificationResult.java} | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) rename application/src/main/java/org/thingsboard/server/service/subscription/{ModifySubscriptionResult.java => SubscriptionModificationResult.java} (96%) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index d1addf8b0d..8abb78d9c1 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -168,17 +168,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer TenantId tenantId = subscription.getTenantId(); EntityId entityId = subscription.getEntityId(); log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription); - ModifySubscriptionResult modifySubscriptionResult; + SubscriptionModificationResult result; subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); - modifySubscriptionResult = modifySubscription(tenantId, entityId, subscription, true); + result = modifySubscription(tenantId, entityId, subscription, true); } finally { subsLock.unlock(); } - if (modifySubscriptionResult.hasEvent()) { - pushSubscriptionEvent(modifySubscriptionResult); + if (result.hasEvent()) { + pushSubscriptionEvent(result); } } @@ -215,7 +215,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @Override public void cancelSubscription(String sessionId, int subscriptionId) { log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); - ModifySubscriptionResult modifySubscriptionResult = null; + SubscriptionModificationResult result = null; subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); @@ -225,7 +225,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer if (sessionSubscriptions.isEmpty()) { subscriptionsBySessionId.remove(sessionId); } - modifySubscriptionResult = modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); + result = modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false); } else { log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); } @@ -235,21 +235,21 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } finally { subsLock.unlock(); } - if (modifySubscriptionResult != null && modifySubscriptionResult.hasEvent()) { - pushSubscriptionEvent(modifySubscriptionResult); + if (result != null && result.hasEvent()) { + pushSubscriptionEvent(result); } } @Override public void cancelAllSessionSubscriptions(String sessionId) { log.debug("[{}] Going to remove session subscriptions.", sessionId); - List result = new ArrayList<>(); + List results = new ArrayList<>(); subsLock.lock(); try { Map> sessionSubscriptions = subscriptionsBySessionId.remove(sessionId); if (sessionSubscriptions != null) { for (TbSubscription subscription : sessionSubscriptions.values()) { - result.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false)); + results.add(modifySubscription(subscription.getTenantId(), subscription.getEntityId(), subscription, false)); } } else { log.debug("[{}] No session subscriptions found!", sessionId); @@ -257,7 +257,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } finally { subsLock.unlock(); } - result.stream().filter(ModifySubscriptionResult::hasEvent).forEach(this::pushSubscriptionEvent); + results.stream().filter(SubscriptionModificationResult::hasEvent).forEach(this::pushSubscriptionEvent); } @Override @@ -420,7 +420,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer callback.onSuccess(); } - private ModifySubscriptionResult modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription subscription, boolean add) { + private SubscriptionModificationResult modifySubscription(TenantId tenantId, EntityId entityId, TbSubscription subscription, boolean add) { TbSubscription missedUpdatesCandidate = null; TbEntitySubEvent event = null; try { @@ -435,20 +435,20 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } catch (Exception e) { log.warn("[{}][{}] Failed to {} subscription {} due to ", tenantId, entityId, add ? "add" : "remove", subscription, e); } - return new ModifySubscriptionResult(tenantId, entityId, subscription, missedUpdatesCandidate, event); + return new SubscriptionModificationResult(tenantId, entityId, subscription, missedUpdatesCandidate, event); } - private void pushSubscriptionEvent(ModifySubscriptionResult modifySubResult) { + private void pushSubscriptionEvent(SubscriptionModificationResult modificationResult) { try { - TbEntitySubEvent event = modifySubResult.getEvent(); - log.trace("[{}][{}][{}] Event: {}", modifySubResult.getTenantId(), modifySubResult.getEntityId(), modifySubResult.getSubscription().getSubscriptionId(), event); - pushSubEventToManagerService(modifySubResult.getTenantId(), modifySubResult.getEntityId(), event); - TbSubscription missedUpdatesCandidate = modifySubResult.getMissedUpdatesCandidate(); + TbEntitySubEvent event = modificationResult.getEvent(); + log.trace("[{}][{}][{}] Event: {}", modificationResult.getTenantId(), modificationResult.getEntityId(), modificationResult.getSubscription().getSubscriptionId(), event); + pushSubEventToManagerService(modificationResult.getTenantId(), modificationResult.getEntityId(), event); + TbSubscription missedUpdatesCandidate = modificationResult.getMissedUpdatesCandidate(); if (missedUpdatesCandidate != null) { checkMissedUpdates(missedUpdatesCandidate); } } catch (Exception e) { - log.warn("[{}][{}] Failed to push subscription event {} due to ", modifySubResult.getTenantId(), modifySubResult.getEntityId(), modifySubResult.getEvent(), e); + log.warn("[{}][{}] Failed to push subscription event {} due to ", modificationResult.getTenantId(), modificationResult.getEntityId(), modificationResult.getEvent(), e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/ModifySubscriptionResult.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/subscription/ModifySubscriptionResult.java rename to application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java index 483c250065..a31ecedb8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/ModifySubscriptionResult.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionModificationResult.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId; */ @Builder @Data -public class ModifySubscriptionResult { +public class SubscriptionModificationResult { private TenantId tenantId; private EntityId entityId; From 74a94efef724f7387223a7c51db79a49481051b0 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 21 May 2024 18:36:32 +0300 Subject: [PATCH 121/121] Truncate notification table on test tear down --- .../service/notification/AbstractNotificationApiTest.java | 6 +++++- .../server/service/notification/NotificationApiTest.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index ae9dbe92c4..15650ff2e6 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.util.Pair; +import org.springframework.jdbc.core.JdbcTemplate; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.server.common.data.User; @@ -102,6 +103,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest protected SqlPartitioningRepository partitioningRepository; @Autowired protected DefaultNotifications defaultNotifications; + @Autowired + private JdbcTemplate jdbcTemplate; public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; @@ -112,7 +115,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID); notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID); notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID); - partitioningRepository.dropPartitionsBefore("notification", Long.MAX_VALUE, 1); + jdbcTemplate.execute("TRUNCATE TABLE notification"); + partitioningRepository.cleanupPartitionsCache("notification", Long.MAX_VALUE, 0); notificationSettingsService.deleteNotificationSettings(TenantId.SYS_TENANT_ID); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 1a788d437b..883a173edb 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -331,7 +331,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testNotificationUpdatesForSeveralUsers() throws Exception { - int usersCount = 150; + int usersCount = 50; Map sessions = new HashMap<>(); List targets = new ArrayList<>();