From 4e91b28a66b629194a6b784343d4376ee4e8673b Mon Sep 17 00:00:00 2001 From: Jan Bols Date: Thu, 19 Jul 2018 10:36:18 +0200 Subject: [PATCH 01/30] Cache assets when finding them by tenant and name cfr the caching of devices. --- .../server/common/data/CacheConstants.java | 1 + .../server/dao/asset/AssetService.java | 2 +- .../server/dao/asset/BaseAssetService.java | 29 ++++++++++++++----- 3 files changed, 24 insertions(+), 8 deletions(-) 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 88f76313f2..21de402721 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 @@ -19,4 +19,5 @@ public class CacheConstants { public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String RELATIONS_CACHE = "relations"; public static final String DEVICE_CACHE = "devices"; + public static final String ASSET_CACHE = "assets"; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index 6dc15c0017..e373f0e27a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -34,7 +34,7 @@ public interface AssetService { ListenableFuture findAssetByIdAsync(AssetId assetId); - Optional findAssetByTenantIdAndName(TenantId tenantId, String name); + Asset findAssetByTenantIdAndName(TenantId tenantId, String name); Asset saveAsset(Asset asset); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 1eafb07442..6c8b60914e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -21,6 +21,10 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; @@ -48,15 +52,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.CacheConstants.ASSET_CACHE; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; -import static org.thingsboard.server.dao.service.Validator.validateId; -import static org.thingsboard.server.dao.service.Validator.validateIds; -import static org.thingsboard.server.dao.service.Validator.validatePageLink; -import static org.thingsboard.server.dao.service.Validator.validateString; +import static org.thingsboard.server.dao.service.Validator.*; @Service @Slf4j @@ -75,6 +76,9 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Autowired private CustomerDao customerDao; + @Autowired + private CacheManager cacheManager; + @Override public Asset findAssetById(AssetId assetId) { log.trace("Executing findAssetById [{}]", assetId); @@ -89,13 +93,16 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ return assetDao.findByIdAsync(assetId.getId()); } + @Cacheable(cacheNames = ASSET_CACHE, key = "{#tenantId, #name}") @Override - public Optional findAssetByTenantIdAndName(TenantId tenantId, String name) { + public Asset findAssetByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); - return assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name); + return assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name) + .orElse(null); } + @CacheEvict(cacheNames = ASSET_CACHE, key = "{#asset.tenantId, #asset.name}") @Override public Asset saveAsset(Asset asset) { log.trace("Executing saveAsset [{}]", asset); @@ -122,6 +129,14 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ log.trace("Executing deleteAsset [{}]", assetId); validateId(assetId, INCORRECT_ASSET_ID + assetId); deleteEntityRelations(assetId); + + Cache cache = cacheManager.getCache(ASSET_CACHE); + Asset asset = assetDao.findById(assetId.getId()); + List list = new ArrayList<>(); + list.add(asset.getTenantId()); + list.add(asset.getName()); + cache.evict(list); + assetDao.removeById(assetId.getId()); } From fb294561e6403c2afdace8f6629776057c8c72c6 Mon Sep 17 00:00:00 2001 From: Jan Bols Date: Thu, 19 Jul 2018 11:21:04 +0200 Subject: [PATCH 02/30] Cache assets when finding them by tenant and name cfr the caching of devices => add asset in cache directives of config files --- application/src/main/resources/thingsboard.yml | 3 +++ dao/src/test/resources/application-test.properties | 3 +++ 2 files changed, 6 insertions(+) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2a2728dbb4..89dde0ae51 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -291,6 +291,9 @@ caffeine: devices: timeToLiveInMinutes: 1440 maxSize: 100000 + assets: + timeToLiveInMinutes: 1440 + maxSize: 100000 redis: # standalone or cluster diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index f2dab45491..20cf91c3dd 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -21,6 +21,9 @@ caffeine.specs.deviceCredentials.maxSize=100000 caffeine.specs.devices.timeToLiveInMinutes=1440 caffeine.specs.devices.maxSize=100000 +caffeine.specs.assets.timeToLiveInMinutes=1440 +caffeine.specs.assets.maxSize=100000 + caching.specs.devices.timeToLiveInMinutes=1440 caching.specs.devices.maxSize=100000 From 7c0087e6dfd95c791e5e4ce7f862d94088553a2a Mon Sep 17 00:00:00 2001 From: Jan Bols Date: Thu, 19 Jul 2018 18:27:22 +0200 Subject: [PATCH 03/30] Make RemoteToRuleChainTellNextMsg's parent class RuleNodeToRuleChainTellNextMsg Serializable. --- .../server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java | 2 +- .../actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java index fe0c2efae1..2dcd60af66 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RemoteToRuleChainTellNextMsg.java @@ -28,7 +28,7 @@ import java.io.Serializable; * Created by ashvayka on 19.03.18. */ @Data -final class RemoteToRuleChainTellNextMsg extends RuleNodeToRuleChainTellNextMsg implements TenantAwareMsg, RuleChainAwareMsg, Serializable { +final class RemoteToRuleChainTellNextMsg extends RuleNodeToRuleChainTellNextMsg implements TenantAwareMsg, RuleChainAwareMsg { private static final long serialVersionUID = 2459605482321657447L; private final TenantId tenantId; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java index 9414892a98..9914463981 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java @@ -21,14 +21,16 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; +import java.io.Serializable; import java.util.Set; /** * Created by ashvayka on 19.03.18. */ @Data -class RuleNodeToRuleChainTellNextMsg implements TbActorMsg { +class RuleNodeToRuleChainTellNextMsg implements TbActorMsg, Serializable { + private static final long serialVersionUID = 4577026446412871820L; private final RuleNodeId originator; private final Set relationTypes; private final TbMsg msg; From 222e6fcfe6d56cffa067dde84c5676f8dc2ccb81 Mon Sep 17 00:00:00 2001 From: Mauro Cicolella Date: Fri, 27 Jul 2018 09:23:04 +0200 Subject: [PATCH 04/30] Update Italian translation --- ui/src/app/locale/locale.constant-it_IT.json | 86 ++++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json index e8f973b0c5..cdae11a4b9 100644 --- a/ui/src/app/locale/locale.constant-it_IT.json +++ b/ui/src/app/locale/locale.constant-it_IT.json @@ -498,25 +498,25 @@ "public-link": "Link pubblico", "copy-public-link": "Copia link pubblico", "public-link-copied-message": "Link pubblico della dashboard copiato negli appunti", - "manage-states": "Manage dashboard states", - "states": "Dashboard states", - "search-states": "Search dashboard states", - "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} } selected", - "edit-state": "Edit dashboard state", - "delete-state": "Delete dashboard state", - "add-state": "Add dashboard state", - "state": "Dashboard state", + "manage-states": "Gestisci stati dashboard", + "states": "Stati dashboard", + "search-states": "Ricerca stati dashboard", + "selected-states": "{ count, plural, 1 {1 stato dashboard selezionato} other {# stati dashboard selezionati} }", + "edit-state": "Modifica stato dashboard", + "delete-state": "Elimina stato dashboard", + "add-state": "Aggiungi stato dashboard", + "state": "Stato dashboard", "state-name": "Nome", - "state-name-required": "Dashboard state name is required.", - "state-id": "State Id", - "state-id-required": "Dashboard state id is required.", - "state-id-exists": "Dashboard state with the same id is already exists.", - "is-root-state": "Root state", - "delete-state-title": "Delete dashboard state", - "delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?", + "state-name-required": "Nome stato dashboard obbligatorio.", + "state-id": "Id stato", + "state-id-required": "Id stato dashboard obbligatorio.", + "state-id-exists": "Uno stato della dashboard con lo stesso id è già presente.", + "is-root-state": "Stato radice", + "delete-state-title": "Elimina stato dashboard", + "delete-state-text": "Sei sicuro di voler eliminare lo stato della dashboard di nome '{{stateName}}'?", "show-details": "Mostra dettagli", "hide-details": "Nascondi dettagli", - "select-state": "Select target state", + "select-state": "Seleziona stato target", "state-controller": "Stato controller" }, "datakey": { @@ -570,7 +570,7 @@ "alias-required": "Alias dispositivo richesto.", "remove-alias": "Rimuovi alias dispositivo", "add-alias": "Aggiungi alias dispositivo", - "name-starts-with": "Dispositivo il cui nome comincia per", + "name-starts-with": "Dispositivo il cui nome inizia per", "device-list": "Lista dispositivi", "use-device-name-filter": "Usa filtro", "device-list-empty": "Nessun dispositivo selezionato.", @@ -680,7 +680,7 @@ "entity-list-empty": "Nessuna entità selezionata.", "entity-type-list-empty": "Nessun tipo di entità selezionato.", "entity-name-filter-required": "Filtro nome entità obbligatorio.", - "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", + "entity-name-filter-no-entity-matched": "Nessuna entità che inizia per '{{entity}}' è stata trovata.", "all-subtypes": "Tutte", "select-entities": "Seleziona entità", "no-aliases-found": "Nessun alias trovato.", @@ -733,7 +733,7 @@ "type-rulechains": "Rule chains", "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'", - "type-current-customer": "Current Customer", + "type-current-customer": "Cliente attuale", "search": "Ricerca entità", "selected-entities": "{ count, plural, 1 {1 entità selezionata} other {# entità selezionate} }", "entity-name": "Nome entità", @@ -788,13 +788,13 @@ "delete-extension-text": "Attenzione, dopo la conferma l'estensione e tutti i suoi data non saranno più recuperabili.", "delete-extensions-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 estensione} other {# estensioni} }?", "delete-extensions-text": "Attenzione, dopo la conferma tutte le estensioni selezionate saranno eliminate.", - "converters": "Converters", - "converter-id": "Converter id", + "converters": "Convertitori", + "converter-id": "Id convertitore", "configuration": "Configurazione", - "converter-configurations": "Converter configurations", + "converter-configurations": "Configurazioni convertitore", "token": "Token di sicurezza", - "add-converter": "Add converter", - "add-config": "Add converter configuration", + "add-converter": "Aggiungi convertitore", + "add-config": "Aggiungi configurazione convertitore", "device-name-expression": "Device name expression", "device-type-expression": "Device type expression", "custom": "Custom", @@ -828,7 +828,7 @@ "drop-file": "Trascina un file o fai clic per selezionare un file da caricare.", "mapping": "Mapping", "topic-filter": "Filtro topic", - "converter-type": "Converter type", + "converter-type": "Tipo convertitore", "converter-json": "Json", "json-name-expression": "Device name json expression", "topic-name-expression": "Device name topic expression", @@ -847,10 +847,10 @@ "converter-json-required": "Convertitore json obbligatorio.", "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", + "connect-requests": "Richieste di connessione", + "add-connect-request": "Aggiungi richiesta di connessione", + "disconnect-requests": "Richieste di disconnessione", + "add-disconnect-request": "Aggiungi richiesta di disconnessione", "attribute-requests": "Attribute requests", "add-attribute-request": "Add attribute request", "attribute-updates": "Attribute updates", @@ -919,8 +919,8 @@ "not-available": "Non disponibile" }, - "export-extensions-configuration": "Export extensions configuration", - "import-extensions-configuration": "Import extensions configuration", + "export-extensions-configuration": "Esporta configurazione estensioni", + "import-extensions-configuration": "Importa configurazione estensioni", "import-extensions": "Importa estensione", "import-extension": "Importa estensione", "export-extension": "Esporta estensione", @@ -928,7 +928,7 @@ "invalid-file-error": "File estensione non valido" }, "fullscreen": { - "expand": "Expand to fullscreen", + "expand": "Espandi a tutto schermo", "exit": "Esci da schermo intero", "toggle": "Commuta modalità schermo intero", "fullscreen": "Schermo intero" @@ -937,16 +937,16 @@ "function": "Funzione" }, "grid": { - "delete-item-title": "Are you sure you want to delete this item?", - "delete-item-text": "Be careful, after the confirmation this item and all related data will become unrecoverable.", - "delete-items-title": "Are you sure you want to delete { count, plural, 1 {1 item} other {# items} }?", - "delete-items-action-title": "Delete { count, plural, 1 {1 item} other {# items} }", - "delete-items-text": "Be careful, after the confirmation all selected items will be removed and all related data will become unrecoverable.", - "add-item-text": "Add new item", - "no-items-text": "No items found", - "item-details": "Item details", - "delete-item": "Delete Item", - "delete-items": "Delete Items", + "delete-item-title": "Sei sicuro di voler eliminare questo elemento?", + "delete-item-text": "Attenzione, dopo la conferma questo elemento e tutti i suoi dati non saranno più recuperabili.", + "delete-items-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 elemento} other {# elementi} }?", + "delete-items-action-title": "Elimina { count, plural, 1 {1 elemento} other {# elementi} }", + "delete-items-text": "Attenzione, dopo la conferma tutti gli elementi selezionati saranno rimossi e i relativi dati non saranno più recuperabili.", + "add-item-text": "Aggiungi nuovo elemento", + "no-items-text": "Nessun elemento trovato", + "item-details": "Dettagli elemento", + "delete-item": "Elimina elemento", + "delete-items": "Elimina elementi", "scroll-to-top": "Scroll to top" }, "help": { @@ -985,7 +985,7 @@ "settings": "Impostazioni layout", "color": "Colore", "main": "Main", - "right": "Right", + "right": "Destra", "select": "Select target layout" }, "legend": { From 0b13fa2f0e74e5de26afe9fb0fc1874660fd4342 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Fri, 3 Aug 2018 12:08:57 +0300 Subject: [PATCH 05/30] Hotfix for actor restarts during creation failure --- .../device/DeviceActorMessageProcessor.java | 2 +- .../actors/session/SessionManagerActor.java | 19 ++++++++++++++++++- .../server/actors/tenant/TenantActor.java | 7 ++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 9527171624..a2ea0487cf 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -114,7 +114,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso private String deviceType; private TbMsgMetaData defaultMetaData; - public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, DeviceId deviceId) { + DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, DeviceId deviceId) { super(systemContext, logger); this.tenantId = tenantId; this.deviceId = deviceId; diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java index 624a9f49c7..9039d302e3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java @@ -15,13 +15,17 @@ */ package org.thingsboard.server.actors.session; +import akka.actor.ActorInitializationException; import akka.actor.ActorRef; import akka.actor.InvalidActorNameException; import akka.actor.LocalActorRef; +import akka.actor.OneForOneStrategy; import akka.actor.Props; +import akka.actor.SupervisorStrategy; import akka.actor.Terminated; import akka.event.Logging; import akka.event.LoggingAdapter; +import akka.japi.Function; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.service.ContextAwareActor; import org.thingsboard.server.actors.service.ContextBasedCreator; @@ -34,6 +38,7 @@ import org.thingsboard.server.common.msg.cluster.ClusterEventMsg; import org.thingsboard.server.common.msg.core.ActorSystemToDeviceSessionActorMsg; import org.thingsboard.server.common.msg.core.SessionCloseMsg; import org.thingsboard.server.common.msg.session.SessionCtrlMsg; +import scala.concurrent.duration.Duration; import java.util.HashMap; import java.util.Map; @@ -46,11 +51,16 @@ public class SessionManagerActor extends ContextAwareActor { private final Map sessionActors; - public SessionManagerActor(ActorSystemContext systemContext) { + SessionManagerActor(ActorSystemContext systemContext) { super(systemContext); this.sessionActors = new HashMap<>(INITIAL_SESSION_MAP_SIZE); } + @Override + public SupervisorStrategy supervisorStrategy() { + return strategy; + } + @Override protected boolean process(TbActorMsg msg) { //TODO Move everything here, to work with TbActorMsg @@ -160,4 +170,11 @@ public class SessionManagerActor extends ContextAwareActor { } } + private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), new Function() { + @Override + public SupervisorStrategy.Directive apply(Throwable t) { + logger.error(t, "Unknown failure"); + return SupervisorStrategy.stop(); + } + }); } diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index b4ab0d2cb2..460b64c772 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.actors.tenant; +import akka.actor.ActorInitializationException; import akka.actor.ActorRef; import akka.actor.OneForOneStrategy; import akka.actor.Props; @@ -170,7 +171,11 @@ public class TenantActor extends RuleChainManagerActor { @Override public SupervisorStrategy.Directive apply(Throwable t) { logger.error(t, "Unknown failure"); - return SupervisorStrategy.resume(); + if(t instanceof ActorInitializationException){ + return SupervisorStrategy.stop(); + } else { + return SupervisorStrategy.resume(); + } } }); From a069e9bbac34ba6c65ffd6cea70147efaa0542ae Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 3 Aug 2018 13:34:49 +0300 Subject: [PATCH 06/30] Update UI ver. --- ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/package.json b/ui/package.json index 06b7feea78..1b8ceda7f2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard", "private": true, - "version": "2.1.0", + "version": "2.1.1", "description": "Thingsboard UI", "licenses": [ { From 2aaa327ff39889684e1e46cda476968309a56df5 Mon Sep 17 00:00:00 2001 From: souhaiebtar Date: Mon, 6 Aug 2018 10:52:37 +0100 Subject: [PATCH 07/30] change to reduce the docker image by around 10 mb --- docker/tb/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/tb/Dockerfile b/docker/tb/Dockerfile index 8917657bc0..24281f9f3e 100644 --- a/docker/tb/Dockerfile +++ b/docker/tb/Dockerfile @@ -20,5 +20,7 @@ ADD run-application.sh /run-application.sh ADD thingsboard.deb /thingsboard.deb RUN apt-get update \ - && apt-get install -y nmap \ + && apt-get install --no-install-recommends -y nmap \ + && apt-get clean \ + && rm -r /var/lib/apt/lists/* \ && chmod +x /run-application.sh From 10fead768e369a076b5c21cd1fe6ef6755e82eae Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Mon, 6 Aug 2018 19:49:13 +0300 Subject: [PATCH 08/30] Alarm Controller audit log fix --- .../java/org/thingsboard/server/controller/AlarmController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 51ef5668bd..586e8c35ef 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -86,7 +86,7 @@ public class AlarmController extends BaseController { savedAlarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); return savedAlarm; } catch (Exception e) { - logEntityAction(emptyId(EntityType.ASSET), alarm, + logEntityAction(emptyId(EntityType.ALARM), alarm, null, alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); throw handleException(e); } From 515ef81f9ff385025617a8aeb3b278cdbabce695 Mon Sep 17 00:00:00 2001 From: Mauro Cicolella Date: Tue, 7 Aug 2018 17:22:16 +0200 Subject: [PATCH 09/30] Update Italian translation --- ui/src/app/locale/locale.constant-it_IT.json | 86 ++++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json index cdae11a4b9..b304cac78c 100644 --- a/ui/src/app/locale/locale.constant-it_IT.json +++ b/ui/src/app/locale/locale.constant-it_IT.json @@ -30,8 +30,8 @@ "apply": "Applica", "apply-changes": "Applica modifiche", "edit-mode": "Modalità modifica", - "enter-edit-mode": "Attiva la modalità di modifica", - "decline-changes": "Annulla le modifiche", + "enter-edit-mode": "Attiva modalità di modifica", + "decline-changes": "Annulla modifiche", "close": "Chiudi", "back": "Indietro", "run": "Esegui", @@ -108,7 +108,7 @@ "no-alarms-prompt": "Nessun allarme trovato", "created-time": "Orario di creazione", "type": "Tipo", - "severity": "Gravità", + "severity": "Livello di gravità", "originator": "Origine", "originator-type": "Tipo origine", "details": "Dettagli", @@ -125,7 +125,7 @@ "severity-indeterminate": "Indeterminato", "acknowledge": "Conferma", "clear": "Cancella", - "search": "Ricerca allarmi", + "search": "Cerca allarmi", "selected-alarms": "{ count, plural, 1 {1 allarme selezionato} other {# allarmi selezionati} }", "no-data": "Nessun dato da visualizzare", "polling-interval": "Intervallo di polling (sec) Allarmi", @@ -267,7 +267,7 @@ }, "audit-log": { "audit": "Audit", - "audit-logs": "Audit Logs", + "audit-logs": "Log Audit", "timestamp": "Timestamp", "entity-type": "Tipo Entità", "entity-name": "Nome Entità", @@ -294,7 +294,7 @@ "no-audit-logs-prompt": "Log non trovati", "action-data": "Action data", "failure-details": "Failure details", - "search": "Ricerca log audit", + "search": "Cerca log audit", "clear-search": "Cancella ricerca" }, "confirm-on-exit": { @@ -319,7 +319,7 @@ "password": "Password", "enter-username": "Inserisci nome utente", "enter-password": "Inserisci password", - "enter-search": "Inserisci ricerca" + "enter-search": "Cerca ..." }, "content-type": { "json": "Json", @@ -391,7 +391,7 @@ "unassign-from-customer": "Unassign from customer", "make-public": "Rendi pubblica la dashboard", "make-private": "Rendi privata la dashboard", - "manage-assigned-customers": "Gestisci i clienti assegnati", + "manage-assigned-customers": "Gestisci clienti assegnati", "assigned-customers": "Clienti assegnati", "assign-to-customers": "Assegna Dashboard ai Clienti", "assign-to-customers-text": "Seleziona i clienti da assegnare alla/alle dashboard", @@ -407,7 +407,7 @@ "title-required": "Titolo obbligatorio.", "description": "Descrizione", "details": "Dettagli", - "dashboard-details": "Dettagli Dashboard", + "dashboard-details": "Dettagli dashboard", "add-dashboard-text": "Aggiungi nuova dashboard", "assign-dashboards": "Assegna dashboard", "assign-new-dashboard": "Assegna nuova dashboard", @@ -472,7 +472,7 @@ "title-color": "Colore titolo", "display-dashboards-selection": "Mostra selezione dashboard", "display-entities-selection": "Mostra selezione entità", - "display-dashboard-timewindow": "Mostra finestra temporale", + "display-dashboard-timewindow": "Mostra intervallo temporale", "display-dashboard-export": "Mostra esportazione", "import": "Importa dashboard", "export": "Esporta dashboard", @@ -577,7 +577,7 @@ "device-name-filter-required": "Filtro nome dispositivo obbligatorio.", "device-name-filter-no-device-matched": "Nessun dispositivo il cui nome inizia per '{{device}}' è stato trovato.", "add": "Aggiungi Dispositivo", - "assign-to-customer": "Assigna al cliente", + "assign-to-customer": "Assegna al cliente", "assign-device-to-customer": "Assegna dispositivo/dispositivi al Cliente", "assign-device-to-customer-text": "Seleziona i dispositivi da assegnare al cliente", "make-public": "Rendi pubblico il dispositivo", @@ -605,7 +605,7 @@ "delete-device-text": "Attenzione, dopo la conferma il dispositivo e tutti i suoi dati non saranno più recuperabili.", "delete-devices-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 dispositivo} other {# dispositivi} }?", "delete-devices-action-title": "Elimina { count, plural, 1 {1 dispositivo} other {# dispositivi} }", - "delete-devices-text": "Attenzione, dopo la conferma tutti i dispositivi selezionati saranno elimininati e i relativi dati non saranno più recuperabili.", + "delete-devices-text": "Attenzione, dopo la conferma tutti i dispositivi selezionati saranno eliminati e i relativi dati non saranno più recuperabili.", "unassign-device-title": "Sei sicuro di voler annullare l'assegnazione del dispositivo '{{deviceName}}'?", "unassign-device-text": "Dopo la conferma sarà annullata l'assegnazione del dispositivo e questo non sarà più accessibile dal cliente.", "unassign-device": "Annulla assegnazione dispositivo", @@ -947,7 +947,7 @@ "item-details": "Dettagli elemento", "delete-item": "Elimina elemento", "delete-items": "Elimina elementi", - "scroll-to-top": "Scroll to top" + "scroll-to-top": "Scorri verso l'alto" }, "help": { "goto-help-page": "Go to help page" @@ -1030,7 +1030,7 @@ }, "relation": { "relations": "Relations", - "direction": "Direction", + "direction": "Direzione", "search-direction": { "FROM": "Da", "TO": "A" @@ -1072,39 +1072,39 @@ }, "rulechain": { "rulechain": "Rule chain", - "rulechains": "Rule chains", + "rulechains": "Rule chain", "root": "Root", - "delete": "Delete rule chain", + "delete": "Cancella rule chain", "name": "Nome", "name-required": "Nome obbligatorio.", "description": "Descrizione", - "add": "Add Rule Chain", + "add": "Aggiungi 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", + "delete-rulechain-title": "Sei sicuro di voler eliminare la rule chain '{{ruleChainName}}'?", + "delete-rulechain-text": "Attenzione, dopo la conferma la rule chain e tutti i dati relativi non saranno più recuperabili.", + "delete-rulechains-title": "Sei sicuro di voler eliminare { count, plural, 1 {1 rule chain} other {# rule chain} }?", + "delete-rulechains-action-title": "Elimina { count, plural, 1 {1 rule chain} other {# rule chain} }", + "delete-rulechains-text": "Attenzione, dopo la conferma tutte le rule chain selezionate saranno rimosse e tutti i relativi dati non saranno più recuperabili.", + "add-rulechain-text": "Aggiungi nuova rule chain", + "no-rulechains-text": "Nessuna rule chain trovata", + "rulechain-details": "Dettagli rule chain", "details": "Dettagli", "events": "Eventi", "system": "Sistema", - "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", + "import": "Importa rule chain", + "export": "Esporta rule chain", + "export-failed-error": "Impossibile esportare rule chain: {{error}}", + "create-new-rulechain": "Crea nuova rule chain", + "rulechain-file": "File rule chain", + "invalid-rulechain-file-error": "Impossibile importare rule chain: struttura dati rule chain non valida.", + "copyId": "Copia Id rule chain", + "idCopiedMessage": "Id rule chain copiato negli appunti", + "select-rulechain": "Seleziona rule chain", + "no-rulechains-matching": "Nessuna rule chain corrispondente a '{{entity}}' è stata trovata.", + "rulechain-required": "Rule chain obbligatoria", + "management": "Gestione regole", "debug-mode": "Modalità debug" }, "rulenode": { @@ -1209,10 +1209,10 @@ "history": "Cronologia", "last-prefix": "ultimo", "period": "from {{ startTime }} to {{ endTime }}", - "edit": "Edit timewindow", - "date-range": "Date range", + "edit": "Modifica intervallo temporale", + "date-range": "Intervallo date", "last": "Ultimo", - "time-period": "Time period" + "time-period": "Intervallo temporale" }, "user": { "user": "Utente", @@ -1379,10 +1379,10 @@ "mobile-mode-settings": "Impostazioni modalità mobile", "order": "Ordinamento", "height": "Altezza", - "units": "Simbolo speciale da mostrare vicino al valore", + "units": "Simbolo speciale da mostrare accanto al valore", "decimals": "Numero di cifre decimali", - "timewindow": "Timewindow", - "use-dashboard-timewindow": "Use dashboard timewindow", + "timewindow": "Intervallo temporale", + "use-dashboard-timewindow": "Usa intervallo temporale dashboard", "display-legend": "Mostra legenda", "datasources": "Sorgenti dei dati", "maximum-datasources": "Massimo { count, plural, 1 {1 sorgente dati consentita.} other {# sorgenti dati consentite} }", From 435eac59ceebba5b2710086cd1974e6f9ad2af59 Mon Sep 17 00:00:00 2001 From: nabil rejeb Date: Wed, 15 Aug 2018 09:25:23 +0100 Subject: [PATCH 10/30] Update french language --- ui/src/app/locale/locale.constant-en_US.json | 1 + ui/src/app/locale/locale.constant-es_ES.json | 1 + ui/src/app/locale/locale.constant-fr_FR.json | 1461 ++++++++++++++++++ ui/src/app/locale/locale.constant-it_IT.json | 1 + ui/src/app/locale/locale.constant-ko_KR.json | 1 + ui/src/app/locale/locale.constant-ru_RU.json | 1 + ui/src/app/locale/locale.constant-zh_CN.json | 1 + 7 files changed, 1467 insertions(+) create mode 100644 ui/src/app/locale/locale.constant-fr_FR.json diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 22aad1a90d..b57f12fb83 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -1451,6 +1451,7 @@ "language": { "language": "Language", "locales": { + "fr_FR": "French", "zh_CN": "Chinese", "en_US": "English", "it_IT": "Italian", diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json index 9e5951f0c8..f9f4d3a209 100644 --- a/ui/src/app/locale/locale.constant-es_ES.json +++ b/ui/src/app/locale/locale.constant-es_ES.json @@ -1307,6 +1307,7 @@ "language": "Lenguaje", "locales": { "en_US": "Inglés", + "fr_FR": "Francés", "ko_KR": "Coreano", "zh_CN": "Chino", "ru_RU": "Ruso", diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json new file mode 100644 index 0000000000..2c0a290e16 --- /dev/null +++ b/ui/src/app/locale/locale.constant-fr_FR.json @@ -0,0 +1,1461 @@ +{ +"access":{ + "access-forbidden": "Accès interdit", + "access-forbidden-text": "Vous n'avez pas accès à cet emplacement!
Essayez de vous connecter avec un autre utilisateur si vous souhaitez toujours accéder à cet emplacement.", + "refresh-token-expired": "La session a expiré", + "refresh-token-failed": "Impossible de rafraîchir la session", + "unauthorized": "non autorisé", + "unauthorized-access": "accès non autorisé", + "unauthorized-access-text": "Vous devez vous connecter pour avoir accès à cette ressource!" + }, +"action":{ + "activate": "Activer", + "add": "Ajouter", + "apply": "Appliquer", + "apply-changes": "Appliquer les modifications", + "assign": "Attribuer", + "back": "retour", + "cancel": "Annuler", + "clear-search": "Effacer la recherche", + "close": "Fermer", + "copy": "Copier", + "copy-reference": "Copier la référence", + "create": "Créer", + "decline-changes": "Refuser les modifications", + "delete": "Supprimer", + "drag": "Drag", + "edit": "Modifier", + "edit-mode": "Mode édition", + "enter-edit-mode": "Entrer en mode édition", + "export": "Exporter", + "import": "Importer", + "make-private": "Rendre privé", + "no": "Non", + "ok": "OK", + "paste": "coller", + "paste-reference": "Coller référence", + "refresh": "Rafraîchir", + "remove": "Supprimer", + "run": "Exécuter", + "save": "Enregistrer", + "saveAs": "Enregistrer sous", + "search": "Rechercher", + "share": "Partager", + "share-via": "Partager via {{provider}}", + "sign-in": "Connectez-vous!", + "suspend": "Suspendre", + "unassign": "Retirer", + "undo": "Annuler", + "update": "mise à jour", + "view": "Afficher", + "yes": "Oui" + }, +"admin":{ + "base-url": "URL de base", + "base-url-required": "L'URL de base est requise.", + "enable-tls": "Activer TLS", + "general": "Général", + "general-settings": "Paramètres généraux", + "mail-from": "Mail de", + "mail-from-required": "Mail de est requis.", + "outgoing-mail": "courrier sortant", + "outgoing-mail-settings": "Paramètres de courrier sortant", + "send-test-mail": "Envoyer un mail de test", + "smtp-host": "Hôte SMTP", + "smtp-host-required": "L'hôte SMTP est requis.", + "smtp-port": "Port SMTP", + "smtp-port-invalid": "Cela ne ressemble pas à un port smtp valide.", + "smtp-port-required": "Vous devez fournir un port smtp.", + "smtp-protocol": "Protocole SMTP", + "system-settings": "Paramètres système", + "test-mail-sent": "Le courrier de test a été envoyé avec succès!", + "timeout-invalid": "Cela ne ressemble pas à un délai d'expiration valide.", + "timeout-msec": "Délai (msec)", + "timeout-required": "Le délai est requis." + }, +"aggregation":{ + "aggregation": "agrégation", + "avg": "Moyenne", + "count": "Compte", + "function": "Fonction d'agrégation de données", + "group-interval": "Intervalle de regroupement", + "limit": "Valeurs maximales", + "max": "Max", + "min": "Min", + "none": "Aucune", + "sum": "Somme" + }, +"alarm":{ + "ack-time": "Heure d'acquittement", + "acknowledge": "Acquitter", + "aknowledge-alarms-text": "Etes-vous sûr de vouloir acquitter {count, plural, 1 {1 alarme} other {# alarmes}}?", + "aknowledge-alarms-title": "Acquitter {count, plural, 1 {1 alarme} other {# alarmes}}", + "alarm": "Alarme", + "alarm-details": "Détails de l'alarme", + "alarm-required": "Une alarme est requise", + "alarm-status": "Etat d'alarme", + "alarms": "Alarmes", + "clear": "Effacer", + "clear-alarms-text": "Êtes-vous sûr de vouloir effacer {count, plural, 1 {1 alarme} other {# alarmes}}?", + "clear-alarms-title": "Effacer {count, plural, 1 {1 alarme} other {# alarmes}}", + "clear-time": "Heure d'éffacement", + "created-time": "Heure de création", + "details": "Détails", + "display-status":{ + "ACTIVE_ACK": "Active acquittée", + "ACTIVE_UNACK": "Active non acquittée", + "CLEARED_ACK": "effacée acquittée", + "CLEARED_UNACK": "effacée non acquittée" + }, + "end-time": "Heure de fin", + "min-polling-interval-message": "Un intervalle d'interrogation d'au moins 1 seconde est autorisé.", + "no-alarms-matching": "Aucune alarme correspondant à {{entity}} n'a été trouvée. ", + "no-alarms-prompt": "Aucune alarme trouvée", + "no-data": "Aucune donnée à afficher", + "originator": "Source", + "originator-type": "Type de Source", + "polling-interval": "Intervalle d'interrogation des alarmes (sec)", + "polling-interval-required": "L'intervalle d'interrogation des alarmes est requis.", + "search": "Rechercher des alarmes", + "search-status":{ + "ACK": "acquitté", + "ACTIVE": "active", + "ANY": "Toutes", + "CLEARED": "effacée", + "UNACK": "non acquittée" + }, + "select-alarm": "Sélectionnez une alarme", + "selected-alarms": "{count, plural, 1 {1 alarme} other {# alarmes}} sélectionnées", + "severity": "Gravitée", + "severity-critical": "Critique", + "severity-indeterminate": "indéterminée", + "severity-major": "Majeure", + "severity-minor": "mineure", + "severity-warning": "Avertissement", + "start-time": "Heure de début", + "status": "Etat", + "type": "Type" + }, +"alias":{ + "add": "Ajouter un alias", + "all-entities": "Toutes les entités", + "any-relation": "toutes", + "default-entity-parameter-name": "Par défaut", + "default-state-entity": "Entité d'état par défaut", + "duplicate-alias": "Un alias portant le même nom existe déjà.", + "edit": "Modifier l'alias", + "entity-filter": "Filtre d'entité", + "entity-filter-no-entity-matched": "Aucune entité correspondant au filtre spécifié n'a été trouvée.", + "filter-type": "Type de filtre", + "filter-type-asset-search-query": "requête de recherche d'Assets", + "filter-type-asset-search-query-description": "Assets de types {{assetTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-asset-type": "type d'Asset", + "filter-type-asset-type-and-name-description": "Assets de type '{{assetType}}' et dont le nom commence par '{{prefix}}'", + "filter-type-asset-type-description": "Assets de type '{{assetType}}'", + "filter-type-device-search-query": "Requête de recherche de dispositif", + "filter-type-device-search-query-description": "Dispositifs de types {{deviceTypes}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-device-type": "Type de dispositif", + "filter-type-device-type-and-name-description": "Dispositifs de type '{{deviceType}}' et dont le nom commence par '{{prefix}}'", + "filter-type-device-type-description": "Dispositifs de type '{{deviceType}}'", + "filter-type-entity-list": "Liste d'entités", + "filter-type-entity-name": "Nom d'entité", + "filter-type-relations-query": "Interrogation des relations", + "filter-type-relations-query-description": "{{entities}} ayant {{relationType}} relation {{direction}} {{rootEntity}}", + "filter-type-required": "Le type de filtre est requis.", + "filter-type-single-entity": "Entité unique", + "filter-type-state-entity": "Entité de l'état du tableau de bord", + "filter-type-state-entity-description": "Entité extraite des paramètres d'état du tableau de bord", + "max-relation-level": "Niveau de relation maximum", + "name": "Nom de l'alias", + "name-required": "Le nom d'alias est requis", + "no-entity-filter-specified": "Aucun filtre d'entité spécifié", + "resolve-multiple": "Résoudre en plusieurs entités", + "root-entity": "Entité racine", + "root-state-entity": "Utiliser l'entité d'état du tableau de bord en tant que racine", + "state-entity": "Entité d'état du tableau de bord", + "state-entity-parameter-name": "Nom du paramètre d'entité d'état", + "unlimited-level": "niveau illimité" + }, +"asset":{ + "add": "Ajouter un Asset", + "add-asset-text": "Ajouter un nouvel Asset", + "any-asset": "Tout Asset", + "asset": "Asset", + "asset-details": "Détails de l'Asset", + "asset-public": "L'Asset est public", + "asset-required": "Asset requis", + "asset-type": "Type d'Asset", + "asset-type-list-empty": "Aucun type d'Asset sélectionné.", + "asset-type-required": "Le type d'Asset est requis.", + "asset-types": "Types d'Asset", + "assets": "Assets", + "assign-asset-to-customer": "Attribuer des Assets au client", + "assign-asset-to-customer-text": "Veuillez sélectionner les Assets à attribuer au client", + "assign-assets": "Attribuer des Assets", + "assign-assets-text": "Attribuer {count, plural, 1 {1 asset} other {# assets}} au client", + "assign-new-asset": "Attribuer un nouvel Asset", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les Assets", + "assignedToCustomer": "attribué au client", + "copyId": "Copier l'Id de l'Asset", + "delete": "Supprimer un Asset", + "delete-asset-text": "Faites attention, après la confirmation, l'Asset et toutes les données associées deviendront irrécupérables.", + "delete-asset-title": "Êtes-vous sûr de vouloir supprimer l'Asset '{{assetName}}'?", + "delete-assets": "Supprimer des Assets", + "delete-assets-action-title": "Supprimer {count, plural, 1 {1 asset} other {# assets}}", + "delete-assets-text": "Attention, après la confirmation, tous les Assets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-assets-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 asset} other {# assets}}?", + "description": "Description", + "details": "Détails", + "enter-asset-type": "Entrez le type d'Asset", + "events": "Evènements", + "idCopiedMessage": "L'Id d'asset a été copié dans le presse-papier", + "make-private": "Rendre l'Asset privé", + "make-private-asset-text": "Après la confirmation, l'Asset et toutes ses données seront rendus privés et ne seront pas accessibles par d'autres.", + "make-private-asset-title": "Etes-vous sûr de vouloir rendre l'Asset '{{assetName}}' privé '?", + "make-public": "Rendre l'Asset public", + "make-public-asset-text": "Après la confirmation, l'asset et toutes ses données seront rendus publics et accessibles aux autres.", + "make-public-asset-title": "Êtes-vous sûr de vouloir rendre l'Asset '{{assetName}}' public '?", + "management": "Gestion d'Assets", + "name": "Nom", + "name-required": "Nom est requis.", + "name-starts-with": "Le nom de l'Asset commence par", + "no-asset-types-matching": "Aucun type d'Asset correspondant à {{entitySubtype}} n'a été trouvé. ", + "no-assets-matching": "Aucun Asset correspondant à {{entity}} n'a été trouvé. ", + "no-assets-text": "Aucun Asset trouvé", + "public": "Public", + "select-asset": "Sélectionner un Asset", + "select-asset-type": "Sélectionner le type d'Asset", + "type": "Type", + "type-required": "Le type est requis.", + "unassign-asset": "Retirer l'Asset", + "unassign-asset-text": "Après la confirmation, l'Asset sera non attribué et ne sera pas accessible au client.", + "unassign-asset-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'Asset '{{assetName}}'?", + "unassign-assets": "Retirer les Assets", + "unassign-assets-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets}} du client", + "unassign-assets-text": "Après la confirmation, tous les Assets sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", + "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?", + "unassign-from-customer": "Retirer du client", + "view-assets": "Afficher les Assets" + }, +"attribute":{ + "add": "Ajouter un attribut", + "add-to-dashboard": "Ajouter au tableau de bord", + "add-widget-to-dashboard": "Ajouter un widget au tableau de bord", + "attributes": "Attributs", + "attributes-scope": "Etendue des attributs d'entité", + "delete-attributes": "Supprimer les attributs", + "delete-attributes-text": "Attention, après la confirmation, tous les attributs sélectionnés seront supprimés.", + "delete-attributes-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 attribut} other {# attributs}}?", + "enter-attribute-value": "Entrez la valeur de l'attribut", + "key": "Clé", + "key-required": "La Clé d'attribut est requise.", + "last-update-time": "Dernière mise à jour", + "latest-telemetry": "Dernière télémétrie", + "next-widget": "Widget suivant", + "prev-widget": "Widget précédent", + "scope-client": "Attributs du client", + "scope-latest-telemetry": "Dernière télémétrie", + "scope-server": "Attributs du serveur", + "scope-shared": "Attributs partagés", + "selected-attributes": "{count, plural, 1 {1 attribut} other {# attributs}} sélectionnés", + "selected-telemetry": "{count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie}} sélectionnées", + "show-on-widget": "Afficher sur le widget", + "value": "Valeur", + "value-required": "La valeur d'attribut est obligatoire.", + "widget-mode": "Mode du widget" + }, +"audit-log":{ + "action-data": "Action data", + "audit": "Audit", + "audit-log-details": "Détails du journal d'audit", + "audit-logs": "Journaux d'audit", + "clear-search": "Effacer la recherche", + "details": "Détails", + "entity-name": "Nom de l'entité", + "entity-type": "Type d'entité", + "failure-details": "Détails de l'échec", + "no-audit-logs-prompt": "Aucun journal trouvé", + "search": "Rechercher les journaux d'audit", + "status": "Etat", + "status-failure": "Échec", + "status-success": "Succès", + "timestamp": "Horodatage", + "type": "Type", + "type-activated": "Activé", + "type-added": "Ajouté", + "type-alarm-ack": "Acquitté", + "type-alarm-clear": "Effacé", + "type-assigned-to-customer": "Attribué au client", + "type-attributes-deleted": "Attributs supprimés", + "type-attributes-read": "Attributs lus", + "type-attributes-updated": "Attributs mis à jour", + "type-credentials-read": "Lecture des informations d'identification", + "type-credentials-updated": "Informations d'identification actualisées", + "type-deleted": "Supprimé", + "type-relation-add-or-update": "Relation mise à jour", + "type-relation-delete": "Relation supprimée", + "type-relations-delete": "Toutes les relations ont été supprimées", + "type-rpc-call": "Appel RPC", + "type-suspended": "Suspendu", + "type-unassigned-from-customer": "Non attribué du client", + "type-updated": "Mise à jour", + "user": "Utilisateur" + }, +"common":{ + "enter-password": "Entrez le mot de passe", + "enter-search": "Entrez la recherche", + "enter-username": "Entrez le nom d'utilisateur", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, +"confirm-on-exit":{ + "html-message": "Vous avez des modifications non enregistrées.
Êtes-vous sûr de vouloir quitter cette page?", + "message": "Vous avez des modifications non enregistrées. Êtes-vous sûr de vouloir quitter cette page?", + "title": "Modifications non enregistrées" + }, +"contact":{ + "address": "Adresse", + "address2": "adresse 2", + "city": "Ville", + "country": "Pays", + "email": "Email", + "no-address": "Pas d'adresse", + "phone": "Téléphone", + "postal-code": "Code postal", + "postal-code-invalid": "Format de code postal / code postal invalide", + "state": "Etat / Province" + }, +"content-type":{ + "binary": "Binaire (Base64)", + "json": "Json", + "text": "Texte" + }, +"custom":{ + "widget-action":{ + "action-cell-button": "Action cell button", + "marker-click": "On marker click", + "row-click": "On row click", + "tooltip-tag-action": "Tooltip tag action" + } + }, +"customer":{ + "add": "Ajouter un client", + "add-customer-text": "Ajouter un nouveau client", + "assets": "Assets du client", + "copyId": "Copier l'id du client", + "customer": "Client", + "customer-details": "Détails du client", + "customer-required": "Le client est requis", + "customers": "Clients", + "dashboard": "Tableau de bord du client", + "dashboards": "tableaux de bord du client", + "default-customer": "Client par défaut", + "default-customer-required": "Le client par défaut est requis pour déboguer le tableau de bord au niveau du Tenant", + "delete": "Supprimer le client", + "delete-customer-text": "Faites attention, après la confirmation, le client et toutes les données associées deviendront irrécupérables.", + "delete-customer-title": "Êtes-vous sûr de vouloir supprimer le client '{{customerTitle}}'?", + "delete-customers-action-title": "Supprimer {count, plural, 1 {1 client} other {# clients}}", + "delete-customers-text": "Faites attention, après la confirmation, tous les clients sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-customers-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 client} other {# clients}}?", + "description": "Description", + "details": "Détails", + "devices": "Dispositifs du client", + "events": "Événements", + "idCopiedMessage": "L'Id du client a été copié dans le presse-papier", + "manage-assets": "Gérer les Assets", + "manage-customer-assets": "Gérer les Assets du client", + "manage-customer-dashboards": "Gérer les tableaux de bord du client", + "manage-customer-devices": "Gérer les dispositifs du client", + "manage-customer-users": "Gérer les utilisateurs du client", + "manage-dashboards": "Gérer les tableaux de bord", + "manage-devices": "Gérer les dispositifs", + "manage-public-assets": "Gérer les Assets publics", + "manage-public-dashboards": "Gérer les tableaux de bord publics", + "manage-public-devices": "Gérer les dispositifs publics", + "manage-users": "Gérer les utilisateurs", + "management": "Gestion des clients", + "no-customers-matching": "Aucun client correspondant à '{{entity}} n'a été trouvé.", + "no-customers-text": "Aucun client trouvé", + "public-assets": "Assets publics", + "public-dashboards": "Tableaux de bord publics", + "public-devices": "Dispositifs publics", + "select-customer": "Sélectionner un client", + "select-default-customer": "Sélectionnez le client par défaut", + "title": "Titre", + "title-required": "Le titre est requis." + }, +"dashboard":{ + "add": "Ajouter un tableau de bord", + "add-dashboard-text": "Ajouter un nouveau tableau de bord", + "add-state": "Ajouter un état du tableau de bord", + "add-widget": "Ajouter un nouveau widget", + "alias-resolution-error-title": "Erreur de configuration des alias de tableau de bord", + "assign-dashboard-to-customer": "Attribuer des tableaux de bord au client", + "assign-dashboard-to-customer-text": "Veuillez sélectionner les tableaux de bord à affecter au client", + "assign-dashboards": "Attribuer des tableaux de bord", + "assign-dashboards-text": "Attribuer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} aux clients", + "assign-new-dashboard": "Attribuer un nouveau tableau de bord", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les tableaux de bord", + "assign-to-customers": "Attribuer des tableaux de bord aux clients", + "assign-to-customers-text": "Veuillez sélectionner les clients pour attribuer les tableaux de bord", + "assigned-customers": "clients affectés", + "assignedToCustomer": "Attribué au client", + "assignedToCustomers": "attribué aux clients", + "autofill-height": "Hauteur de remplissage automatique", + "background-color": "Couleur de fond", + "background-image": "Image d'arrière-plan", + "background-size-mode": "Mode de taille d'arrière-plan", + "close-toolbar": "Fermer la barre d'outils", + "columns-count": "Nombre de colonnes", + "columns-count-required": "Le nombre de colonnes est requis.", + "configuration-error": "Erreur de configuration", + "copy-public-link": "Copier le lien public", + "create-new": "Créer un nouveau tableau de bord", + "create-new-dashboard": "Créer un nouveau tableau de bord", + "create-new-widget": "Créer un nouveau widget", + "dashboard": "Tableau de bord", + "dashboard-details": "Détails du tableau de bord", + "dashboard-file": "Fichier du tableau de bord", + "dashboard-import-missing-aliases-title": "Configurer les alias utilisés par le tableau de bord importé", + "dashboard-required": "Le tableau de bord est requis.", + "dashboards": "Tableaux de bord", + "delete": "Supprimer le tableau de bord", + "delete-dashboard-text": "Faites attention, après la confirmation, le tableau de bord et toutes les données associées deviendront irrécupérables.", + "delete-dashboard-title": "Êtes-vous sûr de vouloir supprimer le tableau de bord '{{dashboardTitle}}'?", + "delete-dashboards": "Supprimer les tableaux de bord", + "delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}", + "delete-dashboards-text": "Attention, après la confirmation, tous les tableaux de bord sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", + "delete-state": "Supprimer l'état du tableau de bord", + "delete-state-text": "Etes-vous sûr de vouloir supprimer l'état du tableau de bord avec le nom '{{stateName}}'?", + "delete-state-title": "Supprimer l'état du tableau de bord", + "description": "Description", + "details": "Détails", + "display-dashboard-export": "Afficher l'exportation", + "display-dashboard-timewindow": "Afficher fenêtre de temps", + "display-dashboards-selection": "Afficher la sélection des tableaux de bord", + "display-entities-selection": "Afficher la sélection des entités", + "display-title": "Afficher le titre du tableau de bord", + "drop-image": "Déposer une image ou cliquez pour sélectionner un fichier à télécharger.", + "edit-state": "Modifier l'état du tableau de bord", + "export": "Exporter le tableau de bord", + "export-failed-error": "Impossible d'exporter le tableau de bord: {{error}}", + "hide-details": "Masquer les détails", + "horizontal-margin": "Marge horizontale", + "horizontal-margin-required": "Une valeur de marge horizontale est requise.", + "import": "Importer le tableau de bord", + "import-widget": "Importer un widget", + "invalid-aliases-config": "Impossible de trouver des dispositifs correspondant à certains filtres d'alias.
Veuillez contacter votre administrateur pour résoudre ce problème.", + "invalid-dashboard-file-error": "Impossible d'importer le tableau de bord: structure de données du tableau de bord non valide", + "invalid-widget-file-error": "Impossible d'importer le widget: structure de données de widget invalide.", + "is-root-state": "Etat racine", + "make-private": "Rendre privé le tableau de bord", + "make-private-dashboard": "Rendre privé le tableau de bord", + "make-private-dashboard-text": "Après la confirmation, le tableau de bord sera rendu privé et ne sera plus accessible aux autres.", + "make-private-dashboard-title": "Etes-vous sûr de vouloir rendre le tableau de bord '{{dashboardTitle}}' privé?", + "make-public": "Rendre public le tableau de bord", + "manage-assigned-customers": "Gérer les clients affectés", + "manage-states": "Gérer les états du tableau de bord", + "management": "Gestion du tableau de bord", + "max-columns-count-message": "Seulement 1000 colonnes maximum sont autorisées.", + "max-horizontal-margin-message": "Seulement 50 sont autorisés en tant que valeur de marge horizontale maximale.", + "max-mobile-row-height-message": "Seuls 200 pixels sont autorisés en tant que valeur maximale de hauteur de ligne mobile.", + "max-vertical-margin-message": "Seulement 50 sont autorisés en tant que valeur de marge verticale maximale.", + "min-columns-count-message": "Seul un nombre minimum de 10 colonnes est autorisé.", + "min-horizontal-margin-message": "Seul 0 est autorisé comme valeur de marge horizontale minimale.", + "min-mobile-row-height-message": "Seuls 5 pixels sont autorisés en tant que valeur minimale de hauteur de ligne mobile.", + "min-vertical-margin-message": "Seul 0 est autorisé comme valeur de marge verticale minimale.", + "mobile-layout": "Paramètres de mise en page mobiles", + "mobile-row-height": "Hauteur de ligne mobile, px", + "mobile-row-height-required": "Une valeur de hauteur de ligne mobile est requise.", + "new-dashboard-title": "Nouveau titre du tableau de bord", + "no-dashboards-matching": "Aucun tableau de bord correspondant à {{entity}} n'a été trouvé. ", + "no-dashboards-text": "Aucun tableau de bord trouvé", + "no-image": "Aucune image sélectionnée", + "no-widgets": "Aucun widget configuré", + "open-dashboard": "Ouvrir le tableau de bord", + "open-toolbar": "Ouvrir la barre d'outils du tableau de bord", + "public": "Public", + "public-dashboard-notice": " Remarque: N'oubliez pas de rendre publics les dispositifs associés pour accéder à leurs données.", + "public-dashboard-text": "Votre tableau de bord {{dashboardTitle}} est maintenant public et accessible via le lien public : ", + "public-dashboard-title": "Le tableau de bord est maintenant public", + "public-link": "Lien public", + "public-link-copied-message": "Le lien public du tableau de bord a été copié dans le presse-papier", + "search-states": "Recherche des états du tableau de bord", + "select-dashboard": "Sélectionner le tableau de bord", + "select-devices": "Selectionner les dispositifs", + "select-existing": "Sélectionnez un tableau de bord existant", + "select-state": "Sélectionnez l'état cible", + "select-widget-subtitle": "Liste des types de widgets disponibles", + "select-widget-title": "Sélectionner un widget", + "selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord}} sélectionnés", + "set-background": "Définir l'arrière-plan", + "settings": "Paramètres", + "show-details": "Afficher les détails", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", + "state": "Etat du tableau de bord", + "state-controller": "Contrôleur d'état", + "state-id": "ID d'état", + "state-id-exists": "L'état du tableau de bord avec le même Id existe déjà.", + "state-id-required": "L'Id d'état du tableau de bord est requis.", + "state-name": "Nom", + "state-name-required": "Le nom de l'état du tableau de bord est requis", + "states": "Etats du tableau de bord", + "title": "Titre", + "title-color": "Couleur du titre", + "title-required": "Le titre est requis.", + "toolbar-always-open": "Garder la barre d'outils ouverte", + "unassign-dashboard": "Retirer le tableau de bord", + "unassign-dashboard-text": "Après la confirmation, le tableau de bord ne sera pas attribué et ne sera pas accessible au client.", + "unassign-dashboard-title": "Êtes-vous sûr de vouloir annuler l'affectation du tableau de bord '{{dashboardTitle}}'?", + "unassign-dashboards": "Retirer les tableaux de bord", + "unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} des clients", + "unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} du client", + "unassign-dashboards-text": "Après la confirmation, tous les tableaux de bord sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", + "unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", + "unassign-from-customer": "Retirer du client", + "unassign-from-customers": "Retirer les tableaux de bord des clients", + "unassign-from-customers-text": "Veuillez sélectionner les clients à annuler l'affectation du ou des tableaux de bord", + "vertical-margin": "Marge verticale", + "vertical-margin-required": "Une valeur de marge verticale est requise", + "view-dashboards": "Afficher les tableaux de bord", + "widget-file": "Fichier du Widget", + "widget-import-missing-aliases-title": "Configurer les alias utilisés par le widget importé", + "widgets-margins": "Marge entre les widgets" + }, +"datakey":{ + "advanced": "Avancé", + "alarm": "Champs d'alarme", + "alarm-fields-required": "Les champs d'alarme sont obligatoires.", + "attributes": "Attributs", + "color": "Couleur", + "configuration": "Configuration de la clé de données", + "data-generation-func": "Fonction de génération de données", + "decimals": "Nombre de chiffres après virgule flottante", + "function-types": "Types de fonctions", + "function-types-required": "Les types de fonctions sont obligatoires", + "label": "Label", + "maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés}}", + "maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés}}", + "settings": "Paramètres", + "timeseries": "Timeseries", + "timeseries-or-attributes-required": "Les timeseries / attributs d'entité sont obligatoires.", + "timeseries-required": "Les Timeseries de l'entité sont obligatoires.", + "units": "Symbole spécial à afficher à côté de la valeur", + "use-data-post-processing-func": "Utiliser la fonction de post-traitement des données" + }, +"datasource":{ + "add-datasource-prompt": "Veuillez ajouter une source de données", + "name": "Nom", + "type": "Type de source de données" + }, +"datetime":{ + "date-from": "Date de", + "date-to": "Date à", + "time-from": "Heure de", + "time-to": "Heure à" + }, +"details":{ + "edit-mode": "Mode édition", + "toggle-edit-mode": "Activer le mode édition" + }, +"device":{ + "access-token": "Jeton d'accès", + "access-token-invalid": "La longueur du jeton d'accès doit être comprise entre 1 et 20 caractères.", + "access-token-required": "Le jeton d'accès est requis.", + "accessTokenCopiedMessage": "Le jeton d'accès au dispositif a été copié dans le presse-papier", + "add": "Ajouter un dispositif", + "add-alias": "Ajouter un alias de dispositif", + "add-device-text": "Ajouter un nouveau dispositif", + "alias": "Alias", + "alias-required": "Un alias du dispositif est requis.", + "aliases": "Alias ​​du dispositif", + "any-device": "N'importe quel dispositif", + "assign-device-to-customer": "Affecter des dispositifs au client", + "assign-device-to-customer-text": "Veuillez sélectionner les dispositif à affecter au client", + "assign-devices": "Attribuer des dispositifs", + "assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs}} au client", + "assign-new-device": "Attribuer un nouveau dispositif", + "assign-to-customer": "Attribuer au client", + "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les dispositifs", + "assignedToCustomer": "Attribué au client", + "configure-alias": "Configurer '{{alias}}' alias", + "copyAccessToken": "Copier le jeton d'accès", + "copyId": "Copier l'Id du dispositif", + "create-new-alias": "Créez un nouveau!", + "create-new-key": "Créez un nouveau!", + "credentials": "Informations d'identification", + "credentials-type": "Type d'identification", + "delete": "Supprimer le dispositif", + "delete-device-text": "Faites attention, après la confirmation, le dispositif et toutes les données associées deviendront irrécupérables.", + "delete-device-title": "Êtes-vous sûr de vouloir supprimer le dispositif '{{deviceName}}'?", + "delete-devices": "Supprimer les dispositifs", + "delete-devices-action-title": "Supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}", + "delete-devices-text": "Faites attention, après la confirmation, tous les dispositifs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}?", + "description": "Description", + "details": "Détails", + "device": "Dispositif", + "device-alias": "Alias ​​du dispositif", + "device-credentials": "Informations d'identification du dispositif", + "device-details": "Détails du dispositif", + "device-list": "Liste des dispositifs", + "device-list-empty": "Aucun dispositif sélectionné.", + "device-name-filter-no-device-matched": "Aucun dispositif commençant par '{{device}} n'a été trouvé.", + "device-name-filter-required": "Le filtre de nom de dispositif est requis.", + "device-public": "Le dispositif est public", + "device-required": "Le dispositif est requis.", + "device-type": "Type de dispositif", + "device-type-list-empty": "Aucun type de dispositif sélectionné.", + "device-type-required": "Le type de dispositif est requis.", + "device-types": "Types de dispositif", + "devices": "Dispositifs", + "duplicate-alias-error": "Alias ​​en double trouvé '{{alias}}'.
Les alias de dispositifs doivent être uniques dans le tableau de bord.", + "enter-device-type": "Entrez le type de dispositif", + "events": "Événements", + "idCopiedMessage": "l'Id du dispositif a été copié dans le presse-papiers", + "is-gateway": "Est une passerelle", + "make-private": "Rendre le dispositif privé", + "make-private-device-text": "Après la confirmation, le dispositif et toutes ses données seront rendues privées et ne seront pas accessibles par d'autres.", + "make-private-device-title": "Etes-vous sûr de vouloir rendre le dispositif {{deviceName}} privé?", + "make-public": "Rendre le dispositif public", + "make-public-device-text": "Après la confirmation, le dispositif et toutes ses données seront rendus publics et accessibles par d'autres.", + "make-public-device-title": "Êtes-vous sûr de vouloir rendre le dispositif {{deviceName}} 'public?", + "manage-credentials": "Gérer les informations d'identification", + "management": "Gestion des dispositifs", + "name": "Nom", + "name-required": "Le nom est requis.", + "name-starts-with": "Le nom du dispositif commence par", + "no-alias-matching": "'{{alias}}' introuvable.", + "no-aliases-found": "Aucun alias trouvé.", + "no-device-types-matching": "Aucun type de dispositif correspondant à {{entitySubtype}} n'a été trouvé.", + "no-devices-matching": "Aucun dispositif correspondant à '{{entity}} n'a été trouvé.", + "no-devices-text": "Aucun dispositif trouvé", + "no-key-matching": "'{{key}}' introuvable.", + "no-keys-found": "Aucune clé trouvée", + "public": "Public", + "remove-alias": "Supprimer l'alias du dispositif", + "rsa-key": "Clé publique RSA", + "rsa-key-required": "La clé publique RSA est requise.", + "secret": "Secret", + "secret-required": "Code secret est requis.", + "select-device": "Selectionner un dispositif", + "select-device-type": "Sélectionner le type d'appareil", + "unable-delete-device-alias-text": "L'alias du dispositif '{{deviceAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
{{widgetsList}}", + "unable-delete-device-alias-title": "Impossible de supprimer l'alias du dispositif", + "unassign-device": "Annuler l'affectation du dispositif", + "unassign-device-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client.", + "unassign-device-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", + "unassign-devices": "Annuler l'affectation des dispositifs", + "unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 dispositif} other {#dispositifs}} du client", + "unassign-devices-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par le client.", + "unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 dispositif} other {# dispositifs}}?", + "unassign-from-customer": "Retirer du client", + "use-device-name-filter": "Utiliser le filtre", + "view-credentials": "Afficher les informations d'identification", + "view-devices": "Afficher les dispositifs" + }, +"dialog":{ + "close": "Fermer le dialogue" + }, +"entity" : { + "add-alias": "Ajouter un alias d'entité", + "alarm-name-starts-with": "Les alarmes dont le nom commence par '{{prefix}}'", + "alias": "Alias", + "alias-required": "Un alias d'entité est requis.", + "aliases": "alias d'entité", + "all-subtypes": "Tout", + "any-entity": "Toute entité", + "asset-name-starts-with": "Les Assets dont le nom commence par '{{prefix}}'", + "configure-alias": "Configurer '{{alias}}' alias", + "create-new-alias": "Créez un nouveau!", + "create-new-key": "Créez un nouveau!", + "customer-name-starts-with": "Les clients dont les noms commencent par '{{prefix}}'", + "dashboard-name-starts-with": "Les tableaux de bord dont les noms commencent par '{{prefix}}'", + "details": "Détails de l'entité", + "device-name-starts-with": "Dispositifs dont le nom commence par '{{prefix}}'", + "duplicate-alias-error": "Alias ​​en double trouvé '{{alias}}'.
Les alias d'entité doivent être uniques dans le tableau de bord.", + "enter-entity-type": "Entrez le type d'entité", + "entities": "Entités", + "entity": "Entité", + "entity-alias": "Alias de l'entité", + "entity-list": "Liste d'entités", + "entity-list-empty": "Aucune entité sélectionnée.", + "entity-name": "Nom de l'entité", + "entity-name-filter-no-entity-matched": "Aucune entité commençant par '{{entity}}' n'a été trouvée.", + "entity-name-filter-required": "Le filtre de nom d'entité est requis.", + "entity-type": "Type d'entité", + "entity-type-list": "Liste de types d'entités", + "entity-type-list-empty": "Aucun type d'entité sélectionné.", + "entity-types": "Types d'entité", + "key": "Clé", + "key-name": "Nom de la clé", + "list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes}}", + "list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets}}", + "list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients}}", + "list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord}}", + "list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs}}", + "list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins}}", + "list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles}}", + "list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles}}", + "list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles}}", + "list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants}}", + "list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs}}", + "missing-entity-filter-error": "Le filtre est manquant pour l'alias '{{alias}}'.", + "name-starts-with": "Nom commence par", + "no-alias-matching": "'{{alias}}' introuvable.", + "no-aliases-found": "Aucun alias trouvé.", + "no-data": "Aucune donnée à afficher", + "no-entities-matching": "Aucune entité correspondant à '{{entity}}' n'a été trouvée.", + "no-entities-prompt": "Aucune entité trouvée", + "no-entity-types-matching": "Aucun type d'entité correspondant à {{entityType}} n'a été trouvé. ", + "no-key-matching": "'{{key}}' introuvable.", + "no-keys-found": "Aucune clé trouvée", + "plugin-name-starts-with": "Plugins dont les noms commencent par '{{prefix}}'", + "remove-alias": "Supprimer l'alias d'entité", + "rule-name-starts-with": "Règles dont les noms commencent par '{{prefix}}'", + "rulechain-name-starts-with": "Chaînes de règles dont les noms commencent par '{{prefix}}'", + "rulenode-name-starts-with": "Les noeuds de règles dont le nom commence par '{{prefix}}'", + "search": "Recherche d'entités", + "select-entities": "Sélectionner des entités", + "selected-entities": "{count, plural, 1 {1 entité} other {# entités}} sélectionnées", + "tenant-name-starts-with": "Les Tenant dont le nom commence par '{{prefix}}'", + "type": "Type", + "type-alarm": "Alarme", + "type-alarms": "Alarmes", + "type-asset": "Asset", + "type-assets": "Assets", + "type-current-customer": "Client actuel", + "type-customer": "Client", + "type-customers": "Clients", + "type-dashboard": "Tableau de bord", + "type-dashboards": "Tableaux de bord", + "type-device": "Dispositif", + "type-devices": "Dispositifs", + "type-plugin": "Plugin", + "type-plugins": "Plugins", + "type-required": "Le type d'entité est obligatoire.", + "type-rule": "Règle", + "type-rulechain": "Chaîne de règles", + "type-rulechains": "Chaînes de règles", + "type-rulenode": "Noeud de règle", + "type-rulenodes": "Noeuds de règle", + "type-rules": "Règles", + "type-tenant": "Tenant", + "type-tenants": "Tenants", + "type-user": "Utilisateur", + "type-users": "Utilisateurs", + "unable-delete-entity-alias-text": "L'alias d'entité '{{entityAlias}}' ne peut pas être supprimé car il est utilisé par les widgets suivants:
{{widgetsList}}", + "unable-delete-entity-alias-title": "Impossible de supprimer l'alias d'entité", + "use-entity-name-filter": "Utiliser un filtre", + "user-name-starts-with": "Utilisateurs dont les noms commencent par '{{prefix}}'" + }, +"error":{ + "unable-to-connect": "Impossible de se connecter au serveur! Veuillez vérifier votre connexion Internet.", + "unhandled-error-code": "Code d'erreur non géré: {{errorCode}}", + "unknown-error": "Erreur inconnue" + }, +"event":{ + "alarm": "Alarme", + "body": "Corps", + "data": "Données", + "data-type": "Type de données", + "entity": "Entité", + "error": "erreur", + "errors-occurred": "Des erreurs sont survenues", + "event": "événement", + "event-time": "Heure de l'événement", + "event-type": "Type d'événement", + "failed": "Échec", + "message-id": "Message Id", + "message-type": "Type de message", + "messages-processed": "Messages traités", + "metadata": "Métadonnées", + "method": "Méthode", + "no-events-prompt": "Aucun événement trouvé", + "relation-type": "Type de relation", + "server": "Serveur", + "status": "Etat", + "success": "Succès", + "type": "Type", + "type-debug-rule-chain": "Debug", + "type-debug-rule-node": "Debug", + "type-error": "Erreur", + "type-lc-event": "Evénement du cycle de vie", + "type-stats": "Statistiques" + }, +"extension":{ + "add": "Ajouter une extension", + "add-attribute": "Ajouter un attribut", + "add-attribute-request": "Ajouter une demande d'attribut", + "add-attribute-update": "Ajouter une mise à jour d'attribut", + "add-broker": "Ajouter un Broker", + "add-config": "Ajouter une configuration de convertisseur", + "add-connect-request": "Ajouter une demande de connexion", + "add-converter": "Ajouter un convertisseur", + "add-device": "Ajouter un dispositif", + "add-disconnect-request": "Ajouter une demande de déconnexion", + "add-map": "Ajouter un élément de mappage", + "add-server-side-rpc-request": "Ajouter une requête RPC côté serveur", + "add-timeseries": "Ajouter des timeseries", + "anonymous": "Anonyme", + "attr-json-key-expression": "Expression json de la clé d'attribut", + "attr-topic-key-expression": "Expression du topic de la clé d'attribut", + "attribute-filter": "Filtre d'attribut", + "attribute-key-expression": "Expression de clé d'attribut", + "attribute-requests": "Demandes d'attributs", + "attribute-updates": "Mises à jour des attributs", + "attributes": "Attributs", + "basic": "Basic", + "brokers": "Brokers", + "ca-cert": "Fichier de certificat CA", + "cert": "Fichier de certificat *", + "client-scope": "Portée client", + "configuration": "Configuration", + "connect-requests": "Demandes de connexion", + "converter-configurations": "Configurations du convertisseur", + "converter-id": "ID du convertisseur", + "converter-json": "Json", + "converter-json-parse": "Impossible d'analyser le convertisseur json.", + "converter-json-required": "Le convertisseur json est requis.", + "converter-type": "Type de convertisseur", + "converters": "Convertisseurs", + "credentials": "Informations d'identification", + "custom": "Custom", + "delete": "Supprimer l'extension", + "delete-extension-text": "Attention, après la confirmation, l'extension et toutes les données associées deviendront irrécupérables.", + "delete-extension-title": "Êtes-vous sûr de vouloir supprimer l'extension '{{extensionId}}'?", + "delete-extensions-text": "Attention, après la confirmation, toutes les extensions sélectionnées seront supprimées.", + "delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions}}?", + "device-name-expression": "expression du nom du dispositif", + "device-name-filter": "Filtre de nom de dispositif", + "device-type-expression": "expression de type de dispositif", + "disconnect-requests": "Demandes de déconnection", + "drop-file": "Déposez un fichier ou cliquez pour sélectionner un fichier à télécharger.", + "edit": "Modifier l'extension", + "export-extension": "Exporter l'extension", + "export-extensions-configuration": "Exporter la configuration des extensions", + "extension-id": "Id de l'extension", + "extension-type": "Type d'extension", + "extensions": "Extensions", + "field-required": "Le champ est obligatoire", + "file": "Fichier d'extensions", + "filter-expression": "Expression du filtre", + "host": "Hôte", + "id": "Id", + "import-extension": "Importer une extension", + "import-extensions": "Importer des extensions", + "import-extensions-configuration": "Importer la configuration des extensions", + "invalid-file-error": "Fichier d'extension non valide", + "json-name-expression": "Expression json du nom du dispositif", + "json-parse": "Impossible d'analyser json transformer.", + "json-required": "Transformer json est requis.", + "json-type-expression": "Expression json du type de dispositif", + "key": "Clé", + "mapping": "Mappage", + "method-filter": "Filtre de méthode", + "modbus-add-server": "Ajouter serveur/esclave", + "modbus-add-server-prompt": "Veuillez ajouter serveur/esclave", + "modbus-attributes-poll-period": "Période d'interrogation des attributs (ms)", + "modbus-baudrate": "Débit en bauds", + "modbus-byte-order": "Ordre des octets", + "modbus-databits": "Bits de données", + "modbus-databits-range": "Les bits de données doivent être compris entre 7 et 8.", + "modbus-device-name": "Nom du dispositif", + "modbus-encoding": "Encodage", + "modbus-function": "Fonction", + "modbus-parity": "parité", + "modbus-poll-period": "Période d'interrogation (ms)", + "modbus-poll-period-range": "La période d'interrogation doit être une valeur positive.", + "modbus-port-name": "Nom du port série", + "modbus-register-address": "Adresse du registre", + "modbus-register-address-range": "L'adresse du registre doit être comprise entre 0 et 65535.", + "modbus-register-bit-index": "Bit index", + "modbus-register-bit-index-range": "L'index de bit doit être compris entre 0 et 15.", + "modbus-register-count": "Nombre de registre", + "modbus-register-count-range": "Le nombre de registres doit être une valeur positive.", + "modbus-server": "Serveurs / esclaves", + "modbus-stopbits": "Bits d'arrêt", + "modbus-stopbits-range": "Les bits d'arrêt doivent être compris entre 1 et 2.", + "modbus-tag": "Tag", + "modbus-timeseries-poll-period": "Période d'interrogation des Timeseries (ms)", + "modbus-transport": "Transport", + "modbus-unit-id": "Id de l'unité", + "modbus-unit-id-range": "L'ID de l'unité doit être compris entre 1 et 247.", + "no-file": "Aucun fichier sélectionné.", + "opc-add-server": "Ajouter un serveur", + "opc-add-server-prompt": "Veuillez ajouter un serveur", + "opc-application-name": "Nom de l'application", + "opc-application-uri": "Uri de l'application", + "opc-device-name-pattern": "modèle de nom du dispositif", + "opc-device-node-pattern": "modèle de noeud de dispositif", + "opc-identity": "Identité", + "opc-keystore": "Magasin de clés", + "opc-keystore-alias": "Alias", + "opc-keystore-key-password": "Mot de passe de la clé", + "opc-keystore-location": "Emplacement *", + "opc-keystore-password": "Mot de passe", + "opc-keystore-type": "Type", + "opc-scan-period-in-seconds": "Période d'analyse en secondes", + "opc-security": "Sécurité", + "opc-server": "Serveurs", + "opc-type": "Type", + "password": "Mot de passe", + "pem": "PEM", + "port": "Port", + "port-range": "Le port doit être compris entre 1 et 65535.", + "private-key": "Fichier de clé privée *", + "request-id-expression": "Expression de demande d'id", + "request-id-json-expression": "Expression json de la demande d'id", + "request-id-topic-expression": "Expression de la demande d'id du topic", + "request-topic-expression": "Expression de la demande du topic", + "response-timeout": "Délai de réponse en millisecondes", + "response-topic-expression": "Expression du topic de la réponse", + "retry-interval": "Intervalle de nouvelle tentative en millisecondes", + "selected-extensions": "{count, plural, 1 {1 extension} other {# extensions}} sélectionné", + "server-side-rpc": "RPC côté serveur", + "ssl": "Ssl", + "sync":{ + "last-sync-time": "Dernière heure de synchronisation", + "not-available": "Non disponible", + "not-sync": "Non sync", + "status": "Status", + "sync": "Sync" + }, + "timeout": "Délai d'attente en millisecondes", + "timeseries": "Timeseries", + "to-double": "To Double", + "token": "Jeton de sécurité", + "topic": "Topic", + "topic-expression": "Expression du topic", + "topic-filter": "Filtre du topic", + "topic-name-expression": "Expression du nom du dispositif (topic)", + "topic-type-expression": "Expression de type de dispositif (topic)", + "transformer": "Transformer", + "transformer-json": "JSON *", + "type": "Type", + "unique-id-required": "L'identifiant d'extension actuel existe déjà.", + "username": "Nom d'utilisateur", + "value": "Valeur", + "value-expression": "Expression de la valeur" + }, +"fullscreen":{ + "exit": "Quitter le plein écran", + "expand": "Afficher en plein écran", + "fullscreen": "Plein écran", + "toggle": "Activer le mode plein écran" + }, +"function":{ + "function": "Fonction" + }, +"grid":{ + "add-item-text": "Ajouter un nouvel élément", + "delete-item": "Supprimer l'élément", + "delete-item-text": "Faites attention, après la confirmation, cet élément et toutes les données associées deviendront irrécupérables.", + "delete-item-title": "Êtes-vous sûr de vouloir supprimer cet élément?", + "delete-items": "Supprimer les éléments", + "delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments}}", + "delete-items-text": "Attention, après la confirmation, tous les éléments sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-items-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments}}?", + "item-details": "Détails de l'élément", + "no-items-text": "Aucun élément trouvé", + "scroll-to-top": "Défiler vers le haut" + }, +"help":{ + "goto-help-page": "Aller à la page d'aide" + }, +"home":{ + "avatar": "Avatar", + "home": "Accueil", + "logout": "Déconnexion", + "menu": "Menu", + "open-user-menu": "Ouvrir le menu utilisateur", + "profile": "Profile" + }, +"icon":{ + "icon": "Icône", + "material-icons": "Material icons", + "select-icon": "Sélectionner l'icône", + "show-all": "Afficher toutes les icônes" + }, +"import":{ + "drop-file": "Déposez un fichier JSON ou cliquez pour sélectionner un fichier à télécharger.", + "no-file": "Aucun fichier sélectionné" + }, +"item":{ + "selected": "Sélectionné" + }, +"js-func":{ + "no-return-error": "La fonction doit renvoyer une valeur!", + "return-type-mismatch": "La fonction doit renvoyer une valeur de type '{{type}}' !", + "tidy": "Tidy" + }, +"key-val":{ + "add-entry": "Ajouter une entrée", + "key": "Clé", + "no-data": "Aucune entrée", + "remove-entry": "Supprimer l'entrée", + "value": "Valeur" + }, +"language":{ + "language": "Language", + "locales":{ + "en_US": "Anglais", + "fr_FR": "Français", + "es_ES": "Espagnol", + "it_IT": "Italien", + "ko_KR": "Coréen", + "ru_RU": "Russe", + "zh_CN": "Chinois" + } + }, +"layout":{ + "color": "Couleur", + "layout": "Mise en page", + "main": "Principal", + "manage": "Gérer les mises en page", + "right": "Droite", + "select": "Sélectionner la mise en page cible", + "settings": "Paramètres de mise en page" + }, +"legend":{ + "avg": "avg", + "max": "max", + "min": "min", + "position": "Position de la légende", + "settings": "Paramètres de la légende", + "show-avg": "Afficher la valeur moyenne", + "show-max": "Afficher la valeur maximale", + "show-min": "Afficher la valeur min", + "show-total": "Afficher la valeur totale", + "total": "total" + }, +"login":{ + "create-password": "Créer un mot de passe", + "email": "Email", + "forgot-password": "Mot de passe oublié?", + "login": "Login", + "new-password": "Nouveau mot de passe", + "new-password-again": "nouveau mot de passe", + "password-again": "Mot de passe à nouveau", + "password-link-sent-message": "Le lien de réinitialisation du mot de passe a été envoyé avec succès!", + "password-reset": "Mot de passe réinitialisé", + "passwords-mismatch-error": "Les mots de passe saisis doivent être identiques!", + "remember-me": "Se souvenir de moi", + "request-password-reset": "Demander la réinitialisation du mot de passe", + "reset-password": "Réinitialiser le mot de passe", + "sign-in": "Veuillez vous connecter", + "username": "Nom d'utilisateur (email)" + }, +"position":{ + "bottom": "Bas", + "left": "Gauche", + "right": "Droite", + "top": "Haut" + }, +"profile":{ + "change-password": "Modifier le mot de passe", + "current-password": "Mot de passe actuel", + "profile": "Profile" + }, +"relation":{ + "add": "Ajouter une relation", + "add-relation-filter": "Ajouter un filtre de relation", + "additional-info": "Informations supplémentaires (JSON)", + "any-relation": "toute relation", + "any-relation-type": "N'importe quel type", + "delete": "Supprimer la relation", + "delete-from-relation-text": "Attention, après la confirmation, l'entité actuelle ne sera pas liée à l'entité '{{entityName}}'.", + "delete-from-relation-title": "Etes-vous sûr de vouloir supprimer la relation de l'entité '{{entityName}}'?", + "delete-from-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et l'entité actuelle ne sera pas liée aux entités correspondantes.", + "delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", + "delete-to-relation-text": "Attention, après la confirmation, l'entité '{{entityName}} ne sera plus liée à l'entité actuelle.", + "delete-to-relation-title": "Êtes-vous sûr de vouloir supprimer la relation avec l'entité '{{entityName}}'?", + "delete-to-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et les entités correspondantes ne seront pas liées à l'entité en cours.", + "delete-to-relations-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", + "direction": "Sens", + "direction-type":{ + "FROM": "de", + "TO": "à" + }, + "edit": "Modifier la relation", + "from-entity": "De l'entité", + "from-entity-name": "Du nom d'entité", + "from-entity-type": "Du type d'entité", + "from-relations": "Relations sortantes", + "invalid-additional-info": "Impossible d'analyser les informations supplémentaires json.", + "relation-filters": "Filtres de relation", + "relation-type": "Type de relation", + "relation-type-required": "Le type de relation est requis.", + "relations": "Relations", + "remove-relation-filter": "Supprimer le filtre de relation", + "search-direction":{ + "FROM": "De", + "TO": "À" + }, + "selected-relations": "{count, plural, 1 {1 relation} other {# relations}} sélectionné", + "to-entity": "À l'entité", + "to-entity-name": "vers le nom de l'entité", + "to-entity-type": "Vers le type d'entité", + "to-relations": "Relations entrantes", + "type": "Type" + }, +"rulechain":{ + "add": "Ajouter une chaîne de règles", + "add-rulechain-text": "Ajouter une nouvelle chaîne de règles", + "copyId": "Copier l'identifiant de la chaîne de règles", + "create-new-rulechain": "Créer une nouvelle chaîne de règles", + "debug-mode": "Mode de débogage", + "delete": "Supprimer la chaîne de règles", + "delete-rulechain-text": "Attention, après la confirmation, la chaîne de règles et toutes les données associées deviendront irrécupérables.", + "delete-rulechain-title": "Voulez-vous vraiment supprimer la chaîne de règles '{{ruleChainName}}'?", + "delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}", + "delete-rulechains-text": "Attention, après la confirmation, toutes les chaînes de règles sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.", + "delete-rulechains-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}?", + "description": "Description", + "details": "Détails", + "events": "Evénements", + "export": "Exporter la chaîne de règles", + "export-failed-error": "Impossible d'exporter la chaîne de règles: {{error}}", + "idCopiedMessage": "L'ID de la chaîne de règles a été copié dans le presse-papier", + "import": "Importer la chaîne de règles", + "invalid-rulechain-file-error": "Impossible d'importer la chaîne de règles: structure de données de la chaîne de règles non valide", + "management": "Gestion des règles", + "name": "Nom", + "name-required": "Le nom est requis.", + "no-rulechains-matching": "Aucune chaîne de règles correspondant à {{entity}} n'a été trouvée.", + "no-rulechains-text": "Aucune chaîne de règles trouvée", + "root": "Racine", + "rulechain": "Chaîne de règles", + "rulechain-details": "Détails de la chaîne de règles", + "rulechain-file": "Fichier de chaîne de règles", + "rulechain-required": "Chaîne de règles requise", + "rulechains": "Chaînes de règles", + "select-rulechain": "Sélectionner la chaîne de règles", + "set-root": "Rend la chaîne de règles racine (root) ", + "set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.", + "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", + "system": "Système" + }, +"rulenode":{ + "add": "Ajouter un noeud de règle", + "add-link": "Ajouter un lien", + "configuration": "Configuration", + "copy-selected": "Copier les éléments sélectionnés", + "create-new-link-label": "Créez un nouveau!", + "custom-link-label": "Etiquette de lien personnalisée", + "custom-link-label-required": "Une étiquette de lien personnalisée est requise", + "debug-mode": "Mode de débogage", + "delete": "Supprimer le noeud de règle", + "delete-selected": "Supprimer les éléments sélectionnés", + "delete-selected-objects": "Supprimer les nœuds et les connexions sélectionnés", + "description": "Description", + "deselect-all": "Désélectionner tout", + "deselect-all-objects": "Désélectionnez tous les nœuds et toutes les connexions", + "details": "Détails", + "directive-is-not-loaded": "La directive de configuration définie '{{directiveName}} n'est pas disponible.", + "events": "Événements", + "help": "Aide", + "invalid-target-rulechain": "Impossible de résoudre la chaîne de règles cible!", + "link": "Lien", + "link-details": "Détails du lien du noeud de la règle", + "link-label": "Étiquette du lien", + "link-label-required": "L'étiquette du lien est obligatoire", + "link-labels": "Étiquettes de lien", + "link-labels-required": "Les étiquettes de lien sont obligatoires", + "message": "Message", + "message-type": "Type de message", + "message-type-required": "Le type de message est obligatoire", + "metadata": "Métadonnées", + "metadata-required": "Les entrées de métadonnées ne peuvent pas être vides.", + "name": "Nom", + "name-required": "Le nom est requis.", + "no-link-label-matching": "'{{label}}' introuvable.", + "no-link-labels-found": "Aucune étiquette de lien trouvée", + "open-node-library": "Ouvrir la bibliothèque de noeud", + "output": "Output", + "rulenode-details": "Détails du noeud de la règle", + "search": "Recherche de noeuds", + "select-all": "Tout sélectionner", + "select-all-objects": "Sélectionnez tous les noeuds et connexions", + "select-message-type": "Sélectionner le type de message", + "test": "Test", + "test-script-function": "Tester le script", + "type": "Type", + "type-action": "Action", + "type-action-details": "Effectuer une action spéciale", + "type-enrichment": "Enrichissement", + "type-enrichment-details": "Ajouter des informations supplémentaires dans les métadonnées de message", + "type-external": "Externe", + "type-external-details": "Interagit avec le système externe", + "type-filter": "Filtre", + "type-filter-details": "Filtrer les messages entrants avec des conditions configurées", + "type-input": "Input", + "type-input-details": "Entrée logique de la chaîne de règles, transmet les messages entrants au prochain nœud de règle associé", + "type-rule-chain": "Chaîne de règles", + "type-rule-chain-details": "Transmet les messages entrants à la chaîne de règles spécifiée", + "type-transformation": "Transformation", + "type-transformation-details": "Modifier le payload du message et les métadonnées ", + "type-unknown": "Inconnu", + "type-unknown-details": "Noeud de règle non résolu", + "ui-resources-load-error": "Impossible de charger les ressources de configuration de l'interface utilisateur." + }, +"tenant":{ + "add": "Ajouter un Tenant", + "add-tenant-text": "Ajouter un nouveau Tenant", + "admins": "Admins", + "copyId": "Copier l'Id du Tenant", + "delete": "Supprimer le Tenant", + "delete-tenant-text": "Attention, après la confirmation, le Tenant et toutes les données associées deviendront irrécupérables.", + "delete-tenant-title": "Etes-vous sûr de vouloir supprimer le tenant '{{tenantTitle}}'?", + "delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants}}", + "delete-tenants-text": "Attention, après la confirmation, tous les Tenants sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-tenants-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants}}?", + "description": "Description", + "details": "Détails", + "events": "Événements", + "idCopiedMessage": "L'Id du Tenant a été copié dans le Presse-papiers", + "manage-tenant-admins": "Gérer les administrateurs du Tenant", + "management": "Gestion des Tenants", + "no-tenants-matching": "Aucun Tenant correspondant à {{entity}} n'a été trouvé. ", + "no-tenants-text": "Aucun Tenant trouvé", + "select-tenant": "Sélectionner un Tenant", + "tenant": "Tenant", + "tenant-details": "Détails du Tenant", + "tenant-required": "Tenant requis", + "tenants": "Tenants", + "title": "Titre", + "title-required": "Le titre est requis." + }, +"timeinterval":{ + "advanced": "Avancé", + "days": "Jours", + "days-interval": "{days, plural, 1 {1 jour} other {# jours}}", + "hours": "Heures", + "hours-interval": "{hours, plural, 1 {1 heure} other {# heures}}", + "minutes": "Minutes", + "minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes}}", + "seconds": "Secondes", + "seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes}}" + }, +"timewindow":{ + "date-range": "Plage de dates", + "days": "{days, plural, 1 {jour} other {# jours}}", + "edit": "Modifier timewindow", + "history": "Historique", + "hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures}}", + "last": "Dernier", + "last-prefix": "dernier", + "minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes}}", + "period": "de {{startTime}} à {{endTime}}", + "realtime": "Temps réel", + "seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds}}", + "time-period": "Période" + }, +"user":{ + "activation-email-sent-message": "L'e-mail d'activation a été envoyé avec succès!", + "activation-link": "Lien d'activation utilisateur", + "activation-link-copied-message": "le lien d'activation de l'utilisateur a été copié dans le presse-papier", + "activation-link-text": "Pour activer l'utilisateur, utilisez le lien d'activation suivant: ", + "activation-method": "Méthode d'activation", + "add": "Ajouter un utilisateur", + "add-user-text": "Ajouter un nouvel utilisateur", + "always-fullscreen": "Toujours en plein écran", + "anonymous": "Anonyme", + "copy-activation-link": "Copier le lien d'activation", + "customer": "Client", + "customer-users": "Utilisateurs du client", + "default-dashboard": "Tableau de bord par défaut", + "delete": "Supprimer l'utilisateur", + "delete-user-text": "Attention, après la confirmation, l'utilisateur et toutes les données associées deviendront irrécupérables.", + "delete-user-title": "Etes-vous sûr de vouloir supprimer l'utilisateur '{{userEmail}}'?", + "delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}", + "delete-users-text": "Attention, après la confirmation, tous les utilisateurs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-users-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}?", + "description": "Description", + "details": "Détails", + "display-activation-link": "Afficher le lien d'activation", + "email": "Email", + "email-required": "Email est requis.", + "first-name": "Prénom", + "invalid-email-format": "Format de courrier électronique non valide", + "last-name": "Nom de famille", + "no-users-matching": "Aucun utilisateur correspondant à '{{entity}}' n'a été trouvé.", + "no-users-text": "Aucun utilisateur trouvé", + "resend-activation": "Renvoyer l'activation", + "select-user": "Sélectionner l'utilisateur", + "send-activation-mail": "Envoyer un mail d'activation", + "sys-admin": "Administrateur du système", + "tenant-admin": "Administrateur du Tenant", + "tenant-admins": "administrateurs du Tenant", + "user": "utilisateur", + "user-details": "Détails de l'utilisateur", + "user-required": "L'utilisateur est requis", + "users": "Utilisateurs" + }, +"value":{ + "boolean": "booléen", + "boolean-value": "Valeur booléenne", + "double": "Double", + "double-value": "Valeur double", + "false": "Faux", + "integer": "Entier", + "integer-value": "Valeur entière", + "invalid-integer-value": "Valeur entière invalide", + "long": "Long", + "string": "String", + "string-value": "Valeur String", + "true": "Vrai", + "type": "Type de valeur" + }, +"widget":{ + "add": "Ajouter un widget", + "add-resource": "Ajouter une ressource", + "add-widget-type": "Ajouter un nouveau type de widget", + "alarm": "Widget d'alarme", + "css": "CSS", + "datakey-settings-schema": "Schéma des paramètres de Data key", + "edit": "Modifier le widget", + "editor": " Editeur de widget", + "export": "Exporter widget", + "html": "HTML", + "javascript": "Javascript", + "latest-values": "Dernières valeurs", + "management": "Gestion des widgets", + "missing-widget-title-error": "Le titre du widget doit être spécifié!", + "no-data-found": "Aucune donnée trouvée", + "remove": "Supprimer le widget", + "remove-resource": "Supprimer une ressource", + "remove-widget-text": "Après la confirmation, le widget et toutes les données associées deviendront irrécupérables.", + "remove-widget-title": "Êtes-vous sûr de vouloir supprimer le widget '{{widgetTitle}}'?", + "remove-widget-type": "Supprimer le type de widget", + "remove-widget-type-text": "Après la confirmation, le type de widget et toutes les données associées deviendront irrécupérables.", + "remove-widget-type-title": "Êtes-vous sûr de vouloir supprimer le type de widget '{{widgetName}}'?", + "resource-url": "URL JavaScript / CSS", + "resources": "Ressources", + "rpc": "Widget de contrôle", + "run": "Exécuter un widget", + "save": "Enregistrer le widget", + "save-widget-type-as": "Enregistrer le type de widget sous", + "save-widget-type-as-text": "Veuillez saisir un nouveau titre de widget et / ou sélectionner un ensemble de widgets cibles", + "saveAs": "Enregistrer le widget sous", + "search-data": "Rechercher des données", + "select-widget-type": "Sélectionnez le type de widget", + "select-widgets-bundle": "Sélectionner un ensemble de widgets", + "settings-schema": "Schéma des paramètres", + "static": "Widget statique", + "tidy": "Tidy", + "timeseries": "Séries chronologiques", + "title": "Titre du widget", + "title-required": "Le titre du widget est requis.", + "toggle-fullscreen": "Basculer le mode plein écran", + "type": "Type de widget", + "unable-to-save-widget-error": "Impossible de sauvegarder le widget! Le widget a des erreurs!", + "undo": "Annuler les modifications du widget", + "widget-bundle": "Ensemble de widget", + "widget-library": "Bibliothèque de widgets", + "widget-saved": "Widget enregistré", + "widget-template-load-failed-error": "Impossible de charger le modèle de widget!", + "widget-type-load-error": "Le widget n'a pas été chargé à cause des erreurs suivantes:", + "widget-type-load-failed-error": "Impossible de charger le type de widget!", + "widget-type-not-found": "Problème de chargement de la configuration du widget.
Le type de widget associé a probablement été supprimé." + }, +"widget-action":{ + "custom": "Action personnalisée", + "header-button": "Bouton d'en-tête de widget", + "open-dashboard": "Naviguer vers un autre tableau de bord", + "open-dashboard-state": "Naviguer vers un nouvel état du tableau de bord", + "open-right-layout": "Ouvrir la disposition du tableau de bord droite (vue mobile)", + "set-entity-from-widget": "Définir l'entité à partir du widget", + "target-dashboard": "Tableau de bord cible", + "target-dashboard-state": "Etat du tableau de bord cible", + "target-dashboard-state-required": "L'état du tableau de bord cible est requis", + "update-dashboard-state": "Mettre à jour l'état actuel du tableau de bord" + }, +"widget-config":{ + "action": "Action", + "action-icon": "Icône", + "action-name": "Nom", + "action-name-not-unique": "Une autre action portant le même nom existe déjà.
Le nom de l'action doit être unique dans la même source d'action.", + "action-name-required": "Le nom de l'action est requis", + "action-source": "Source de l'action", + "action-source-required": "Une source d'action est requise.", + "action-type": "Type", + "action-type-required": "Le type d'action est requis.", + "actions": "Actions", + "add-action": "Ajouter une action", + "add-datasource": "Ajouter une source de données", + "advanced": "Avancé", + "alarm-source": "Source d'alarme", + "background-color": "couleur de fond", + "data": "Données", + "datasource-parameters": "Paramètres", + "datasource-type": "Type", + "datasources": "Sources de données", + "decimals": "Nombre de chiffres après virgule flottante", + "delete-action": "Supprimer l'action", + "delete-action-text": "Etes-vous sûr de vouloir supprimer l'action du widget nommé '{{actionName}}'?", + "delete-action-title": "Supprimer l'action du widget", + "display-legend": "Afficher la légende", + "display-title": "Afficher le titre", + "drop-shadow": "Ombre portée", + "edit-action": "Modifier l'action", + "enable-fullscreen": "Activer le plein écran", + "general-settings": "Paramètres généraux", + "height": "Hauteur", + "margin": "Marge", + "maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés}}", + "mobile-mode-settings": "Paramètres du mode mobile", + "order": "Ordre", + "padding": "Padding", + "remove-datasource": "Supprimer la source de données", + "search-actions": "Recherche d'actions", + "settings": "Paramètres", + "target-device": "Dispositif cible", + "text-color": "Couleur du texte", + "timewindow": "Fenêtre de temps", + "title": "Titre", + "title-style": "Style de titre", + "units": "Symbole spécial à afficher à côté de la valeur", + "use-dashboard-timewindow": "Utiliser la fenêtre de temps du tableau de bord", + "widget-style": "Style du widget" + }, +"widget-type":{ + "create-new-widget-type": "Créer un nouveau type de widget", + "export": "Exporter le type de widget", + "export-failed-error": "Impossible d'exporter le type de widget: {{error}}", + "import": "Importer le type de widget", + "invalid-widget-type-file-error": "Impossible d'importer le type de widget: structure de données de type widget invalide.", + "widget-type-file": "Fichier de type Widget" + }, +"widgets-bundle":{ + "add": "Ajouter un groupe de widgets", + "add-widgets-bundle-text": "Ajouter un nouveau groupe de widgets", + "create-new-widgets-bundle": "Créer un nouveau groupe de widgets", + "current": "Groupe actuel", + "delete": "Supprimer le groupe de widgets", + "delete-widgets-bundle-text": "Attention, après la confirmation, le groupe de widgets et toutes les données associées deviendront irrécupérables.", + "delete-widgets-bundle-title": "Êtes-vous sûr de vouloir supprimer le groupe de widgets '{{widgetsBundleTitle}}'?", + "delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}", + "delete-widgets-bundles-text": "Attention, après la confirmation, tous les groupes de widgets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}?", + "details": "Détails", + "empty": "Le groupe de widgets est vide", + "export": "Exporter le groupe de widgets", + "export-failed-error": "Impossible d'exporter le groupe de widgets: {{error}}", + "import": "Importer un groupe de widgets", + "invalid-widgets-bundle-file-error": "Impossible d'importer un groupe de widgets: structure de données du groupe de widgets non valides.", + "no-widgets-bundles-matching": "Aucun groupe de widgets correspondant à {{widgetsBundle}} n'a été trouvé.", + "no-widgets-bundles-text": "Aucun groupe de widgets trouvé", + "system": "Système", + "title": "Titre", + "title-required": "Le titre est requis.", + "widgets-bundle-details": "Détails des groupes de widgets", + "widgets-bundle-file": "Fichier de groupe de widgets", + "widgets-bundle-required": "Un groupe de widgets est requis.", + "widgets-bundles": "Groupes de widgets" + } +} diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json index b304cac78c..36ddaec08a 100644 --- a/ui/src/app/locale/locale.constant-it_IT.json +++ b/ui/src/app/locale/locale.constant-it_IT.json @@ -1434,6 +1434,7 @@ "language": { "language": "Lingua", "locales": { + "fr_FR": "Francese", "zh_CN": "Cinese", "ko_KR": "Coreano", "en_US": "Inglese", diff --git a/ui/src/app/locale/locale.constant-ko_KR.json b/ui/src/app/locale/locale.constant-ko_KR.json index a0d9e0dd8e..d2dae3a3b2 100644 --- a/ui/src/app/locale/locale.constant-ko_KR.json +++ b/ui/src/app/locale/locale.constant-ko_KR.json @@ -1329,6 +1329,7 @@ "language": "언어", "locales": { "en_US": "영어", + "fr_FR": "프랑스의", "ko_KR": "한글", "zh_CN": "중국어", "ru_RU": "러시아어", diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json index 2e384e95dc..098e20d544 100644 --- a/ui/src/app/locale/locale.constant-ru_RU.json +++ b/ui/src/app/locale/locale.constant-ru_RU.json @@ -1354,6 +1354,7 @@ "language": "Язык", "locales": { "en_US": "Английский", + "fr_FR": "Французский", "zh_CN": "Китайский", "ko_KR": "Корейский", "es_ES": "Испанский", diff --git a/ui/src/app/locale/locale.constant-zh_CN.json b/ui/src/app/locale/locale.constant-zh_CN.json index 037230d8a6..e45cd7f810 100644 --- a/ui/src/app/locale/locale.constant-zh_CN.json +++ b/ui/src/app/locale/locale.constant-zh_CN.json @@ -1438,6 +1438,7 @@ "language": "语言", "locales": { "en_US": "英语", + "fr_FR": "法国", "ko_KR": "韩语", "zh_CN": "汉语", "ru_RU": "俄语", From e20ba0a63fdc86daaf1e128714042e2dd7322ddf Mon Sep 17 00:00:00 2001 From: Rileyjs Date: Wed, 15 Aug 2018 16:20:08 -0500 Subject: [PATCH 11/30] Update locale.constant-en_US.json --- ui/src/app/locale/locale.constant-en_US.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 22aad1a90d..2432a17ba1 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -1456,7 +1456,8 @@ "it_IT": "Italian", "ko_KR": "Korean", "ru_RU": "Russian", - "es_ES": "Spanish" + "es_ES": "Spanish", + "ja_JA": "Japanese" } } } From 3edf4978ad954a4948781630c0b3db8defe1668f Mon Sep 17 00:00:00 2001 From: Rileyjs Date: Wed, 15 Aug 2018 16:20:47 -0500 Subject: [PATCH 12/30] Add files via upload --- ui/src/app/locale/locale.constant-ja_JA.json | 1463 ++++++++++++++++++ 1 file changed, 1463 insertions(+) create mode 100644 ui/src/app/locale/locale.constant-ja_JA.json diff --git a/ui/src/app/locale/locale.constant-ja_JA.json b/ui/src/app/locale/locale.constant-ja_JA.json new file mode 100644 index 0000000000..77765efddb --- /dev/null +++ b/ui/src/app/locale/locale.constant-ja_JA.json @@ -0,0 +1,1463 @@ +{ + "access": { + "unauthorized": "無許可", + "unauthorized-access": "不正アクセス", + "unauthorized-access-text": "このリソースにアクセスするにはサインインする必要があります。", + "access-forbidden": "アクセス禁止", + "access-forbidden-text": "あなたはこの場所へのアクセス権を持っていません!この場所にアクセスしたい場合は、別のユーザーとサインインしてみてください。", + "refresh-token-expired": "セッションが終了しました", + "refresh-token-failed": "セッションをリフレッシュできません" + }, + "action": { + "activate": "アクティブ化する", + "suspend": "サスペンド", + "save": "セーブ", + "saveAs": "名前を付けて保存", + "cancel": "キャンセル", + "ok": "[OK]", + "delete": "削除", + "add": "追加", + "yes": "はい", + "no": "いいえ", + "update": "更新", + "remove": "削除する", + "search": "サーチ", + "clear-search": "検索をクリアする", + "assign": "割り当てます", + "unassign": "割り当て解除", + "share": "シェア", + "make-private": "プライベートにする", + "apply": "適用", + "apply-changes": "変更を適用する", + "edit-mode": "編集モード", + "enter-edit-mode": "編集モードに入る", + "decline-changes": "変更を拒否する", + "close": "閉じる", + "back": "バック", + "run": "走る", + "sign-in": "サインイン!", + "edit": "編集", + "view": "ビュー", + "create": "作成する", + "drag": "ドラッグ", + "refresh": "リフレッシュ", + "undo": "元に戻す", + "copy": "コピー", + "paste": "ペースト", + "copy-reference": "コピーリファレンス", + "paste-reference": "参照貼り付け", + "import": "インポート", + "export": "輸出する", + "share-via": "{{provider}}" + }, + "aggregation": { + "aggregation": "集約", + "function": "データ集約機能", + "limit": "最大値", + "group-interval": "グループ化の間隔", + "min": "分", + "max": "最大", + "avg": "平均", + "sum": "和", + "count": "カウント", + "none": "なし" + }, + "admin": { + "general": "一般", + "general-settings": "一般設定", + "outgoing-mail": "送信メール", + "outgoing-mail-settings": "送信メールの設定", + "system-settings": "システム設定", + "test-mail-sent": "テストメールが正常に送信されました!", + "base-url": "ベースURL", + "base-url-required": "ベースURLは必須です。", + "mail-from": "メール", + "mail-from-required": "メールの送信元が必要です。", + "smtp-protocol": "SMTPプロトコル", + "smtp-host": "SMTPホスト", + "smtp-host-required": "SMTPホストが必要です。", + "smtp-port": "SMTPポート", + "smtp-port-required": "smtpポートを指定する必要があります。", + "smtp-port-invalid": "それは有効なsmtpポートのようには見えません。", + "timeout-msec": "タイムアウト(ミリ秒)", + "timeout-required": "タイムアウトが必要です。", + "timeout-invalid": "それは有効なタイムアウトのようには見えません。", + "enable-tls": "TLSを有効にする", + "send-test-mail": "テストメールを送信する" + }, + "alarm": { + "alarm": "警報", + "alarms": "アラーム", + "select-alarm": "アラームを選択", + "no-alarms-matching": "'{{entity}}'発見されました。", + "alarm-required": "アラームが必要です", + "alarm-status": "アラーム状態", + "search-status": { + "ANY": "どれか", + "ACTIVE": "アクティブ", + "CLEARED": "クリアされた", + "ACK": "承認された", + "UNACK": "未確認の" + }, + "display-status": { + "ACTIVE_UNACK": "アクティブ未確認", + "ACTIVE_ACK": "Active Acknowledged", + "CLEARED_UNACK": "クリアされた未確認のメッセージ", + "CLEARED_ACK": "承認された承認済み" + }, + "no-alarms-prompt": "アラームが見つかりません", + "created-time": "作成時刻", + "type": "タイプ", + "severity": "重大度", + "originator": "創始者", + "originator-type": "発信者タイプ", + "details": "詳細", + "status": "状態", + "alarm-details": "アラームの詳細", + "start-time": "始まる時間", + "end-time": "終了時間", + "ack-time": "確認された時間", + "clear-time": "クリアされた時間", + "severity-critical": "クリティカル", + "severity-major": "メジャー", + "severity-minor": "マイナー", + "severity-warning": "警告", + "severity-indeterminate": "不確定", + "acknowledge": "認める", + "clear": "クリア", + "search": "アラームの検索", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} }選択された", + "no-data": "表示するデータがありません", + "polling-interval": "アラームポーリング間隔(秒)", + "polling-interval-required": "アラームのポーリング間隔が必要です。", + "min-polling-interval-message": "少なくとも1秒間のポーリング間隔が許可されます。", + "aknowledge-alarms-title": "{ count, plural, 1 {1 alarm} other {# alarms} }", + "aknowledge-alarms-text": "{ count, plural, 1 {1 alarm} other {# alarms} }?", + "clear-alarms-title": "{ count, plural, 1 {1 alarm} other {# alarms} }", + "clear-alarms-text": "{ count, plural, 1 {1 alarm} other {# alarms} }?" + }, + "alias": { + "add": "エイリアスを追加する", + "edit": "エイリアスを編集する", + "name": "エイリアス名", + "name-required": "エイリアス名は必須です", + "duplicate-alias": "同じ名前のエイリアスは既に存在します。", + "filter-type-single-entity": "単一のエンティティ", + "filter-type-entity-list": "エンティティリスト", + "filter-type-entity-name": "エンティティ名", + "filter-type-state-entity": "ダッシュボード状態からのエンティティ", + "filter-type-state-entity-description": "ダッシュボードの状態パラメータから取得されたエンティティ", + "filter-type-asset-type": "資産の種類", + "filter-type-asset-type-description": "'{{assetType}}'", + "filter-type-asset-type-and-name-description": "'{{assetType}}''{{prefix}}'", + "filter-type-device-type": "デバイスタイプ", + "filter-type-device-type-description": "'{{deviceType}}'", + "filter-type-device-type-and-name-description": "'{{deviceType}}''{{prefix}}'", + "filter-type-relations-query": "関係クエリ", + "filter-type-relations-query-description": "{{entities}}{{relationType}}{{direction}}{{rootEntity}}", + "filter-type-asset-search-query": "資産検索クエリ", + "filter-type-asset-search-query-description": "{{assetTypes}}{{relationType}}{{direction}}{{rootEntity}}", + "filter-type-device-search-query": "デバイス検索クエリ", + "filter-type-device-search-query-description": "{{deviceTypes}}{{relationType}}{{direction}}{{rootEntity}}", + "entity-filter": "エンティティフィルタ", + "resolve-multiple": "複数のエンティティとして解決する", + "filter-type": "フィルタタイプ", + "filter-type-required": "フィルタタイプが必要です。", + "entity-filter-no-entity-matched": "指定されたフィルタに一致するエンティティは見つかりませんでした。", + "no-entity-filter-specified": "エンティティフィルタが指定されていない", + "root-state-entity": "ルートとしてダッシュボードの状態エンティティを使用する", + "root-entity": "ルートエンティティ", + "state-entity-parameter-name": "状態エンティティのパラメータ名", + "default-state-entity": "デフォルト状態エンティティ", + "default-entity-parameter-name": "デフォルトでは", + "max-relation-level": "最大関連レベル", + "unlimited-level": "無制限レベル", + "state-entity": "ダッシュボードの状態エンティティ", + "all-entities": "すべてのエンティティ", + "any-relation": "どれか" + }, + "asset": { + "asset": "資産", + "assets": "資産", + "management": "資産運用管理", + "view-assets": "アセットの表示", + "add": "アセットを追加", + "assign-to-customer": "顧客に割り当てる", + "assign-asset-to-customer": "顧客に資産を割り当てる", + "assign-asset-to-customer-text": "顧客に割り当てる資産を選択してください", + "no-assets-text": "アセットが見つかりません", + "assign-to-customer-text": "資産を割り当てる顧客を選択してください", + "public": "パブリック", + "assignedToCustomer": "顧客に割り当てられた", + "make-public": "アセットを公開する", + "make-private": "アセットをプライベートにする", + "unassign-from-customer": "顧客からの割り当て解除", + "delete": "アセットを削除", + "asset-public": "資産は公開されています", + "asset-type": "資産の種類", + "asset-type-required": "資産の種類が必要です。", + "select-asset-type": "アセットタイプを選択", + "enter-asset-type": "アセットタイプを入力", + "any-asset": "すべてのアセット", + "no-asset-types-matching": "'{{entitySubtype}}'発見されました。", + "asset-type-list-empty": "選択されたアセットタイプはありません。", + "asset-types": "資産タイプ", + "name": "名", + "name-required": "名前は必須です。", + "description": "説明", + "type": "タイプ", + "type-required": "タイプが必要です。", + "details": "詳細", + "events": "イベント", + "add-asset-text": "新しいアセットを追加する", + "asset-details": "資産の詳細", + "assign-assets": "アセットの割り当て", + "assign-assets-text": "{ count, plural, 1 {1 asset} other {# assets} }顧客に", + "delete-assets": "アセットを削除する", + "unassign-assets": "アセットの割り当てを解除する", + "unassign-assets-action-title": "{ count, plural, 1 {1 asset} other {# assets} }顧客から", + "assign-new-asset": "新しいアセットを割り当てる", + "delete-asset-title": "'{{assetName}}'?", + "delete-asset-text": "確認後、資産と関連するすべてのデータが回復不能になることに注意してください。", + "delete-assets-title": "{ count, plural, 1 {1 asset} other {# assets} }?", + "delete-assets-action-title": "{ count, plural, 1 {1 asset} other {# assets} }", + "delete-assets-text": "確認後、選択したすべての資産が削除され、関連するすべてのデータは回復不能になりますので注意してください。", + "make-public-asset-title": "'{{assetName}}'パブリック?", + "make-public-asset-text": "確認後、資産とそのすべてのデータは公開され、他の人がアクセスできるようになります。", + "make-private-asset-title": "'{{assetName}}'プライベート?", + "make-private-asset-text": "確認後、資産とそのすべてのデータは非公開にされ、他の人がアクセスすることはできません。", + "unassign-asset-title": "'{{assetName}}'?", + "unassign-asset-text": "確認後、資産は割り当て解除され、顧客はアクセスできなくなります。", + "unassign-asset": "アセットの割り当てを解除する", + "unassign-assets-title": "{ count, plural, 1 {1 asset} other {# assets} }?", + "unassign-assets-text": "確認後、選択されたすべての資産が割り当て解除され、顧客がアクセスできなくなります。", + "copyId": "アセットIDをコピーする", + "idCopiedMessage": "アセットIDがクリップボードにコピーされました", + "select-asset": "アセットを選択", + "no-assets-matching": "'{{entity}}'発見されました。", + "asset-required": "資産が必要です", + "name-starts-with": "アセット名はで始まります" + }, + "attribute": { + "attributes": "属性", + "latest-telemetry": "最新テレメトリ", + "attributes-scope": "エンティティ属性のスコープ", + "scope-latest-telemetry": "最新テレメトリ", + "scope-client": "クライアントの属性", + "scope-server": "サーバーの属性", + "scope-shared": "共有属性", + "add": "属性を追加する", + "key": "キー", + "last-update-time": "最終更新時間", + "key-required": "属性キーは必須です。", + "value": "値", + "value-required": "属性値は必須です。", + "delete-attributes-title": "{ count, plural, 1 {1 attribute} other {# attributes} }?", + "delete-attributes-text": "注意してください。確認後、選択したすべての属性が削除されます。", + "delete-attributes": "属性を削除する", + "enter-attribute-value": "属性値を入力", + "show-on-widget": "ウィジェットで表示", + "widget-mode": "ウィジェットモード", + "next-widget": "次のウィジェット", + "prev-widget": "前のウィジェット", + "add-to-dashboard": "ダッシュボードに追加", + "add-widget-to-dashboard": "ウィジェットをダッシュ​​ボードに追加する", + "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} }選択された", + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} }選択された" + }, + "audit-log": { + "audit": "監査", + "audit-logs": "監査ログ", + "timestamp": "タイムスタンプ", + "entity-type": "エンティティタイプ", + "entity-name": "エンティティ名", + "user": "ユーザー", + "type": "タイプ", + "status": "状態", + "details": "詳細", + "type-added": "追加された", + "type-deleted": "削除済み", + "type-updated": "更新しました", + "type-attributes-updated": "属性が更新されました", + "type-attributes-deleted": "属性が削除されました", + "type-rpc-call": "RPC呼び出し", + "type-credentials-updated": "資格が更新されました", + "type-assigned-to-customer": "顧客に割り当てられた", + "type-unassigned-from-customer": "顧客から割り当てられていない", + "type-activated": "活性化", + "type-suspended": "一時停止中", + "type-credentials-read": "信用証明書を読む", + "type-attributes-read": "読み取られた属性", + "type-relation-add-or-update": "関係が更新されました", + "type-relation-delete": "関係が削除されました", + "type-relations-delete": "すべてのリレーションを削除", + "type-alarm-ack": "承認された", + "type-alarm-clear": "クリアされた", + "status-success": "成功", + "status-failure": "失敗", + "audit-log-details": "監査ログの詳細", + "no-audit-logs-prompt": "ログが見つかりません", + "action-data": "行動データ", + "failure-details": "失敗の詳細", + "search": "監査ログの検索", + "clear-search": "検索をクリアする" + }, + "confirm-on-exit": { + "message": "保存されていない変更があります。あなたは本当にこのページを出るのですか?", + "html-message": "保存していない変更があります。
このページを終了してもよろしいですか?", + "title": "保存されていない変更" + }, + "contact": { + "country": "国", + "city": "シティ", + "state": "州/県", + "postal-code": "郵便番号", + "postal-code-invalid": "無効な郵便番号形式です。", + "address": "住所", + "address2": "アドレス2", + "phone": "電話", + "email": "Eメール", + "no-address": "住所がありません" + }, + "common": { + "username": "ユーザー名", + "password": "パスワード", + "enter-username": "ユーザーネームを入力してください", + "enter-password": "パスワードを入力する", + "enter-search": "検索を入力" + }, + "content-type": { + "json": "Json", + "text": "テキスト", + "binary": "バイナリ(Base64)" + }, + "customer": { + "customer": "顧客", + "customers": "顧客", + "management": "顧客管理", + "dashboard": "カスタマーダッシュボード", + "dashboards": "カスタマーダッシュボード", + "devices": "顧客デバイス", + "assets": "顧客資産", + "public-dashboards": "パブリックダッシュボード", + "public-devices": "パブリックデバイス", + "public-assets": "公的資産", + "add": "顧客を追加", + "delete": "顧客を削除する", + "manage-customer-users": "顧客ユーザーを管理する", + "manage-customer-devices": "顧客のデバイスを管理する", + "manage-customer-dashboards": "顧客ダッシュボードの管理", + "manage-public-devices": "パブリックデバイスを管理する", + "manage-public-dashboards": "公開ダッシュボードの管理", + "manage-customer-assets": "顧客資産の管理", + "manage-public-assets": "公的資産を管理する", + "add-customer-text": "新規顧客を追加", + "no-customers-text": "顧客が見つかりません", + "customer-details": "お客様情報", + "delete-customer-title": "'{{customerTitle}}'?", + "delete-customer-text": "確認後、お客様および関連するすべてのデータが回復不能になるので注意してください。", + "delete-customers-title": "{ count, plural, 1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "{ count, plural, 1 {1 customer} other {# customers} }", + "delete-customers-text": "確認後、選択したすべての顧客は削除され、関連するすべてのデータは回復不能になります。", + "manage-users": "ユーザーを管理する", + "manage-assets": "アセットを管理する", + "manage-devices": "デバイスを管理する", + "manage-dashboards": "ダッシュボードの管理", + "title": "タイトル", + "title-required": "タイトルは必須です。", + "description": "説明", + "details": "詳細", + "events": "イベント", + "copyId": "顧客IDをコピー", + "idCopiedMessage": "顧客IDがクリップボードにコピーされました", + "select-customer": "顧客を選択", + "no-customers-matching": "'{{entity}}'発見されました。", + "customer-required": "顧客は必須です", + "select-default-customer": "デフォルトの顧客を選択", + "default-customer": "デフォルトの顧客", + "default-customer-required": "テナントレベルのダッシュボードをデバッグするには、デフォルトの顧客が必要です" + }, + "datetime": { + "date-from": "デートから", + "time-from": "からの時間", + "date-to": "日付", + "time-to": "の時間" + }, + "dashboard": { + "dashboard": "ダッシュボード", + "dashboards": "ダッシュボード", + "management": "ダッシュボード管理", + "view-dashboards": "ダッシュボードを表示する", + "add": "ダッシュボードを追加", + "assign-dashboard-to-customer": "顧客にダッシュボードを割り当てる", + "assign-dashboard-to-customer-text": "顧客に割り当てるダッシュボードを選択してください", + "assign-to-customer-text": "ダッシュボードを割り当てる顧客を選択してください", + "assign-to-customer": "顧客に割り当てる", + "unassign-from-customer": "顧客からの割り当て解除", + "make-public": "ダッシュボードを公開する", + "make-private": "ダッシュボードを非公開にする", + "manage-assigned-customers": "割り当てられた顧客を管理する", + "assigned-customers": "割り当てられた顧客", + "assign-to-customers": "顧客にダッシュボードを割り当てる", + "assign-to-customers-text": "ダッシュボードを割り当てる顧客を選択してください", + "unassign-from-customers": "顧客からのダッシュボードの割り当て解除", + "unassign-from-customers-text": "ダッシュボードから割り当て解除する顧客を選択してください", + "no-dashboards-text": "ダッシュボードが見つかりません", + "no-widgets": "ウィジェットは設定されていません", + "add-widget": "新しいウィジェットを追加", + "title": "タイトル", + "select-widget-title": "ウィジェットを選択", + "select-widget-subtitle": "利用可能なウィジェットタイプのリスト", + "delete": "ダッシュボードの削除", + "title-required": "タイトルは必須です。", + "description": "説明", + "details": "詳細", + "dashboard-details": "ダッシュボードの詳細", + "add-dashboard-text": "新しいダッシュボードを追加する", + "assign-dashboards": "ダッシュボードの割り当て", + "assign-new-dashboard": "新しいダッシュボードを割り当てる", + "assign-dashboards-text": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客に", + "unassign-dashboards-action-text": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客から", + "delete-dashboards": "ダッシュボードの削除", + "unassign-dashboards": "ダッシュボードの割り当てを解除する", + "unassign-dashboards-action-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }顧客から", + "delete-dashboard-title": "'{{dashboardTitle}}'?", + "delete-dashboard-text": "確認後、ダッシュボードとすべての関連データが回復不能になるので注意してください。", + "delete-dashboards-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }?", + "delete-dashboards-action-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text": "注意してください。確認後、選択したダッシュボードはすべて削除され、関連するすべてのデータは回復不能になります。", + "unassign-dashboard-title": "'{{dashboardTitle}}'?", + "unassign-dashboard-text": "確認後、ダッシュボードは割り当てられなくなり、顧客はアクセスできなくなります。", + "unassign-dashboard": "ダッシュボードの割り当てを解除する", + "unassign-dashboards-title": "{ count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-text": "確認の後、選択したすべてのダッシュボードは割り当てられなくなり、顧客はアクセスできなくなります。", + "public-dashboard-title": "ダッシュボードは公開されました", + "public-dashboard-text": "{{dashboardTitle}} is now public and accessible via next public link:", + "public-dashboard-notice": "注:データにアクセスするために、関連するデバイスを公開することを忘れないでください。", + "make-private-dashboard-title": "'{{dashboardTitle}}'プライベート?", + "make-private-dashboard-text": "確認の後、ダッシュボードはプライベートにされ、他の人がアクセスすることはできません。", + "make-private-dashboard": "ダッシュボードを非公開にする", + "socialshare-text": "'{{dashboardTitle}}'ThingsBoardを搭載", + "socialshare-title": "'{{dashboardTitle}}'ThingsBoardを搭載", + "select-dashboard": "ダッシュボードを選択", + "no-dashboards-matching": "'{{entity}}'発見されました。", + "dashboard-required": "ダッシュボードが必要です。", + "select-existing": "既存のダッシュボードを選択", + "create-new": "新しいダッシュボードを作成する", + "new-dashboard-title": "新しいダッシュボードのタイトル", + "open-dashboard": "ダッシュボードを開く", + "set-background": "背景を設定する", + "background-color": "背景色", + "background-image": "背景画像", + "background-size-mode": "背景サイズモード", + "no-image": "選択した画像がありません", + "drop-image": "画像をドロップするか、クリックしてアップロードするファイルを選択します。", + "settings": "設定", + "columns-count": "列数", + "columns-count-required": "列数が必要です。", + "min-columns-count-message": "わずか10の最小列数が許可されます。", + "max-columns-count-message": "最大1000の列カウントのみが許可されます。", + "widgets-margins": "ウィジェット間のマージン", + "horizontal-margin": "水平マージン", + "horizontal-margin-required": "水平余白値が必要です。", + "min-horizontal-margin-message": "最小水平マージン値としては0だけが許容されます。", + "max-horizontal-margin-message": "最大水平マージン値は50だけです。", + "vertical-margin": "垂直マージン", + "vertical-margin-required": "垂直マージン値が必要です。", + "min-vertical-margin-message": "最小の垂直マージン値として0のみが許可されます。", + "max-vertical-margin-message": "最大垂直マージン値は50のみです。", + "autofill-height": "自動レイアウトの高さ", + "mobile-layout": "モバイルレイアウトの設定", + "mobile-row-height": "モバイル行の高さ、px", + "mobile-row-height-required": "モバイル行の高さ値が必要です。", + "min-mobile-row-height-message": "最小の行の高さの値として、5ピクセルしか許可されません。", + "max-mobile-row-height-message": "移動可能な行の高さの最大値として許可されるのは200ピクセルだけです。", + "display-title": "ダッシュボードのタイトルを表示する", + "toolbar-always-open": "ツールバーを開いたままにする", + "title-color": "タイトルカラー", + "display-dashboards-selection": "ダッシュボードの選択を表示する", + "display-entities-selection": "エンティティの選択を表示する", + "display-dashboard-timewindow": "タイムウィンドウを表示する", + "display-dashboard-export": "エクスポートの表示", + "import": "インポートダッシュボード", + "export": "エクスポートダッシュボード", + "export-failed-error": "{{error}}", + "create-new-dashboard": "新しいダッシュボードを作成する", + "dashboard-file": "ダッシュボードファイル", + "invalid-dashboard-file-error": "ダッシュボードをインポートできません:ダッシュボードのデータ構造が無効です。", + "dashboard-import-missing-aliases-title": "インポートされたダッシュボードで使用されるエイリアスを設定する", + "create-new-widget": "新しいウィジェットを作成する", + "import-widget": "インポートウィジェット", + "widget-file": "ウィジェットファイル", + "invalid-widget-file-error": "ウィジェットをインポートできません:ウィジェットのデータ構造が無効です。", + "widget-import-missing-aliases-title": "インポートされたウィジェットで使用されるエイリアスを設定する", + "open-toolbar": "ダッシュボードツールバーを開く", + "close-toolbar": "ツールバーを閉じる", + "configuration-error": "設定エラー", + "alias-resolution-error-title": "ダッシュボードエイリアス設定エラー", + "invalid-aliases-config": "エイリアスフィルタの一部に一致するデバイスを見つけることができません。
この問題を解決するには、管理者に連絡してください。", + "select-devices": "デバイスの選択", + "assignedToCustomer": "顧客に割り当てられた", + "assignedToCustomers": "顧客に割り当てられた", + "public": "パブリック", + "public-link": "パブリックリンク", + "copy-public-link": "パブリックリンクをコピーする", + "public-link-copied-message": "ダッシュボードのパブリックリンクがクリップボードにコピーされました", + "manage-states": "ダッシュボードの状態を管理する", + "states": "ダッシュボードの状態", + "search-states": "検索ダッシュボードの状態", + "selected-states": "{ count, plural, 1 {1 dashboard state} other {# dashboard states} }選択された", + "edit-state": "ダッシュボードの状態を編集する", + "delete-state": "ダッシュボードの状態を削除する", + "add-state": "ダッシュボードの状態を追加する", + "state": "ダッシュボードの状態", + "state-name": "名", + "state-name-required": "ダッシュボードの状態名は必須です。", + "state-id": "状態ID", + "state-id-required": "ダッシュボードの状態IDは必須です。", + "state-id-exists": "同じIDを持つダッシュボードの状態は既に存在します。", + "is-root-state": "ルート状態", + "delete-state-title": "ダッシュボードの状態を削除する", + "delete-state-text": "'{{stateName}}'?", + "show-details": "詳細を表示", + "hide-details": "詳細を隠す", + "select-state": "ターゲット状態を選択する", + "state-controller": "状態コントローラ" + }, + "datakey": { + "settings": "設定", + "advanced": "上級", + "label": "ラベル", + "color": "色", + "units": "値の隣に表示する特別なシンボル", + "decimals": "浮動小数点の後の桁数", + "data-generation-func": "データ生成関数", + "use-data-post-processing-func": "データ後処理機能を使用する", + "configuration": "データキー設定", + "timeseries": "タイムズ", + "attributes": "属性", + "alarm": "アラームフィールド", + "timeseries-required": "エンティティの時系列データが必要です。", + "timeseries-or-attributes-required": "エンティティのtimeseries /属性は必須です。", + "maximum-timeseries-or-attributes": "{ count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", + "alarm-fields-required": "アラームフィールドが必要です。", + "function-types": "関数型", + "function-types-required": "関数型が必要です。", + "maximum-function-types": "{ count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }" + }, + "datasource": { + "type": "データソースタイプ", + "name": "名", + "add-datasource-prompt": "データソースを追加してください" + }, + "details": { + "edit-mode": "編集モード", + "toggle-edit-mode": "編集モードを切り替える" + }, + "device": { + "device": "デバイス", + "device-required": "デバイスが必要です。", + "devices": "デバイス", + "management": "端末管理", + "view-devices": "デバイスの表示", + "device-alias": "デバイスエイリアス", + "aliases": "デバイスエイリアス", + "no-alias-matching": "'{{alias}}'見つかりません。", + "no-aliases-found": "別名は見つかりませんでした。", + "no-key-matching": "'{{key}}'見つかりません。", + "no-keys-found": "キーが見つかりません。", + "create-new-alias": "新しいものを作成してください!", + "create-new-key": "新しいものを作成してください!", + "duplicate-alias-error": "'{{alias}}'
デバイスエイリアスは、ダッシュボード内で一意である必要があります。", + "configure-alias": "'{{alias}}'エイリアス", + "no-devices-matching": "'{{entity}}'発見されました。", + "alias": "エイリアス", + "alias-required": "デバイスエイリアスが必要です。", + "remove-alias": "デバイスエイリアスを削除する", + "add-alias": "デバイスエイリアスを追加する", + "name-starts-with": "デバイス名はで始まります", + "device-list": "デバイスリスト", + "use-device-name-filter": "フィルタを使用する", + "device-list-empty": "デバイスが選択されていません。", + "device-name-filter-required": "デバイス名フィルタが必要です。", + "device-name-filter-no-device-matched": "'{{device}}'発見されました。", + "add": "デバイスを追加", + "assign-to-customer": "顧客に割り当てる", + "assign-device-to-customer": "顧客にデバイスを割り当てる", + "assign-device-to-customer-text": "顧客に割り当てるデバイスを選択してください", + "make-public": "端末を公開する", + "make-private": "デバイスを非公開にする", + "no-devices-text": "デバイスが見つかりません", + "assign-to-customer-text": "デバイスを割り当てる顧客を選択してください", + "device-details": "デバイスの詳細", + "add-device-text": "新しいデバイスを追加する", + "credentials": "資格情報", + "manage-credentials": "資格情報を管理する", + "delete": "デバイスを削除する", + "assign-devices": "デバイスを割り当てる", + "assign-devices-text": "{ count, plural, 1 {1 device} other {# devices} }顧客に", + "delete-devices": "デバイスを削除する", + "unassign-from-customer": "顧客からの割り当て解除", + "unassign-devices": "デバイスの割り当てを解除する", + "unassign-devices-action-title": "{ count, plural, 1 {1 device} other {# devices} }顧客から", + "assign-new-device": "新しいデバイスを割り当てる", + "make-public-device-title": "'{{deviceName}}'パブリック?", + "make-public-device-text": "確認後、デバイスとそのすべてのデータは公開され、他のユーザーがアクセスできるようになります。", + "make-private-device-title": "'{{deviceName}}'プライベート?", + "make-private-device-text": "確認後、デバイスとそのすべてのデータは非公開になり、他人がアクセスできなくなります。", + "view-credentials": "資格情報を表示する", + "delete-device-title": "'{{deviceName}}'?", + "delete-device-text": "確認後、デバイスと関連するすべてのデータが回復不能になるので注意してください。", + "delete-devices-title": "{ count, plural, 1 {1 device} other {# devices} }?", + "delete-devices-action-title": "{ count, plural, 1 {1 device} other {# devices} }", + "delete-devices-text": "注意してください。確認後、選択したすべてのデバイスが削除され、関連するすべてのデータは回復不能になります。", + "unassign-device-title": "'{{deviceName}}'?", + "unassign-device-text": "確認の後、デバイスは割り当てが解除され、顧客がアクセスできなくなります。", + "unassign-device": "デバイスの割り当てを解除する", + "unassign-devices-title": "{ count, plural, 1 {1 device} other {# devices} }?", + "unassign-devices-text": "確認の後、選択されたすべてのデバイスが割り当て解除され、顧客がアクセスできなくなります。", + "device-credentials": "デバイス資格情報", + "credentials-type": "資格情報タイプ", + "access-token": "アクセストークン", + "access-token-required": "アクセストークンが必要です。", + "access-token-invalid": "アクセストークンの長さは、1〜20文字でなければなりません。", + "rsa-key": "RSA公開鍵", + "rsa-key-required": "RSA公開鍵が必要です。", + "secret": "秘密", + "secret-required": "秘密が必要です。", + "device-type": "デバイスタイプ", + "device-type-required": "デバイスタイプが必要です。", + "select-device-type": "デバイスタイプを選択", + "enter-device-type": "デバイスタイプを入力", + "any-device": "すべてのデバイス", + "no-device-types-matching": "'{{entitySubtype}}'発見されました。", + "device-type-list-empty": "選択されたデバイスタイプはありません。", + "device-types": "デバイスの種類", + "name": "名", + "name-required": "名前は必須です。", + "description": "説明", + "events": "イベント", + "details": "詳細", + "copyId": "デバイスIDをコピーする", + "copyAccessToken": "コピーアクセストークン", + "idCopiedMessage": "デバイスIDがクリップボードにコピーされました", + "accessTokenCopiedMessage": "デバイスアクセストークンがクリップボードにコピーされました", + "assignedToCustomer": "顧客に割り当てられた", + "unable-delete-device-alias-title": "デバイスエイリアスを削除できません", + "unable-delete-device-alias-text": "'{{deviceAlias}}'{{widgetsList}}", + "is-gateway": "ゲートウェイです", + "public": "パブリック", + "device-public": "デバイスは公開されています", + "select-device": "デバイスの選択" + }, + "dialog": { + "close": "ダイアログを閉じる" + }, + "error": { + "unable-to-connect": "サーバーに接続できません!インターネット接続を確認してください。", + "unhandled-error-code": "{{errorCode}}", + "unknown-error": "不明なエラー" + }, + "entity": { + "entity": "エンティティ", + "entities": "エンティティ", + "aliases": "エンティティエイリアス", + "entity-alias": "エンティティエイリアス", + "unable-delete-entity-alias-title": "エンティティエイリアスを削除できません", + "unable-delete-entity-alias-text": "'{{entityAlias}}'{{widgetsList}}", + "duplicate-alias-error": "'{{alias}}'
エンティティのエイリアスは、ダッシュボード内で一意である必要があります。", + "missing-entity-filter-error": "'{{alias}}'.", + "configure-alias": "'{{alias}}'エイリアス", + "alias": "エイリアス", + "alias-required": "エンティティエイリアスが必要です。", + "remove-alias": "エンティティエイリアスを削除する", + "add-alias": "エンティティエイリアスを追加する", + "entity-list": "エンティティリスト", + "entity-type": "エンティティタイプ", + "entity-types": "エンティティタイプ", + "entity-type-list": "エンティティタイプリスト", + "any-entity": "任意のエンティティ", + "enter-entity-type": "エンティティタイプを入力", + "no-entities-matching": "'{{entity}}'発見されました。", + "no-entity-types-matching": "'{{entityType}}'発見されました。", + "name-starts-with": "名前はで始まる", + "use-entity-name-filter": "フィルタを使用する", + "entity-list-empty": "選択されたエンティティはありません", + "entity-type-list-empty": "エンティティタイプは選択されていません。", + "entity-name-filter-required": "エンティティ名フィルタが必要です。", + "entity-name-filter-no-entity-matched": "'{{entity}}'発見されました。", + "all-subtypes": "すべて", + "select-entities": "エンティティの選択", + "no-aliases-found": "別名は見つかりませんでした。", + "no-alias-matching": "'{{alias}}'見つかりません。", + "create-new-alias": "新しいものを作成してください!", + "key": "キー", + "key-name": "キー名", + "no-keys-found": "キーが見つかりません。", + "no-key-matching": "'{{key}}'見つかりません。", + "create-new-key": "新しいものを作成してください!", + "type": "タイプ", + "type-required": "エンティティタイプが必要です。", + "type-device": "デバイス", + "type-devices": "デバイス", + "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", + "device-name-starts-with": "'{{prefix}}'", + "type-asset": "資産", + "type-assets": "資産", + "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", + "asset-name-starts-with": "'{{prefix}}'", + "type-rule": "ルール", + "type-rules": "ルール", + "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", + "rule-name-starts-with": "'{{prefix}}'", + "type-plugin": "プラグイン", + "type-plugins": "プラグイン", + "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", + "plugin-name-starts-with": "'{{prefix}}'", + "type-tenant": "テナント", + "type-tenants": "テナント", + "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", + "tenant-name-starts-with": "'{{prefix}}'", + "type-customer": "顧客", + "type-customers": "顧客", + "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", + "customer-name-starts-with": "'{{prefix}}'", + "type-user": "ユーザー", + "type-users": "ユーザー", + "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", + "user-name-starts-with": "'{{prefix}}'", + "type-dashboard": "ダッシュボード", + "type-dashboards": "ダッシュボード", + "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", + "dashboard-name-starts-with": "'{{prefix}}'", + "type-alarm": "警報", + "type-alarms": "アラーム", + "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", + "alarm-name-starts-with": "'{{prefix}}'", + "type-rulechain": "ルールチェーン", + "type-rulechains": "ルールチェーン", + "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", + "rulechain-name-starts-with": "'{{prefix}}'", + "type-rulenode": "ルールノード", + "type-rulenodes": "ルールノード", + "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", + "rulenode-name-starts-with": "'{{prefix}}'", + "type-current-customer": "現在の顧客", + "search": "検索エンティティ", + "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} }選択された", + "entity-name": "エンティティ名", + "details": "エンティティの詳細", + "no-entities-prompt": "エンティティが見つかりません", + "no-data": "表示するデータがありません" + }, + "event": { + "event-type": "イベントタイプ", + "type-error": "エラー", + "type-lc-event": "ライフサイクルイベント", + "type-stats": "統計", + "type-debug-rule-node": "デバッグ", + "type-debug-rule-chain": "デバッグ", + "no-events-prompt": "イベントは見つかりませんでした", + "error": "エラー", + "alarm": "警報", + "event-time": "イベント時間", + "server": "サーバ", + "body": "体", + "method": "方法", + "type": "タイプ", + "entity": "エンティティ", + "message-id": "メッセージID", + "message-type": "メッセージタイプ", + "data-type": "データ・タイプ", + "relation-type": "関係タイプ", + "metadata": "メタデータ", + "data": "データ", + "event": "イベント", + "status": "状態", + "success": "成功", + "failed": "失敗", + "messages-processed": "処理されたメッセージ", + "errors-occurred": "エラーが発生しました" + }, + "extension": { + "extensions": "拡張機能", + "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} }選択された", + "type": "タイプ", + "key": "キー", + "value": "値", + "id": "イド", + "extension-id": "内線番号", + "extension-type": "拡張タイプ", + "transformer-json": "JSON *", + "unique-id-required": "現在の拡張IDは既に存在します。", + "delete": "拡張子を削除", + "add": "内線番号を追加", + "edit": "拡張機能を編集する", + "delete-extension-title": "'{{extensionId}}'?", + "delete-extension-text": "確認後、拡張子と関連するすべてのデータが回復不能になることに注意してください。", + "delete-extensions-title": "{ count, plural, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "注意してください。確認後、選択したすべての内線番号が削除されます。", + "converters": "コンバーター", + "converter-id": "コンバーターID", + "configuration": "構成", + "converter-configurations": "コンバータ構成", + "token": "セキュリティトークン", + "add-converter": "コンバータを追加する", + "add-config": "コンバータ設定を追加する", + "device-name-expression": "デバイス名式", + "device-type-expression": "デバイスタイプの式", + "custom": "カスタム", + "to-double": "ダブル", + "transformer": "トランス", + "json-required": "トランスフォーマーjsonが必要です。", + "json-parse": "変圧器jsonを解析できません。", + "attributes": "属性", + "add-attribute": "属性を追加する", + "add-map": "マッピング要素を追加する", + "timeseries": "タイムズ", + "add-timeseries": "時系列を追加する", + "field-required": "フィールドは必須項目です", + "brokers": "ブローカー", + "add-broker": "ブローカーを追加", + "host": "ホスト", + "port": "ポート", + "port-range": "ポートは1〜65535の範囲内にある必要があります。", + "ssl": "SSL", + "credentials": "資格情報", + "username": "ユーザー名", + "password": "パスワード", + "retry-interval": "ミリ秒単位の再試行間隔", + "anonymous": "匿名", + "basic": "ベーシック", + "pem": "PEM", + "ca-cert": "CA証明書ファイル*", + "private-key": "秘密鍵ファイル*", + "cert": "証明書ファイル*", + "no-file": "ファイルが選択されていません。", + "drop-file": "ファイルをドロップするか、クリックしてアップロードするファイルを選択します。", + "mapping": "マッピング", + "topic-filter": "トピックフィルタ", + "converter-type": "コンバータタイプ", + "converter-json": "Json", + "json-name-expression": "デバイス名json式", + "topic-name-expression": "デバイス名トピック表現", + "json-type-expression": "デバイスタイプjson式", + "topic-type-expression": "デバイスタイプトピック表現", + "attribute-key-expression": "属性キー式", + "attr-json-key-expression": "属性キーjson式", + "attr-topic-key-expression": "属性キートピック式", + "request-id-expression": "要求ID式", + "request-id-json-expression": "リクエストID json式", + "request-id-topic-expression": "リクエストIDトピック表現", + "response-topic-expression": "応答トピック表現", + "value-expression": "値式", + "topic": "トピック", + "timeout": "タイムアウト(ミリ秒)", + "converter-json-required": "コンバータjsonが必要です。", + "converter-json-parse": "コンバータjsonを解析できません。", + "filter-expression": "フィルタ式", + "connect-requests": "接続要求", + "add-connect-request": "接続要求を追加", + "disconnect-requests": "切断要求", + "add-disconnect-request": "切断リクエストを追加する", + "attribute-requests": "属性要求", + "add-attribute-request": "属性要求を追加する", + "attribute-updates": "属性の更新", + "add-attribute-update": "属性の更新を追加する", + "server-side-rpc": "サーバー側RPC", + "add-server-side-rpc-request": "サーバー側RPC要求を追加する", + "device-name-filter": "デバイス名フィルタ", + "attribute-filter": "属性フィルタ", + "method-filter": "方法フィルター", + "request-topic-expression": "トピック表現を要求する", + "response-timeout": "応答タイムアウト(ミリ秒)", + "topic-expression": "トピック表現", + "client-scope": "クライアントスコープ", + "add-device": "デバイスを追加", + "opc-server": "サーバー", + "opc-add-server": "サーバーを追加", + "opc-add-server-prompt": "サーバーを追加してください", + "opc-application-name": "アプリケーション名", + "opc-application-uri": "アプリケーションURI", + "opc-scan-period-in-seconds": "スキャン時間(秒)", + "opc-security": "セキュリティ", + "opc-identity": "身元", + "opc-keystore": "キーストア", + "opc-type": "タイプ", + "opc-keystore-type": "タイプ", + "opc-keystore-location": "ロケーション*", + "opc-keystore-password": "パスワード", + "opc-keystore-alias": "エイリアス", + "opc-keystore-key-password": "キーのパスワード", + "opc-device-node-pattern": "デバイスノードパターン", + "opc-device-name-pattern": "デバイス名パターン", + "modbus-server": "サーバー/スレーブ", + "modbus-add-server": "サーバー/スレーブを追加する", + "modbus-add-server-prompt": "サーバー/スレーブを追加してください", + "modbus-transport": "輸送", + "modbus-port-name": "シリアルポート名", + "modbus-encoding": "エンコーディング", + "modbus-parity": "パリティ", + "modbus-baudrate": "ボーレート", + "modbus-databits": "データビット", + "modbus-stopbits": "ストップビット", + "modbus-databits-range": "データビットは7〜8の範囲内にある必要があります。", + "modbus-stopbits-range": "ストップビットは1〜2の範囲内でなければなりません。", + "modbus-unit-id": "ユニットID", + "modbus-unit-id-range": "ユニットIDは1〜247の範囲で指定してください。", + "modbus-device-name": "装置名", + "modbus-poll-period": "投票期間(ミリ秒)", + "modbus-attributes-poll-period": "属性のポーリング期間(ミリ秒)", + "modbus-timeseries-poll-period": "時系列ポーリング期間(ミリ秒)", + "modbus-poll-period-range": "投票期間は正の値でなければなりません。", + "modbus-tag": "タグ", + "modbus-function": "関数", + "modbus-register-address": "登録アドレス", + "modbus-register-address-range": "レジスタのアドレスは0〜65535の範囲内である必要があります。", + "modbus-register-bit-index": "ビットインデックス", + "modbus-register-bit-index-range": "ビットインデックスは0〜15の範囲内である必要があります。", + "modbus-register-count": "レジスタ数", + "modbus-register-count-range": "レジスタ数は正の値でなければなりません。", + "modbus-byte-order": "バイト順", + + "sync": { + "status": "状態", + "sync": "同期", + "not-sync": "同期しない", + "last-sync-time": "前回の同期時間", + "not-available": "利用不可" + }, + + "export-extensions-configuration": "エクステンション設定のエクスポート", + "import-extensions-configuration": "エクステンション設定のインポート", + "import-extensions": "拡張機能のインポート", + "import-extension": "インポート拡張", + "export-extension": "輸出延長", + "file": "拡張機能ファイル", + "invalid-file-error": "無効な拡張ファイル" + }, + "fullscreen": { + "expand": "フルスクリーンに拡大", + "exit": "全画面表示を終了", + "toggle": "フルスクリーンモードを切り替える", + "fullscreen": "全画面表示" + }, + "function": { + "function": "関数" + }, + "grid": { + "delete-item-title": "このアイテムを削除してもよろしいですか?", + "delete-item-text": "注意してください。確認後、この項目と関連するすべてのデータは回復不能になります。", + "delete-items-title": "{ count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "{ count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "注意してください。確認後、選択したすべてのアイテムが削除され、関連するすべてのデータは回復不能になります。", + "add-item-text": "新しいアイテムを追加", + "no-items-text": "項目は見つかりませんでした", + "item-details": "商品詳細", + "delete-item": "アイテムを削除", + "delete-items": "アイテムを削除する", + "scroll-to-top": "トップにスクロールします" + }, + "help": { + "goto-help-page": "ヘルプページに行く" + }, + "home": { + "home": "ホーム", + "profile": "プロフィール", + "logout": "ログアウト", + "menu": "メニュー", + "avatar": "アバター", + "open-user-menu": "ユーザーメニューを開く" + }, + "import": { + "no-file": "ファイルが選択されていません", + "drop-file": "JSONファイルをドロップするか、アップロードするファイルをクリックして選択します。" + }, + "item": { + "selected": "選択された" + }, + "js-func": { + "no-return-error": "関数は値を返す必要があります!", + "return-type-mismatch": "'{{type}}'タイプ!", + "tidy": "きちんとした" + }, + "key-val": { + "key": "キー", + "value": "値", + "remove-entry": "エントリを削除", + "add-entry": "エントリを追加", + "no-data": "エントリなし" + }, + "layout": { + "layout": "レイアウト", + "manage": "レイアウトの管理", + "settings": "レイアウト設定", + "color": "色", + "main": "メイン", + "right": "右", + "select": "ターゲットレイアウトを選択" + }, + "legend": { + "position": "伝説の位置", + "show-max": "最大値を表示", + "show-min": "最小値を表示する", + "show-avg": "平均値を表示", + "show-total": "合計値を表示", + "settings": "凡例の設定", + "min": "分", + "max": "最大", + "avg": "平均", + "total": "合計" + }, + "login": { + "login": "ログイン", + "request-password-reset": "リクエストパスワードのリセット", + "reset-password": "パスワードを再設定する", + "create-password": "パスワードの作成", + "passwords-mismatch-error": "入力されたパスワードは同じでなければなりません!", + "password-again": "パスワードをもう一度", + "sign-in": "サインインしてください", + "username": "ユーザー名(電子メール)", + "remember-me": "私を覚えてますか", + "forgot-password": "パスワードをお忘れですか?", + "password-reset": "パスワードのリセット", + "new-password": "新しいパスワード", + "new-password-again": "新しいパスワードを再入力", + "password-link-sent-message": "パスワードリセットリンクが正常に送信されました!", + "email": "Eメール" + }, + "position": { + "top": "上", + "bottom": "ボトム", + "left": "左", + "right": "右" + }, + "profile": { + "profile": "プロフィール", + "change-password": "パスワードを変更する", + "current-password": "現在のパスワード" + }, + "relation": { + "relations": "関係", + "direction": "方向", + "search-direction": { + "FROM": "から", + "TO": "に" + }, + "direction-type": { + "FROM": "から", + "TO": "に" + }, + "from-relations": "アウトバウンド関係", + "to-relations": "インバウンド関係", + "selected-relations": "{ count, plural, 1 {1 relation} other {# relations} }選択された", + "type": "タイプ", + "to-entity-type": "エンティティタイプへ", + "to-entity-name": "エンティティ名に", + "from-entity-type": "エンティティタイプから", + "from-entity-name": "エンティティ名から", + "to-entity": "実体へ", + "from-entity": "エンティティから", + "delete": "関係を削除する", + "relation-type": "関係タイプ", + "relation-type-required": "関係タイプが必要です。", + "any-relation-type": "いかなるタイプ", + "add": "関係を追加する", + "edit": "関係を編集する", + "delete-to-relation-title": "'{{entityName}}'?", + "delete-to-relation-text": "'{{entityName}}'現在のエンティティとは無関係です。", + "delete-to-relations-title": "{ count, plural, 1 {1 relation} other {# relations} }?", + "delete-to-relations-text": "注意してください。確認後、選択されたリレーションはすべて削除され、対応するエンティティは現在のエンティティとは無関係になります。", + "delete-from-relation-title": "'{{entityName}}'?", + "delete-from-relation-text": "'{{entityName}}'.", + "delete-from-relations-title": "{ count, plural, 1 {1 relation} other {# relations} }?", + "delete-from-relations-text": "注意してください。確認後、選択されたリレーションはすべて削除され、現在のエンティティは対応するエンティティとは無関係になります。", + "remove-relation-filter": "関係フィルタを削除する", + "add-relation-filter": "関係フィルタを追加する", + "any-relation": "関係", + "relation-filters": "関係フィルタ", + "additional-info": "追加情報(JSON)", + "invalid-additional-info": "追加情報jsonを解析できません。" + }, + "rulechain": { + "rulechain": "ルールチェーン", + "rulechains": "ルールチェーン", + "root": "ルート", + "delete": "ルールチェーンの削除", + "name": "名", + "name-required": "名前は必須です。", + "description": "説明", + "add": "ルールチェーンを追加する", + "set-root": "ルールチェーンのルートを作る", + "set-root-rulechain-title": "'{{ruleChainName}}'ルート?", + "set-root-rulechain-text": "確認後、ルールチェーンはルートになり、すべての受信トランスポートメッセージを処理します。", + "delete-rulechain-title": "'{{ruleChainName}}'?", + "delete-rulechain-text": "確認後、ルールチェーンと関連するすべてのデータが回復不能になるので注意してください。", + "delete-rulechains-title": "{ count, plural, 1 {1 rule chain} other {# rule chains} }?", + "delete-rulechains-action-title": "{ count, plural, 1 {1 rule chain} other {# rule chains} }", + "delete-rulechains-text": "確認後、選択したすべてのルールチェーンが削除され、関連するすべてのデータが回復不能になるので注意してください。", + "add-rulechain-text": "新しいルールチェーンを追加する", + "no-rulechains-text": "ルールチェーンが見つかりません", + "rulechain-details": "ルールチェーンの詳細", + "details": "詳細", + "events": "イベント", + "system": "システム", + "import": "ルールチェーンのインポート", + "export": "ルールチェーンのエクスポート", + "export-failed-error": "{{error}}", + "create-new-rulechain": "新しいルールチェーンを作成する", + "rulechain-file": "ルールチェーンファイル", + "invalid-rulechain-file-error": "ルールチェーンをインポートできません:ルールチェーンのデータ構造が無効です。", + "copyId": "ルールチェーンIDのコピー", + "idCopiedMessage": "ルールチェーンIDがクリップボードにコピーされました", + "select-rulechain": "ルールチェーンの選択", + "no-rulechains-matching": "'{{entity}}'発見されました。", + "rulechain-required": "ルールチェーンが必要です", + "management": "ルール管理", + "debug-mode": "デバッグモード" + }, + "rulenode": { + "details": "詳細", + "events": "イベント", + "search": "検索ノード", + "open-node-library": "オープンノードライブラリ", + "add": "ルールノードを追加する", + "name": "名", + "name-required": "名前は必須です。", + "type": "タイプ", + "description": "説明", + "delete": "ルールノードを削除", + "select-all-objects": "すべてのノードと接続を選択する", + "deselect-all-objects": "すべてのノードと接続の選択を解除する", + "delete-selected-objects": "選択したノードと接続を削除する", + "delete-selected": "選択を削除します", + "select-all": "すべて選択", + "copy-selected": "選択したコピー", + "deselect-all": "すべての選択を解除", + "rulenode-details": "ルールノードの詳細", + "debug-mode": "デバッグモード", + "configuration": "構成", + "link": "リンク", + "link-details": "ルールノードのリンクの詳細", + "add-link": "リンクを追加", + "link-label": "リンクラベル", + "link-label-required": "リンクラベルが必要です。", + "custom-link-label": "カスタムリンクラベル", + "custom-link-label-required": "カスタムリンクラベルが必要です。", + "link-labels": "リンクラベル", + "link-labels-required": "リンクラベルが必要です。", + "no-link-labels-found": "リンクラベルが見つかりません", + "no-link-label-matching": "'{{label}}'見つかりません。", + "create-new-link-label": "新しいものを作成してください!", + "type-filter": "フィルタ", + "type-filter-details": "設定された条件で着信メッセージをフィルタリングする", + "type-enrichment": "豊かな", + "type-enrichment-details": "メッセージメタデータに追加情報を追加する", + "type-transformation": "変換", + "type-transformation-details": "メッセージペイロードとメタデータの変更", + "type-action": "アクション", + "type-action-details": "特別なアクションを実行する", + "type-external": "外部", + "type-external-details": "外部システムとの相互作用", + "type-rule-chain": "ルールチェーン", + "type-rule-chain-details": "受信したメッセージを指定したルールチェーンに転送する", + "type-input": "入力", + "type-input-details": "ルールチェーンの論理入力、次の関連ルールノードへの着信メッセージの転送", + "type-unknown": "未知の", + "type-unknown-details": "未解決のルールノード", + "directive-is-not-loaded": "'{{directiveName}}'利用できません。", + "ui-resources-load-error": "構成UIリソースをロードできませんでした。", + "invalid-target-rulechain": "ターゲットルールチェーンを解決できません!", + "test-script-function": "テストスクリプト機能", + "message": "メッセージ", + "message-type": "メッセージタイプ", + "select-message-type": "メッセージタイプを選択", + "message-type-required": "メッセージタイプは必須です", + "metadata": "メタデータ", + "metadata-required": "メタデータのエントリを空にすることはできません。", + "output": "出力", + "test": "テスト", + "help": "助けて" + }, + "tenant": { + "tenant": "テナント", + "tenants": "テナント", + "management": "テナント管理", + "add": "テナントを追加", + "admins": "管理者", + "manage-tenant-admins": "テナント管理者の管理", + "delete": "テナントの削除", + "add-tenant-text": "新しいテナントを追加する", + "no-tenants-text": "テナントは見つかりませんでした", + "tenant-details": "テナントの詳細", + "delete-tenant-title": "'{{tenantTitle}}'?", + "delete-tenant-text": "確認後、テナントと関連するすべてのデータが回復不能になるので注意してください。", + "delete-tenants-title": "{ count, plural, 1 {1 tenant} other {# tenants} }?", + "delete-tenants-action-title": "{ count, plural, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "注意してください。確認後、選択されたすべてのテナントが削除され、関連するすべてのデータは回復不能になります。", + "title": "タイトル", + "title-required": "タイトルは必須です。", + "description": "説明", + "details": "詳細", + "events": "イベント", + "copyId": "テナントIDをコピーする", + "idCopiedMessage": "テナントIDがクリップボードにコピーされました", + "select-tenant": "テナントを選択", + "no-tenants-matching": "'{{entity}}'発見されました。", + "tenant-required": "テナントが必要です" + }, + "timeinterval": { + "seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }", + "minutes-interval": "{ minutes, select, 1 {1 minute} other {# minutes} }", + "hours-interval": "{ hours, select, 1 {1 hour} other {# hours} }", + "days-interval": "{ days, select, 1 {1 day} other {# days} }", + "days": "日々", + "hours": "時間", + "minutes": "分", + "seconds": "秒", + "advanced": "上級" + }, + "timewindow": { + "days": "{ days, select, 1 { day } other {# days } }", + "hours": "{ hours, select, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes": "{ minutes, select, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds": "{ seconds, select, 0 { second } 1 {1 second } other {# seconds } }", + "realtime": "リアルタイム", + "history": "歴史", + "last-prefix": "最終", + "period": "{{ startTime }}{{ endTime }}", + "edit": "タイムウィンドウを編集", + "date-range": "期間", + "last": "最終", + "time-period": "期間" + }, + "user": { + "user": "ユーザー", + "users": "ユーザー", + "customer-users": "顧客ユーザー", + "tenant-admins": "テナント管理者", + "sys-admin": "システム管理者", + "tenant-admin": "テナント管理者", + "customer": "顧客", + "anonymous": "匿名", + "add": "ユーザーを追加する", + "delete": "ユーザーを削除", + "add-user-text": "新しいユーザーを追加", + "no-users-text": "ユーザが見つかりませんでした", + "user-details": "ユーザーの詳細", + "delete-user-title": "'{{userEmail}}'?", + "delete-user-text": "確認後、ユーザーと関連するすべてのデータが回復不能になるので注意してください。", + "delete-users-title": "{ count, plural, 1 {1 user} other {# users} }?", + "delete-users-action-title": "{ count, plural, 1 {1 user} other {# users} }", + "delete-users-text": "注意してください。確認後、選択したすべてのユーザーが削除され、関連するすべてのデータは回復不能になります。", + "activation-email-sent-message": "アクティベーション電子メールが正常に送信されました!", + "resend-activation": "アクティブ化を再送", + "email": "Eメール", + "email-required": "電子メールが必要です。", + "invalid-email-format": "メールフォーマットが無効です。", + "first-name": "ファーストネーム", + "last-name": "苗字", + "description": "説明", + "default-dashboard": "デフォルトのダッシュボード", + "always-fullscreen": "常に全画面表示", + "select-user": "ユーザーを選択", + "no-users-matching": "'{{entity}}'発見されました。", + "user-required": "ユーザーは必須です", + "activation-method": "起動方法", + "display-activation-link": "アクティブ化リンクを表示する", + "send-activation-mail": "アクティベーションメールを送信する", + "activation-link": "ユーザーアクティベーションリンク", + "activation-link-text": "activation link :", + "copy-activation-link": "アクティブ化リンクをコピーする", + "activation-link-copied-message": "ユーザーのアクティベーションリンクがクリップボードにコピーされました", + "details": "詳細" + }, + "value": { + "type": "値のタイプ", + "string": "文字列", + "string-value": "文字列値", + "integer": "整数", + "integer-value": "整数値", + "invalid-integer-value": "整数値が無効です", + "double": "ダブル", + "double-value": "二重価値", + "boolean": "ブール", + "boolean-value": "ブール値", + "false": "偽", + "true": "真", + "long": "長いです" + }, + "widget": { + "widget-library": "ウィジェットライブラリ", + "widget-bundle": "ウィジェットバンドル", + "select-widgets-bundle": "ウィジェットのバンドルを選択", + "management": "ウィジェット管理", + "editor": "ウィジェットエディタ", + "widget-type-not-found": "ウィジェットの設定を読み込む際に問題が発生しました。
おそらく関連付けられているウィジェットのタイプが削除されています。", + "widget-type-load-error": "次のエラーのためにウィジェットが読み込まれませんでした:", + "remove": "ウィジェットを削除", + "edit": "ウィジェットの編集", + "remove-widget-title": "'{{widgetTitle}}'?", + "remove-widget-text": "確認後、ウィジェットと関連するすべてのデータは回復不能になります。", + "timeseries": "時系列", + "search-data": "検索データ", + "no-data-found": "何もデータが見つかりませんでした", + "latest-values": "最新の値", + "rpc": "コントロールウィジェット", + "alarm": "アラームウィジェット", + "static": "静的ウィジェット", + "select-widget-type": "ウィジェットタイプを選択", + "missing-widget-title-error": "ウィジェットのタイトルを指定する必要があります!", + "widget-saved": "ウィジェットが保存されました", + "unable-to-save-widget-error": "ウィジェットを保存できません!ウィジェットにエラーがあります!", + "save": "ウィジェットを保存", + "saveAs": "ウィジェットを次のように保存する", + "save-widget-type-as": "ウィジェットタイプを次のように保存します", + "save-widget-type-as-text": "新しいウィジェットのタイトルを入力したり、ターゲットウィジェットのバンドルを選択してください", + "toggle-fullscreen": "フルスクリーン切り替え", + "run": "ウィジェットを実行する", + "title": "ウィジェットのタイトル", + "title-required": "ウィジェットのタイトルが必要です。", + "type": "ウィジェットタイプ", + "resources": "リソース", + "resource-url": "JavaScript / CSS URL", + "remove-resource": "リソースを削除する", + "add-resource": "リソースを追加", + "html": "HTML", + "tidy": "きちんとした", + "css": "CSS", + "settings-schema": "設定スキーマ", + "datakey-settings-schema": "データキー設定のスキーマ", + "javascript": "Javascript", + "remove-widget-type-title": "'{{widgetName}}'?", + "remove-widget-type-text": "確認後、ウィジェットのタイプと関連するすべてのデータは回復不能になります。", + "remove-widget-type": "ウィジェットタイプを削除", + "add-widget-type": "新しいウィジェットタイプを追加する", + "widget-type-load-failed-error": "ウィジェットタイプの読み込みに失敗しました!", + "widget-template-load-failed-error": "ウィジェットテンプレートを読み込めませんでした!", + "add": "ウィジェットを追加", + "undo": "ウィジェットの変更を元に戻す", + "export": "ウィジェットの書き出し" + }, + "widget-action": { + "header-button": "ウィジェットのヘッダーボタン", + "open-dashboard-state": "新しいダッシュボードの状態に移動する", + "update-dashboard-state": "現在のダッシュボードの状態を更新する", + "open-dashboard": "他のダッシュボードに移動する", + "custom": "カスタムアクション", + "target-dashboard-state": "ターゲットダッシュボードの状態", + "target-dashboard-state-required": "ターゲットダッシュボードの状態が必要です", + "set-entity-from-widget": "エンティティをウィジェットから設定する", + "target-dashboard": "ターゲットダッシュボード", + "open-right-layout": "右ダッシュボードレイアウトを開く(モバイルビュー)" + }, + "widgets-bundle": { + "current": "現在のバンドル", + "widgets-bundles": "ウィジェットバンドル", + "add": "ウィジェットのバンドルを追加", + "delete": "ウィジェットのバンドルを削除する", + "title": "タイトル", + "title-required": "タイトルは必須です。", + "add-widgets-bundle-text": "新しいウィジェットのバンドルを追加する", + "no-widgets-bundles-text": "ウィジェットバンドルが見つかりません", + "empty": "ウィジェットのバンドルが空です", + "details": "詳細", + "widgets-bundle-details": "ウィジェットのバンドルの詳細", + "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "確認後、ウィジェットはバンドルされ、関連するすべてのデータは回復不能になります。", + "delete-widgets-bundles-title": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} }?", + "delete-widgets-bundles-action-title": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text": "確認後、選択したすべてのウィジェットバンドルは削除され、関連するすべてのデータは回復不能になります。", + "no-widgets-bundles-matching": "'{{widgetsBundle}}'発見されました。", + "widgets-bundle-required": "ウィジェットバンドルが必要です。", + "system": "システム", + "import": "インポートウィジェットバンドル", + "export": "ウィジェットのエクスポートバンドル", + "export-failed-error": "{{error}}", + "create-new-widgets-bundle": "新しいウィジェットバンドルを作成する", + "widgets-bundle-file": "ウィジェットのバンドルファイル", + "invalid-widgets-bundle-file-error": "ウィジェットをインポートできません。bundle:データ構造が無効です。" + }, + "widget-config": { + "data": "データ", + "settings": "設定", + "advanced": "上級", + "title": "タイトル", + "general-settings": "一般設定", + "display-title": "タイトルを表示", + "drop-shadow": "影を落とす", + "enable-fullscreen": "フルスクリーンを有効にする", + "background-color": "背景色", + "text-color": "テキストの色", + "padding": "パディング", + "margin": "マージン", + "widget-style": "ウィジェットスタイル", + "title-style": "タイトルスタイル", + "mobile-mode-settings": "モバイルモードの設定", + "order": "注文", + "height": "高さ", + "units": "値の隣に表示する特別なシンボル", + "decimals": "浮動小数点の後の桁数", + "timewindow": "タイムウィンドウ", + "use-dashboard-timewindow": "ダッシュボードのタイムウィンドウを使用する", + "display-legend": "伝説を表示", + "datasources": "データソース", + "maximum-datasources": "{ count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", + "datasource-type": "タイプ", + "datasource-parameters": "パラメーター", + "remove-datasource": "データソースを削除", + "add-datasource": "データソースを追加", + "target-device": "ターゲットデバイス", + "alarm-source": "アラームソース", + "actions": "行動", + "action": "アクション", + "add-action": "アクションを追加", + "search-actions": "検索アクション", + "action-source": "アクションソース", + "action-source-required": "アクションソースが必要です。", + "action-name": "名", + "action-name-required": "アクション名は必須です。", + "action-name-not-unique": "同じ名前の別のアクションがすでに存在します。
アクション名は、同じアクションソース内で一意である必要があります。", + "action-icon": "アイコン", + "action-type": "タイプ", + "action-type-required": "アクションタイプが必要です。", + "edit-action": "アクションの編集", + "delete-action": "アクションの削除", + "delete-action-title": "ウィジェットアクションを削除する", + "delete-action-text": "'{{actionName}}'?" + }, + "widget-type": { + "import": "インポートウィジェットタイプ", + "export": "ウィジェットのタイプをエクスポートする", + "export-failed-error": "{{error}}", + "create-new-widget-type": "新しいウィジェットタイプを作成する", + "widget-type-file": "ウィジェットタイプファイル", + "invalid-widget-type-file-error": "ウィジェットタイプをインポートできません:ウィジェットタイプのデータ構造が無効です。" + }, + "icon": { + "icon": "アイコン", + "select-icon": "選択アイコン", + "material-icons": "マテリアルアイコン", + "show-all": "すべてのアイコンを表示する" + }, + "custom": { + "widget-action": { + "action-cell-button": "アクションセルボタン", + "row-click": "行のクリック", + "marker-click": "マーカークリック", + "tooltip-tag-action": "ツールチップのタグアクション" + } + }, + "language": { + "language": "言語", + "locales": { + "en_US": "英語", + "ko_KR": "韓国語", + "it_IT": "イタリアの", + "zh_CN": "中国語", + "ru_RU": "ロシア", + "es_ES": "スペイン語", + "ja_JA": "日本語" + } + } +} From a2d27348dcf3fcbc3f0174fe9d8230669bd5c1d6 Mon Sep 17 00:00:00 2001 From: Rileyjs Date: Wed, 15 Aug 2018 16:22:45 -0500 Subject: [PATCH 13/30] Update locale.constant-es_ES.json --- ui/src/app/locale/locale.constant-es_ES.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json index 9e5951f0c8..5747894e97 100644 --- a/ui/src/app/locale/locale.constant-es_ES.json +++ b/ui/src/app/locale/locale.constant-es_ES.json @@ -1311,7 +1311,8 @@ "zh_CN": "Chino", "ru_RU": "Ruso", "es_ES": "Español", - "it_IT": "Italiano" + "it_IT": "Italiano", + "ja_JA": "Japonés" } } -} \ No newline at end of file +} From fdee0968107f0eb4c4b271f511db05a5e0032e96 Mon Sep 17 00:00:00 2001 From: Rileyjs Date: Wed, 15 Aug 2018 16:23:23 -0500 Subject: [PATCH 14/30] Update locale.constant-it_IT.json --- ui/src/app/locale/locale.constant-it_IT.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json index b304cac78c..7212b30cdd 100644 --- a/ui/src/app/locale/locale.constant-it_IT.json +++ b/ui/src/app/locale/locale.constant-it_IT.json @@ -1439,7 +1439,8 @@ "en_US": "Inglese", "it_IT": "Italiano", "ru_RU": "Russo", - "es_ES": "Spagnolo" + "es_ES": "Spagnolo", + "ja_JA": "Giapponese" } } } From 7be79d8f95bb09b3379ab7428c37a823b5739699 Mon Sep 17 00:00:00 2001 From: Rileyjs Date: Wed, 15 Aug 2018 16:24:03 -0500 Subject: [PATCH 15/30] Update locale.constant-ko_KR.json --- ui/src/app/locale/locale.constant-ko_KR.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/src/app/locale/locale.constant-ko_KR.json b/ui/src/app/locale/locale.constant-ko_KR.json index a0d9e0dd8e..a4c8bcb755 100644 --- a/ui/src/app/locale/locale.constant-ko_KR.json +++ b/ui/src/app/locale/locale.constant-ko_KR.json @@ -1333,7 +1333,8 @@ "zh_CN": "중국어", "ru_RU": "러시아어", "es_ES": "스페인어", - "it_IT": "이탈리아 사람" + "it_IT": "이탈리아 사람", + "ja_JA": "일본어" } } -} \ No newline at end of file +} From 421924cd7438203b2ba5cd3d2b5033a3a4d80606 Mon Sep 17 00:00:00 2001 From: Rileyjs Date: Wed, 15 Aug 2018 16:24:30 -0500 Subject: [PATCH 16/30] Update locale.constant-ru_RU.json --- ui/src/app/locale/locale.constant-ru_RU.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json index 2e384e95dc..d017c631ff 100644 --- a/ui/src/app/locale/locale.constant-ru_RU.json +++ b/ui/src/app/locale/locale.constant-ru_RU.json @@ -1358,8 +1358,9 @@ "ko_KR": "Корейский", "es_ES": "Испанский", "it_IT": "Итальянский", - "ru_RU": "Русский" + "ru_RU": "Русский", + "ja_JA": "Японский" } } -} \ No newline at end of file +} From 3b948fcb15980695473bb6a071a1755d3a72ebcd Mon Sep 17 00:00:00 2001 From: Rileyjs Date: Wed, 15 Aug 2018 16:24:59 -0500 Subject: [PATCH 17/30] Update locale.constant-zh_CN.json --- ui/src/app/locale/locale.constant-zh_CN.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/app/locale/locale.constant-zh_CN.json b/ui/src/app/locale/locale.constant-zh_CN.json index 037230d8a6..d9cb7b1c70 100644 --- a/ui/src/app/locale/locale.constant-zh_CN.json +++ b/ui/src/app/locale/locale.constant-zh_CN.json @@ -1442,7 +1442,8 @@ "zh_CN": "汉语", "ru_RU": "俄语", "es_ES": "西班牙语", - "it_IT": "意大利" + "it_IT": "意大利", + "ja_JA": "日本" } } } From 88307d6ef1bfd599fb46f3f68b46662856b95a7a Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Aug 2018 12:03:12 +0300 Subject: [PATCH 18/30] address issue Add partition granularity to time series data #1006 https://github.com/thingsboard/thingsboard/issues/1006 --- .../CassandraBaseTimeseriesDao.java | 37 +++++++++++++------ .../dao/timeseries/TsPartitionDate.java | 7 +++- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 7aa317cbf9..591023443c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -54,10 +54,7 @@ import javax.annotation.PreDestroy; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; @@ -76,6 +73,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem public static final String SELECT_PREFIX = "SELECT "; public static final String EQUALS_PARAM = " = ? "; + + private static List FIXED_PARTITION = Arrays.asList(new Long[]{0l}); + @Autowired private Environment environment; @@ -163,14 +163,28 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } } + public boolean isFixedPartitioning(){ + return tsFormat.getTruncateUnit().equals(TsPartitionDate.FIXED_PARTITION); + } + + private ListenableFuture> getPartitionsFuture(TsKvQuery query, EntityId entityId, long minPartition, long maxPartition){ + + if(isFixedPartitioning()){ //no need to fetch partitions from DB + return Futures.immediateFuture(FIXED_PARTITION); + } + ResultSetFuture partitionsFuture = fetchPartitions(entityId, query.getKey(), minPartition, maxPartition); + + return Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); + + } + private ListenableFuture> findAllAsyncWithLimit(EntityId entityId, TsKvQuery query) { + long minPartition = toPartitionTs(query.getStartTs()); long maxPartition = toPartitionTs(query.getEndTs()); - ResultSetFuture partitionsFuture = fetchPartitions(entityId, query.getKey(), minPartition, maxPartition); - + final ListenableFuture> partitionsListFuture = getPartitionsFuture(query,entityId,minPartition,maxPartition); final SimpleListenableFuture> resultFuture = new SimpleListenableFuture<>(); - final ListenableFuture> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); Futures.addCallback(partitionsListFuture, new FutureCallback>() { @Override @@ -181,7 +195,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t); + log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), toPartitionTs(query.getStartTs()), toPartitionTs(query.getEndTs()), t); } }, readResultsProcessingExecutor); @@ -229,10 +243,8 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem final long endTs = query.getEndTs(); final long ts = startTs + (endTs - startTs) / 2; - ResultSetFuture partitionsFuture = fetchPartitions(entityId, key, minPartition, maxPartition); - - ListenableFuture> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); + ListenableFuture> partitionsListFuture = getPartitionsFuture(query,entityId, minPartition, maxPartition); ListenableFuture> aggregationChunks = Futures.transformAsync(partitionsListFuture, getFetchChunksAsyncFunction(entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor); @@ -308,6 +320,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem @Override public ListenableFuture savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) { + if(isFixedPartitioning()){ + return getFuture(null, rs -> null); + } ttl = computeTtl(ttl); long partition = toPartitionTs(tsKvEntryTs); log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java index 4148004dad..d9dfc7dc93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java @@ -16,22 +16,25 @@ package org.thingsboard.server.dao.timeseries; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalUnit; import java.util.Optional; public enum TsPartitionDate { - MINUTES("yyyy-MM-dd-HH-mm", ChronoUnit.MINUTES), HOURS("yyyy-MM-dd-HH", ChronoUnit.HOURS), DAYS("yyyy-MM-dd", ChronoUnit.DAYS), MONTHS("yyyy-MM", ChronoUnit.MONTHS), YEARS("yyyy", ChronoUnit.YEARS); + MINUTES("yyyy-MM-dd-HH-mm", ChronoUnit.MINUTES), HOURS("yyyy-MM-dd-HH", ChronoUnit.HOURS), DAYS("yyyy-MM-dd", ChronoUnit.DAYS), MONTHS("yyyy-MM", ChronoUnit.MONTHS), YEARS("yyyy", ChronoUnit.YEARS),INDEFINITE("",ChronoUnit.FOREVER); private final String pattern; private final transient TemporalUnit truncateUnit; + public final static LocalDateTime FIXED_PARTITION = LocalDateTime.ofEpochSecond(0,0, ZoneOffset.UTC); TsPartitionDate(String pattern, TemporalUnit truncateUnit) { this.pattern = pattern; this.truncateUnit = truncateUnit; } + public String getPattern() { return pattern; } @@ -46,6 +49,8 @@ public enum TsPartitionDate { return time.truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1); case YEARS: return time.truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); + case INDEFINITE: + return FIXED_PARTITION; default: return time.truncatedTo(truncateUnit); } From 2b212d777c67d1eb7b6a30011ae267cf29c47554 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Aug 2018 17:24:59 +0300 Subject: [PATCH 19/30] formatting ,added INDEFINITE to possible partitioning in yml ,savePartition returns immediateFuture(null) if FixedPartition mode enabled --- .../src/main/resources/thingsboard.yml | 2 +- .../dao/timeseries/BaseTimeseriesService.java | 8 +++--- .../CassandraBaseTimeseriesDao.java | 26 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f510ec48a5..4d7dc7f41e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -201,7 +201,7 @@ cassandra: read_consistency_level: "${CASSANDRA_READ_CONSISTENCY_LEVEL:ONE}" write_consistency_level: "${CASSANDRA_WRITE_CONSISTENCY_LEVEL:ONE}" default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}" - # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS + # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS,INDEFINITE ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" ts_key_value_ttl: "${TS_KV_TTL:0}" buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}" diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index c981378939..6c00735c4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2018 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 - * + *

+ * 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. diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 591023443c..f25a8b4e63 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2018 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 - * + *

+ * 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. @@ -163,18 +163,18 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } } - public boolean isFixedPartitioning(){ - return tsFormat.getTruncateUnit().equals(TsPartitionDate.FIXED_PARTITION); + public boolean isFixedPartitioning() { + return tsFormat.getTruncateUnit().equals(TsPartitionDate.FIXED_PARTITION); } - private ListenableFuture> getPartitionsFuture(TsKvQuery query, EntityId entityId, long minPartition, long maxPartition){ + private ListenableFuture> getPartitionsFuture(TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { - if(isFixedPartitioning()){ //no need to fetch partitions from DB + if (isFixedPartitioning()) { //no need to fetch partitions from DB return Futures.immediateFuture(FIXED_PARTITION); } ResultSetFuture partitionsFuture = fetchPartitions(entityId, query.getKey(), minPartition, maxPartition); - return Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); + return Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); } @@ -183,7 +183,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem long minPartition = toPartitionTs(query.getStartTs()); long maxPartition = toPartitionTs(query.getEndTs()); - final ListenableFuture> partitionsListFuture = getPartitionsFuture(query,entityId,minPartition,maxPartition); + final ListenableFuture> partitionsListFuture = getPartitionsFuture(query, entityId, minPartition, maxPartition); final SimpleListenableFuture> resultFuture = new SimpleListenableFuture<>(); Futures.addCallback(partitionsListFuture, new FutureCallback>() { @@ -244,7 +244,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem final long ts = startTs + (endTs - startTs) / 2; - ListenableFuture> partitionsListFuture = getPartitionsFuture(query,entityId, minPartition, maxPartition); + ListenableFuture> partitionsListFuture = getPartitionsFuture(query, entityId, minPartition, maxPartition); ListenableFuture> aggregationChunks = Futures.transformAsync(partitionsListFuture, getFetchChunksAsyncFunction(entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor); @@ -320,8 +320,8 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem @Override public ListenableFuture savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) { - if(isFixedPartitioning()){ - return getFuture(null, rs -> null); + if (isFixedPartitioning()) { + return Futures.immediateFuture(null); } ttl = computeTtl(ttl); long partition = toPartitionTs(tsKvEntryTs); From 45cf12c4d9a40c34807f200a0a98a9f30189fd3e Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Aug 2018 18:04:29 +0300 Subject: [PATCH 20/30] fixed License format --- .../server/dao/timeseries/BaseTimeseriesService.java | 8 ++++---- .../server/dao/timeseries/CassandraBaseTimeseriesDao.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 6c00735c4d..c981378939 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2018 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 - *

+ * + * 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. diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index f25a8b4e63..7d5e7d4665 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2018 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 - *

+ * + * 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. From 16fa955a4c737a8c57b855457b577c0f790966ab Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Aug 2018 22:22:32 +0300 Subject: [PATCH 21/30] fixed wildcard imports --- .../server/dao/timeseries/CassandraBaseTimeseriesDao.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 7d5e7d4665..8aeb32aeca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -54,7 +54,11 @@ import javax.annotation.PreDestroy; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.*; +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; +import java.util.Optional; +import java.util.Collections; import java.util.stream.Collectors; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; @@ -164,7 +168,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } public boolean isFixedPartitioning() { - return tsFormat.getTruncateUnit().equals(TsPartitionDate.FIXED_PARTITION); + return tsFormat.getTruncateUnit().equals(TsPartitionDate.EPOCH_START); } private ListenableFuture> getPartitionsFuture(TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { From 3363a33ccd2528445c3cbd711f1df58b5553f981 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Aug 2018 22:24:37 +0300 Subject: [PATCH 22/30] changed to EPOCH_START --- .../thingsboard/server/dao/timeseries/TsPartitionDate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java index d9dfc7dc93..93283fdda8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TsPartitionDate.java @@ -27,7 +27,7 @@ public enum TsPartitionDate { private final String pattern; private final transient TemporalUnit truncateUnit; - public final static LocalDateTime FIXED_PARTITION = LocalDateTime.ofEpochSecond(0,0, ZoneOffset.UTC); + public final static LocalDateTime EPOCH_START = LocalDateTime.ofEpochSecond(0,0, ZoneOffset.UTC); TsPartitionDate(String pattern, TemporalUnit truncateUnit) { this.pattern = pattern; @@ -50,7 +50,7 @@ public enum TsPartitionDate { case YEARS: return time.truncatedTo(ChronoUnit.DAYS).withDayOfYear(1); case INDEFINITE: - return FIXED_PARTITION; + return EPOCH_START; default: return time.truncatedTo(truncateUnit); } From 7f01a9800364c469dad63739b881b2921f5e8dfa Mon Sep 17 00:00:00 2001 From: David Date: Thu, 16 Aug 2018 22:46:28 +0300 Subject: [PATCH 23/30] extra space styling --- .../server/dao/timeseries/CassandraBaseTimeseriesDao.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 8aeb32aeca..c025e64a31 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -78,7 +78,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem public static final String EQUALS_PARAM = " = ? "; - private static List FIXED_PARTITION = Arrays.asList(new Long[]{0l}); + private static List FIXED_PARTITION = Arrays.asList(new Long[]{0L}); @Autowired private Environment environment; @@ -172,14 +172,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem } private ListenableFuture> getPartitionsFuture(TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { - if (isFixedPartitioning()) { //no need to fetch partitions from DB return Futures.immediateFuture(FIXED_PARTITION); } ResultSetFuture partitionsFuture = fetchPartitions(entityId, query.getKey(), minPartition, maxPartition); - return Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); - } private ListenableFuture> findAllAsyncWithLimit(EntityId entityId, TsKvQuery query) { From 796294f33faf87ac31f14b320210600cabb89bc3 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 17 Aug 2018 15:59:14 +0300 Subject: [PATCH 24/30] Login As User feature. --- .../server/controller/UserController.java | 57 +++++++++++++ .../src/main/resources/thingsboard.yml | 16 ++-- ui/src/app/api/user.service.js | 84 +++++++++++++++---- ui/src/app/locale/locale.constant-en_US.json | 4 +- ui/src/app/user/user-fieldset.tpl.html | 3 + ui/src/app/user/user.controller.js | 16 ++++ ui/src/app/user/user.directive.js | 9 +- ui/src/app/user/users.tpl.html | 1 + 8 files changed, 165 insertions(+), 25 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 5f6c1cef21..a050fa3260 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -15,7 +15,12 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -39,7 +44,11 @@ import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.security.model.token.JwtToken; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.servlet.http.HttpServletRequest; @@ -50,9 +59,21 @@ public class UserController extends BaseController { public static final String USER_ID = "userId"; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s"; + + @Value("${security.user_token_access_enabled}") + @Getter + private boolean userTokenAccessEnabled; + @Autowired private MailService mailService; + @Autowired + private JwtTokenFactory tokenFactory; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) @ResponseBody @@ -71,6 +92,42 @@ public class UserController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET) + @ResponseBody + public boolean isUserTokenAccessEnabled() { + return userTokenAccessEnabled; + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET) + @ResponseBody + public JsonNode getUserToken(@PathVariable(USER_ID) String strUserId) throws ThingsboardException { + checkParameter(USER_ID, strUserId); + try { + UserId userId = new UserId(toUUID(strUserId)); + SecurityUser authUser = getCurrentUser(); + User user = userService.findUserById(userId); + if (!userTokenAccessEnabled || (authUser.getAuthority() == Authority.SYS_ADMIN && user.getAuthority() != Authority.TENANT_ADMIN) + || (authUser.getAuthority() == Authority.TENANT_ADMIN && !authUser.getTenantId().equals(user.getTenantId()))) { + throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION, + ThingsboardErrorCode.PERMISSION_DENIED); + } + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + UserCredentials credentials = userService.findUserCredentialsByUserId(userId); + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); + JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode tokenObject = objectMapper.createObjectNode(); + tokenObject.put("token", accessToken.getToken()); + tokenObject.put("refreshToken", refreshToken.getToken()); + return tokenObject; + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user", method = RequestMethod.POST) @ResponseBody diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f510ec48a5..3e62453e4f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -66,12 +66,16 @@ plugins: # Comma seperated package list used during classpath scanning for plugins scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}" -# JWT Token parameters -security.jwt: - tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins) - refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour) - tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" - tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" +# Security parameters +security: + # JWT Token parameters + jwt: + tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins) + refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour) + tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}" + tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" + # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator + user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" # Device communication protocol parameters http: diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index 48c811bb8d..fa4d63c8e2 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -27,6 +27,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi currentUserDetails = null, lastPublicDashboardId = null, allowedDashboardIds = [], + userTokenAccessEnabled = false, userLoaded = false; var refreshTokenQueue = []; @@ -59,7 +60,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi forceDefaultPlace: forceDefaultPlace, updateLastPublicDashboardId: updateLastPublicDashboardId, logout: logout, - reloadUser: reloadUser + reloadUser: reloadUser, + isUserTokenAccessEnabled: isUserTokenAccessEnabled, + loginAsUser: loginAsUser } reloadUser(); @@ -105,6 +108,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi currentUser = null; currentUserDetails = null; lastPublicDashboardId = null; + userTokenAccessEnabled = false; allowedDashboardIds = []; if (!jwtToken) { clearTokenData(); @@ -299,24 +303,36 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi } else if (currentUser) { currentUser.authority = "ANONYMOUS"; } + var sysParamsPromise = loadSystemParams(); if (currentUser.isPublic) { $rootScope.forceFullscreen = true; - fetchAllowedDashboardIds(); + sysParamsPromise.then( + () => { fetchAllowedDashboardIds(); }, + () => { deferred.reject(); } + ); } else if (currentUser.userId) { getUser(currentUser.userId, true).then( function success(user) { - currentUserDetails = user; - updateUserLang(); - $rootScope.forceFullscreen = false; - if (userForceFullscreen()) { - $rootScope.forceFullscreen = true; - } - if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' || - currentUser.authority === 'CUSTOMER_USER')) { - fetchAllowedDashboardIds(); - } else { - deferred.resolve(); - } + sysParamsPromise.then( + () => { + currentUserDetails = user; + updateUserLang(); + $rootScope.forceFullscreen = false; + if (userForceFullscreen()) { + $rootScope.forceFullscreen = true; + } + if ($rootScope.forceFullscreen && (currentUser.authority === 'TENANT_ADMIN' || + currentUser.authority === 'CUSTOMER_USER')) { + fetchAllowedDashboardIds(); + } else { + deferred.resolve(); + } + }, + () => { + deferred.reject(); + logout(); + } + ); }, function fail() { deferred.reject(); @@ -353,6 +369,30 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi return deferred.promise; } + function loadIsUserTokenAccessEnabled() { + var deferred = $q.defer(); + if (currentUser.authority === 'SYS_ADMIN' || currentUser.authority === 'TENANT_ADMIN') { + var url = '/api/user/tokenAccessEnabled'; + $http.get(url).then(function success(response) { + userTokenAccessEnabled = response.data; + deferred.resolve(response.data); + }, function fail() { + userTokenAccessEnabled = false; + deferred.reject(); + }); + } else { + userTokenAccessEnabled = false; + deferred.resolve(false); + } + return deferred.promise; + } + + function loadSystemParams() { + var promises = []; + promises.push(loadIsUserTokenAccessEnabled()); + return $q.all(promises); + } + function notifyUserLoaded() { if (!userLoaded) { userLoaded = true; @@ -520,7 +560,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi } ); } - $state.go(place, params); + $state.go(place, params, {reload: true}); } else { $state.go('login', params); } @@ -549,4 +589,18 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi } } + function isUserTokenAccessEnabled() { + return userTokenAccessEnabled; + } + + function loginAsUser(userId) { + var url = '/api/user/' + userId + '/token'; + $http.get(url).then(function success(response) { + var token = response.data.token; + var refreshToken = response.data.refreshToken; + setUserFromJwtToken(token, refreshToken, true); + }, function fail() { + }); + } + } diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 22aad1a90d..a98733c671 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -1270,7 +1270,9 @@ "activation-link-text": "In order to activate user use the following activation link :", "copy-activation-link": "Copy activation link", "activation-link-copied-message": "User activation link has been copied to clipboard", - "details": "Details" + "details": "Details", + "login-as-tenant-admin": "Login as Tenant Admin", + "login-as-customer-user": "Login as Customer User" }, "value": { "type": "Value type", diff --git a/ui/src/app/user/user-fieldset.tpl.html b/ui/src/app/user/user-fieldset.tpl.html index 15b32ecc80..d3b5111f15 100644 --- a/ui/src/app/user/user-fieldset.tpl.html +++ b/ui/src/app/user/user-fieldset.tpl.html @@ -21,6 +21,9 @@ {{ 'user.resend-activation' | translate }} +{{ + (isTenantAdmin() ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }} + {{ 'user.delete' | translate }} diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js index 4fd6b25828..8d7f214b9a 100644 --- a/ui/src/app/user/user.controller.js +++ b/ui/src/app/user/user.controller.js @@ -30,6 +30,17 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d var usersType = $state.$current.data.usersType; var userActionsList = [ + { + onAction: function ($event, item) { + loginAsUser(item); + }, + name: function() { return $translate.instant('login.login') }, + details: function() { return $translate.instant(usersType === 'tenant' ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') }, + icon: "login", + isEnabled: function() { + return userService.isUserTokenAccessEnabled(); + } + }, { onAction: function ($event, item) { vm.grid.deleteItem($event, item); @@ -78,6 +89,7 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d vm.displayActivationLink = displayActivationLink; vm.resendActivation = resendActivation; + vm.loginAsUser = loginAsUser; initController(); @@ -184,4 +196,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d toast.showSuccess($translate.instant('user.activation-email-sent-message')); }); } + + function loginAsUser(user) { + userService.loginAsUser(user.id.id); + } } diff --git a/ui/src/app/user/user.directive.js b/ui/src/app/user/user.directive.js index 1c01c8f4b4..b32d028e89 100644 --- a/ui/src/app/user/user.directive.js +++ b/ui/src/app/user/user.directive.js @@ -22,18 +22,20 @@ import userFieldsetTemplate from './user-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function UserDirective($compile, $templateCache/*, dashboardService*/) { +export default function UserDirective($compile, $templateCache, userService) { var linker = function (scope, element) { var template = $templateCache.get(userFieldsetTemplate); element.html(template); scope.isTenantAdmin = function() { return scope.user && scope.user.authority === 'TENANT_ADMIN'; - } + }; scope.isCustomerUser = function() { return scope.user && scope.user.authority === 'CUSTOMER_USER'; - } + }; + + scope.loginAsUserEnabled = userService.isUserTokenAccessEnabled(); $compile(element.contents())(scope); } @@ -46,6 +48,7 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi theForm: '=', onDisplayActivationLink: '&', onResendActivation: '&', + onLoginAsUser: '&', onDeleteUser: '&' } }; diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html index b73b5a60b9..a311d98a67 100644 --- a/ui/src/app/user/users.tpl.html +++ b/ui/src/app/user/users.tpl.html @@ -27,6 +27,7 @@ the-form="vm.grid.detailsForm" on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)" on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)" + on-login-as-user="vm.loginAsUser(vm.grid.detailsConfig.currentItem)" on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> From 44a1fa2911b1f51830c97ea98e4c221a67c4e2ad Mon Sep 17 00:00:00 2001 From: Chi Joung So Date: Thu, 23 Aug 2018 10:02:28 +0200 Subject: [PATCH 25/30] ReconnectDelay and MaxBytesInMessage configurable --- .../thingsboard/mqtt/MqttClientConfig.java | 36 +++++++++++++++++++ .../org/thingsboard/mqtt/MqttClientImpl.java | 4 +-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java index a59d83b14b..bcb8bd15ba 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java @@ -40,6 +40,8 @@ public final class MqttClientConfig { private Class channelClass = NioSocketChannel.class; private boolean reconnect = true; + private long reconnectDelay = 1L; + private int maxBytesInMessage = 8092; public MqttClientConfig() { this(null); @@ -146,4 +148,38 @@ public final class MqttClientConfig { public void setReconnect(boolean reconnect) { this.reconnect = reconnect; } + + public long getReconnectDelay() { + return reconnectDelay; + } + + /** + * Sets the reconnect delay in seconds. Defaults to 1 second. + * @param reconnectDelay + * @throws IllegalArgumentException if reconnectDelay is smaller than 1. + */ + public void setReconnectDelay(long reconnectDelay) { + if (reconnectDelay <= 0) { + throw new IllegalArgumentException("reconnectDelay must be > 0"); + } + this.reconnectDelay = reconnectDelay; + } + + public int getMaxBytesInMessage() { + return maxBytesInMessage; + } + + /** + * Sets the maximum number of bytes in the message for the {@link io.netty.handler.codec.mqtt.MqttDecoder}. + * Default value is 8092 as specified by Netty. The absolute maximum size is 256MB as set by the MQTT spec. + * + * @param maxBytesInMessage + * @throws IllegalArgumentException if maxBytesInMessage is smaller than 1 or greater than 256_000_000. + */ + public void setMaxBytesInMessage(int maxBytesInMessage) { + if (maxBytesInMessage <= 0 || maxBytesInMessage > 256_000_000) { + throw new IllegalArgumentException("maxBytesInMessage must be > 0 or < 256_000_000"); + } + this.maxBytesInMessage = maxBytesInMessage; + } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index a5df846899..b9460b3555 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -155,7 +155,7 @@ final class MqttClientImpl implements MqttClient { if (reconnect) { this.reconnect = true; } - eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), 1L, TimeUnit.SECONDS); + eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), clientConfig.getReconnectDelay(), TimeUnit.SECONDS); } } @@ -512,7 +512,7 @@ final class MqttClientImpl implements MqttClient { ch.pipeline().addLast(sslContext.newHandler(ch.alloc(), host, port)); } - ch.pipeline().addLast("mqttDecoder", new MqttDecoder()); + ch.pipeline().addLast("mqttDecoder", new MqttDecoder(clientConfig.getMaxBytesInMessage())); ch.pipeline().addLast("mqttEncoder", MqttEncoder.INSTANCE); ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds(), MqttClientImpl.this.clientConfig.getTimeoutSeconds(), 0)); ch.pipeline().addLast("mqttPingHandler", new MqttPingHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds())); From 43d7c16f66eb3b64d9dc9cc9ed104f7b275007c4 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 23 Aug 2018 14:38:41 +0300 Subject: [PATCH 26/30] Cache async relation queries. --- .../dao/relation/BaseRelationService.java | 58 ++++++++++++++++--- .../dao/service/BaseRelationServiceTest.java | 13 +++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 9b89fe29f3..d88a1c4c42 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -16,9 +16,7 @@ package org.thingsboard.server.dao.relation; import com.google.common.base.Function; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; @@ -272,8 +270,10 @@ public class BaseRelationService implements RelationService { @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}") @Override public List findByFrom(EntityId from, RelationTypeGroup typeGroup) { + validate(from); + validateTypeGroup(typeGroup); try { - return findByFromAsync(from, typeGroup).get(); + return relationDao.findAllByFrom(from, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -284,7 +284,28 @@ public class BaseRelationService implements RelationService { log.trace("Executing findByFrom [{}][{}]", from, typeGroup); validate(from); validateTypeGroup(typeGroup); - return relationDao.findAllByFrom(from, typeGroup); + + List fromAndTypeGroup = new ArrayList<>(); + fromAndTypeGroup.add(from); + fromAndTypeGroup.add(typeGroup); + + Cache cache = cacheManager.getCache(RELATIONS_CACHE); + List fromCache = cache.get(fromAndTypeGroup, List.class); + if (fromCache != null) { + return Futures.immediateFuture(fromCache); + } else { + ListenableFuture> relationsFuture = relationDao.findAllByFrom(from, typeGroup); + Futures.addCallback(relationsFuture, + new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + cache.putIfAbsent(fromAndTypeGroup, result); + } + @Override + public void onFailure(Throwable t) {} + }); + return relationsFuture; + } } @Override @@ -327,8 +348,10 @@ public class BaseRelationService implements RelationService { @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}") @Override public List findByTo(EntityId to, RelationTypeGroup typeGroup) { + validate(to); + validateTypeGroup(typeGroup); try { - return findByToAsync(to, typeGroup).get(); + return relationDao.findAllByTo(to, typeGroup).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -339,7 +362,28 @@ public class BaseRelationService implements RelationService { log.trace("Executing findByTo [{}][{}]", to, typeGroup); validate(to); validateTypeGroup(typeGroup); - return relationDao.findAllByTo(to, typeGroup); + + List toAndTypeGroup = new ArrayList<>(); + toAndTypeGroup.add(to); + toAndTypeGroup.add(typeGroup); + + Cache cache = cacheManager.getCache(RELATIONS_CACHE); + List fromCache = cache.get(toAndTypeGroup, List.class); + if (fromCache != null) { + return Futures.immediateFuture(fromCache); + } else { + ListenableFuture> relationsFuture = relationDao.findAllByTo(to, typeGroup); + Futures.addCallback(relationsFuture, + new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + cache.putIfAbsent(toAndTypeGroup, result); + } + @Override + public void onFailure(Throwable t) {} + }); + return relationsFuture; + } } @Override diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java index 743aefcc40..d269126d90 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java @@ -227,6 +227,13 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { Assert.assertTrue(relations.contains(relationA)); Assert.assertTrue(relations.contains(relationB)); Assert.assertTrue(relations.contains(relationC)); + + //Test from cache + relations = relationService.findByQuery(query).get(); + Assert.assertEquals(3, relations.size()); + Assert.assertTrue(relations.contains(relationA)); + Assert.assertTrue(relations.contains(relationB)); + Assert.assertTrue(relations.contains(relationC)); } @Test @@ -253,6 +260,12 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { Assert.assertEquals(2, relations.size()); Assert.assertTrue(relations.contains(relationAB)); Assert.assertTrue(relations.contains(relationBC)); + + //Test from cache + relations = relationService.findByQuery(query).get(); + Assert.assertEquals(2, relations.size()); + Assert.assertTrue(relations.contains(relationAB)); + Assert.assertTrue(relations.contains(relationBC)); } From 2b34574440c916f30f2b7d990998c130c6223561 Mon Sep 17 00:00:00 2001 From: Jan Bols Date: Fri, 24 Aug 2018 11:44:36 +0200 Subject: [PATCH 27/30] missing break in switch statement of AppActor --- .../main/java/org/thingsboard/server/actors/app/AppActor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 8e7a9ae7e1..6a78f78d1b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -116,6 +116,7 @@ public class AppActor extends RuleChainManagerActor { break; case ACTOR_SYSTEM_TO_DEVICE_SESSION_ACTOR_MSG: onToDeviceSessionMsg((BasicActorSystemToDeviceSessionActorMsg) msg); + break; default: return false; } From 142a422b7a630d6653195c579f9535388a2c0ac8 Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Tue, 28 Aug 2018 16:54:31 +0300 Subject: [PATCH 28/30] deduplicate JS scripts in JsScriptEngine --- .../AbstractNashornJsSandboxService.java | 59 ++++++++++++++----- .../script/RuleNodeJsScriptEngine.java | 2 +- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java index 7f274ec6d7..724f652609 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java @@ -22,6 +22,7 @@ import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -42,9 +43,10 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic private ScriptEngine engine; private ExecutorService monitorExecutorService; - private Map functionsMap = new ConcurrentHashMap<>(); - - private Map blackListedFunctions = new ConcurrentHashMap<>(); + private final Map functionsMap = new ConcurrentHashMap<>(); + private final Map blackListedFunctions = new ConcurrentHashMap<>(); + private final Map> scriptToId = new ConcurrentHashMap<>(); + private final Map scriptIdToCount = new ConcurrentHashMap<>(); @PostConstruct public void init() { @@ -78,19 +80,27 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @Override public ListenableFuture eval(JsScriptType scriptType, String scriptBody, String... argNames) { - UUID scriptId = UUID.randomUUID(); - String functionName = "invokeInternal_" + scriptId.toString().replace('-','_'); - String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); - try { - if (useJsSandbox()) { - sandbox.eval(jsScript); - } else { - engine.eval(jsScript); + Pair deduplicated = deduplicate(scriptType, scriptBody); + UUID scriptId = deduplicated.getLeft(); + AtomicInteger duplicateCount = deduplicated.getRight(); + + if(duplicateCount.compareAndSet(0, 1)) { + String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_'); + String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); + try { + if (useJsSandbox()) { + sandbox.eval(jsScript); + } else { + engine.eval(jsScript); + } + functionsMap.put(scriptId, functionName); + } catch (Exception e) { + duplicateCount.decrementAndGet(); + log.warn("Failed to compile JS script: {}", e.getMessage(), e); + return Futures.immediateFailedFuture(e); } - functionsMap.put(scriptId, functionName); - } catch (Exception e) { - log.warn("Failed to compile JS script: {}", e.getMessage(), e); - return Futures.immediateFailedFuture(e); + } else { + duplicateCount.incrementAndGet(); } return Futures.immediateFuture(scriptId); } @@ -122,6 +132,13 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @Override public ListenableFuture release(UUID scriptId) { + AtomicInteger count = scriptIdToCount.get(scriptId); + if(count != null) { + if(count.decrementAndGet() > 0) { + return Futures.immediateFuture(null); + } + } + String functionName = functionsMap.get(scriptId); if (functionName != null) { try { @@ -156,4 +173,16 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic throw new RuntimeException("No script factory implemented for scriptType: " + scriptType); } } + + private Pair deduplicate(JsScriptType scriptType, String scriptBody) { + Pair precomputed = Pair.of(UUID.randomUUID(), new AtomicInteger()); + + Pair pair = scriptToId.computeIfAbsent(deduplicateKey(scriptType, scriptBody), i -> precomputed); + AtomicInteger duplicateCount = scriptIdToCount.computeIfAbsent(pair.getLeft(), i -> pair.getRight()); + return Pair.of(pair.getLeft(), duplicateCount); + } + + private String deduplicateKey(JsScriptType scriptType, String scriptBody) { + return scriptType + "_" + scriptBody; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index 98b0e77a0a..2ba87ec8da 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -45,7 +45,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S try { this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get(); } catch (Exception e) { - throw new IllegalArgumentException("Can't compile script: " + e.getMessage()); + throw new IllegalArgumentException("Can't compile script: " + e.getMessage(), e); } } From 9586569386da4461faa1b05b251656d41852377b Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Wed, 29 Aug 2018 18:54:46 +0300 Subject: [PATCH 29/30] use direction in relation search cache --- .../dao/relation/BaseRelationService.java | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index d88a1c4c42..55c5987c74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -39,7 +39,6 @@ import org.thingsboard.server.dao.exception.DataValidationException; import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -92,10 +91,10 @@ public class BaseRelationService implements RelationService { @Caching(evict = { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}") + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") }) @Override public boolean saveRelation(EntityRelation relation) { @@ -106,10 +105,10 @@ public class BaseRelationService implements RelationService { @Caching(evict = { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}") + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") }) @Override public ListenableFuture saveRelationAsync(EntityRelation relation) { @@ -120,10 +119,10 @@ public class BaseRelationService implements RelationService { @Caching(evict = { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}") + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") }) @Override public boolean deleteRelation(EntityRelation relation) { @@ -134,10 +133,10 @@ public class BaseRelationService implements RelationService { @Caching(evict = { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}") + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup, 'TO'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup, 'TO'}") }) @Override public ListenableFuture deleteRelationAsync(EntityRelation relation) { @@ -148,10 +147,10 @@ public class BaseRelationService implements RelationService { @Caching(evict = { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}") + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup, 'TO'}") }) @Override public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { @@ -162,10 +161,10 @@ public class BaseRelationService implements RelationService { @Caching(evict = { @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"), - @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}") + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup, 'TO'}") }) @Override public ListenableFuture deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { @@ -248,26 +247,30 @@ public class BaseRelationService implements RelationService { fromTypeAndTypeGroup.add(relation.getFrom()); fromTypeAndTypeGroup.add(relation.getType()); fromTypeAndTypeGroup.add(relation.getTypeGroup()); + fromTypeAndTypeGroup.add(EntitySearchDirection.FROM.name()); cache.evict(fromTypeAndTypeGroup); List fromAndTypeGroup = new ArrayList<>(); fromAndTypeGroup.add(relation.getFrom()); fromAndTypeGroup.add(relation.getTypeGroup()); + fromAndTypeGroup.add(EntitySearchDirection.FROM.name()); cache.evict(fromAndTypeGroup); List toAndTypeGroup = new ArrayList<>(); toAndTypeGroup.add(relation.getTo()); toAndTypeGroup.add(relation.getTypeGroup()); + toAndTypeGroup.add(EntitySearchDirection.TO.name()); cache.evict(toAndTypeGroup); List toTypeAndTypeGroup = new ArrayList<>(); - fromTypeAndTypeGroup.add(relation.getTo()); - fromTypeAndTypeGroup.add(relation.getType()); - fromTypeAndTypeGroup.add(relation.getTypeGroup()); + toTypeAndTypeGroup.add(relation.getTo()); + toTypeAndTypeGroup.add(relation.getType()); + toTypeAndTypeGroup.add(relation.getTypeGroup()); + toTypeAndTypeGroup.add(EntitySearchDirection.TO.name()); cache.evict(toTypeAndTypeGroup); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}") + @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup, 'FROM'}") @Override public List findByFrom(EntityId from, RelationTypeGroup typeGroup) { validate(from); @@ -288,6 +291,7 @@ public class BaseRelationService implements RelationService { List fromAndTypeGroup = new ArrayList<>(); fromAndTypeGroup.add(from); fromAndTypeGroup.add(typeGroup); + fromAndTypeGroup.add(EntitySearchDirection.FROM.name()); Cache cache = cacheManager.getCache(RELATIONS_CACHE); List fromCache = cache.get(fromAndTypeGroup, List.class); @@ -326,7 +330,7 @@ public class BaseRelationService implements RelationService { }); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}") + @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup, 'FROM'}") @Override public List findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) { try { @@ -345,7 +349,7 @@ public class BaseRelationService implements RelationService { return relationDao.findAllByFromAndType(from, relationType, typeGroup); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}") + @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup, 'TO'}") @Override public List findByTo(EntityId to, RelationTypeGroup typeGroup) { validate(to); @@ -366,6 +370,7 @@ public class BaseRelationService implements RelationService { List toAndTypeGroup = new ArrayList<>(); toAndTypeGroup.add(to); toAndTypeGroup.add(typeGroup); + toAndTypeGroup.add(EntitySearchDirection.TO.name()); Cache cache = cacheManager.getCache(RELATIONS_CACHE); List fromCache = cache.get(toAndTypeGroup, List.class); @@ -415,7 +420,7 @@ public class BaseRelationService implements RelationService { }); } - @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}") + @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup, 'TO'}") @Override public List findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) { try { From 291634c10fb62fe00843fbfeaff0c6e216bf7ad8 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Thu, 30 Aug 2018 11:42:53 +0300 Subject: [PATCH 30/30] Fix for notification to the devices when shared attribute is saved via rule engine --- .../DefaultTelemetrySubscriptionService.java | 15 +++++++++++++++ .../engine/api/RuleEngineTelemetryService.java | 5 +++++ .../engine/telemetry/TbMsgAttributesNode.java | 8 +++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 263b4849fa..5c8503650b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -24,12 +24,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; import org.thingsboard.rule.engine.api.util.DonAsynchron; +import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseTsKvQuery; @@ -42,6 +45,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvQuery; +import org.thingsboard.server.common.msg.cluster.SendToClusterMsg; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -101,6 +105,10 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio @Lazy private DeviceStateService stateService; + @Autowired + @Lazy + private ActorService actorService; + private ExecutorService tsCallBackExecutor; private ExecutorService wsCallBackExecutor; @@ -203,6 +211,13 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio , System.currentTimeMillis())), callback); } + @Override + public void onSharedAttributesUpdate(TenantId tenantId, DeviceId deviceId, Set attributes) { + DeviceAttributesEventNotificationMsg notificationMsg = DeviceAttributesEventNotificationMsg.onUpdate(tenantId, + deviceId, DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)); + actorService.onMsg(new SendToClusterMsg(deviceId, notificationMsg)); + } + @Override public void onNewRemoteSubscription(ServerAddress serverAddress, byte[] data) { ClusterAPIProtos.SubscriptionProto proto; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java index aa57a027b8..c70b9060ee 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java @@ -16,11 +16,14 @@ package org.thingsboard.rule.engine.api; import com.google.common.util.concurrent.FutureCallback; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.List; +import java.util.Set; /** * Created by ashvayka on 02.04.18. @@ -41,4 +44,6 @@ public interface RuleEngineTelemetryService { void saveAttrAndNotify(EntityId entityId, String scope, String key, boolean value, FutureCallback callback); + void onSharedAttributesUpdate(TenantId tenantId, DeviceId deviceId, Set attributes); + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 4f82884f33..5cdb04ed5b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -17,12 +17,15 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; 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.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -62,6 +65,9 @@ public class TbMsgAttributesNode implements TbNode { String src = msg.getData(); Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(src)).getAttributes(); ctx.getTelemetryService().saveAndNotify(msg.getOriginator(), config.getScope(), new ArrayList<>(attributes), new TelemetryNodeCallback(ctx, msg)); + if (msg.getOriginator().getEntityType() == EntityType.DEVICE && DataConstants.SHARED_SCOPE.equals(config.getScope())) { + ctx.getTelemetryService().onSharedAttributesUpdate(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()), attributes); + } } @Override