From bf7454ad3c75f7f704d917081c9cfb54eb2ea8b8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 28 Apr 2023 14:28:16 +0300 Subject: [PATCH 01/47] Move rate limiting to one service --- .../config/RateLimitProcessingFilter.java | 64 +++------- .../server/controller/AuthController.java | 19 +-- .../controller/plugin/TbWebSocketHandler.java | 37 +++--- .../service/apiusage/limits/LimitedApi.java | 37 ------ .../DefaultNotificationCenter.java | 4 +- .../DefaultNotificationRuleProcessor.java | 4 +- .../auth/mfa/DefaultTwoFactorAuthService.java | 52 ++++----- .../DefaultEntitiesExportImportService.java | 4 +- .../service/limits/RateLimitServiceTest.java | 110 ++++++++++++++++++ .../notification/NotificationRuleApiTest.java | 4 +- .../TenantProfileNotFoundException.java | 31 +++++ .../CassandraBufferedRateReadExecutor.java | 9 +- .../CassandraBufferedRateWriteExecutor.java | 6 +- .../util/AbstractBufferedRateExecutor.java | 45 +++---- .../util}/limits/DefaultRateLimitService.java | 58 +++++---- .../server/dao/util/limits/LimitedApi.java | 67 +++++++++++ .../dao/util}/limits/RateLimitService.java | 9 +- 17 files changed, 338 insertions(+), 222 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java create mode 100644 application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java rename {application/src/main/java/org/thingsboard/server/service/apiusage => dao/src/main/java/org/thingsboard/server/dao/util}/limits/DefaultRateLimitService.java (57%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java rename {application/src/main/java/org/thingsboard/server/service/apiusage => dao/src/main/java/org/thingsboard/server/dao/util}/limits/RateLimitService.java (75%) diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java index 0593b9765a..89f9b751fd 100644 --- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java @@ -15,64 +15,53 @@ */ package org.thingsboard.server.config; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.security.model.SecurityUser; import javax.servlet.FilterChain; import javax.servlet.ServletException; -import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @Slf4j @Component +@RequiredArgsConstructor public class RateLimitProcessingFilter extends OncePerRequestFilter { - @Autowired - private ThingsboardErrorResponseHandler errorResponseHandler; - - @Autowired - @Lazy - private TbTenantProfileCache tenantProfileCache; - - private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap perCustomerLimits = new ConcurrentHashMap<>(); + private final ThingsboardErrorResponseHandler errorResponseHandler; + private final RateLimitService rateLimitService; @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { SecurityUser user = getCurrentUser(); if (user != null && !user.isSystemAdmin()) { - var profile = tenantProfileCache.get(user.getTenantId()); - if (profile == null) { + try { + if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId())) { + rateLimitExceeded(EntityType.TENANT, response); + return; + } + } catch (TenantProfileNotFoundException e) { log.debug("[{}] Failed to lookup tenant profile", user.getTenantId()); errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), response); return; } - var profileConfiguration = profile.getDefaultProfileConfiguration(); - if (!checkRateLimits(user.getTenantId(), profileConfiguration.getTenantServerRestLimitsConfiguration(), perTenantLimits, response)) { - return; - } + if (user.isCustomerUser()) { - if (!checkRateLimits(user.getCustomerId(), profileConfiguration.getCustomerServerRestLimitsConfiguration(), perCustomerLimits, response)) { + if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId(), user.getCustomerId())) { + rateLimitExceeded(EntityType.CUSTOMER, response); return; } } @@ -90,23 +79,8 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter { return false; } - private boolean checkRateLimits(I ownerId, String rateLimitConfig, Map rateLimitsMap, ServletResponse response) { - if (StringUtils.isNotEmpty(rateLimitConfig)) { - TbRateLimits rateLimits = rateLimitsMap.get(ownerId); - if (rateLimits == null || !rateLimits.getConfiguration().equals(rateLimitConfig)) { - rateLimits = new TbRateLimits(rateLimitConfig); - rateLimitsMap.put(ownerId, rateLimits); - } - - if (!rateLimits.tryConsume()) { - errorResponseHandler.handle(new TbRateLimitsException(ownerId.getEntityType()), (HttpServletResponse) response); - return false; - } - } else { - rateLimitsMap.remove(ownerId); - } - - return true; + private void rateLimitExceeded(EntityType type, HttpServletResponse response) { + errorResponseHandler.handle(new TbRateLimitsException(type), response); } protected SecurityUser getCurrentUser() { diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 32b997c21f..566b6900a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -42,19 +42,18 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; -import org.thingsboard.server.common.msg.tools.TbRateLimits; -import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; -import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; import org.thingsboard.server.service.security.model.ResetPasswordRequest; import org.thingsboard.server.service.security.model.SecurityUser; @@ -65,8 +64,6 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; import java.net.URI; import java.net.URISyntaxException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @RestController @TbCoreComponent @@ -77,12 +74,11 @@ public class AuthController extends BaseController { @Value("${server.rest.rate_limits.reset_password_per_user:5:3600}") private String defaultLimitsConfiguration; - private final ConcurrentMap resetPasswordRateLimits = new ConcurrentHashMap<>(); private final BCryptPasswordEncoder passwordEncoder; private final JwtTokenFactory tokenFactory; private final MailService mailService; private final SystemSecurityService systemSecurityService; - private final AuditLogService auditLogService; + private final RateLimitService rateLimitService; private final ApplicationEventPublisher eventPublisher; @@ -210,8 +206,7 @@ public class AuthController extends BaseController { UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); if (userCredentials != null) { - TbRateLimits tbRateLimits = getTbRateLimits(userCredentials.getUserId()); - if (!tbRateLimits.tryConsume()) { + if (!rateLimitService.checkRateLimit(LimitedApi.PASSWORD_RESET, userCredentials.getUserId(), defaultLimitsConfiguration)) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build(); } try { @@ -314,8 +309,4 @@ public class AuthController extends BaseController { eventPublisher.publishEvent(new UserSessionInvalidationEvent(user.getSessionId())); } - private TbRateLimits getTbRateLimits(UserId userId) { - return resetPasswordRateLimits.computeIfAbsent(userId, - key -> new TbRateLimits(defaultLimitsConfiguration, true)); - } } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 7a396ff795..bc51c5fdd1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -28,16 +28,16 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.config.WebSocketConfiguration; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; @@ -80,6 +80,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke @Autowired private TbTenantProfileCache tenantProfileCache; + @Autowired + private RateLimitService rateLimitService; + @Value("${server.ws.send_timeout:5000}") private long sendTimeout; @Value("${server.ws.ping_timeout:30000}") @@ -88,7 +91,6 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke private int wsMaxQueueMessagesPerSession; private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); - private final ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); private final ConcurrentMap> tenantSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap> customerSessionsMap = new ConcurrentHashMap<>(); @@ -331,24 +333,17 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (internalId != null) { SessionMetaData sessionMd = internalSessionMap.get(internalId); if (sessionMd != null) { - var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef); - if (tenantProfileConfiguration != null) { - if (StringUtils.isNotEmpty(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())) { - TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(tenantProfileConfiguration.getWsUpdatesPerSessionRateLimit())); - if (!rateLimits.tryConsume()) { - if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { - log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" - , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); - } - return; - } else { - log.debug("[{}][{}][{}] Session is no longer blacklisted.", sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId); - blacklistedSessions.remove(externalId); - } - } else { - perSessionUpdateLimits.remove(sessionRef.getSessionId()); + TenantId tenantId = sessionRef.getSecurityCtx().getTenantId(); + if (!rateLimitService.checkRateLimit(LimitedApi.WS_UPDATES_PER_SESSION, tenantId, (Object) sessionRef.getSessionId())) { + if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) { + log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached" + , tenantId, sessionRef.getSecurityCtx().getId(), externalId); + sessionMd.sendMsg("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"); } + return; + } else { + log.debug("[{}][{}][{}] Session is no longer blacklisted.", tenantId, sessionRef.getSecurityCtx().getId(), externalId); + blacklistedSessions.remove(externalId); } sessionMd.sendMsg(msg); } else { @@ -464,7 +459,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (tenantProfileConfiguration == null) return; String sessionId = session.getId(); - perSessionUpdateLimits.remove(sessionRef.getSessionId()); + rateLimitService.cleanUp(LimitedApi.WS_UPDATES_PER_SESSION, sessionRef.getSessionId()); blacklistedSessions.remove(sessionRef.getSessionId()); if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) { Set tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java deleted file mode 100644 index 4f216ffaca..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.apiusage.limits; - -import lombok.RequiredArgsConstructor; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; - -import java.util.function.Function; - -@RequiredArgsConstructor -public enum LimitedApi { - - ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit), - ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit), - NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit), - NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit); - - private final Function configExtractor; - - public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) { - return configExtractor.apply(profileConfiguration); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 91bc0863db..79b424b9fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -65,8 +65,8 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.notification.channels.NotificationChannel; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java index 57dba39063..ab3a2f4ce6 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java @@ -42,8 +42,8 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.executors.NotificationExecutorService; import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache; import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 4231208850..a58c801583 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.security.auth.mfa; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Service; @@ -29,8 +30,9 @@ import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettin import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider; @@ -41,8 +43,6 @@ import java.util.Collection; import java.util.EnumMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; @Service @RequiredArgsConstructor @@ -52,14 +52,13 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private final TwoFaConfigManager configManager; private final SystemSecurityService systemSecurityService; private final UserService userService; + private final RateLimitService rateLimitService; private final Map> providers = new EnumMap<>(TwoFaProviderType.class); private static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS); private static final ThingsboardException PROVIDER_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS); private static final ThingsboardException PROVIDER_NOT_AVAILABLE_ERROR = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL); - - private final ConcurrentMap> verificationCodeSendingRateLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap> verificationCodeCheckingRateLimits = new ConcurrentHashMap<>(); + private static final ThingsboardException TOO_MANY_REQUESTS_ERROR = new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); @Override public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { @@ -91,7 +90,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { if (minVerificationCodeSendPeriod != null && minVerificationCodeSendPeriod > 4) { rateLimit = "1:" + minVerificationCodeSendPeriod; } - checkRateLimits(user.getId(), accountConfig.getProviderType(), rateLimit, verificationCodeSendingRateLimits); + if (!rateLimitService.checkRateLimit(LimitedApi.TWO_FA_VERIFICATION_CODE_SEND, + Pair.of(user.getId(), accountConfig.getProviderType()), rateLimit)) { + throw TOO_MANY_REQUESTS_ERROR; + } } TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) @@ -116,7 +118,10 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); if (checkLimits) { - checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeCheckRateLimit(), verificationCodeCheckingRateLimits); + if (!rateLimitService.checkRateLimit(LimitedApi.TWO_FA_VERIFICATION_CODE_CHECK, + Pair.of(user.getId(), accountConfig.getProviderType()), twoFaSettings.getVerificationCodeCheckRateLimit())) { + throw TOO_MANY_REQUESTS_ERROR; + } } TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType()) .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR); @@ -131,43 +136,28 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { try { systemSecurityService.validateTwoFaVerification(user, verificationSuccess, twoFaSettings); } catch (LockedException e) { - verificationCodeCheckingRateLimits.remove(user.getId()); - verificationCodeSendingRateLimits.remove(user.getId()); + cleanUpRateLimits(user.getId()); throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION); } if (verificationSuccess) { - verificationCodeCheckingRateLimits.remove(user.getId()); - verificationCodeSendingRateLimits.remove(user.getId()); + cleanUpRateLimits(user.getId()); } } return verificationSuccess; } - private void checkRateLimits(UserId userId, TwoFaProviderType providerType, String rateLimitConfig, - ConcurrentMap> rateLimits) throws ThingsboardException { - if (StringUtils.isNotEmpty(rateLimitConfig)) { - ConcurrentMap providersRateLimits = rateLimits.computeIfAbsent(userId, i -> new ConcurrentHashMap<>()); - - TbRateLimits rateLimit = providersRateLimits.get(providerType); - if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) { - rateLimit = new TbRateLimits(rateLimitConfig, true); - providersRateLimits.put(providerType, rateLimit); - } - if (!rateLimit.tryConsume()) { - throw new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); - } - } else { - rateLimits.remove(userId); - } - } - - @Override public TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException { TwoFaProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType); return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig); } + private void cleanUpRateLimits(UserId userId) { + for (TwoFaProviderType providerType : TwoFaProviderType.values()) { + rateLimitService.cleanUp(LimitedApi.TWO_FA_VERIFICATION_CODE_SEND, Pair.of(userId, providerType)); + rateLimitService.cleanUp(LimitedApi.TWO_FA_VERIFICATION_CODE_CHECK, Pair.of(userId, providerType)); + } + } private TwoFaProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException { return configManager.getPlatformTwoFaSettings(tenantId, true) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 793c25d625..b1c5ea31be 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -32,8 +32,8 @@ import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.sync.ie.exporting.EntityExportService; import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService; diff --git a/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java new file mode 100644 index 0000000000..05ee66f327 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java @@ -0,0 +1,110 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.limits; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.util.limits.DefaultRateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; + +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class RateLimitServiceTest { + + private RateLimitService rateLimitService; + private TbTenantProfileCache tenantProfileCache; + private TenantId tenantId; + + @Before + public void beforeEach() { + tenantProfileCache = Mockito.mock(TbTenantProfileCache.class); + rateLimitService = new DefaultRateLimitService(tenantProfileCache, 60, 100); + tenantId = new TenantId(UUID.randomUUID()); + } + + @Test + public void testRateLimits() { + int max = 2; + String rateLimit = max + ":600"; + DefaultTenantProfileConfiguration profileConfiguration = new DefaultTenantProfileConfiguration(); + profileConfiguration.setTenantEntityExportRateLimit(rateLimit); + profileConfiguration.setTenantEntityImportRateLimit(rateLimit); + profileConfiguration.setTenantNotificationRequestsRateLimit(rateLimit); + profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(rateLimit); + profileConfiguration.setTenantServerRestLimitsConfiguration(rateLimit); + profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit); + profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit); + profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit); + updateTenantProfileConfiguration(profileConfiguration); + + for (LimitedApi limitedApi : List.of( + LimitedApi.ENTITY_EXPORT, + LimitedApi.ENTITY_IMPORT, + LimitedApi.NOTIFICATION_REQUESTS, + LimitedApi.REST_REQUESTS, + LimitedApi.CASSANDRA_QUERIES + )) { + testRateLimits(limitedApi, max, tenantId); + } + + CustomerId customerId = new CustomerId(UUID.randomUUID()); + testRateLimits(LimitedApi.REST_REQUESTS, max, customerId); + + NotificationRuleId notificationRuleId = new NotificationRuleId(UUID.randomUUID()); + testRateLimits(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, max, notificationRuleId); + + String wsSessionId = UUID.randomUUID().toString(); + testRateLimits(LimitedApi.WS_UPDATES_PER_SESSION, max, wsSessionId); + } + + private void testRateLimits(LimitedApi limitedApi, int max, Object level) { + for (int i = 1; i <= max; i++) { + boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); + assertTrue(success); + } + boolean success = rateLimitService.checkRateLimit(limitedApi, tenantId, level); + assertFalse(success); + } + + private void updateTenantProfileConfiguration(DefaultTenantProfileConfiguration profileConfiguration) { + reset(tenantProfileCache); + TenantProfile tenantProfile = new TenantProfile(); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(profileConfiguration); + tenantProfile.setProfileData(profileData); + when(tenantProfileCache.get(eq(tenantId))).thenReturn(tenantProfile); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index dae2d06075..1c871f06cf 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -69,8 +69,8 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.ArrayList; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java new file mode 100644 index 0000000000..565ee493ea --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantProfileNotFoundException.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.exception; + +import lombok.Getter; +import org.thingsboard.server.common.data.id.TenantId; + +public class TenantProfileNotFoundException extends RuntimeException { + + @Getter + private final TenantId tenantId; + + public TenantProfileNotFoundException(TenantId tenantId) { + super("Profile for tenant with id " + tenantId + " not found"); + this.tenantId = tenantId; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java index 4c13fe3b25..598e180ce8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java @@ -22,15 +22,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.stats.DefaultCounter; -import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; import org.thingsboard.server.dao.util.AsyncTaskContext; import org.thingsboard.server.dao.util.NoSqlAnyDao; +import org.thingsboard.server.dao.util.limits.RateLimitService; import javax.annotation.PreDestroy; @@ -55,9 +52,9 @@ public class CassandraBufferedRateReadExecutor extends AbstractBufferedRateExecu @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq, @Autowired StatsFactory statsFactory, @Autowired EntityService entityService, - @Autowired TbTenantProfileCache tenantProfileCache) { + @Autowired RateLimitService rateLimitService) { super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq, statsFactory, - entityService, tenantProfileCache, printTenantNames); + entityService, rateLimitService, printTenantNames); } @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java index 55b1b5410d..1cb2eaf3df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java @@ -24,10 +24,10 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.entity.EntityService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; import org.thingsboard.server.dao.util.AsyncTaskContext; import org.thingsboard.server.dao.util.NoSqlAnyDao; +import org.thingsboard.server.dao.util.limits.RateLimitService; import javax.annotation.PreDestroy; @@ -52,9 +52,9 @@ public class CassandraBufferedRateWriteExecutor extends AbstractBufferedRateExec @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq, @Autowired StatsFactory statsFactory, @Autowired EntityService entityService, - @Autowired TbTenantProfileCache tenantProfileCache) { + @Autowired RateLimitService rateLimitService) { super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq, statsFactory, - entityService, tenantProfileCache, printTenantNames); + entityService, rateLimitService, printTenantNames); } @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index c518de6a5c..64b4eb3666 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -30,25 +30,21 @@ import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; @@ -74,7 +70,6 @@ public abstract class AbstractBufferedRateExecutor perTenantLimits = new ConcurrentHashMap<>(); private final AtomicInteger printQueriesIdx = new AtomicInteger(0); @@ -82,14 +77,14 @@ public abstract class AbstractBufferedRateExecutor tenantNamesCache = new HashMap<>(); public AbstractBufferedRateExecutor(int queueLimit, int concurrencyLimit, long maxWaitTime, int dispatcherThreads, int callbackThreads, long pollMs, int printQueriesFreq, StatsFactory statsFactory, - EntityService entityService, TbTenantProfileCache tenantProfileCache, boolean printTenantNames) { + EntityService entityService, RateLimitService rateLimitService, boolean printTenantNames) { this.maxWaitTime = maxWaitTime; this.pollMs = pollMs; this.concurrencyLimit = concurrencyLimit; @@ -103,7 +98,7 @@ public abstract class AbstractBufferedRateExecutor new TbRateLimits(tenantProfileConfiguration.getCassandraQueryTenantRateLimitsConfiguration()) - ); - if (!rateLimits.tryConsume()) { - stats.incrementRateLimitedTenant(task.getTenantId()); - stats.getTotalRateLimited().increment(); - settableFuture.setException(new TenantRateLimitException()); - perTenantLimitReached = true; - } + TenantId tenantId = task.getTenantId(); + if (tenantId != null && !tenantId.isSysTenantId()) { + if (!rateLimitService.checkRateLimit(LimitedApi.CASSANDRA_QUERIES, tenantId)) { + stats.incrementRateLimitedTenant(tenantId); + stats.getTotalRateLimited().increment(); + settableFuture.setException(new TenantRateLimitException()); + perTenantLimitReached = true; } - } else if (!TenantId.SYS_TENANT_ID.equals(task.getTenantId())) { - perTenantLimits.remove(task.getTenantId()); + } else if (tenantId == null) { + log.info("Invalid task received: {}", task); } if (!perTenantLimitReached) { diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java b/dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java similarity index 57% rename from application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java rename to dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java index 9a0282c840..ba5a50ab1d 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/limits/DefaultRateLimitService.java @@ -13,83 +13,95 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.apiusage.limits; +package org.thingsboard.server.dao.util.limits; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import javax.annotation.PostConstruct; import java.util.concurrent.TimeUnit; @Service -@RequiredArgsConstructor @Slf4j public class DefaultRateLimitService implements RateLimitService { private final TbTenantProfileCache tenantProfileCache; - @Value("${cache.rateLimits.timeToLiveInMinutes:60}") - private int rateLimitsTtl; - @Value("${cache.rateLimits.maxSize:100000}") - private int rateLimitsCacheMaxSize; - private Cache rateLimits; - - @PostConstruct - private void init() { - rateLimits = Caffeine.newBuilder() + public DefaultRateLimitService(TbTenantProfileCache tenantProfileCache, + @Value("${cache.rateLimits.timeToLiveInMinutes:60}") int rateLimitsTtl, + @Value("${cache.rateLimits.maxSize:100000}") int rateLimitsCacheMaxSize) { + this.tenantProfileCache = tenantProfileCache; + this.rateLimits = Caffeine.newBuilder() .expireAfterAccess(rateLimitsTtl, TimeUnit.MINUTES) .maximumSize(rateLimitsCacheMaxSize) .build(); } + private final Cache rateLimits; + @Override public boolean checkRateLimit(LimitedApi api, TenantId tenantId) { return checkRateLimit(api, tenantId, tenantId); } @Override - public boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId) { + public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level) { if (tenantId.isSysTenantId()) { return true; } - RateLimitKey key = new RateLimitKey(api, entityId); + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + if (tenantProfile == null) { + throw new TenantProfileNotFoundException(tenantId); + } + + String rateLimitConfig = tenantProfile.getProfileConfiguration() + .map(profileConfiguration -> api.getLimitConfig(profileConfiguration, level)) + .orElse(null); + return checkRateLimit(api, level, rateLimitConfig); + } - String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration() - .map(api::getLimitConfig).orElse(null); + @Override + public boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig) { + RateLimitKey key = new RateLimitKey(api, level); if (StringUtils.isEmpty(rateLimitConfig)) { rateLimits.invalidate(key); return true; } - log.trace("[{}] Checking rate limit for {} ({})", entityId, api, rateLimitConfig); + log.trace("[{}] Checking rate limit for {} ({})", level, api, rateLimitConfig); TbRateLimits rateLimit = rateLimits.asMap().compute(key, (k, limit) -> { if (limit == null || !limit.getConfiguration().equals(rateLimitConfig)) { - limit = new TbRateLimits(rateLimitConfig); - log.trace("[{}] Created new rate limit bucket for {} ({})", entityId, api, rateLimitConfig); + limit = new TbRateLimits(rateLimitConfig, api.isRefillRateLimitIntervally()); + log.trace("[{}] Created new rate limit bucket for {} ({})", level, api, rateLimitConfig); } return limit; }); boolean success = rateLimit.tryConsume(); if (!success) { - log.debug("[{}] Rate limit exceeded for {} ({})", entityId, api, rateLimitConfig); + log.debug("[{}] Rate limit exceeded for {} ({})", level, api, rateLimitConfig); } return success; } + @Override + public void cleanUp(LimitedApi api, Object level) { + RateLimitKey key = new RateLimitKey(api, level); + rateLimits.invalidate(key); + } + @Data(staticConstructor = "of") private static class RateLimitKey { private final LimitedApi api; - private final EntityId entityId; + private final Object level; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java b/dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java new file mode 100644 index 0000000000..ee79230d8a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/limits/LimitedApi.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util.limits; + +import lombok.Getter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; + +import java.util.function.BiFunction; +import java.util.function.Function; + +public enum LimitedApi { + + ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit), + ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit), + NOTIFICATION_REQUESTS(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit), + NOTIFICATION_REQUESTS_PER_RULE(DefaultTenantProfileConfiguration::getTenantNotificationRequestsPerRuleRateLimit), + REST_REQUESTS((profileConfiguration, level) -> ((EntityId) level).getEntityType() == EntityType.TENANT ? + profileConfiguration.getTenantServerRestLimitsConfiguration() : + profileConfiguration.getCustomerServerRestLimitsConfiguration()), + WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit), + CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration), + PASSWORD_RESET(true), + TWO_FA_VERIFICATION_CODE_SEND(true), + TWO_FA_VERIFICATION_CODE_CHECK(true); + + private final BiFunction configExtractor; + @Getter + private final boolean refillRateLimitIntervally; + + LimitedApi(Function configExtractor) { + this((profileConfiguration, level) -> configExtractor.apply(profileConfiguration)); + } + + LimitedApi(BiFunction configExtractor) { + this.configExtractor = configExtractor; + this.refillRateLimitIntervally = false; + } + + LimitedApi(boolean refillRateLimitIntervally) { + this.configExtractor = null; + this.refillRateLimitIntervally = refillRateLimitIntervally; + } + + public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration, Object level) { + if (configExtractor != null) { + return configExtractor.apply(profileConfiguration, level); + } else { + throw new IllegalArgumentException("No tenant profile config for " + name() + " rate limits"); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java b/dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java similarity index 75% rename from application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java rename to dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java index 3fde98618e..c3f2fd179f 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/limits/RateLimitService.java @@ -13,15 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.apiusage.limits; +package org.thingsboard.server.dao.util.limits; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; public interface RateLimitService { boolean checkRateLimit(LimitedApi api, TenantId tenantId); - boolean checkRateLimit(LimitedApi api, TenantId tenantId, EntityId entityId); + boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level); + + boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig); + + void cleanUp(LimitedApi api, Object level); } From c303d4dea3379d8d35521e7435b036288798ae32 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 8 May 2023 17:54:27 +0300 Subject: [PATCH 02/47] Fix PROD-1975: ValueInputComponent change: add updateView() after closing openEditJSONDialog --- ui-ngx/src/app/shared/components/value-input.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/shared/components/value-input.component.ts b/ui-ngx/src/app/shared/components/value-input.component.ts index 946d320241..4ba7abf157 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.ts +++ b/ui-ngx/src/app/shared/components/value-input.component.ts @@ -80,6 +80,7 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor { if (res) { this.modelValue = res; this.inputForm.control.patchValue({value: this.modelValue}); + this.updateView(); } } ); From f0f33c479abf8fbd16613ca66c51361340486067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20N=C3=BA=C3=B1ez?= Date: Mon, 8 May 2023 17:43:59 +0200 Subject: [PATCH 03/47] Update spanish locale to 3.5 --- .../assets/locale/locale.constant-es_ES.json | 917 ++++++++++++++++-- 1 file changed, 861 insertions(+), 56 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 8783ab3cb0..82b568330d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -55,13 +55,18 @@ "continue": "Continuar", "discard-changes": "Cancelar cambios", "download": "Descargar", + "next": "Siguente", "next-with-label": "Siguiente: {{label}}", "read-more": "Leer más", "hide": "Ocultar", "done": "Terminado", "print": "Imprimir", "restore": "Restaurar", - "confirm": "Confirmar" + "confirm": "Confirmar", + "more": "Más", + "less": "Menos", + "skip": "Saltar", + "send": "Enviar" }, "aggregation": { "aggregation": "Agrupación", @@ -76,9 +81,11 @@ "none": "Ninguno" }, "admin": { + "settings": "Ajustes", "general": "General", "general-settings": "Configuración general", "home-settings": "Ajustes Home", + "home": "Home", "outgoing-mail": "Servidor de correo", "outgoing-mail-settings": "Configuración del servidor de correo de salida", "system-settings": "Sistema", @@ -325,12 +332,16 @@ "queue-processing-strategy": "Estrategia de procesamiento", "queue-configuration": "Configuración Cola", "repository-settings": "Ajustes del repositorio", + "repository": "Repositorio", "repository-url": "URL del repositorio", "repository-url-required": "Se requiere la URL del repositorio.", "default-branch": "Nombre de rama por defecto", + "repository-read-only": "Sólo lectura", + "show-merge-commits": "Mostrar commits merge", "authentication-settings": "Ajustes de autenticación", "auth-method": "Método de autenticación", "auth-method-username-password": "Usuario / Contraseña", + "auth-method-username-password-hint": "Los usuarios de GitHub deben usar access tokens con permisos de escritura en el repositorio.", "auth-method-private-key": "Clave privada", "password-access-token": "Contraseña / Tóken de acceso", "change-password-access-token": "Cambiar contraseña / Tóken de acceso", @@ -343,8 +354,9 @@ "check-repository-access-success": "Acceso al repositorio verificado satisfactoriamente!", "delete-repository-settings-title": "Estás seguro de borrar los ajustes del repositorio?", "delete-repository-settings-text": "Atención, tras la confirmación los ajustes del repositorio serán eliminados y la característica de control de versiones no estará disponible.", - "auto-commit-settings": "Ajustes Auto-publicar", - "auto-commit-entities": "Entidades Auto-publicar", + "auto-commit-settings": "Ajustes Auto-commit", + "auto-commit": "Auto-commit", + "auto-commit-entities": "Entidades Auto-commit", "no-auto-commit-entities-prompt": "No hay entidades configuradas para auto-publicar", "delete-auto-commit-settings-title": "Estás seguro de borrar los ajustes de auto-publicar?", "delete-auto-commit-settings-text": "Atención, tras la confirmación los ajustes de auto-publicar serán borrados y la característica de auto-publicar se desactivará para todas las entidades.", @@ -380,14 +392,45 @@ "within-time": "Rango de tiempo (sec)", "within-time-pattern": "Tiempo debe ser un número entero positivo.", "within-time-required": "Tiempo requerido." - } + }, + "jwt": { + "security-settings": "Ajustes de seguridad JWT", + "issuer-name": "Nombre del emisor", + "issuer-name-required": "Se requiere nombre del emisor.", + "signings-key": "Clave de firma", + "signings-key-hint": "Una string codificada en Base64 representando por lo menos 256 bits de datos.", + "signings-key-required": "Se requiere clave de firma.", + "signings-key-min-length": "La clave de firma debe tener al menos 256 bits de datos.", + "signings-key-base64": "La clave de firma debe estar en formato base64.", + "expiration-time": "Caducidad del token (en segundos)", + "expiration-time-required": "Se requiere caducidad del token.", + "expiration-time-pattern": "La caducidad debe ser un número entero positivo.", + "expiration-time-min": "El tiempo mínimo debe ser al menos de 60 segundos (1 minuto).", + "refresh-expiration-time": "Caducidad del token de actualización (en segundos)", + "refresh-expiration-time-required": "Se requiere especificar caducidad del token de actualización.", + "refresh-expiration-time-pattern": "La caducidad debe ser un número entero positivo.", + "refresh-expiration-time-min": "El tiempo mínimo es de 900 segundos (15 minutos).", + "refresh-expiration-time-less-token": "El tiempo de actualización debe ser mayor al de caducidad.", + "generate-key": "Generar clave", + "info-header": "Todos los usuarios necesitarán volver a logearse en la plataforma", + "info-message": "El cambio en la clave de firma del JWT causará que todos los tokens emitidos sean inválidos. Todos los usuarios deberán hacer login de nuevo. Esto también afectará a los scripts que usen la API REST o Websockets." + }, + "resources": "Recursos", + "notifications": "Notificaciones", + "notifications-settings": "Ajustes de notificaciones", + "slack-api-token": "Token de API Slack", + "slack": "Slack", + "slack-settings": "Ajustes de Slack" }, "alarm": { "alarm": "Alarma", "alarms": "Alarmas", + "all-alarms": "Todas las alarmas", "select-alarm": "Seleccionar alarma", "no-alarms-matching": "No se han encontrado alarmas coincidentes con '{{entity}}' .", "alarm-required": "Alarma requerida", + "alarm-filter": "Filtro de alarma", + "filter": "Filtro", "alarm-status": "Estado de Alarma", "alarm-status-list": "Lista de estados de Alarmas", "any-status": "Cualquier estado", @@ -411,12 +454,24 @@ "originator": "Origen", "originator-type": "Tipo de origen", "details": "Detalles", + "originator-label": "Etiqueta de autor", + "assign": "Asignar", + "assignments": "Asignaciones", + "assignee": "Asignado", + "assignee-id": "ID de asignado", + "assignee-first-name": "Nombre de asignado", + "assignee-last-name": "Apellido de asignado", + "assignee-email": "Email del Assignee email", + "unassigned": "Sin asignar", + "assignee-not-set": "Todas", "status": "Estado", "alarm-details": "Detalles Alarma", "start-time": "Hora de inicio", + "assign-time": "Hora de asignación", "end-time": "Hora fin", "ack-time": "Hora de reconocimiento", "clear-time": "Hora de normalización", + "duration": "Duración", "alarm-severity-list": "Lista de gravedad de alarmas", "any-severity": "Cualquier gravedad", "severity-critical": "Crítica", @@ -441,7 +496,9 @@ "clear-alarm-title": "Limpiar Alarma", "clear-alarm-text": "Limpiar Alarma?", "alarm-status-filter": "Filtro de estados de Alarmas", - "alarm-filter": "Filtro de Alarmas", + "alarm-filter-title": "Filtros de alarmas", + "assigned": "Asignadas", + "filter-title": "Filtrar", "max-count-load": "Número máximo de alarmas a cargar (0 - ilimitado)", "max-count-load-required": "Se requiere número máximo de alarmas.", "max-count-load-error-min": "El valor mínimo es 0.", @@ -450,7 +507,28 @@ "fetch-size-error-min": "El valor mínimo es 10.", "alarm-type-list": "Lista de tipos de alarma", "any-type": "Cualquier tipo", - "search-propagated-alarms": "Buscar alarmas propagadas" + "assigned-to-current-user": "Asignadas al ususario actual", + "assigned-to-me": "Asignadas a mí", + "search-propagated-alarms": "Buscar alarmas propagadas", + "comments": "Comentarios de alarma", + "show-more": "Mostrar más", + "additional-info": "Información adicional" + }, + "alarm-activity": { + "add": "Añadir comentario...", + "alarm-comment": "Comentario", + "comments": "Commentarios", + "delete-alarm-comment": "Quieres borrar este comentario?", + "refresh": "Actualizar", + "oldest-first": "Antíguos primero", + "newest-first": "Nuevos primero", + "activity": "Actividad", + "export": "Exportar a CSV", + "author": "Autor", + "created-date": "Fecha de creación", + "edited-date": "Fecha de edición", + "text": "Texto", + "system": "Sistema" }, "alias": { "add": "Añadir alias", @@ -613,9 +691,12 @@ "selected-attributes": "{ count, plural, =1 {1 atributo} other {# atributos} } seleccionados", "selected-telemetry": "{ count, plural, =1 {1 telemetría} other {# telemetrías} } seleccionadas", "no-attributes-text": "No se encontró ningún atributo", - "no-telemetry-text": "No se encontró ninguna telemetría" + "no-telemetry-text": "No se encontró ninguna telemetría", + "copy-key": "Copiar clave", + "copy-value": "Copiar valor" }, "api-usage": { + "api-features": "Características API", "api-usage": "Uso de API", "alarm": "Alarma", "alarms-created": "Alarmas creadas", @@ -624,6 +705,7 @@ "alarms-created-monthly-activity": "Actividad mensual de Alarmas creadas", "data-points": "Puntos de datos", "data-points-storage-days": "Días de guardado de puntos de datos", + "device-api": "API Dispositivos", "email": "Email", "email-messages": "Mensajes de Email", "email-messages-daily-activity": "Actividad diaria de Emails", @@ -704,6 +786,11 @@ "type-relations-delete": "Borradas todas las relaciones", "type-alarm-ack": "Alarma Acusada", "type-alarm-clear": "Alarma Limpiada", + "type-alarm-assign": "Asignada", + "type-alarm-unassign": "Deasignado", + "type-added-comment": "Comentario añadido", + "type-updated-comment": "Comentario actualizado", + "type-deleted-comment": "Comentario borrado", "type-login": "Inicio de sesión", "type-logout": "Cierre de sesión", "type-lockout": "Cierre por bloqueo", @@ -919,6 +1006,7 @@ "vertical-margin-required": "Margen vertical requerido.", "min-vertical-margin-message": "Solo se permite margen vertical mínimo de 0.", "max-vertical-margin-message": "Solo se permite margen vertical máximo de 50.", + "apply-outer-margin": "Aplicar márgen en los lados del diseño", "autofill-height": "Altura diseño auto relleno", "mobile-layout": "Ajustes del diseño móvil", "mobile-row-height": "Altura de fila para móvil, px", @@ -1039,7 +1127,22 @@ "value-description": "el valor actual", "prev-value-description": "resultado de la llamada anterior de la función", "time-prev-description": "hora del valor previo", - "prev-orig-value-description": "valor original previo" + "prev-orig-value-description": "valor original previo", + "aggregation": "Agregación", + "aggregation-type-hint-common": "Por motivos de rendimiento, el cálculo de valores agregados solamente está disponible para intervalos de tiempo fijos como \"día actual\", \"mes actual\", etc, y no está disponible para ventanas de tiempo móviles, como 'últimos 30 minutos' o 'últimas 24 horas'.", + "aggregation-type-none-hint": "Tomar último valor.", + "aggregation-type-min-hint": "Encuentra el valor mínimo de los puntos de datos contenidos en la ventana de tiempo seleccionada.", + "aggregation-type-max-hint": "Encuentra el valor máximo de los puntos de datos contenidos en la ventana de tiempo seleccionada.", + "aggregation-type-avg-hint": "Calcula el valor medio de los puntos de datos contenidos en la ventana de tiempo seleccionada.", + "aggregation-type-sum-hint": "Suma todos los valores de los puntos de datos contenidos en la ventana de tiempo seleccionada.", + "aggregation-type-count-hint": "Cuenta total de los puntos de datos contenidos en la ventana de tiempo seleccionada.", + "delta-calculation": "Cálculo delta", + "enable-delta-calculation": "Activar cálculo delta", + "enable-delta-calculation-hint": "Cuando se activa, el valor se calcula basado en los valores agregados de la ventana de tiempo seleccionada y el período de comparación seleccionado. Por razones de rendimiento, el cálculo delta solamente está disponible en las series históricas y no para los valores en tiempo real. Por ejemplo, puedes calcular el delta entre la energía consumida ayer y la energía consumida anteayer.", + "delta-calculation-result": "Resultado de cálculo delta", + "delta-calculation-result-previous-value": "Valor anterior", + "delta-calculation-result-delta-absolute": "Delta (absoluto)", + "delta-calculation-result-delta-percent": "Delta (percentil)" }, "datasource": { "type": "Típo de fuente de datos", @@ -1130,6 +1233,15 @@ "access-token-invalid": "Tóken de acceso debe tener entre 1 a 32 caracteres.", "certificate-pem-format": "Certificado en formato PEM", "certificate-pem-format-required": "Certificado requerido.", + "copy-access-token": "Copiar Access token", + "copy-certificate": "Copiar Certificado", + "copy-client-id": "Copiar ID Cliente", + "copy-user-name": "Copiar Nombre de usuario", + "copy-password": "Copiar Contraseña", + "generate-client-id": "Generar ID Cliente", + "generate-user-name": "Generar Nombre de usuario", + "generate-password": "Generar contraseña", + "generate-access-token": "Generar Access Token", "lwm2m-security-config": { "identity": "Identidad Cliente", "identity-required": "Identidad Cliente requerida.", @@ -1184,7 +1296,7 @@ "label": "Etiqueta", "events": "Eventos", "details": "Detalles", - "copyId": "Copiar ID", + "copyId": "Copiar ID de dispositivo", "copyAccessToken": "Copiar access token", "copy-mqtt-authentication": "Copiar credenciales MQTT", "idCopiedMessage": "Id del dispositivo copiado al portapapeles", @@ -1195,6 +1307,14 @@ "unable-delete-device-alias-text": "Alias '{{deviceAlias}}' no puede ser eliminado. Esta siendo usado por el(los) widget(s):
{{widgetsList}}", "is-gateway": "Es gateway", "overwrite-activity-time": "Sobreescribir hora de actividad para el dispositivo conectado", + "device-filter": "Filtro de dispositivo", + "device-filter-title": "Filtro de dispositivo", + "filter-title": "Filtro", + "device-state": "Estado de dispositivo", + "state": "Estado", + "any": "Cualquier", + "active": "Activo", + "inactive": "Inactivo", "public": "Público", "device-public": "El dispositivo es público", "select-device": "Seleccionar dispositivo", @@ -1216,6 +1336,51 @@ "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?", "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." }, + "asset-profile": { + "asset-profile": "Perfil de activo", + "asset-profiles": "Perfiles de activos", + "all-asset-profiles": "Todos", + "add": "Añadir perfil de activo", + "edit": "Editar perfil de activo", + "asset-profile-details": "Detalles de perfil de activo", + "no-asset-profiles-text": "No se encontraron perfiles de activo", + "search": "Buscar perfiles de activo", + "selected-asset-profiles": "{ count, plural, =1 {1 perfil} other {# perfiles} } seleccionados", + "no-asset-profiles-matching": "No se encontraron perfiles que coincidan con '{{entity}}'.", + "asset-profile-required": "Se requiere perfil de activo", + "idCopiedMessage": "El ID de perfil, ha sido copiado al portapapeles", + "set-default": "Hace perfil de activo por defecto", + "delete": "Borrar perfil de activo", + "copyId": "Copiar ID de perfil de activo", + "name-max-length": "El nombre debe ser menor de 256", + "new-device-profile-name": "Nombre de perfil de activo", + "new-device-profile-name-required": "Se requiere nombre de perfil", + "name": "Nombre", + "name-required": "Se requiere nombre", + "image": "Imagen de perfil de activo", + "description": "Descripción", + "default": "Por defecto", + "default-rule-chain": "Cadena de reglas por defecto", + "default-edge-rule-chain": "Cadena de reglas por defecto en borde", + "default-edge-rule-chain-hint": "Usado en borde, como cadena de reglas para procesas los datos recibidos para el perfil de activo.", + "mobile-dashboard": "Panel móvil", + "mobile-dashboard-hint": "Usado en la aplicación móvil, como un panel para los detalles del activo", + "select-queue-hint": "Selecciona desde el desplegable.", + "delete-asset-profile-title": "Quieres borrar el perfil de activo '{{assetProfileName}}'?", + "delete-asset-profile-text": "Atención, tras la confirmación, el perfil de activo será borrado y todas su información relacionada será irrecuperable.", + "delete-asset-profiles-title": "Quieres borrar { count, plural, =1 {1 perfil} other {# perfiles} }?", + "delete-asset-profiles-text": "Atención, tras la confirmación, todos los perfiles seleccionados serán borrados y su información relacionada será irrecuperable.", + "set-default-asset-profile-title": "Quieres establecer el perfil '{{assetProfileName}}' como perfil por defecto?", + "set-default-asset-profile-text": "Tras la confirmación, el perfil se marcara por defecto y será usada por los nuevos activos en los que no se especifique perfil.", + "no-asset-profiles-found": "No se han encontrado perfiles de activos.", + "create-new-asset-profile": "Crear uno nuevo!", + "create-asset-profile": "Crear un nuevo perfil de activo", + "import": "Importar perfil de activo", + "export": "Exportar perfil de activo", + "export-failed-error": "No ha sido posible exportar el perfil de activo: {{error}}", + "asset-profile-file": "Fichero de perfil de activo", + "invalid-asset-profile-file-error": "No ha sido posible importar el perfil de activo: Estructura de datos inválida." + }, "device-profile": { "device-profile": "Perfil de dispositivo", "device-profiles": "Perfiles de dispositivo", @@ -1258,12 +1423,14 @@ "profile-configuration": "Configuración de perfil", "transport-configuration": "Configuración de transporte", "default-rule-chain": "Cadena de reglas por defecto", + "default-edge-rule-chain": "Cadena de reglas por defecto en borde", + "default-edge-rule-chain-hint": "Usado en borde, como cadena de reglas para procesas los datos recibidos para el perfil de dispositivo.", "mobile-dashboard": "Panel móvil", "mobile-dashboard-hint": "Usado por la aplicación móvil como panel de detalle", "select-queue-hint": "Selecciona desde el desplegable o añade un nombre personalizado.", "delete-device-profile-title": "Eliminar el perfil '{{deviceProfileName}}'?", "delete-device-profile-text": "Atención, tras la confirmación el perfil y todos sus datos serán borrados e irrecuperables.", - "delete-device-profiles-title": "EEliminar { count, plural, =1 {1 perfil} other {# perfiles} }?", + "delete-device-profiles-title": "Eliminar { count, plural, =1 {1 perfil} other {# perfiles} }?", "delete-device-profiles-text": "Atención, tras la confirmación los perfiles seleccionados y todos sus datos serán borrados e irrecuperables.", "set-default-device-profile-title": "Establecer el perfil '{{deviceProfileName}}' como perfil por defecto?", "set-default-device-profile-text": "Tras la confirmación, el perfil será marcado como por defecto y será usado por todos los nuevos dispositivos que no tengan perfil especificado.", @@ -1271,15 +1438,19 @@ "create-new-device-profile": "Crear un nuevo perfil!", "mqtt-device-topic-filters": "Filtros de topic MQTT", "mqtt-device-topic-filters-unique": "Los filtros de topic de dispositivo MQTT deben ser únicos.", + "mqtt-device-topic-filters-spark-plug": "Nodo MQTT Sparkplug B Edge of Network (EoN).", + "mqtt-device-topic-filters-spark-plug-hint": "Permitir conexiones desde los nodos EoN con payload B y su formato de temas (topics).", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "Métricas SparkPlug para grabar como atributos.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Nombre de las métricas SparkPlug que se grabarán como atributos de dispositivo. Las métricas restantes, se grabarán como telemetría.", "mqtt-device-payload-type": "Payload de dispositivo MQTT", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", "mqtt-enable-compatibility-with-json-payload-format": "Activar compatibilidad con otros formatos de payload.", "mqtt-enable-compatibility-with-json-payload-format-hint": "Si se activa, la plataforma usará un formato de payload Protobuf por defecto. Si el parseo falla, la plataforma intentará usar el formato JSON. Útil para retrocompatibilidad durante actualizaciones de firmware. Por ejemplo, si la versión inicial de firmware usa JSON y una versión posterior usa Protobuf. Durante el proceso de actualización de firmware a los dispositivos, se requiere soportar Protobuf y JSON al mismo tiempo. El modo de retrocompatibilidad introduce una pequeña degradación en el rendimiento, es recomendable desactivarlo una vez que todos los dispositivos estén actualizados.", "mqtt-use-json-format-for-default-downlink-topics": "Usar formato JSON para los downlink", - "mqtt-use-json-format-for-default-downlink-topics-hint": "When enabled, the platform will use Json payload format to push attributes and RPC via the following topics: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. This setting does not impact attribute and rpc subscriptions sent using new (v2) topics: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Where $request_id is an integer request identifier.", + "mqtt-use-json-format-for-default-downlink-topics-hint": "Cuando esté activado, la plataforma usará el formato JSON para enviar atributos y llamadas RPC a través de los siguientes temas (topics): v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. Este ajuste, no afectará a las subscripciones de atributos y a los mensajes RPC que se envíen usando los nuevos temas v2: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Donde $request_id es un número entero para identificar la transacción.", "mqtt-send-ack-on-validation-exception": "Enviar PUBACK en error de validación al publicar (PUBLISH)", - "mqtt-send-ack-on-validation-exception-hint": "By default, the platform will close the MQTT session on message validation failure. When enabled, the platform will send publish acknowledgment instead of closing the session.", + "mqtt-send-ack-on-validation-exception-hint": "Por defecto, la plataforma cerrará la sesión MQTT cuando haya un fallo de validación. Si está activado, la plataforma enviará un mensaje tipo PUBACK, en lugar de cerrar la sesión.", "snmp-add-mapping": "Añadir mapeado SNMP", "snmp-mapping-not-configured": "No hay mapeado OID a series de tiempo/telemetría configurado", "snmp-timseries-or-attribute-name": "Nombre Series de tiempo/nombre atributo para mapeado", @@ -1298,10 +1469,10 @@ "support-level-wildcards": "Se soportan los wilcards únicos [+] y multi-nivel [#].", "telemetry-topic-filter": "Filtro de topic en telemetría", "telemetry-topic-filter-required": "Se requiere filtro de topic (telemetría).", - "attributes-topic-filter": "Attributes publish topic filter", - "attributes-subscribe-topic-filter": "Attributes subscribe topic filter", - "attributes-topic-filter-required": "Attributes publish topic filter is required.", - "attributes-subscribe-topic-filter-required": "Attributes subscribe topic is required", + "attributes-topic-filter": "Filtro de tema (topic) en publicación de atributos", + "attributes-subscribe-topic-filter": "Filtro de tema (topic) en suscripción de atributos", + "attributes-topic-filter-required": "Se requiere filtro de publicación.", + "attributes-subscribe-topic-filter-required": "Se requiere filtro de suscripción", "telemetry-proto-schema": "Proto esquema de telemetría", "telemetry-proto-schema-required": "Se requiere proto esquema de telemetría.", "attributes-proto-schema": "Proto esquema de atributos", @@ -1374,6 +1545,17 @@ "provision-device-secret-required": "Se requiere secreto de aprovisionamiento.", "copy-provision-secret": "Copiar secreto de aprovisionamiento", "provision-secret-copied-message": "Se ha copiado el secreto de aprovisionamiento al portapapeles", + "provision-strategy-x509": { + "certificate-chain": "Cadena de certificados X509", + "certificate-chain-hint": "La estrategia de certificados X.509 se utiliza para aprovisionar dispositivos mediante certificados de cliente en comunicación TLS bidireccional.", + "allow-create-new-devices": "Crear nuevos dispositivos", + "allow-create-new-devices-hint": "Si se selecciona, los nuevos dispositivos se crearán y el certificado del cliente se usara como credenciales del dispositivo.", + "certificate-value": "Certificado en formato PEM", + "certificate-value-required": "Se requiere certificado en formato PEM", + "cn-regex-variable": "Variable de expresión regular CN", + "cn-regex-variable-required": "Se requiere una variable de expresión regular", + "cn-regex-variable-hint": "Requerido para obtener el nombre del dispositivo desde el CN del certificado." + }, "condition": "Condición", "condition-type": "Tipo de condición", "condition-type-simple": "Simple", @@ -1383,7 +1565,7 @@ "condition-type-repeating": "Repetitiva", "condition-type-required": "Se requiere tipo de condición.", "condition-repeating-value": "Nº de eventos", - "condition-repeating-value-range": "El Nº de eventos debe estar en un rango de 1 to 2147483647.", + "condition-repeating-value-range": "El Nº de eventos debe estar en un rango de 1 a 2147483647.", "condition-repeating-value-pattern": "Nº de eventos debe ser un número entero.", "condition-repeating-value-required": "Se requiere Nº de eventos.", "condition-repeat-times": "Repetición { count, plural, =1 {1 vez} other {# veces} }", @@ -1500,7 +1682,7 @@ "tqs": "TQS: Conexiones TCP y SMS activas, TCP en modo cola, SMS en modo estándar (no soportado desde la versión LWM2M 1.1)", "sq": "SQ: Conexión SMS en modo de cola (no soportado desde la versión LWM2M 1.1)" }, - "binding-tooltip": "This is the list in the\"binding\" resource of the LwM2M server object - /1/x/7.\nIndicates the supported binding modes in the LwM2M Client.\nThis value SHOULD be the same as the value in the “Supported Binding and Modes” resource in the Device Object (/3/0/16).\nWhile multiple transports are supported, only one transport binding can be used during the entire Transport Session.\nAs an example, when UDP and SMS are both supported, the LwM2M Client and the LwM2M Server can choose to communicate either over UDP or SMS during the entire Transport Session.", + "binding-tooltip": "Esta es la lista del recurso \"binding\" del objeto de servidor LwM2M - /1/x/7.\nIndica los modos de enlace admitidos por el cliente LwM2M.\nEste valor DEBE ser el mismo que el valor reflejado en el recurso “Supported Binding and Modes” en el objeto (/3/0/16) del dispositivo.\nAunque se admiten varios transportes, solo se puede utilizar un tipo de binding durante toda la sesión.\nPor ejemplo, si UDP y SMS son admitidos, el servidor o el cliente LwM2M, pueden optar por comunicarse a través de UDP o SMS durante toda la sesión de transporte.", "bootstrap-server": "Servidor Bootstrap", "lwm2m-server": "Servidor LwM2M", "include-bootstrap-server": "Incluir actualizaciónes del servidor Bootstrap", @@ -1521,7 +1703,7 @@ "account-after-timeout": "Cuenta tras el tiempo de espera", "account-after-timeout-required": "Se requiere Cuenta tras el tiempo de espera.", "account-after-timeout-pattern": "Cuenta tras el tiempo de espera debe ser un número entero positivo.", - "account-after-timeout-tooltip": "Bootstrap-Server Account after the timeout value given by this resource.", + "account-after-timeout-tooltip": "Cuenta tras el tiempo de espera otorgado por este recurso.", "server-type": "Tipo de servidor", "add-new-server-title": "Añadir nueva configuración", "add-server-config": "Añadir configuración de servidor", @@ -1616,6 +1798,7 @@ "edge": { "edge": "Borde", "edge-instances": "Instancias de Borde", + "instances": "Instancias", "edge-file": "Archivo de borde", "name-max-length": "El nombre debe ser menor de 256", "label-max-length": "La etiqueta debe ser menor de 256", @@ -1666,6 +1849,7 @@ "make-private-edge-title": "¿Está seguro de que desea que el borde '{{edgeName}}' sea privado?", "make-private-edge-text": "Después de la confirmación, el borde y todos sus datos se harán privados y otros no podrán acceder a ellos", "import": "Importar borde", + "install-connect-instructions": "Instrucciones de instalación y conexión", "label": "Etiqueta", "load-entity-error": "Entidad no encontrada. No se pudo cargar la información", "assign-new-edge": "Asignar nuevo borde", @@ -1687,7 +1871,8 @@ "dashboard": "Panel de control Edge", "dashboards": "Paneles de borde", "rulechain-templates": "Plantillas, de cadena de reglas", - "rulechains": "Cadenas de regla de borde", + "edge-rulechain-templates": "Plantillas de cadena de reglas (Borde)", + "rulechains": "Cadenas de regla (Borde)", "search": "Bordes de búsqueda", "selected-edges": "{count, plural, =1 {1 borde} other {# bordes} } seleccionadas", "any-edge": "Cualquier bordee", @@ -1709,6 +1894,7 @@ "type-asset": "Activo", "type-device": "Dispositivo", "type-device-profile": "Perfil de dispositivo", + "type-asset-profile": "Perfil de activo", "type-entity-view": "Vista de entidad", "type-alarm": "Alarma", "type-rule-chain": "Cadena de reglas", @@ -1749,6 +1935,7 @@ "entity": "Entidad", "entities": "Entidades", "entities-count": "Nº de entidades", + "alarms-count": "Nº de alarmas", "aliases": "Alias de entidad", "entity-alias": "Alias de entidad", "unable-delete-entity-alias-title": "No ha sido posible eliminar el alias de entidad", @@ -1795,6 +1982,10 @@ "type-device-profiles": "Perfiles de dispositivo", "list-of-device-profiles": "{ count, plural, =1 {un perfil} other {Lista de # perfiles} }", "device-profile-name-starts-with": "Perfiles cuyo nombre empiece por '{{prefix}}'", + "type-asset-profile": "Perfil de activo", + "type-asset-profiles": "Perfiles de activo", + "list-of-asset-profiles": "{ count, plural, =1 {Un perfil de activo} other {Lista de # perfiles de activos} }", + "asset-profile-name-starts-with": "Perfiles de activo cuyo nombre comience por '{{prefix}}'", "type-asset": "Activo", "type-assets": "Activos", "list-of-assets": "{ count, plural, =1 {Un activo} other {Lista de # activos} }", @@ -1864,7 +2055,14 @@ "list-of-edges": "{count, plural, =1 {Un borde} other {Lista de # bordes} }", "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'", "type-tb-resource": "Recurso", - "type-ota-package": "Paquete OTA" + "type-ota-package": "Paquete OTA", + "type-rpc": "RPC", + "type-queue": "Cola", + "type-notification": "Notificación", + "type-notification-rule": "Regla de notificaciones", + "type-notification-target": "Destinatario de notificaciones", + "type-notification-request": "Solicitud de notificaciones", + "type-notification-template": "Plantilla de notificaciones" }, "entity-field": { "created-time": "Hora de creación", @@ -2014,7 +2212,9 @@ "body": "Cuerpo", "method": "Método", "type": "Tipo", + "message": "Mensaje", "message-id": "Id Mensaje", + "copy-message-id": "Copiar Id de mensaje", "message-type": "Tipo Mensaje", "data-type": "Tipo de Datos", "relation-type": "Tipo de relación", @@ -2025,17 +2225,23 @@ "success": "Éxito", "failed": "Fallo", "messages-processed": "Mensajes procesados", + "max-messages-processed": "Máximo de mensajes procesados", "min-messages-processed": "Mínimo de mensajes procesados", "errors-occurred": "Ocurrieron errores", - "min-errors-occurred": "Minimum errors occurred", + "max-errors-occurred": "Errores máximos ocurridos", + "min-errors-occurred": "Errores mínimos ocurridos", "min-value": "El valor mínimo es 0.", "all-events": "Todos", "has-error": "Tiene error", "entity-id": "Id de Entidad", + "copy-entity-id": "Copiar Id de entidad", "entity-type": "Tipo de entidad", "clear-filter": "Limpiar Filtro", "clear-request-title": "Borrar todos los eventos", - "clear-request-text": "Estás seguro de borrar todos los eventos?" + "clear-request-text": "Estás seguro de borrar todos los eventos?", + "started": "Arrancado", + "updated": "Actualizado", + "stopped": "Parado" }, "extension": { "extensions": "Extensiones", @@ -2194,6 +2400,9 @@ "file": "Fichero de extensiones", "invalid-file-error": "Fichero de extensiones inválido" }, + "feature": { + "advanced-features": "Características avanzadas" + }, "filter": { "add": "Añadir filtro", "edit": "Editar filtro", @@ -2233,6 +2442,7 @@ "ignore-case": "Ignorar mayús/minus", "value": "Valor", "remove-filter": "Borrar filtro", + "duplicate-filter-action": "Duplicar filtro", "preview": "Vista previa de filtro", "no-filters": "No hay filtros configurados", "add-filter": "Añadir filtro", @@ -2493,7 +2703,22 @@ "color": "Color", "main": "Principal", "right": "Derecho", - "select": "Seleccionar diseño de destino" + "left": "Izquierdo", + "select": "Seleccionar diseño de destino", + "percentage-width": "Ancho en porcentaje (%)", + "fixed-width": "Ancho fijo (px)", + "left-width": "Columna izquierda (%)", + "right-width": "Columna derecha (%)", + "pick-fixed-side": "Lado fijo: ", + "layout-fixed-width": "Ancho fijo (px)", + "value-min-error": "El valor debe ser mayor que {{min}}{{unit}}", + "value-max-error": "El valor debe ser menor que {{max}}{{unit}}", + "layout-fixed-width-required": "Se requiere ancho fijo", + "right-width-percentage-required": "Se requiere porcentaje derecha", + "left-width-percentage-required": "Se requiere porcentaje izquierda", + "divider": "Divisor", + "right-side": "Diseño derecho", + "left-side": "Diseño izquierdo" }, "legend": { "direction": "Dirección de la leyenda", @@ -2503,11 +2728,13 @@ "show-min": "Mostrar valor mínimo", "show-avg": "Mostrar valor promedio", "show-total": "Mostrar valor total", + "show-latest": "Mostrar último valor", "settings": "Configuración de la leyenda", "min": "mínimo", "max": "máximo", "avg": "promedio", "total": "total", + "latest": "último", "comparison-time-ago": { "previousInterval": "(intervalo anterior)", "customInterval": "(intervalo personalizado)", @@ -2558,6 +2785,247 @@ "copy-code": "Click para copiar", "copied": "Copiado!" }, + "notification": { + "action-button": "Botón de acción", + "action-type": "Tipo de acción", + "active": "Activo", + "add-notification-recipients-group": "Añadir grupo de destinatarios", + "add-notification-template": "Añadir plantilla de notificaciones", + "add-recipient": "Añadir destinatario", + "add-recipients": "Añadir destinatarios", + "add-rule": "Añadir regla", + "add-stage": "Añadir etapa", + "add-template": "Añadir plantilla", + "after": "Después", + "alarm-assignment-trigger-settings": "Ajustes de activación de asignación de alarmas", + "alarm-comment-trigger-settings": "Ajustes de activación comentarios de alarma", + "alarm-trigger-settings": "Ajustes de activación de alarma", + "all": "Todas", + "api-feature-hint": "Si el campo está vacío, la activación se aplicará a todas las características API", + "api-usage-trigger-settings": "Ajustes de activación de uso API", + "new-platform-version-trigger-settings": "Ajustes de activación de nueva versión de plataforma", + "at-least-one-should-be-selected": "Se debe seleccionar por lo menos uno", + "basic-settings": "Ajustes básicos", + "button-text": "Texto del botón", + "button-text-required": "Se requiere texto del botón", + "button-text-max-length": "El texto del botón debe ser menor o igual que {{ length }} caracteres", + "compose": "Redactar", + "conversation": "Conversación", + "conversation-required": "Se requiere conversación", + "copy-notification-template": "Copiar plantilla de notificación", + "copy-rule": "Copiar regla", + "copy-template": "Copiar plantilla", + "create-new": "Crear nueva", + "created": "Creada", + "delete-notification-text": "Atención, tras la confirmación, la notificación no será recuperable.", + "delete-notification-title": "Borrar la notificación, ¿Estás seguro?", + "delete-notifications-text": "Atención, tras la confirmación, las notificaciones no serán recuperables.", + "delete-notifications-title": "Estás seguro de eliminar { count, plural, =1 {1 notificación} other {# notificaciones} }?", + "delete-recipient-text": "Atención, tras la confirmación, el destinatario no será recuperable.", + "delete-recipient-title": "Estás seguro de eliminar el destinatario '{{recipientName}}'?", + "delete-recipients-text": "Atención, tras la confirmación, los destinatarios no serán recuperables.", + "delete-recipients-title": "Estás seguro de eliminar { count, plural, =1 {1 destinatario} other {# destinatarios} }?", + "delete-request-text": "Atención, tras la confirmación, la solicitud no será recuperable.", + "delete-request-title": "Estás seguro de eliminar la solicitud?", + "delete-requests-text": "Atención, tras la confirmación, las solicitudes no serán recuperables.", + "delete-requests-title": "Estás seguro de eliminar { count, plural, =1 {1 solicitud} other {# solicitudes} }?", + "delete-rule-text": "Atención, tras la confirmación, la regla no será recuperable.", + "delete-rule-title": "Estás seguro de eliminar la regla '{{ruleName}}'?", + "delete-rules-text": "Atención, tras la confirmación, las reglas no serán recuperables.", + "delete-rules-title": "Estás seguro de eliminar { count, plural, =1 {1 regla} other {# reglas} }?", + "delete-template-text": "Atención, tras la confirmación, la plantilla no será recuperable.", + "delete-template-title": "Estás seguro de eliminar la plantilla '{{templateName}}'?", + "delete-templates-text": "Atención, tras la confirmación, las plantillas no serán recuperables.", + "delete-templates-title": "Estás seguro de eliminar { count, plural, =1 {1 plantilla} other {# plantillas} }?", + "deleted": "Borrada", + "delivery-method": { + "delivery-method": "Método de entrega", + "email": "Email", + "email-preview": "Vista previa de notificación eMail", + "slack": "Slack", + "slack-preview": "Vista previa de notificación Slack", + "sms": "SMS", + "sms-preview": "Vista previa de notificación SMS", + "web": "Web", + "web-preview": "Vista previa de notificación Web" + }, + "delivery-method-not-configure-click": "El método de entrega no está configurado. Click para configurar.", + "delivery-method-not-configure-contact": "El método de entrega no está configurado. Contacta al administrador del sistema.", + "delivery-methods": "Métodos de entrega", + "description": "Descripción", + "device-activity-trigger-settings": "Ajustes de activación de dispositivo activo", + "device-list-rule-hint": "Si el campo está vacío, el disparo se aplicará a todos los dispositivos", + "device-profiles-list-rule-hint": "Si el campo está vacío, el disparo se aplicará a todos los perfiles de dispositivo", + "disabled": "Desactivado", + "edit-notification-recipients-group": "Editar grupo de destinatarios para notificaciones", + "edit-notification-template": "Editar plantilla de notificación", + "edit-rule": "Editar regla", + "edit-template": "Editar plantilla", + "enabled": "Activado", + "entities-limit-trigger-settings": "Ajustes de disparo en límites de entidad", + "entity-action-trigger-settings": "Ajustes de disparo en acciones de entidad", + "entity-type": "Tipo de entidad", + "escalation-chain": "Cadena de escalado", + "failed-send": "Fallos en el envío", + "fails": "{ count, plural, =1 {1 fallo} other {# fallos} }", + "filter": "Filtro", + "first-recipient": "Primer destinatario", + "inactive": "Inactivo", + "inbox": "Bandeja de entrada", + "notification-inbox": "Notificaciones / Bandeja de entrada", + "input-field-support-templatization": "El campo soporta plantillas.", + "input-fields-support-templatization": "Los campos soportan plantillas.", + "link": "Enlace", + "link-required": "Se requiere enlace", + "link-type": { + "dashboard": "Abrir panel", + "link": "Abrir enlace URL" + }, + "management": "Administración de notificaciones", + "mark-all-as-read": "Marcar todas como leídas", + "mark-as-read": "Marcar como leída", + "message": "Mensaje", + "message-required": "Se requiere mensaje", + "message-max-length": "El mensaje debe ser menor o igual que {{ length }} caracteres", + "name": "Nombre", + "name-required": "Se requiere nombre", + "new-notification": "Nueva notificación", + "no-inbox-notification": "No se encontraron notificaciones", + "no-notification-request": "Sin solicitud de notificación", + "no-notification-templates": "No se encontraron plantillas", + "no-notifications-yet": "No hay notificaciones", + "no-recipients-notification": "No hay destinatarios", + "no-rule": "No hay regla configurada", + "no-rules-notification": "Sin reglas de notificación", + "no-severity-found": "No se encontró gravedad", + "no-severity-matching": "'{{severity}}' no encontrada.", + "no-template-matching": "No hay recursos que coincidan con '{{template}}'.", + "not-found-slack-recipient": "No se ha encontrado destinatario Slack", + "notification": "Notificación", + "notification-center": "Centro de notificaciones", + "notify": "notificar", + "notify-again": "Notificar de nuevo", + "notify-alarm-action": { + "acknowledged": "Alarma acusada", + "assigned": "Alarma asignada", + "cleared": "Alarma borrada", + "created": "Alarma creada", + "severity-changed": "Gravedad cambiada", + "unassigned": "Alarma no asignada" + }, + "notify-on": "Notificar en", + "notify-on-comment-update": "Notificar cuando haya un comentario nuevo", + "notify-on-required": "Se requiere notificar en", + "notify-on-unassign": "Notificar en deasignación", + "notify-only-user-comments": "Notificar sólo cuando haya comentarios de usuario", + "only-rule-chain-lifecycle-failures": "Sólo fallos de ciclo de vida en cadena de reglas", + "only-rule-node-lifecycle-failures": "Sólo fallos de ciclo de vida en nodos de reglas", + "platform-users": "Usuarios de la plataforma", + "recipient": "Destinatario", + "recipient-group": "Grupo de destinatarios", + "recipient-type": { + "affected-tenant-administrators": "Administradores afectados", + "affected-user": "Usuario afectado", + "affected-user-hint": "Sugerencia en usuario afectado", + "all-users": "Todos los usuarios", + "customer-users": "Usuarios del cliente", + "system-administrators": "Administradores del sistema", + "tenant-administrators": "Administradores de propietarios", + "user-filters": "Filtro de usuarios", + "user-list": "Lista de usuarios", + "users-entity-owner": "Usuarios que sean propietarios de la entidad", + "users-entity-owner-hint": "Sugerencia en usuarios propietarios de la entidad" + }, + "recipients": "Destinatarios", + "notification-recipients": "Notificaciones / Destinatarios", + "recipients-count": "{ count, plural, =1 {1 destinatario} other {# destinatarios} }", + "recipients-required": "Se requieren destinatarios", + "refresh-allow-delivery-method": "Actualiza permitir método de entrega", + "request-search": "Búsqueda de solicitudes", + "request-status": { + "processing": "Procesando", + "scheduled": "En cola", + "sent": "Enviado" + }, + "review": "Examinar", + "rule": "Regla", + "rule-chain-list-rule-hint": "Si el campo está vacío, el disparo se aplicará a todas las cadenas de reglas", + "rule-engine-events-trigger-settings": "Ajustes de disparo en motor de reglas", + "rule-engine-filter": "Filtro de motor de reglas", + "rule-name": "Nombre de regla", + "rule-name-required": "Se requiere nombre", + "rule-node-filter": "Filtro de regla", + "rules": "Reglas", + "notification-rules": "Notificaciones / Reglas", + "scheduler-later": "Programar para más tarde", + "search-notification": "Buscar notificaciones", + "search-recipients": "Buscar destinatarios", + "search-rules": "Buscar reglas", + "search-templates": "Buscar plantillas", + "see-documentation": "Ver documentación", + "selected-notifications": "{ count, plural, =1 {1 notificación} other {# notificaciones} } seleccionadas", + "selected-recipients": "{ count, plural, =1 {1 destinatario} other {# destinatarios} } seleccionados", + "selected-requests": "{ count, plural, =1 {1 solicitud} other {# solicitudes} } seleccionados", + "selected-rules": "{ count, plural, =1 {1 regla} other {# reglas} } seleccionados", + "selected-template": "{ count, plural, =1 {1 plantilla} other {# plantillas} } seleccionados", + "send-notification": "Enviar notificación", + "sent": "Enviado", + "notification-sent": "Notificaciones / Enviadas", + "set-entity-from-notification": "Establecer entidad de notificación a estado de panel", + "slack-chanel-type": "Tipo de canal Slack", + "slack-chanel-types": { + "direct": "Mensaje directo", + "private-channel": "Canal privado", + "public-channel": "Canal público" + }, + "start-from-scratch": "Empezar de 0", + "status": "Estado", + "stop-escalation-alarm-status-become": "Detener la escalada cuando el estado de alarma se convierta en:", + "subject": "Asunto", + "subject-required": "Se requiere asunto", + "template": "Plantilla", + "template-name": "Nombre de plantilla", + "template-required": "Se requiere nombre de plantilla", + "template-type": { + "alarm": "Alarma", + "alarm-assignment": "Asignación de alarma", + "alarm-comment": "Comentario de alarma", + "api-usage-limit": "Límite en uso de API", + "device-activity": "Actividad de dispositivo", + "entities-limit": "Límites de entidades", + "entity-action": "Acciones de entidad", + "general": "General", + "rule-engine-lifecycle-event": "Evento de ciclo de vida en motor de reglas", + "rule-node": "Nodo de regla", + "new-platform-version": "Nueva versión de plataforma" + }, + "templates": "Plantillas", + "notification-templates": "Notificaciones / Plantillas", + "tenant-profiles-list-rule-hint": "Si el campo está vacío, el disparo se aplicará a todos los perfiles de administradores propietarios", + "tenants-list-rule-hint": "Si el campo está vacío, el disparo se aplicará a todos los administradores propietarios", + "threshold": "Umbral", + "time": "Hora", + "track-rule-node-events": "Hacer seguimiento de eventos en nodos de reglas", + "trigger": { + "alarm": "Alarma", + "alarm-assignment": "Asignación de alarma", + "alarm-comment": "Comentario de alarma", + "api-usage-limit": "Límite en uso de API", + "device-activity": "Actividad de dispositivo", + "entities-limit": "Límites de entidades", + "entity-action": "Actiones de entidades", + "rule-engine-lifecycle-event": "Evento de ciclo de vida en Motor de reglas", + "new-platform-version": "Nueva versión de plataforma", + "trigger": "Disparo", + "trigger-required": "Se requiere disparo" + }, + "type": "Tipo", + "unread": "No leído", + "updated": "Actualizado", + "use-template": "Usar plantilla", + "view-all": "Ver todos", + "warning": "Alerta" + }, "ota-update": { "add": "Añadir paquete", "assign-firmware": "Firmware asignado", @@ -2640,8 +3108,12 @@ "tokenCopiedSuccessMessage": "JWT copiado al portapapeles", "tokenCopiedWarnMessage": "JWT caducado, por favor actualiza la página." }, + "profiles": { + "profiles": "Perfiles" + }, "security": { "security": "Seguridad", + "general-settings": "Ajusters generales de seguridad", "2fa": { "2fa": "Autenticación de doble factor (2FA)", "2fa-description": "La autenticación de doble factor proteje tu cuenta de accesos no autorizados. Lo único que tienes que hacer es entrar un código de seguridad al hacer login.", @@ -2786,6 +3258,7 @@ }, "rulechain": { "rulechain": "Cadena de Regla", + "rulechain-events": "Eventos de cadena de reglas", "rulechains": "Cadenas de Reglas", "root": "Raíz", "delete": "Borrar cadena de reglas", @@ -2847,6 +3320,7 @@ "unassign-rulechains": "Anular asignación de cadenas de reglas" }, "rulenode": { + "rule-node-events": "Eventos de nodo de reglas", "details": "Detalles", "events": "Eventos", "search": "Buscar nodos", @@ -2868,6 +3342,7 @@ "deselect-all": "Deshacer selección de todos", "rulenode-details": "Detalles del nodo de reglas", "debug-mode": "Modo Debug", + "singleton-mode": "Módo único", "configuration": "Configuración", "link": "Enlace", "link-details": "Detalles del enlace del nodo de reglas", @@ -2903,6 +3378,8 @@ "ui-resources-load-error": "Error al cargar los recursos de configuración UI.", "invalid-target-rulechain": "No se puede resolver la cadena de reglas objetivo!", "test-script-function": "Probar Script de función", + "script-lang-java-script": "Java Script", + "script-lang-tbel": "TBEL", "message": "Mensaje", "message-type": "Tipo de mensaje", "select-message-type": "Seleccionar tipo de mensaje", @@ -2923,11 +3400,13 @@ }, "queue": { "queue-name": "Cola", + "no-queues-found": "No se encontraron colas.", "no-queues-matching": "No se encontraron colas que coincidan con '{{queue}}'.", "select_name": "Selecciona el nombre de la cola", "name": "Nombre Cola", - "name_required": "Necesario especificar el nombre de cola", + "name-required": "Necesario especificar el nombre de cola", "name-unique": "El nombre de cola ya existe!", + "name-pattern": "El nombre de cola contiene un carácter no ASCII, '.', '_' y '-'!", "queue-required": "Cola requerida!", "topic-required": "Topic cola requerido!", "poll-interval-required": "Intervalo de obtención requerido!", @@ -2959,10 +3438,17 @@ "add" : "Añadir cola", "details": "Detalles cola", "topic": "Topic", + "submit-settings": "Ajustes de envío", "submit-strategy": "Estrategia de envío", + "grouping-parameter": "Parámetros de agrupado", + "processing-settings": "Ajustes de procesamiento", "processing-strategy": "Estrategia de procesamiento", + "retries-settings": "Ajustes de reintentos", + "polling-settings": "Ajustes de obtención", + "batch-processing": "Procesado de lotes", "poll-interval": "Intervalo de obtención", "partitions": "Particiones", + "immediate-processing": "Procesado inmediato", "consumer-per-partition": "Consumidores por partición", "consumer-per-partition-hint": "Activar consumidores separados para cada partición", "processing-timeout": "Timeout de procesamiento, ms", @@ -3002,6 +3488,19 @@ "retry-failed-and-timeout-hint": "Reintentar todos los mensajes fallidos y que den timeout del lote de procesamiento" } }, + "server-error": { + "general": "Error general de servidor", + "authentication": "Error de autenticación", + "jwt-token-expired": "Token JWT expirado", + "tenant-trial-expired": "Prueba propietario expirada", + "credentials-expired": "Credenciales expirados", + "permission-denied": "Permiso denegado", + "invalid-arguments": "Argumentos inválidos", + "bad-request-params": "Parámetros de consulta no válidos", + "item-not-found": "Item no encontrado", + "too-many-requests": "Demasiadas solicitudes", + "too-many-updates": "Demasiadas actualizaciones" + }, "tenant": { "tenant": "Propietario", "tenants": "Propietarios", @@ -3038,6 +3537,7 @@ "tenant-profile": "Perfil de propietario", "tenant-profiles": "Perfiles de propietarios", "add": "Añadir perfil de propietario", + "add-profile": "Añadir perfil", "edit": "Editar perfil de propietario", "tenant-profile-details": "Detalles perfil de propietario", "no-tenant-profiles-text": "No se encontraron perfiles de propietario", @@ -3070,6 +3570,14 @@ "export-failed-error": "No se ha podido exportar el perfil de propietario: {{error}}", "tenant-profile-file": "Archivo de perfil de propietario", "invalid-tenant-profile-file-error": "No se ha podido importar el perfil de propietario: Estructura de datos inválida.", + "advanced-settings": "Ajustes avanzados", + "entities": "Entidades", + "rule-engine": "Cadena de reglas", + "time-to-live": "Time-to-live", + "alarms-and-notifications": "Alarmas y notificaciones", + "ota-files-in-bytes": "Tamaño de fichero OTA en bytes", + "ws-title": "WS", + "unlimited": "(0 - ilimitado)", "maximum-devices": "Nº Máximo de dispositivos (0 - sin límite)", "maximum-devices-required": "Nº Máximo de dispositivos requerido.", "maximum-devices-range": "Nº Máximo de dispositivos no puede ser negativo", @@ -3094,6 +3602,7 @@ "maximum-ota-packages-sum-data-size": "Tamaño máximo de paquetes OTA en bytes (0 - sin límite)", "maximum-ota-package-sum-data-size-required": "Tamaño máximo de paquetes OTA requerido.", "maximum-ota-package-sum-data-size-range": "Tamaño máximo de paquetes OTA no puede ser negativo", + "rest-requests-for-tenant": "Consultas REST por propietario", "transport-tenant-telemetry-msg-rate-limit": "Tasa de mensajes de telemetría por propietario.", "transport-tenant-telemetry-data-points-rate-limit": "Tasa de datapoints por propietario.", "transport-device-msg-rate-limit": "Tasa de mensajes de dispositivo.", @@ -3101,6 +3610,8 @@ "transport-device-telemetry-data-points-rate-limit": "Tasa de datapoints de telemetría de dispositivo.", "tenant-entity-export-rate-limit": "Tasa de creación de versión de entidades", "tenant-entity-import-rate-limit": "Tasa de carga de versión de entidades", + "tenant-notification-request-rate-limit": "Consultas de notificaciones", + "tenant-notification-requests-per-rule-rate-limit": "Consultas de notificaciones por regla de notificación", "max-transport-messages": "Nº Máximo de mensajes de transporte (0 - sin límite)", "max-transport-messages-required": "Nº Máximo de mensajes de transporte requerido.", "max-transport-messages-range": "Nº Máximo de mensajes de transporte no puede ser negativo", @@ -3137,24 +3648,64 @@ "max-created-alarms": "Nº Máximo de alarmas creadas (0 - sin límite)", "max-created-alarms-required": "Nº Máximo de alarmas creadas requerido.", "max-created-alarms-range": "Nº Máximo de alarmas creadas no puede ser negativo", - "no-queue": "No Queue configured", - "add-queue": "Add Queue", - "queues-with-count": "Queues ({{count}})", - "tenant-rest-limits": "Rate limit for REST requests for tenant", - "customer-rest-limits": "Rate limit for REST requests for customer", - "incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60", - "too-small-value-zero": "The value must be bigger than 0", - "too-small-value-one": "The value must be bigger than 1", - "cassandra-tenant-limits-configuration": "Cassandra query rate limit for tenant", - "ws-limit-max-sessions-per-tenant": "Maximum number of WS sessions per tenant", - "ws-limit-max-sessions-per-customer": "Maximum number of WS sessions per customer", - "ws-limit-max-sessions-per-public-user": "Maximum number of WS sessions per public user", - "ws-limit-queue-per-session": "Maximum size of WS message queue per session", - "ws-limit-max-subscriptions-per-tenant": "Maximum number of WS subscriptions per tenant", - "ws-limit-max-subscriptions-per-customer": "Maximum number of WS subscriptions per customer", - "ws-limit-max-subscriptions-per-regular-user": "Maximum number of WS subscriptions per regular user", - "ws-limit-max-subscriptions-per-public-user": "Maximum number of WS subscriptions per public user", - "ws-limit-updates-per-session": "Rate limit for WS updates per session" + "no-queue": "No hay cola configurada", + "add-queue": "Añadir cola", + "queues-with-count": "({{count}}) Colas", + "tenant-rest-limits": "Límite de frecuencia de solicitudes REST por propietario", + "customer-rest-limits": "Límite de frecuencia de solicitudes REST por cliente", + "incorrect-pattern-for-rate-limits": "El formato correcto son pares de valores separados por el símbolo : capacidad y período (en segundos), ejemplo: 100:1,2000:60", + "too-small-value-zero": "El valor debe ser mayor de 0", + "too-small-value-one": "El valor debe ser mayor de 1", + "queue-size-is-limited-by-system-configuration": "El tamaño de la cola, también se limita por la configuración del sistema.", + "cassandra-tenant-limits-configuration": "Límite en tasa de consulta de Cassandra por propietario", + "ws-limit-max-sessions-per-tenant": "Número máximo de sesiones WS por propietario", + "ws-limit-max-sessions-per-customer": "Número máximo de sesiones WS por cliente", + "ws-limit-max-sessions-per-regular-user": "Número máximo de sesiones por usuario regular", + "ws-limit-max-sessions-per-public-user": "Número máximo de sesiones WS por usuario público", + "ws-limit-queue-per-session": "Tamaño máximo de cola de mensaje WS por sesión", + "ws-limit-max-subscriptions-per-tenant": "Número máximo de subscripciones WS por administrador propietario", + "ws-limit-max-subscriptions-per-customer": "Número máximo de subscripciones de WS por cliente", + "ws-limit-max-subscriptions-per-regular-user": "Número máximo de subscripciones de WS por usuario regular", + "ws-limit-max-subscriptions-per-public-user": "Número máximo de subscripciones de WS por usuario público", + "ws-limit-updates-per-session": "Frecuencia de actualizaciones WS por sesión", + "rate-limits": { + "add-limit": "Añadir límite", + "advanced-settings": "Ajustes avanzados", + "edit-limit": "Editar límite", + "but-less-than": "pero menor que", + "edit-transport-tenant-msg-title": "Editar límite de mensajes de transporte (Propietario)", + "edit-transport-tenant-telemetry-msg-title": "Editar límite de mensajes de telemetría (Propietario)", + "edit-transport-tenant-telemetry-data-points-title": "Editar límite de puntos de datos de transporte (Propietario)", + "edit-transport-device-msg-title": "Editar tasa de mensajes de dispositivo", + "edit-transport-device-telemetry-msg-title": "Editar tasa de mensajes de transporte de dispositivo", + "edit-transport-device-telemetry-data-points-title": "Editar límite de puntos de datos de transporte (Dispositivo)", + "edit-tenant-rest-limits-title": "Editar límite de solicitudes REST por propietario", + "edit-customer-rest-limits-title": "Editar límite de solicitudes REST por cliente", + "edit-ws-limit-updates-per-session-title": "Editar límite de actualizaciones WS por sesión", + "edit-cassandra-tenant-limits-configuration-title": "Editar límites de consultas en BD Cassandra por propietario", + "edit-tenant-entity-export-rate-limit-title": "Editar tasa límite de creación de versiones por entidad", + "edit-tenant-entity-import-rate-limit-title": "Editar tasa límite de carga de versión de entidad", + "edit-tenant-notification-request-rate-limit-title": "Editar tasa límite de solicitudes de notificaciones", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Editar tasa límite de solicitudes en reglas por noticación", + "messages-per": "mensajes por", + "not-set": "No configurado", + "number-of-messages": "Número de mensajes", + "number-of-messages-required": "Se requiere número de mensajes.", + "number-of-messages-min": "El valor mínimo es 1.", + "preview": "Previsualizar", + "per-seconds": "Por segundos", + "per-seconds-required": "Se requiere tasa de tiempo.", + "per-seconds-min": "El valor mínimo es 1.", + "rate-limits": "Límites de tasa", + "remove-limit": "Quitar límite", + "transport-tenant-msg": "Mensajes de transporte (propietario)", + "transport-tenant-telemetry-msg": "Mensajes de telemetría (propietario)", + "transport-tenant-telemetry-data-points": "Puntos de datos de telemetría (propietario)", + "transport-device-msg": "Mensajes de transporte (dispositivo)", + "transport-device-telemetry-msg": "Mensajes de telemetría (dispositivo)", + "transport-device-telemetry-data-points": "Puntos de datos de telemetría (dispositivo)", + "sec": "sec" + } }, "timeinterval": { "seconds-interval": "{ seconds, plural, =1 {1 segundo} other {# segundos} }", @@ -3173,6 +3724,8 @@ "previous-week": "Semana anterior (Dom - Sáb)", "previous-week-iso": "Semana anterior (Lun - Dom)", "previous-month": "Mes anterior", + "previous-quarter": "Anterior trimestre", + "previous-half-year": "Anterior semestre", "previous-year": "Año anterior", "current-hour": "Hora actual", "current-day": "Día actual", @@ -3183,6 +3736,10 @@ "current-week-iso-so-far": "Semana actual hasta hoy (Lun - Dom)", "current-month": "Mes actual", "current-month-so-far": "Mes actual hasta hoy", + "current-quarter": "Trimestre actual", + "current-quarter-so-far": "Trimestre actual hasta hoy", + "current-half-year": "Semestre en curso", + "current-half-year-so-far": "Semestre en curso hasta hoy", "current-year": "Año actual", "current-year-so-far": "Año actual hasta ahora" } @@ -3195,20 +3752,35 @@ "days": "Días" }, "timewindow": { + "years": "{ years, plural, =1 { año } other {# años } }", + "months": "{ months, plural, =1 { mes } other {# meses } }", + "weeks": "{ weeks, plural, =1 { semana } other {# semanas } }", "days": "{ days, plural, =1 { día } other {# días } }", "hours": "{ hours, plural, =0 { horas } =1 {1 hora } other {# horas } }", + "hr": "{{ hr }} hr", "minutes": "{ minutes, plural, =0 { minutos } =1 {1 minuto } other {# minutos } }", + "min": "{{ min }} min", "seconds": "{ seconds, plural, =0 { segundos } =1 {1 segundo } other {# segundos } }", + "sec": "{{ sec }} sec", + "short": { + "days": "{ days, plural, =1 {1 día } other {# días } }", + "hours": "{ hours, plural, =1 {1 hora } other {# horas } }", + "minutes": "{{minutes}} min ", + "seconds": "{{seconds}} sec " + }, "realtime": "Tiempo-real", "history": "Histórico", "last-prefix": "último(s)", "period": "desde {{ startTime }} hasta {{ endTime }}", "edit": "Editar ventana de tiempo", "date-range": "Rango de fechas", + "for-all-time": "Desde siempre", "last": "Últimos(s)", "time-period": "Período de tiempo", "hide": "Ocultar", - "interval": "Intervalo" + "interval": "Intervalo", + "just-now": "Ahora mismo", + "ago": "hace" }, "user": { "user": "Usuario", @@ -3259,7 +3831,9 @@ "enable-account-message": "¡La cuenta de usuario se ha habilitado correctamente!", "disable-account-message": "¡La cuenta de usuario se deshabilitó correctamente!", "copyId": "Copiar Id de usuario", - "idCopiedMessage": "El Id de usuario se ha copiado al portapapeles" + "idCopiedMessage": "El Id de usuario se ha copiado al portapapeles", + "user-list": "Lista de usuarios", + "user-list-required": "Se requiere lista de usuarios" }, "value": { "type": "Tipo de valor", @@ -3286,6 +3860,7 @@ "version-control": { "version-control": "Control de Versión", "management": "Administrador de versiones", + "search": "Buscar versiones", "branch": "Rama", "default": "Por defecto", "select-branch": "Seleccionar rama", @@ -3305,12 +3880,15 @@ "no-versions-text": "No se han encontrado versiones", "copy-full-version-id": "Copiar el ID de versión", "create-version": "Crear versión", + "creating-version": "Creando versión... Por favor, espere", "nothing-to-commit": "No hay cambios a publicar", "restore-version": "Restaurar versión", "restore-entity-from-version": "Restaurar entidad desde versión '{{versionName}}'", + "restoring-entity-version": "Restaurando versión de entidad... Por favor, espere", "load-relations": "Cargar relaciones", "load-attributes": "Cargar atributos", "load-credentials": "Cargar credencialess", + "compare-with-current": "Comparar con actual", "diff-entity-with-version": "Diff de la versión de entidad '{{versionName}}'", "previous-difference": "Anterior diferencia", "next-difference": "Siguiente diferencia", @@ -3332,6 +3910,7 @@ "remove-other-entities": "Borrar otras entidades", "find-existing-entity-by-name": "Buscar entidad existente por nombre", "restore-entities-from-version": "Restaurar entidades desde la versión '{{versionName}}'", + "restoring-entities-from-version": "Restaurando entidades... Por favor, espere", "no-entities-restored": "No se restauraron entidades", "created": "{{created}} creadas", "updated": "{{updated}} actualizadas", @@ -3342,7 +3921,9 @@ "sync-strategy-merge-hint": "Crea o actualiza las entidades seleccionadas en el repositorio. Las demás entidades no serán modificadas.", "sync-strategy-overwrite-hint": "Crea o actualiza las entidades seleccionadas en el repositorio. Las demás entidades serán borradas.", "device-credentials-conflict": "Fallo al cargar el dispositivo con ID externo {{entityId}}
debido a que las mismas credenciales están ya presentes en la base de datos para otro dispositivo.
Por favor, considera desactivar el ajuste cargar credenciales en el formulario de restauración.", - "missing-referenced-entity": "Fallo al cargar {{sourceEntityTypeName}} con ID externo {{sourceEntityId}}
porque hace referencia al tipo de entidad {{targetEntityTypeName}} con el ID {{targetEntityId}}." + "missing-referenced-entity": "Fallo al cargar {{sourceEntityTypeName}} con ID externo {{sourceEntityId}}
porque hace referencia al tipo de entidad {{targetEntityTypeName}} con el ID {{targetEntityId}}.", + "runtime-failed": "Fallo: {{message}}", + "auto-commit-settings-read-only-hint": "Los ajustes de Auto-commit no funcionan, si la opción de solo lectura está activada en los ajustes del repositorio." }, "widget": { "widget-library": "Bibloteca de Widgets", @@ -3521,6 +4102,7 @@ "order": "Orden", "height": "Altura", "mobile-hide": "Ocultar widget en modo móvil", + "desktop-hide": "Ocultar widget en modo escritorio", "units": "Caracter especial a mostrar en el siguiente valor", "decimals": "Números de dígitos después de la coma", "timewindow": "Ventana de tiempo", @@ -3530,6 +4112,7 @@ "display-legend": "Mostrar leyenda", "datasources": "Set de datos", "maximum-datasources": "Un máximo de { count, plural, =1 {1 set de datos es permitido.} other {# set de datos son permitidos} }", + "timeseries-key-error": "Se debe especificar al menos un datakey tipo timeseries", "datasource-type": "Tipo", "datasource-parameters": "Parámetros", "remove-datasource": "Eliminar set de datos", @@ -3643,6 +4226,7 @@ "line-settings": "Ajustes de línea", "show-line": "Mostrar línea", "fill-line": "Rellenar línea", + "fill-line-opacity": "Opacidad de línea", "points-settings": "Ajustes de puntos", "show-points": "Mostrar puntos", "points-line-width": "Ancho de línea en puntos", @@ -4071,8 +4655,9 @@ "datakey-value-type-string": "String", "datakey-value-type-double": "Double", "datakey-value-type-integer": "Entero", - "datakey-value-type-boolean-checkbox": "Boolean (Checkbox)", - "datakey-value-type-boolean-switch": "Boolean (Switch)", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "Booleano (Checkbox)", + "datakey-value-type-boolean-switch": "Booleano (Switch)", "datakey-value-type-date-time": "Fecha & Hora", "datakey-value-type-date": "Fecha", "datakey-value-type-time": "Hora", @@ -4095,8 +4680,10 @@ "error-messages": "Mensajes de erro", "min-value-error-message": "Mensaje de error de 'Valor Mínimo'", "max-value-error-message": "Mensaje de error de 'Valor Máximo'", - "invalid-date-error-message": "Mensaje de erro de 'Fecha Inválida'", + "invalid-date-error-message": "Mensaje de error de 'Fecha Inválida'", + "invalid-JSON-error-message": "Mensaje de error de 'JSON inválido'", "icon-settings": "Ajustes de iconos", + "dialog-editor-settings": "Ajustes de editor de diálogo", "use-custom-icon": "Usar icono personalizado", "input-cell-icon": "Icono a mostrar antes del campo de entrada", "value-conversion-settings": "Ajustes de conversión de valor", @@ -4105,12 +4692,16 @@ "get-value-function": "Función getValue", "set-value-settings": "Ajustes de establecimiento de valores", "use-set-value-function": "Usar función setValue", - "set-value-function": "Función setValue" + "set-value-function": "Función setValue", + "json-invalid": "El valor JSON tiene un formato inválido", + "title": "Título", + "cancel-button-label": "Etiqueta del botón 'Cancelar'" }, "invalid-qr-code-text": "Texto de entrara inválido para el código QR. La entrada debe ser de tipo string", "qr-code": { "use-qr-code-text-function": "Usar función de texto QR", "qr-code-text-pattern": "Patrón del código QR (por ej. '${entityName} | ${keyName} - texto adicional.')", + "qr-code-text-pattern-hint": "El patrón del código QR usa el valor de la primera clave encontrada en las entidades del alias.", "qr-code-text-pattern-required": "Se requiere patrón del código QR.", "qr-code-text-function": "Función del código QR" }, @@ -4176,8 +4767,8 @@ }, "rpc-search-status-all": "TODOS", "message-types": { - "false": "Two-way", - "true": "One-way" + "false": "Bidireccional (Two-way)", + "true": "Unidireccional (One-way)" }, "general-settings": "Ajustes Generales", "enable-filter": "Activar filtro", @@ -4295,10 +4886,11 @@ "map-provider-image": "Mapa de imagen", "map-provider-tencent": "Tencent maps", "openstreet-provider": "Proveedor OpenStreet map", - "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Default)", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Defecto)", "openstreet-provider-hot": "OpenStreetMap.HOT", "openstreet-provider-esri-street": "Esri.WorldStreetMap", "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", "openstreet-provider-cartodb-positron": "CartoDB.Positron", "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", "use-custom-provider": "Usar proveedor personalizado", @@ -4317,6 +4909,8 @@ "credentials": "Credenciales", "here-app-id": "HERE app id", "here-app-code": "HERE app code", + "here-api-key": "HERE API key", + "here-use-new-version-api-3": "Usar API versión 3", "tencent-maps-api-key": "API Key Tencent Maps", "tencent-map-type-roadmap": "Carretera", "tencent-map-type-satelite": "Satelite", @@ -4333,6 +4927,7 @@ "default-map-zoom-level": "Nivel de zoom por defecto (0 - 20)", "default-map-center-position": "Posición central del mapa por defecto (0,0)", "disable-scroll-zooming": "Desactivar zoom en scroll", + "disable-double-click-zooming": "Desactivar zoom en doble click", "disable-zoom-control-buttons": "Desactivar botones de zoom", "fit-map-bounds": "Ajustar los límites del mapa para cubrir todos los marcadores", "use-default-map-center-position": "Usar posición central por defecto", @@ -4463,12 +5058,16 @@ "point-color-function": "Función de color de puntos", "use-point-as-anchor": "Usar punto como ancla", "point-as-anchor-function": "Función de punto como ancla", - "independent-point-tooltip": "Sugerencia independiente en punto" + "independent-point-tooltip": "Sugerencia independiente en punto", + "clustering-markers": "Marcadores agrupados", + "use-icon-create-function": "Usar función de color en marcadores", + "marker-color-function": "Función de color del marcador" }, "markdown": { "use-markdown-text-function": "Usar función markdown/HTML", "markdown-text-function": "Función de valor Markdown/HTML", "markdown-text-pattern": "Patrón de Markdown/HTML (markdown o HTML con variables, por ej. '${entityName} o ${keyName} - texto.')", + "apply-default-markdown-style": "Aplicar estilo por defecto en markdown", "markdown-css": "Markdown/HTML CSS" }, "simple-card": { @@ -4509,10 +5108,12 @@ "entity-label-column-title": "Título de columna en etiqueta de entidad", "display-entity-type": "Mostrar columna de tipo de entidad", "default-sort-order": "Ordenación por defecto", + "custom-title": "Título de encabezado", "column-width": "Ancho de columna (px o %)", "default-column-visibility": "Visibilidad por defecto en columna", "column-visibility-visible": "Visible", "column-visibility-hidden": "Oculta", + "column-visibility-hidden-mobile": "Oculto en modo móvil", "column-selection-to-display": "Selección de columnas en 'Columnas a Mostrar'", "column-selection-to-display-enabled": "Activada", "column-selection-to-display-disabled": "Desactivada", @@ -4522,7 +5123,9 @@ "enable-alarm-filter": "Activar filtro de alarmas", "display-alarm-details": "Mostrar detalles de alarma", "allow-alarms-ack": "Permitir reconocimiento de alarmas", - "allow-alarms-clear": "Permitir borrado de alarmas" + "allow-alarms-clear": "Permitir borrado de alarmas", + "display-alarm-activity": "Mostrar actividad de alarmas", + "allow-alarms-assign": "Permitir asignar alarmas" }, "value-source": { "value-source": "Origen valor", @@ -4546,7 +5149,201 @@ "font-weight-bolder": "Negrita+", "font-weight-lighter": "Lighter", "color": "Color", - "shadow-color": "Color sombra" + "shadow-color": "Color de sombra" + }, + "home": { + "no-data-available": "No hay datos disponibles" + }, + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "Disco", + "cpu-warning-text": "Uso de CPU alto. Para evitar fallos en el sistema, optimiza el rendimiento.", + "cpu-critical-text": "Uso de CPU crítico. Para evitar fallos en el sistema, optimiza el rendimiento.", + "ram-warning-text": "Baja reserva de RAM. Para evitar fallos en el sistema, optimiza el rendimiento o incrementa el tamaño de RAM.", + "ram-critical-text": "Reserva de RAM crítica. Para evitar fallos en el sistema, optimiza el rendimiento o incrementa el tamaño de RAM.", + "disk-warning-text": "Espacio libre en disco bajo. Para evitar pérdida de datos, libera o expande el disco.", + "disk-critical-text": "Espacio libre en disco crítico. Para evitar pérdida de datos, libera o expande el disco." + }, + "cluster-info": { + "service-id": "Id. Servicio", + "service-type": "Tipo de servicio", + "no-data": "Sin datos" + }, + "transport-messages": { + "title": "Mensajes de transporte", + "info": "Todos los mensajes que llegan desde los dispositivos" + }, + "activity": { + "title": "Actividad" + }, + "documentation": { + "title": "Documentación", + "add-link": "Añadir enlace", + "add-link-title": "Añadir enlace de documentación", + "name": "Nombre", + "name-required": "Se requiere nombre.", + "link": "Enlace", + "link-required": "Se requiere enlace.", + "columns": "Columnas" + }, + "quick-links": { + "title": "Enlaces rápidos", + "add-link": "Añadir enlace", + "add-link-title": "Añadir enlace rápido", + "quick-link": "Enlace rápido", + "quick-link-required": "Se requiere enlace rápido.", + "no-links-matching": "No se encontraron enlaces que coincidan con '{{name}}'.", + "columns": "Columnas" + }, + "recent-dashboards": { + "title": "Paneles", + "last": "Última visualización", + "starred": "Favorito", + "name": "Nombre", + "last-viewed": "Último visto", + "no-last-viewed-dashboards": "No hay paneles aún" + }, + "configured-features": { + "title": "Características configuradas", + "info": "Estado de las características que requieren configuración", + "email-feature": "Email", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2FA", + "feature-configured": "Característica configurada.\nClick para acceder a configuración", + "feature-not-configured": "Característica no configurada.\nClick para configurar" + }, + "version-info": { + "title": "Versión", + "contact-us": "Contáctanos", + "current-version": "Versión actual", + "current": "Actual", + "available-version": "Versión disponible", + "available": "Disponible", + "upgrade": "Actualizar", + "version-is-up-to-date": "La versión está actualizada" + }, + "usage-info": { + "title": "Uso", + "entities": "Entidades", + "api-calls": "Llamadas API" + }, + "functions": { + "title": "Funciones", + "pe-feature-tooltip": "Sólo en ThingsBoard\nProfessional Edition", + "switch-to-pe": "Cambiar a PE", + "alarms": "Alarmas", + "dashboards": "Paneles", + "entities-and-relations": "Entidades y relaciones", + "profiles": "Perfiles", + "advanced-features": "Características avanzadas", + "notification-center": "Centro de notificaciones", + "api-usage": "Uso de API", + "customers": "Clientes", + "customers-hierarchy": "Jerarquía de clientes", + "roles-and-permissions": "Reglas y permisos", + "groups": "Grupos", + "integrations": "Integraciones", + "solution-templates": "Plantillas de solución", + "scheduler": "Programador", + "white-labeling": "Marca-blanca" + }, + "devices": { + "view-docs": "Ver documentación", + "inactive": "Inactivos", + "active": "Activos", + "total": "Total" + }, + "alarms": { + "critical": "Críticas", + "assigned-to-me": "Asignadas a mí", + "total": "Total" + }, + "getting-started": { + "get-started": "Empezar", + "finish": "Finalizar", + "done-welcome-title": "Bienvenido a bordo", + "done-welcome-text": "Fantástico, lo has terminado!", + "sys-admin": { + "step1": { + "title": "Crear un propietario y un administrador propietario", + "content": "

Un propietario es un individual o una organización, que tiene o produce dispositivos o activos. Los Propietarios, pueden crear usuarios tipo Administrador Propietario.

Sigue la documentación para proceder:

", + "how-to-create-tenant": "Cómo crear un Propietario" + }, + "step2": { + "title": "Configurar característica: Servidor emails", + "content": "

Los usuarios reciben emails de activación y reset de contraseña a través de un servidor SMTP.

Sigue la documentación para proceder:

", + "how-to-configure-mail-server": "Cómo configurar el Servidor eMail" + }, + "step3": { + "title": "Configurar característica: Proveedor SMS", + "content": "

Para distribuir mensajes SMS sobre alarmas, el administrador del sistema debe configurar proveedores SMS.

Sigue la documentación para proceder:

", + "how-to-configure-sms-provider": "Cómo configurar el proveedor SMS" + }, + "step4": { + "title": "Configurar característica: 2FA", + "content": "

Añadiendo autenticación 2FA (doble factor) a tu cuenta, incrementa la seguridad y previene accesos maliciosos.

Sigue la documentación para proceder:

", + "how-to-configure-2fa": "Cómo configurar 2FA" + }, + "step5": { + "title": "Configurar característica: OAuth 2", + "content": "

Puedes proveer a tus clientes con una solución 'Single Sign-On' y crear automáticamente administradores, clientes o subclientes usando una plataforma externa que permita OAuth 2.0.

Sigue la documentación para proceder:

", + "how-to-configure-oauth2": "Cómo configurar OAuth 2" + }, + "step6": { + "title": "Configurar característica: Slack", + "content": "

Los usuarios, seran capaces de recibir notificaciones en Slack, de los eventos que ocurran en el sistema, de acuerdo a las reglas de notificación que hayas configurado.

Sigue la documentación para proceder:

", + "how-to-configure-notifications": "Cómo configurar Slack" + } + }, + "tenant-admin": { + "step1": { + "title": "Crear dispositivo", + "content": "

Aprovisionar el dispositivo de forma manual, usando la interfaz. Sigue la documentación para proceder:

", + "how-to-create-device": "Cómo crear un dispositivo" + }, + "step2": { + "title": "Conectar dispositivo", + "content-before": "

Para conectar un dispositivo, necesitas obtener sus credenciales. Recomendamos usar las credenciales generadas por defecto.

  • Ir a la lista de dispositivos
  • Haz click en el dispositivo, para abrir los detalles
  • Presiona el botón\"Copiar access token\"

Usa comandos simples para publicar datos sobre HTTP:

", + "ubuntu": { + "install-curl": "Instalar cURL en Ubuntu:" + }, + "macos": { + "install-curl": "Instalar cURL en MacOS:" + }, + "windows": { + "install-curl": "En Windows 10 b17063, cURL está disponible por defecto." + }, + "replace-access-token": "Reemplazar $ACCESS_TOKEN con el token de tu dispositivo:", + "content-after": "

Puedes usar también otros protocolos, como MQTT, CoAP, etc.

Sigue la documentación para proceder:

", + "how-to-connect-device": "Cómo conectar un dispositivo" + }, + "step3": { + "title": "Crear un panel", + "content": "

Crear un panel para visualizar los datos de las entidades, como activos, dispositivos, etc..

Sigue la documentación para proceder:

", + "how-to-create-dashboard": "Cómo crear un panel" + }, + "step4": { + "title": "Configurar reglas de alarma", + "alarm-rules": "Reglas de alarma", + "content": "

Cuando la temperatura alcance 25°C, dispararemos una alarma. Sigue la documentación para proceder:

", + "how-to-configure-alarm-rules": "Cómo configurar reglas de alarma" + }, + "step5": { + "title": "Crear alarma", + "content-before": "

Para disparar la alarma, envía una nueva telemetría con un valor de 26°C o superior.

", + "replace-access-token": "Reemplazar $ACCESS_TOKEN con el token de tu dispositivo:", + "content-after": "

Sigue la documentación para proceder:

", + "how-to-create-alarm": "Cómo crear una alarma" + }, + "step6": { + "title": "Crear cliente y asignarle un panel", + "content": "

Creando un panel para un cliente, permitirá al cliente ver sus propios dispositivos. Los datos de otros clientes estarán ocultos.

Sigue la documentación para proceder:

", + "how-to-create-customer-and-assign-dashboard": "Cómo crear un cliente y asignarle un panel" + } + } } }, "icon": { @@ -4576,6 +5373,14 @@ "row-double-click": "Doble clic en la fila" } }, + "paginator" : { + "items-per-page": "Ítems por página:", + "first-page-label": "Primera página", + "last-page-label": "Última página", + "next-page-label": "Siguiente", + "previous-page-label": "Anterior", + "items-per-page-separator": "de" + }, "language": { "language": "Idioma" } From 48177c9e5a3efd6a465cf4209ead6227465996e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20N=C3=BA=C3=B1ez?= Date: Mon, 8 May 2023 17:54:26 +0200 Subject: [PATCH 04/47] Improve Thingsboard Edge translation --- .../assets/locale/locale.constant-es_ES.json | 240 +++++++++--------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 82b568330d..d7925248ed 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -55,7 +55,7 @@ "continue": "Continuar", "discard-changes": "Cancelar cambios", "download": "Descargar", - "next": "Siguente", + "next": "Siguiente", "next-with-label": "Siguiente: {{label}}", "read-more": "Leer más", "hide": "Ocultar", @@ -551,8 +551,8 @@ "filter-type-entity-view-type": "Tipo de vista de entidad", "filter-type-entity-view-type-description": "Vistas de entidad del tipo '{{entityViewTypes}}'", "filter-type-entity-view-type-and-name-description": "Vistas de entidad del tipo '{{entityViewTypes}}' y cuyo nombre comience por '{{prefix}}'", - "filter-type-edge-type": "Tipo de borde", - "filter-type-edge-type-description": "Bordes del tipo '{{edgeTypes}}'", + "filter-type-edge-type": "Tipo de Edge", + "filter-type-edge-type-description": "Edges del tipo '{{edgeTypes}}'", "filter-type-edge-type-and-name-description": "Vistas de entidad del tipo '{{edgeTypes}}' y cuyo nombre comience por '{{prefix}}'", "filter-type-relations-query": "Consulta de relaciones", "filter-type-relations-query-description": "{{entities}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", @@ -563,8 +563,8 @@ "filter-type-entity-view-search-query": "Consulta de búsqueda de vista de entidad", "filter-type-entity-view-search-query-description": "Vistas de entidad con tipos {{entityViewTypes}} que tienen tipo de relación {{relationType}} con dirección {{direction}} {{rootEntity}}", "filter-type-apiUsageState": "Uso de API", - "filter-type-edge-search-query": "Consultar búsqueda de borde", - "filter-type-edge-search-query-description": "Bordes con tipos {{edgeTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Consultar búsqueda de Edge", + "filter-type-edge-search-query-description": "Edges con tipos {{edgeTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", "entity-filter": "Filtro de entidad", "resolve-multiple": "Resolver como múltiples entidades", "filter-type": "Tipo de filtro", @@ -623,8 +623,8 @@ "asset-details": "Detalles de activo", "assign-assets": "Asignar activos", "assign-assets-text": "Asignar { count, plural, =1 {1 activo} other {# activos} } a cliente", - "assign-asset-to-edge-title": "Asignar activo(s) al borde", - "assign-asset-to-edge-text": "Por favor, selecciona lo activos a asignar al borde", + "assign-asset-to-edge-title": "Asignar activo(s) al Edge", + "assign-asset-to-edge-text": "Por favor, selecciona lo activos a asignar al Edge", "delete-assets": "Borrar activos", "unassign-assets": "Cancelar asignación de activo", "unassign-assets-action-title": "Cancelar asignación de { count, plural, =1 {1 activo} other {# activos} } del cliente", @@ -643,7 +643,7 @@ "unassign-asset": "Cancelar asignación de activo", "unassign-assets-title": "Cancelar las asignaciones { count, plural, =1 {1 activo} other {# activos} }?", "unassign-assets-text": "Tras la confirmación todos los activos seleccionados serán desasignados y no serán accesibles por el cliente.", - "unassign-assets-from-edge": "Anular activos de borde", + "unassign-assets-from-edge": "Anular activos de Edge", "copyId": "Copiar ID de activo", "idCopiedMessage": "El ID ha sido copiado al portapapeles", "select-asset": "Seleccionar activo", @@ -655,12 +655,12 @@ "asset-file": "Archivo del activo", "label": "Etiqueta", "search": "Buscar activos", - "assign-asset-to-edge": "Asignar activo(s) al borde", - "unassign-asset-from-edge": "Anular activo de bodre", + "assign-asset-to-edge": "Asignar activo(s) al Edge", + "unassign-asset-from-edge": "Anular activo de Edge", "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", - "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", + "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el Edge no podrá acceder a él", "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, =1 {1 activo} other {# activos} }?", - "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos.", + "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el Edge no podrá acceder a ellos.", "selected-assets": "{ count, plural, =1 {1 activo} other {# activos} } seleccionados" }, "attribute": { @@ -775,8 +775,8 @@ "type-credentials-updated": "Credenciales actualizados", "type-assigned-to-customer": "Asignado a Cliente", "type-unassigned-from-customer": "Deasignado a Cliente", - "type-assigned-to-edge": "Asignado a Borde", - "type-unassigned-from-edge": "Deasignado de Borde", + "type-assigned-to-edge": "Asignado a Edge", + "type-unassigned-from-edge": "Deasignado de Edge", "type-activated": "Activado", "type-suspended": "Suspendido", "type-credentials-read": "Lectura de credenciales", @@ -858,7 +858,7 @@ "public-devices": "Dispositivos Públicos", "public-assets": "Activos Públicos", "public-entity-views": "Vistas de Entidad Públicas", - "public-edges": "Bordes públicos", + "public-edges": "Edges públicos", "add": "Agregar cliente", "delete": "Borrar cliente", "manage-customer-users": "Gestionar usuarios del cliente", @@ -867,8 +867,8 @@ "manage-public-devices": "Gestionar dispositivos públicos", "manage-public-dashboards": "Gestionar paneles públicos", "manage-customer-assets": "Gestionar activos del cliente", - "manage-customer-edges": "Administrar bordes de clientes", - "manage-public-edges": "Administrar bordes públicos", + "manage-customer-edges": "Administrar Edges de clientes", + "manage-public-edges": "Administrar Edges públicos", "manage-public-assets": "Gestionar activos públicos", "add-customer-text": "Agregar nuevo cliente", "no-customers-text": "No se encontraron clientes", @@ -898,8 +898,8 @@ "default-customer-required": "Se requiere cliente por defecto para realizar debu a nivel de propietario", "search": "Buscar clientes", "selected-customers": "{ count, plural, =1 {1 cliente} other {# clientes} } seleccionados", - "edges": "Instancias de borde del cliente", - "manage-edges": "Administrar Bordes" + "edges": "Instancias de Edge del cliente", + "manage-edges": "Administrar Edges" }, "datetime": { "date-from": "Fecha desde", @@ -915,7 +915,7 @@ "add": "Agregar Panel", "assign-dashboard-to-customer": "Asignar panel(es) a cliente", "assign-dashboard-to-customer-text": "Por favor, seleccione algún panel para asignar al Cliente.", - "assign-dashboard-to-edge-title": "Asignar panel(es) a Borde", + "assign-dashboard-to-edge-title": "Asignar panel(es) a Edge", "assign-to-customer-text": "Por favor, seleccione algún cliente para asignar al(los) panel(es).", "assign-to-customer": "Asignar a cliente", "unassign-from-customer": "Desasignar del cliente", @@ -1081,11 +1081,11 @@ "selected-dashboards": "{count, plural, =1 {1 panel} other {# paneles} } seleccionados", "home-dashboard": "Panel principal", "home-dashboard-hide-toolbar": "Ocultar barra de herramientas en panel principal", - "unassign-dashboard-from-edge-text": "Después de la confirmación, el tablero no será asignado y el borde no podrá acceder a él", + "unassign-dashboard-from-edge-text": "Después de la confirmación, el tablero no será asignado y el Edge no podrá acceder a él", "unassign-dashboards-from-edge-title": "Estas seguro de desasignar { count, plural, =1 {1 panel} other {# paneles} }?", - "unassign-dashboards-from-edge-text": "Después de la confirmación, se anulará la asignación de todos los paneles seleccionados y no serán accesibles por de borde", - "assign-dashboard-to-edge": "Asignar panel(es) al borde", - "assign-dashboard-to-edge-text": "Por favor selecciona los paneles para asignar al borde", + "unassign-dashboards-from-edge-text": "Después de la confirmación, se anulará la asignación de todos los paneles seleccionados y no serán accesibles por de Edge", + "assign-dashboard-to-edge": "Asignar panel(es) al Edge", + "assign-dashboard-to-edge-text": "Por favor selecciona los paneles para asignar al Edge", "non-existent-dashboard-state-error": "El panel con id de estado \"{{ stateId }}\" no se ha encontrado" }, "datakey": { @@ -1189,8 +1189,8 @@ "assign-to-customer": "Asignar a cliente", "assign-device-to-customer": "Asignar dispositivo(s) a Cliente", "assign-device-to-customer-text": "Por favor, seleccione los dispositivos que serán asignados al cliente", - "assign-device-to-edge-title": "Asignar Dispositivo(s) a Borde", - "assign-device-to-edge-text": "Selecciona los dispositivos a asignar al Borde", + "assign-device-to-edge-title": "Asignar Dispositivo(s) a Edge", + "assign-device-to-edge-text": "Selecciona los dispositivos a asignar al Edge", "make-public": "Hacer dispositivo público", "make-private": "Hacer dispositivo privado", "no-devices-text": "Ningún dispositivo encontrado", @@ -1207,8 +1207,8 @@ "unassign-devices": "Desasignar dispositivos", "unassign-devices-action-title": "Desasignar { count, plural, =1 {1 dispositivo} other {# dispositivos} } del cliente", "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", - "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", - "unassign-devices-from-edge": "Desasignar dispositivos del Borde", + "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el Edge no podrá acceder a él", + "unassign-devices-from-edge": "Desasignar dispositivos del Edge", "assign-new-device": "Asignar nuevo dispositivo", "make-public-device-title": "¿Hacer el dispositivo '{{deviceName}}' público?", "make-public-device-text": "Tras la confirmación, el dispositivo y la información relacionada serán públicos y podrá ser accesible por otros.", @@ -1334,7 +1334,7 @@ "add-credentials": "Añadir credencial" }, "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, =1 {1 dispositivo} other {# dispositivos} }?", - "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." + "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el Edge no podrá acceder a ellos." }, "asset-profile": { "asset-profile": "Perfil de activo", @@ -1361,8 +1361,8 @@ "description": "Descripción", "default": "Por defecto", "default-rule-chain": "Cadena de reglas por defecto", - "default-edge-rule-chain": "Cadena de reglas por defecto en borde", - "default-edge-rule-chain-hint": "Usado en borde, como cadena de reglas para procesas los datos recibidos para el perfil de activo.", + "default-edge-rule-chain": "Cadena de reglas por defecto en Edge", + "default-edge-rule-chain-hint": "Usado en Edge, como cadena de reglas para procesas los datos recibidos para el perfil de activo.", "mobile-dashboard": "Panel móvil", "mobile-dashboard-hint": "Usado en la aplicación móvil, como un panel para los detalles del activo", "select-queue-hint": "Selecciona desde el desplegable.", @@ -1423,8 +1423,8 @@ "profile-configuration": "Configuración de perfil", "transport-configuration": "Configuración de transporte", "default-rule-chain": "Cadena de reglas por defecto", - "default-edge-rule-chain": "Cadena de reglas por defecto en borde", - "default-edge-rule-chain-hint": "Usado en borde, como cadena de reglas para procesas los datos recibidos para el perfil de dispositivo.", + "default-edge-rule-chain": "Cadena de reglas por defecto en Edge", + "default-edge-rule-chain-hint": "Usado en Edge, como cadena de reglas para procesas los datos recibidos para el perfil de dispositivo.", "mobile-dashboard": "Panel móvil", "mobile-dashboard-hint": "Usado por la aplicación móvil como panel de detalle", "select-queue-hint": "Selecciona desde el desplegable o añade un nombre personalizado.", @@ -1796,97 +1796,97 @@ "row": "Fila" }, "edge": { - "edge": "Borde", - "edge-instances": "Instancias de Borde", + "edge": "Edge", + "edge-instances": "Instancias de Edge", "instances": "Instancias", - "edge-file": "Archivo de borde", + "edge-file": "Archivo de Edge", "name-max-length": "El nombre debe ser menor de 256", "label-max-length": "La etiqueta debe ser menor de 256", "type-max-length": "El tipo debe ser menor de 256", - "management": "Gestión de bordes", - "no-edges-matching": "No se encontraron bordes que coincidan con '{{entity}}'", - "add": "Agregar borde", - "no-edges-text": "No se encontraron bordes", - "edge-details": "Detalles del borde", - "add-edge-text": "Agregar nuevo borde", - "delete": "Eliminar borde", - "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", - "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", - "delete-edges-title": "¿Está seguro de que desea edge {count, plural, =1 {1 borde} other {# bordes} }?", - "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", + "management": "Gestión de Edges", + "no-edges-matching": "No se encontraron Edges que coincidan con '{{entity}}'", + "add": "Agregar Edge", + "no-edges-text": "No se encontraron Edges", + "edge-details": "Detalles del Edge", + "add-edge-text": "Agregar nuevo Edge", + "delete": "Eliminar Edge", + "delete-edge-title": "¿Está seguro de que desea eliminar el Edge '{{edgeName}}'?", + "delete-edge-text": "Tenga cuidado, después de la confirmación, el Edge y todos los datos relacionados serán irrecuperables", + "delete-edges-title": "¿Está seguro de que desea edge {count, plural, =1 {1 Edge} other {# Edges} }?", + "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los Edges seleccionados y todos los datos relacionados se volverán irrecuperables", "name": "Nombre", - "name-starts-with": "Nombre de Borde comienza con", + "name-starts-with": "Nombre de Edge comienza con", "name-required": "Se requiere nombre", "description": "Descripción", "details": "Detalles", "events": "Eventos", - "copy-id": "Copiar ID de borde", - "id-copied-message": "El ID de borde se ha copiado al portapapeles", + "copy-id": "Copiar ID de Edge", + "id-copied-message": "El ID de Edge se ha copiado al portapapeles", "sync": "Sinc Edge", - "edge-required": "Borde Requerido", - "edge-type": "Tipo de Borde", - "edge-type-required": "El tipo de borde es requerido.", + "edge-required": "Edge Requerido", + "edge-type": "Tipo de Edge", + "edge-type-required": "El tipo de Edge es requerido.", "event-action": "Información de la entidad", "entity-id": "ID de entidad", - "select-edge-type": "Seleccionar tipo de borde", + "select-edge-type": "Seleccionar tipo de Edge", "assign-to-customer": "Asignar al cliente", - "assign-to-customer-text": "Seleccione el cliente para asignar los bordes", - "assign-edge-to-customer": "Asignar borde(s) al cliente", - "assign-edge-to-customer-text": "Seleccione los bordes para asignar al cliente", + "assign-to-customer-text": "Seleccione el cliente para asignar los Edges", + "assign-edge-to-customer": "Asignar Edge(s) al cliente", + "assign-edge-to-customer-text": "Seleccione los Edges para asignar al cliente", "assignedToCustomer": "Asignada a la cliente", "edge-public": "Edge es pública", "assigned-to-customer": "Asignado al cliente", "unassign-from-customer": "Anular asignación del cliente", - "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", - "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", - "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, =1 {1 borde} other {# bordes} }?", - "unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", - "make-public": "Hacer público el borde", + "unassign-edge-title": "¿Está seguro de que desea desasignar el Edge '{{edgeName}}'?", + "unassign-edge-text": "Después de la confirmación, el Edge quedará sin asignar y el cliente no podrá acceder a él", + "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, =1 {1 Edge} other {# Edges} }?", + "unassign-edges-text": "Después de la confirmación de todos los Edges seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", + "make-public": "Hacer público el Edge", "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", - "make-public-edge-text": "Después de la confirmación, el borde y todos sus datos serán públicos y accesibles para otros", + "make-public-edge-text": "Después de la confirmación, el Edge y todos sus datos serán públicos y accesibles para otros", "make-private": "Hacer que edge sea privado", "public": "Público", - "make-private-edge-title": "¿Está seguro de que desea que el borde '{{edgeName}}' sea privado?", - "make-private-edge-text": "Después de la confirmación, el borde y todos sus datos se harán privados y otros no podrán acceder a ellos", - "import": "Importar borde", + "make-private-edge-title": "¿Está seguro de que desea que el Edge '{{edgeName}}' sea privado?", + "make-private-edge-text": "Después de la confirmación, el Edge y todos sus datos se harán privados y otros no podrán acceder a ellos", + "import": "Importar Edge", "install-connect-instructions": "Instrucciones de instalación y conexión", "label": "Etiqueta", "load-entity-error": "Entidad no encontrada. No se pudo cargar la información", - "assign-new-edge": "Asignar nuevo borde", - "unassign-from-edge": "Anular asignación de borde", - "edge-key": "Clave de borde", - "copy-edge-key": "Copiar clave de borde", - "edge-key-copied-message": "La clave de borde se ha copiado al portapapeles", - "edge-secret": "Borde secreto", - "copy-edge-secret": "Copiar borde secreto", - "edge-secret-copied-message": "El secreto de borde se ha copiado al portapapeles", - "edge-assets": "Gestionar activos de bordes", - "edge-devices": "Gestionar dispositivos de borde", - "edge-entity-views": "Gestionar vistas de entidad de borde", - "edge-dashboards": "Administrar paneles de borde", - "edge-rulechains": "Cadenas de reglas de borde", - "assets": "Activos de borde", - "devices": "Dispositivos de borde", - "entity-views": "Vistas de entidad de borde", + "assign-new-edge": "Asignar nuevo Edge", + "unassign-from-edge": "Anular asignación de Edge", + "edge-key": "Clave de Edge", + "copy-edge-key": "Copiar clave de Edge", + "edge-key-copied-message": "La clave de Edge se ha copiado al portapapeles", + "edge-secret": "Secreto del Edge", + "copy-edge-secret": "Copiar secreto del Edge", + "edge-secret-copied-message": "El secreto de Edge se ha copiado al portapapeles", + "edge-assets": "Gestionar activos de Edges", + "edge-devices": "Gestionar dispositivos de Edge", + "edge-entity-views": "Gestionar vistas de entidad de Edge", + "edge-dashboards": "Administrar paneles de Edge", + "edge-rulechains": "Cadenas de reglas de Edge", + "assets": "Activos de Edge", + "devices": "Dispositivos de Edge", + "entity-views": "Vistas de entidad de Edge", "dashboard": "Panel de control Edge", - "dashboards": "Paneles de borde", + "dashboards": "Paneles de Edge", "rulechain-templates": "Plantillas, de cadena de reglas", - "edge-rulechain-templates": "Plantillas de cadena de reglas (Borde)", - "rulechains": "Cadenas de regla (Borde)", - "search": "Bordes de búsqueda", - "selected-edges": "{count, plural, =1 {1 borde} other {# bordes} } seleccionadas", - "any-edge": "Cualquier bordee", + "edge-rulechain-templates": "Plantillas de cadena de reglas (Edge)", + "rulechains": "Cadenas de regla (Edge)", + "search": "Edges de búsqueda", + "selected-edges": "{count, plural, =1 {1 Edge} other {# Edges} } seleccionadas", + "any-edge": "Cualquier Edge", "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", - "edge-type-list-empty": "No se seleccionó ningún tipo de borde.", - "edge-types": "Tipos de bordes", - "enter-edge-type": "Ingrese el tipo de borde", + "edge-type-list-empty": "No se seleccionó ningún tipo de Edge.", + "edge-types": "Tipos de Edges", + "enter-edge-type": "Ingrese el tipo de Edge", "deployed": "Desplegada", "pending": "Pendiente", "downlinks": "Enlaces descendentes", "no-downlinks-prompt": "No se encontraron enlaces descendentes", "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", - "missing-related-rule-chains-title": "Al borde le faltan cadenas de reglas relacionadas", - "missing-related-rule-chains-text": "Asignado a la (s) cadena (s) de reglas de borde usa nodos de reglas que reenvían mensajes a cadenas de reglas que no están asignadas a este borde.

Lista de cadenas de reglas faltantes:
{{missingRuleChains}}", + "missing-related-rule-chains-title": "Al Edge le faltan cadenas de reglas relacionadas", + "missing-related-rule-chains-text": "Asignado a la (s) cadena (s) de reglas de Edge usa nodos de reglas que reenvían mensajes a cadenas de reglas que no están asignadas a este Edge.

Lista de cadenas de reglas faltantes:
{{missingRuleChains}}", "widget-datasource-error": "Este widget solo admite la fuente de datos de la entidad EDGE" }, "edge-event": { @@ -1899,7 +1899,7 @@ "type-alarm": "Alarma", "type-rule-chain": "Cadena de reglas", "type-rule-chain-metadata": "Metadatos de Cadena de Reglas", - "type-edge": "Borde", + "type-edge": "Edge", "type-user": "Usuario", "type-customer": "Cliente", "type-relation": "Relación", @@ -1921,8 +1921,8 @@ "action-type-rpc-call": "Llamada RPC", "action-type-alarm-ack": "ACK Alarma", "action-type-alarm-clear": "Borrado de Alarma", - "action-type-assigned-to-edge": "Asignado a Borde", - "action-type-unassigned-from-edge": "Desasignado de Borde", + "action-type-assigned-to-edge": "Asignado a Edge", + "action-type-unassigned-from-edge": "Desasignado de Edge", "action-type-credentials-request": "Obtención de Credenciales", "action-type-entity-merge-request": "Unión de entidades" }, @@ -2050,10 +2050,10 @@ "no-data": "No hay datos que mostrar", "columns-to-display": "Columnas a Mostrar", "type-api-usage-state": "Estado de uso de la API", - "type-edge": "Borde", - "type-edges": "Bordes", - "list-of-edges": "{count, plural, =1 {Un borde} other {Lista de # bordes} }", - "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'", + "type-edge": "Edge", + "type-edges": "Edges", + "list-of-edges": "{count, plural, =1 {Un Edge} other {Lista de # Edges} }", + "edge-name-starts-with": "Edges cuyos nombres comienzan con '{{prefijo}}'", "type-tb-resource": "Recurso", "type-ota-package": "Paquete OTA", "type-rpc": "RPC", @@ -2115,7 +2115,7 @@ "assign-to-customer": "Asignar a cliente", "assign-entity-view-to-customer": "Asignar vista de entidad a cliente", "assign-entity-view-to-customer-text": "Por favor, seleccione las vistas de entidad para asignar al cliente", - "assign-entity-view-to-edge-title": "Asignar vista(s) de entidad a Borde", + "assign-entity-view-to-edge-title": "Asignar vista(s) de entidad a Edge", "no-entity-views-text": "No se encontraron vistas de entidad", "assign-to-customer-text": "Por favor, seleccione el cliente para asignar la vista de entidad", "entity-view-details": "Detalles de la vista de entidad", @@ -2186,14 +2186,14 @@ "make-public-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán públicos y accesibles para otros.", "make-private-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea privada?", "make-private-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros.", - "assign-entity-view-to-edge": "Asignar vista (s) de entidad a borde", - "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", + "assign-entity-view-to-edge": "Asignar vista (s) de entidad a Edge", + "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al Edge", "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", - "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella", - "unassign-entity-views-from-edge-action-title": "Anular asignación {count, plural, =1 {1 vista de entidad} other {# vistas de entidad} } del borde", + "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el Edge no podrá acceder a ella", + "unassign-entity-views-from-edge-action-title": "Anular asignación {count, plural, =1 {1 vista de entidad} other {# vistas de entidad} } del Edge", "unassign-entity-view-from-edge": "Anular asignación de vista de entidad", "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, =1 {1 vista de entidad} other {# vistas de entidad} }?", - "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas" + "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el Edge no podrá acceder a ellas" }, "event": { "event-type": "Tipo de evento", @@ -3300,22 +3300,22 @@ "assign-new-rulechain": "Asignar nueva cadena de reglas", "edge-template-root": "Raíz de plantilla", "assign-to-edge": "Asignar a Edge", - "edge-rulechain": "Cadena de reglas de borde", - "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella", + "edge-rulechain": "Cadena de reglas de Edge", + "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el Edge no podrá acceder a ella", "unassign-rulechains-from-edge-title": "Estás seguro de desasignar { count, plural, =1 {1 cadena de reglas} other {# cadenas de reglas} }?", - "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas", - "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", - "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", - "set-edge-template-root-rulechain": "Hacer raíz de plantilla de borde de cadena de reglas", - "set-edge-template-root-rulechain-title": "¿Está seguro de que desea que la cadena de reglas '{{ruleChainName}}' sea la raíz de la plantilla de borde?", - "set-edge-template-root-rulechain-text": "Después de la confirmación, la cadena de reglas se convertirá en la raíz de la plantilla de borde y será la cadena de reglas raíz para los bordes recién creados.", + "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el Edge no podrá acceder a ellas", + "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a Edge", + "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al Edge", + "set-edge-template-root-rulechain": "Hacer raíz de plantilla de Edge de cadena de reglas", + "set-edge-template-root-rulechain-title": "¿Está seguro de que desea que la cadena de reglas '{{ruleChainName}}' sea la raíz de la plantilla de Edge?", + "set-edge-template-root-rulechain-text": "Después de la confirmación, la cadena de reglas se convertirá en la raíz de la plantilla de Edge y será la cadena de reglas raíz para los Edges recién creados.", "invalid-rulechain-type-error": "No se puede importar la cadena de reglas: Tipo de cadena de reglas no válido. El tipo esperado es {{expectedRuleChainType}}", - "set-auto-assign-to-edge": "Asignar cadena de reglas a los bordes en la creación", - "set-auto-assign-to-edge-title": "¿Está seguro de que desea asignar automáticamente la cadena de reglas de borde '{{ruleChainName}}' a los bordes en la creación?", - "set-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de borde se asignará automáticamente a los bordes en la creación.", - "unset-auto-assign-to-edge": "Desmarcar asignar cadena de reglas a los bordes en la creación", - "unset-auto-assign-to-edge-title": "¿Está seguro de que desea anular la asignación de la cadena de reglas de borde '{{ruleChainName}}' a los bordes en la creación?", - "unset-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de borde ya no se asignará automáticamente a los bordes en la creación.", + "set-auto-assign-to-edge": "Asignar cadena de reglas a los Edges en la creación", + "set-auto-assign-to-edge-title": "¿Está seguro de que desea asignar automáticamente la cadena de reglas de Edge '{{ruleChainName}}' a los Edges en la creación?", + "set-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de Edge se asignará automáticamente a los Edges en la creación.", + "unset-auto-assign-to-edge": "Desmarcar asignar cadena de reglas a los Edges en la creación", + "unset-auto-assign-to-edge-title": "¿Está seguro de que desea anular la asignación de la cadena de reglas de Edge '{{ruleChainName}}' a los Edges en la creación?", + "unset-auto-assign-to-edge-text": "Después de la confirmación, la cadena de reglas de Edge ya no se asignará automáticamente a los Edges en la creación.", "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainName}}'?", "unassign-rulechains": "Anular asignación de cadenas de reglas" }, From 784ab8cb654bbe3f0e6345c9b5e50df02530426a Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 11 May 2023 16:15:31 +0300 Subject: [PATCH 05/47] UI: Fixed disabled queue name --- .../profile/queue/tenant-profile-queues.component.ts | 5 +++-- .../home/components/queue/queue-form.component.ts | 11 +++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts index c3ff3ccc4c..0a5e5dc3ee 100644 --- a/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -53,7 +53,7 @@ import { takeUntil } from 'rxjs/operators'; } ] }) -export class TenantProfileQueuesComponent implements ControlValueAccessor, Validator, OnDestroy { +export class TenantProfileQueuesComponent implements ControlValueAccessor, Validator, OnDestroy, OnInit { tenantProfileQueuesFormGroup: UntypedFormGroup; newQueue = false; @@ -107,6 +107,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + this.newQueue = false; if (this.disabled) { this.tenantProfileQueuesFormGroup.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index 3368bd28ff..58cbbc13a9 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -129,11 +129,6 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr this.queueFormGroup.get('submitStrategy').get('type').valueChanges.subscribe(() => { this.submitStrategyTypeChanged(); }); - if (this.newQueue) { - this.queueFormGroup.get('name').enable({emitEvent: false}); - } else { - this.queueFormGroup.get('name').disable({emitEvent: false}); - } } ngOnDestroy() { @@ -149,7 +144,11 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr this.queueFormGroup.disable({emitEvent: false}); } else { this.queueFormGroup.enable({emitEvent: false}); - this.queueFormGroup.get('name').disable({emitEvent: false}); + if (this.newQueue) { + this.queueFormGroup.get('name').enable({emitEvent: false}); + } else { + this.queueFormGroup.get('name').disable({emitEvent: false}); + } } } From 20b8da59ebd069fd88dbc1b49e13ffdb6579b157 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 11 May 2023 16:18:11 +0300 Subject: [PATCH 06/47] UI: Fixed batch size value --- .../app/modules/home/components/queue/queue-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index 58cbbc13a9..f4c91bbf88 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -202,7 +202,7 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr const type: QueueSubmitStrategyTypes = form.get('type').value; const batchSizeField = form.get('batchSize'); if (type === QueueSubmitStrategyTypes.BATCH) { - batchSizeField.patchValue(1000, {emitEvent: false}); + batchSizeField.patchValue(batchSizeField.value ? batchSizeField.value : 1000, {emitEvent: false}); batchSizeField.setValidators([Validators.min(1), Validators.required]); batchSizeField.updateValueAndValidity({emitEvent: false}); this.hideBatchSize = true; From 1c650274d0d3a6903b5981a7503bfdbbbffd042f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 11 May 2023 16:23:03 +0300 Subject: [PATCH 07/47] UI: Remove gap from radio buttons --- .../modules/home/components/queue/queue-form.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html index 8f8f509e94..9fee72fef4 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.html @@ -40,7 +40,7 @@
- + {{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }}
- + {{ queueProcessingStrategyTypesMap.get(queueProcessingStrategyTypes[strategy]).label | translate }} Date: Thu, 11 May 2023 17:19:21 +0300 Subject: [PATCH 08/47] UI: refactoring --- .../app/modules/home/components/queue/queue-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts index f4c91bbf88..b123cf2ad5 100644 --- a/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts @@ -202,7 +202,7 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestr const type: QueueSubmitStrategyTypes = form.get('type').value; const batchSizeField = form.get('batchSize'); if (type === QueueSubmitStrategyTypes.BATCH) { - batchSizeField.patchValue(batchSizeField.value ? batchSizeField.value : 1000, {emitEvent: false}); + batchSizeField.patchValue(batchSizeField.value ?? 1000, {emitEvent: false}); batchSizeField.setValidators([Validators.min(1), Validators.required]); batchSizeField.updateValueAndValidity({emitEvent: false}); this.hideBatchSize = true; From a9cc5a042a16aaf8a128ede20dd22fdde02dbeff Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 12 May 2023 10:48:21 +0300 Subject: [PATCH 09/47] UI: Fixed mat suffix icon tooltip --- ui-ngx/src/styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 39885d1191..296e9b26fe 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -567,6 +567,10 @@ mat-label { } } + .mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix { + z-index: 1; + } + .mat-toolbar.mat-mdc-table-toolbar .mat-mdc-form-field.mat-form-field-appearance-fill, .mat-mdc-form-field.mat-form-field-appearance-fill.tb-appearance-transparent { .mdc-text-field--filled { From b7f19a12cfa82f84e2cfb2c465f0e91acf0c3862 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 12 May 2023 14:54:14 +0300 Subject: [PATCH 10/47] Version control for notification configs --- .../main/data/upgrade/3.5.0/schema_update.sql | 53 +++++++ .../DefaultEntitiesExportImportService.java | 3 +- .../DefaultExportableEntitiesService.java | 58 +------ .../impl/BaseEntityExportService.java | 11 ++ .../impl/NotificationRuleExportService.java | 103 ++++++++++++ .../impl/NotificationTargetExportService.java | 55 +++++++ .../NotificationTemplateExportService.java | 42 +++++ .../impl/NotificationRuleImportService.java | 150 ++++++++++++++++++ .../impl/NotificationTargetImportService.java | 109 +++++++++++++ .../NotificationTemplateImportService.java | 72 +++++++++ .../server/dao/entity/EntityDaoService.java | 4 + .../notification/rule/NotificationRule.java | 6 +- .../targets/NotificationTarget.java | 16 +- .../template/NotificationTemplate.java | 17 +- .../template/NotificationTemplateConfig.java | 9 ++ .../server/common/data/sync/JsonTbEntity.java | 8 +- .../org/thingsboard/server/dao/DaoUtil.java | 8 + .../dao/asset/AssetProfileServiceImpl.java | 6 + .../server/dao/asset/BaseAssetService.java | 6 + .../dao/customer/CustomerServiceImpl.java | 6 + .../dao/dashboard/DashboardServiceImpl.java | 6 + .../dao/device/DeviceProfileServiceImpl.java | 6 + .../server/dao/device/DeviceServiceImpl.java | 6 + .../server/dao/model/BaseSqlEntity.java | 7 +- .../dao/model/sql/NotificationRuleEntity.java | 6 + .../model/sql/NotificationTargetEntity.java | 5 + .../model/sql/NotificationTemplateEntity.java | 5 + .../DefaultNotificationRuleService.java | 5 + .../DefaultNotificationTargetService.java | 5 + .../DefaultNotificationTemplateService.java | 5 + .../dao/notification/NotificationRuleDao.java | 3 +- .../notification/NotificationTargetDao.java | 3 +- .../notification/NotificationTemplateDao.java | 4 +- .../server/dao/rule/BaseRuleChainService.java | 6 + .../notification/JpaNotificationRuleDao.java | 26 ++- .../JpaNotificationTargetDao.java | 20 +++ .../JpaNotificationTemplateDao.java | 21 +++ .../NotificationRuleRepository.java | 10 +- .../NotificationTargetRepository.java | 10 +- .../NotificationTemplateRepository.java | 10 +- .../dao/widget/WidgetsBundleServiceImpl.java | 5 + .../main/resources/sql/schema-entities.sql | 12 +- 42 files changed, 854 insertions(+), 74 deletions(-) create mode 100644 application/src/main/data/upgrade/3.5.0/schema_update.sql create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTemplateExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java diff --git a/application/src/main/data/upgrade/3.5.0/schema_update.sql b/application/src/main/data/upgrade/3.5.0/schema_update.sql new file mode 100644 index 0000000000..c80f5d8ba5 --- /dev/null +++ b/application/src/main/data/upgrade/3.5.0/schema_update.sql @@ -0,0 +1,53 @@ +-- +-- Copyright © 2016-2023 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +-- NOTIFICATION CONFIGS VERSION CONTROL START + +ALTER TABLE notification_template + ADD COLUMN IF NOT EXISTS external_id UUID; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_notification_template_external_id') THEN + ALTER TABLE notification_template ADD CONSTRAINT uq_notification_template_external_id UNIQUE (tenant_id, external_id); + END IF; + END; +$$; + +ALTER TABLE notification_target + ADD COLUMN IF NOT EXISTS external_id UUID; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_notification_target_external_id') THEN + ALTER TABLE notification_target ADD CONSTRAINT uq_notification_target_external_id UNIQUE (tenant_id, external_id); + END IF; + END; +$$; + +ALTER TABLE notification_rule + ADD COLUMN IF NOT EXISTS external_id UUID; +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'uq_notification_rule_external_id') THEN + ALTER TABLE notification_rule ADD CONSTRAINT uq_notification_rule_external_id UNIQUE (tenant_id, external_id); + END IF; + END; +$$; + +-- NOTIFICATION CONFIGS VERSION CONTROL END + diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 822bd82e00..f1855d963d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -67,7 +67,8 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS protected static final List SUPPORTED_ENTITY_TYPES = List.of( EntityType.CUSTOMER, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.RULE_CHAIN, EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE, - EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE + EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE, + EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java index 456eeb8b86..3358e5182f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java @@ -22,36 +22,21 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; -import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; -import org.thingsboard.server.dao.asset.AssetProfileService; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.dao.entity.EntityDaoService; +import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.AccessControlService; import java.util.Collection; import java.util.HashMap; import java.util.Map; -import java.util.function.BiConsumer; @Service @TbCoreComponent @@ -60,11 +45,10 @@ import java.util.function.BiConsumer; public class DefaultExportableEntitiesService implements ExportableEntitiesService { private final Map> daos = new HashMap<>(); - private final Map> removers = new HashMap<>(); + private final EntityServiceRegistry entityServiceRegistry; private final AccessControlService accessControlService; - @Override public , I extends EntityId> E findEntityByTenantIdAndExternalId(TenantId tenantId, I externalId) { EntityType entityType = externalId.getEntityType(); @@ -152,11 +136,11 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi @Override public void removeById(TenantId tenantId, I id) { EntityType entityType = id.getEntityType(); - BiConsumer entityRemover = removers.get(entityType); - if (entityRemover == null) { + EntityDaoService entityService = entityServiceRegistry.getServiceByEntityType(entityType); + if (entityService == null) { throw new IllegalArgumentException("Unsupported entity type " + entityType); } - entityRemover.accept(tenantId, id); + entityService.deleteEntity(tenantId, id); } private > ExportableEntityDao getExportableEntityDao(EntityType entityType) { @@ -182,34 +166,4 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi }); } - @Autowired - private void setRemovers(CustomerService customerService, AssetService assetService, RuleChainService ruleChainService, - DashboardService dashboardService, DeviceProfileService deviceProfileService, - AssetProfileService assetProfileService, DeviceService deviceService, WidgetsBundleService widgetsBundleService) { - removers.put(EntityType.CUSTOMER, (tenantId, entityId) -> { - customerService.deleteCustomer(tenantId, (CustomerId) entityId); - }); - removers.put(EntityType.ASSET, (tenantId, entityId) -> { - assetService.deleteAsset(tenantId, (AssetId) entityId); - }); - removers.put(EntityType.RULE_CHAIN, (tenantId, entityId) -> { - ruleChainService.deleteRuleChainById(tenantId, (RuleChainId) entityId); - }); - removers.put(EntityType.DASHBOARD, (tenantId, entityId) -> { - dashboardService.deleteDashboard(tenantId, (DashboardId) entityId); - }); - removers.put(EntityType.DEVICE_PROFILE, (tenantId, entityId) -> { - deviceProfileService.deleteDeviceProfile(tenantId, (DeviceProfileId) entityId); - }); - removers.put(EntityType.ASSET_PROFILE, (tenantId, entityId) -> { - assetProfileService.deleteAssetProfile(tenantId, (AssetProfileId) entityId); - }); - removers.put(EntityType.DEVICE, (tenantId, entityId) -> { - deviceService.deleteDevice(tenantId, (DeviceId) entityId); - }); - removers.put(EntityType.WIDGETS_BUNDLE, (tenantId, entityId) -> { - widgetsBundleService.deleteWidgetsBundle(tenantId, (WidgetsBundleId) entityId); - }); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java index 891a6411a3..09b96d68fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java @@ -24,7 +24,11 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; +import java.util.Collection; import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; public abstract class BaseEntityExportService, D extends EntityExportData> extends DefaultEntityExportService { @@ -47,4 +51,11 @@ public abstract class BaseEntityExportService getExternalIdOrElseInternalByUuid(ctx, uuid)); } + protected Stream toExternalIds(Collection internalIds, Function entityIdCreator, + EntitiesExportCtx ctx) { + return internalIds.stream().map(entityIdCreator) + .map(entityId -> getExternalIdOrElseInternal(ctx, entityId)) + .map(EntityId::getId); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java new file mode 100644 index 0000000000..0906fb83ef --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +public class NotificationRuleExportService, D extends EntityExportData> extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, NotificationRule notificationRule, EntityExportData exportData) { + notificationRule.setTemplateId(getExternalIdOrElseInternal(ctx, notificationRule.getTemplateId())); + + NotificationRuleTriggerConfig ruleTriggerConfig = notificationRule.getTriggerConfig(); + switch (ruleTriggerConfig.getTriggerType()) { + case DEVICE_ACTIVITY: { + DeviceActivityNotificationRuleTriggerConfig triggerConfig = (DeviceActivityNotificationRuleTriggerConfig) ruleTriggerConfig; + Set devices = triggerConfig.getDevices(); + if (devices != null) { + triggerConfig.setDevices(toExternalIds(devices, DeviceId::new, ctx).collect(Collectors.toSet())); + } + + Set deviceProfiles = triggerConfig.getDeviceProfiles(); + if (deviceProfiles != null) { + triggerConfig.setDeviceProfiles(toExternalIds(deviceProfiles, DeviceProfileId::new, ctx).collect(Collectors.toSet())); + } + break; + } + case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: + RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; + Set ruleChains = triggerConfig.getRuleChains(); + if (ruleChains != null) { + triggerConfig.setRuleChains(toExternalIds(ruleChains, RuleChainId::new, ctx).collect(Collectors.toSet())); + } + break; + } + + NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig(); + switch (ruleTriggerConfig.getTriggerType()) { + case ALARM: { + EscalatedNotificationRuleRecipientsConfig recipientsConfig = (EscalatedNotificationRuleRecipientsConfig) ruleRecipientsConfig; + Map> escalationTable = new LinkedHashMap<>(recipientsConfig.getEscalationTable()); + escalationTable.replaceAll((delay, targets) -> { + return toExternalIds(targets, NotificationTargetId::new, ctx).collect(Collectors.toList()); + }); + recipientsConfig.setEscalationTable(escalationTable); + break; + } + default: { + DefaultNotificationRuleRecipientsConfig recipientsConfig = (DefaultNotificationRuleRecipientsConfig) ruleRecipientsConfig; + List targets = recipientsConfig.getTargets(); + targets = toExternalIds(targets, NotificationTargetId::new, ctx).collect(Collectors.toList()); + recipientsConfig.setTargets(targets); + break; + } + } + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.NOTIFICATION_RULE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java new file mode 100644 index 0000000000..f0a5da3efc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTargetExportService.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.Set; + +@Service +@TbCoreComponent +public class NotificationTargetExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, NotificationTarget notificationTarget, EntityExportData exportData) { + if (notificationTarget.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) { + UsersFilter usersFilter = ((PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration()).getUsersFilter(); + switch (usersFilter.getType()) { + case CUSTOMER_USERS: + CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) usersFilter; + customerUsersFilter.setCustomerId(getExternalIdOrElseInternal(ctx, new CustomerId(customerUsersFilter.getCustomerId())).getId()); + break; + } + } + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.NOTIFICATION_TARGET); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTemplateExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTemplateExportService.java new file mode 100644 index 0000000000..d28f352bae --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationTemplateExportService.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.Set; + +@Service +@TbCoreComponent +public class NotificationTemplateExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData exportData) { + + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.NOTIFICATION_TEMPLATE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java new file mode 100644 index 0000000000..a7d7670c8c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java @@ -0,0 +1,150 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class NotificationRuleImportService extends BaseEntityImportService> { + + private final NotificationRuleService notificationRuleService; + + @Override + protected void setOwner(TenantId tenantId, NotificationRule notificationRule, IdProvider idProvider) { + notificationRule.setTenantId(tenantId); + } + + @Override + protected NotificationRule prepare(EntitiesImportCtx ctx, NotificationRule notificationRule, NotificationRule oldNotificationRule, EntityExportData exportData, IdProvider idProvider) { + notificationRule.setTemplateId(idProvider.getInternalId(notificationRule.getTemplateId())); + + NotificationRuleTriggerConfig ruleTriggerConfig = notificationRule.getTriggerConfig(); + NotificationRuleTriggerType triggerType = ruleTriggerConfig.getTriggerType(); + switch (triggerType) { + case DEVICE_ACTIVITY: { + DeviceActivityNotificationRuleTriggerConfig triggerConfig = (DeviceActivityNotificationRuleTriggerConfig) ruleTriggerConfig; + Set devices = triggerConfig.getDevices(); + if (devices != null) { + triggerConfig.setDevices(devices.stream().map(DeviceId::new) + .map(idProvider::getInternalId).map(UUIDBased::getId) + .collect(Collectors.toSet())); + } + + Set deviceProfiles = triggerConfig.getDeviceProfiles(); + if (deviceProfiles != null) { + triggerConfig.setDeviceProfiles(deviceProfiles.stream().map(DeviceProfileId::new) + .map(idProvider::getInternalId).map(UUIDBased::getId) + .collect(Collectors.toSet())); + } + break; + } + case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: + RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; + Set ruleChains = triggerConfig.getRuleChains(); + if (ruleChains != null) { + triggerConfig.setRuleChains(ruleChains.stream().map(RuleChainId::new) + .map(idProvider::getInternalId).map(UUIDBased::getId) + .collect(Collectors.toSet())); + } + break; + } + if (!triggerType.isTenantLevel()) { + throw new IllegalArgumentException("Trigger type " + triggerType + " is not available for tenants"); + } + + NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig(); + switch (triggerType) { + case ALARM: { + EscalatedNotificationRuleRecipientsConfig recipientsConfig = (EscalatedNotificationRuleRecipientsConfig) ruleRecipientsConfig; + Map> escalationTable = new LinkedHashMap<>(recipientsConfig.getEscalationTable()); + escalationTable.replaceAll((delay, targets) -> targets.stream() + .map(NotificationTargetId::new).map(idProvider::getInternalId) + .map(UUIDBased::getId).collect(Collectors.toList())); + recipientsConfig.setEscalationTable(escalationTable); + break; + } + default: { + DefaultNotificationRuleRecipientsConfig recipientsConfig = (DefaultNotificationRuleRecipientsConfig) ruleRecipientsConfig; + List targets = recipientsConfig.getTargets().stream() + .map(NotificationTargetId::new).map(idProvider::getInternalId) + .map(UUIDBased::getId).collect(Collectors.toList()); + recipientsConfig.setTargets(targets); + break; + } + } + return notificationRule; + } + + @Override + protected NotificationRule saveOrUpdate(EntitiesImportCtx ctx, NotificationRule notificationRule, EntityExportData exportData, IdProvider idProvider) { + ConstraintValidator.validateFields(notificationRule); + return notificationRuleService.saveNotificationRule(ctx.getTenantId(), notificationRule); + } + + @Override + protected void onEntitySaved(User user, NotificationRule savedEntity, NotificationRule oldEntity) throws ThingsboardException { + entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, + oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); + clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedEntity.getId(), + oldEntity == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + } + + @Override + protected NotificationRule deepCopy(NotificationRule notificationRule) { + return new NotificationRule(notificationRule); + } + + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_RULE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java new file mode 100644 index 0000000000..a950a1592e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; +import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +import java.util.stream.Collectors; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class NotificationTargetImportService extends BaseEntityImportService> { + + private final NotificationTargetService notificationTargetService; + + @Override + protected void setOwner(TenantId tenantId, NotificationTarget notificationTarget, IdProvider idProvider) { + notificationTarget.setTenantId(tenantId); + } + + @Override + protected NotificationTarget prepare(EntitiesImportCtx ctx, NotificationTarget notificationTarget, NotificationTarget oldNotificationTarget, EntityExportData exportData, IdProvider idProvider) { + if (notificationTarget.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) { + UsersFilter usersFilter = ((PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration()).getUsersFilter(); + switch (usersFilter.getType()) { + case CUSTOMER_USERS: + CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) usersFilter; + customerUsersFilter.setCustomerId(idProvider.getInternalId(new CustomerId(customerUsersFilter.getCustomerId())).getId()); + break; + case USER_LIST: + UserListFilter userListFilter = (UserListFilter) usersFilter; + userListFilter.setUsersIds(userListFilter.getUsersIds().stream() + .map(UserId::new).map(idProvider::getInternalId) + .map(UUIDBased::getId).collect(Collectors.toList()) + ); + break; + case TENANT_ADMINISTRATORS: + if (CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantsIds()) || + CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantProfilesIds())) { + throw new IllegalArgumentException("Permission denied"); + } + break; + case SYSTEM_ADMINISTRATORS: + throw new AccessDeniedException("Permission denied"); + } + } + return notificationTarget; + } + + @Override + protected NotificationTarget saveOrUpdate(EntitiesImportCtx ctx, NotificationTarget notificationTarget, EntityExportData exportData, IdProvider idProvider) { + ConstraintValidator.validateFields(notificationTarget); + return notificationTargetService.saveNotificationTarget(ctx.getTenantId(), notificationTarget); + } + + @Override + protected void onEntitySaved(User user, NotificationTarget savedEntity, NotificationTarget oldEntity) throws ThingsboardException { + entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, + oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); + } + + @Override + protected NotificationTarget deepCopy(NotificationTarget notificationTarget) { + return new NotificationTarget(notificationTarget); + } + + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TARGET; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java new file mode 100644 index 0000000000..b98c17a412 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class NotificationTemplateImportService extends BaseEntityImportService> { + + private final NotificationTemplateService notificationTemplateService; + + @Override + protected void setOwner(TenantId tenantId, NotificationTemplate notificationTemplate, IdProvider idProvider) { + notificationTemplate.setTenantId(tenantId); + } + + @Override + protected NotificationTemplate prepare(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, NotificationTemplate oldEntity, EntityExportData exportData, IdProvider idProvider) { + return notificationTemplate; + } + + @Override + protected NotificationTemplate saveOrUpdate(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData exportData, IdProvider idProvider) { + ConstraintValidator.validateFields(notificationTemplate); + return notificationTemplateService.saveNotificationTemplate(ctx.getTenantId(), notificationTemplate); + } + + @Override + protected void onEntitySaved(User user, NotificationTemplate savedEntity, NotificationTemplate oldEntity) throws ThingsboardException { + entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, + oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); + } + + @Override + protected NotificationTemplate deepCopy(NotificationTemplate notificationTemplate) { + return new NotificationTemplate(notificationTemplate); + } + + @Override + public EntityType getEntityType() { + return EntityType.NOTIFICATION_TEMPLATE; + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java index e1b8bec633..3094efb5be 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java @@ -30,6 +30,10 @@ public interface EntityDaoService { throw new IllegalArgumentException("Not implemented for " + getEntityType()); } + default void deleteEntity(TenantId tenantId, EntityId id) { + throw new IllegalArgumentException(getEntityType().getNormalName() + " deletion not supported"); + } + EntityType getEntityType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java index 450fbdb23f..73b39c98df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.NotificationRuleId; @@ -39,7 +40,7 @@ import java.io.Serializable; @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) -public class NotificationRule extends BaseData implements HasTenantId, HasName, Serializable { +public class NotificationRule extends BaseData implements HasTenantId, HasName, ExportableEntity, Serializable { private TenantId tenantId; @NotBlank @@ -60,6 +61,8 @@ public class NotificationRule extends BaseData implements Ha private NotificationRuleConfig additionalConfig; + private NotificationRuleId externalId; + public NotificationRule(NotificationRule other) { super(other); this.tenantId = other.tenantId; @@ -69,6 +72,7 @@ public class NotificationRule extends BaseData implements Ha this.triggerConfig = other.triggerConfig; this.recipientsConfig = other.recipientsConfig; this.additionalConfig = other.additionalConfig; + this.externalId = other.externalId; } @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java index 9a2f9a5306..df488d3b1e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTarget.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.notification.targets; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.NotificationTargetId; @@ -31,7 +32,7 @@ import javax.validation.constraints.NotNull; @Data @EqualsAndHashCode(callSuper = true) -public class NotificationTarget extends BaseData implements HasTenantId, HasName { +public class NotificationTarget extends BaseData implements HasTenantId, HasName, ExportableEntity { private TenantId tenantId; @NotBlank @@ -42,4 +43,17 @@ public class NotificationTarget extends BaseData implement @Valid private NotificationTargetConfig configuration; + private NotificationTargetId externalId; + + public NotificationTarget() { + } + + public NotificationTarget(NotificationTarget other) { + super(other); + this.tenantId = other.tenantId; + this.name = other.name; + this.configuration = other.configuration; + this.externalId = other.externalId; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java index 71da2f862b..48bc5dd409 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.notification.template; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.NotificationTemplateId; @@ -32,7 +33,7 @@ import javax.validation.constraints.NotNull; @Data @EqualsAndHashCode(callSuper = true) -public class NotificationTemplate extends BaseData implements HasTenantId, HasName { +public class NotificationTemplate extends BaseData implements HasTenantId, HasName, ExportableEntity { private TenantId tenantId; @NoXss @@ -46,4 +47,18 @@ public class NotificationTemplate extends BaseData imple @NotNull private NotificationTemplateConfig configuration; + private NotificationTemplateId externalId; + + public NotificationTemplate() { + } + + public NotificationTemplate(NotificationTemplate other) { + super(other); + this.tenantId = other.tenantId; + this.name = other.name; + this.notificationType = other.notificationType; + this.configuration = other.configuration != null ? other.configuration.copy() : null; + this.externalId = other.externalId; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplateConfig.java index 1dcb4aec0c..91b10ed2be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplateConfig.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import javax.validation.Valid; import javax.validation.constraints.NotEmpty; +import java.util.HashMap; import java.util.Map; @Data @@ -29,4 +30,12 @@ public class NotificationTemplateConfig { @NotEmpty private Map deliveryMethodsTemplates; + public NotificationTemplateConfig copy() { + Map templates = new HashMap<>(deliveryMethodsTemplates); + templates.replaceAll((deliveryMethod, template) -> template.copy()); + NotificationTemplateConfig copy = new NotificationTemplateConfig(); + copy.setDeliveryMethodsTemplates(templates); + return copy; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index dcb5c29e53..23a98b395b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -27,6 +27,9 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -48,7 +51,10 @@ import java.lang.annotation.Target; @Type(name = "DASHBOARD", value = Dashboard.class), @Type(name = "CUSTOMER", value = Customer.class), @Type(name = "ENTITY_VIEW", value = EntityView.class), - @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundle.class) + @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundle.class), + @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), + @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), + @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) public @interface JsonTbEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 5a328daadb..63b13bca66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -109,6 +109,14 @@ public abstract class DaoUtil { return ids; } + public static I toEntityId(UUID uuid, Function creator) { + if (uuid != null) { + return creator.apply(uuid); + } else { + return null; + } + } + public static void processInBatches(Function> finder, int batchSize, Consumer processor) { processBatches(finder, batchSize, batch -> batch.getData().forEach(processor)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index c464d06857..eb297e0049 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -273,6 +273,12 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService tenantDevicesRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java index 916c459a6b..de3b81ee74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java @@ -22,6 +22,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.dao.DaoUtil; import javax.persistence.Column; import javax.persistence.Id; @@ -85,11 +86,7 @@ public abstract class BaseSqlEntity implements BaseEntity { } protected static I getEntityId(UUID uuid, Function creator) { - if (uuid != null) { - return creator.apply(uuid); - } else { - return null; - } + return DaoUtil.toEntityId(uuid, creator); } protected static TenantId getTenantId(UUID uuid) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationRuleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationRuleEntity.java index 0e8264d4f7..af3932c33a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationRuleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationRuleEntity.java @@ -69,6 +69,9 @@ public class NotificationRuleEntity extends BaseSqlEntity { @Column(name = ModelConstants.NOTIFICATION_RULE_ADDITIONAL_CONFIG_PROPERTY) private JsonNode additionalConfig; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + public NotificationRuleEntity() {} public NotificationRuleEntity(NotificationRule notificationRule) { @@ -81,6 +84,7 @@ public class NotificationRuleEntity extends BaseSqlEntity { setTriggerConfig(toJson(notificationRule.getTriggerConfig())); setRecipientsConfig(toJson(notificationRule.getRecipientsConfig())); setAdditionalConfig(toJson(notificationRule.getAdditionalConfig())); + setExternalId(getUuid(notificationRule.getExternalId())); } public NotificationRuleEntity(NotificationRuleEntity other) { @@ -93,6 +97,7 @@ public class NotificationRuleEntity extends BaseSqlEntity { this.triggerConfig = other.triggerConfig; this.recipientsConfig = other.recipientsConfig; this.additionalConfig = other.additionalConfig; + this.externalId = other.externalId; } @Override @@ -107,6 +112,7 @@ public class NotificationRuleEntity extends BaseSqlEntity { notificationRule.setTriggerConfig(fromJson(triggerConfig, NotificationRuleTriggerConfig.class)); notificationRule.setRecipientsConfig(fromJson(recipientsConfig, NotificationRuleRecipientsConfig.class)); notificationRule.setAdditionalConfig(fromJson(additionalConfig, NotificationRuleConfig.class)); + notificationRule.setExternalId(getEntityId(externalId, NotificationRuleId::new)); return notificationRule; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTargetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTargetEntity.java index a4748ae0c2..62dbfc20fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTargetEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTargetEntity.java @@ -49,6 +49,9 @@ public class NotificationTargetEntity extends BaseSqlEntity @Column(name = ModelConstants.NOTIFICATION_TARGET_CONFIGURATION_PROPERTY, nullable = false) private JsonNode configuration; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + public NotificationTargetEntity() {} public NotificationTargetEntity(NotificationTarget notificationTarget) { @@ -57,6 +60,7 @@ public class NotificationTargetEntity extends BaseSqlEntity setTenantId(getTenantUuid(notificationTarget.getTenantId())); setName(notificationTarget.getName()); setConfiguration(toJson(notificationTarget.getConfiguration())); + setExternalId(getUuid(notificationTarget.getExternalId())); } @Override @@ -67,6 +71,7 @@ public class NotificationTargetEntity extends BaseSqlEntity notificationTarget.setTenantId(getTenantId(tenantId)); notificationTarget.setName(name); notificationTarget.setConfiguration(fromJson(configuration, NotificationTargetConfig.class)); + notificationTarget.setExternalId(getEntityId(externalId, NotificationTargetId::new)); return notificationTarget; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTemplateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTemplateEntity.java index 09b0ee7ee9..157217afe2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTemplateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTemplateEntity.java @@ -56,6 +56,9 @@ public class NotificationTemplateEntity extends BaseSqlEntity { +public interface NotificationRuleDao extends Dao, ExportableEntityDao { PageData findByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java index 2edbfc580a..912e873bcc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetDao.java @@ -22,11 +22,12 @@ import org.thingsboard.server.common.data.notification.targets.NotificationTarge import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.List; -public interface NotificationTargetDao extends Dao, TenantEntityDao { +public interface NotificationTargetDao extends Dao, TenantEntityDao, ExportableEntityDao { PageData findByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java index e88f3351cf..a2a896ffa4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/NotificationTemplateDao.java @@ -15,16 +15,18 @@ */ package org.thingsboard.server.dao.notification; +import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import java.util.List; -public interface NotificationTemplateDao extends Dao { +public interface NotificationTemplateDao extends Dao, ExportableEntityDao { PageData findByTenantIdAndNotificationTypesAndPageLink(TenantId tenantId, List notificationTypes, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 76947c5043..ba4afa7bd0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -754,6 +754,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return ruleChainDao.countByTenantId(tenantId); } + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id) { + deleteRuleChainById(tenantId, (RuleChainId) id); + } + @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java index 46910f119f..e37ba2a73a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationRuleDao.java @@ -55,9 +55,9 @@ public class JpaNotificationRuleDao extends JpaAbstractDao findInfosByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink) { return DaoUtil.pageToPageData(notificationRuleRepository.findInfosByTenantIdAndSearchText(tenantId.getId(), - Strings.nullToEmpty(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink, Map.of( - "templateName", "t.name" - )))) + Strings.nullToEmpty(pageLink.getTextSearch()), DaoUtil.toPageable(pageLink, Map.of( + "templateName", "t.name" + )))) .mapData(NotificationRuleInfoEntity::toData); } @@ -82,6 +82,26 @@ public class JpaNotificationRuleDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(notificationRuleRepository.findByTenantId(tenantId, DaoUtil.toPageable(pageLink))); + } + + @Override + public NotificationRuleId getExternalIdByInternal(NotificationRuleId internalId) { + return DaoUtil.toEntityId(notificationRuleRepository.getExternalIdByInternal(internalId.getId()), NotificationRuleId::new); + } + @Override protected Class getEntityClass() { return NotificationRuleEntity.class; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java index 4bc6dada62..d976802fd2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTargetDao.java @@ -76,6 +76,26 @@ public class JpaNotificationTargetDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(notificationTargetRepository.findByTenantId(tenantId, DaoUtil.toPageable(pageLink))); + } + + @Override + public NotificationTargetId getExternalIdByInternal(NotificationTargetId internalId) { + return DaoUtil.toEntityId(notificationTargetRepository.getExternalIdByInternal(internalId.getId()), NotificationTargetId::new); + } + @Override protected Class getEntityClass() { return NotificationTargetEntity.class; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java index a83ca07c28..8a7cecb3d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationTemplateDao.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -57,6 +58,26 @@ public class JpaNotificationTemplateDao extends JpaAbstractDao findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(notificationTemplateRepository.findByTenantId(tenantId, DaoUtil.toPageable(pageLink))); + } + + @Override + public NotificationTemplateId getExternalIdByInternal(NotificationTemplateId internalId) { + return DaoUtil.toEntityId(notificationTemplateRepository.getExternalIdByInternal(internalId.getId()), NotificationTemplateId::new); + } + @Override protected JpaRepository getRepository() { return notificationTemplateRepository; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRuleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRuleRepository.java index 32c6d5388e..d47201d02b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRuleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRuleRepository.java @@ -23,6 +23,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.NotificationRuleEntity; import org.thingsboard.server.dao.model.sql.NotificationRuleInfoEntity; @@ -30,7 +31,7 @@ import java.util.List; import java.util.UUID; @Repository -public interface NotificationRuleRepository extends JpaRepository { +public interface NotificationRuleRepository extends JpaRepository, ExportableEntityRepository { String RULE_INFO_QUERY = "SELECT new org.thingsboard.server.dao.model.sql.NotificationRuleInfoEntity(r, t.name, t.configuration) " + "FROM NotificationRuleEntity r INNER JOIN NotificationTemplateEntity t ON r.templateId = t.id"; @@ -59,4 +60,11 @@ public interface NotificationRuleRepository extends JpaRepository findByTenantId(UUID tenantId, Pageable pageable); + + @Query("SELECT externalId FROM NotificationRuleEntity WHERE id = :id") + UUID getExternalIdByInternal(@Param("id") UUID internalId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java index 7ba92db7d9..278fea696e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java @@ -22,13 +22,14 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.NotificationTargetEntity; import java.util.List; import java.util.UUID; @Repository -public interface NotificationTargetRepository extends JpaRepository { +public interface NotificationTargetRepository extends JpaRepository, ExportableEntityRepository { @Query("SELECT t FROM NotificationTargetEntity t WHERE t.tenantId = :tenantId " + "AND (:searchText = '' OR lower(t.name) LIKE lower(concat('%', :searchText, '%')))") @@ -52,4 +53,11 @@ public interface NotificationTargetRepository extends JpaRepository findByTenantId(UUID tenantId, Pageable pageable); + + @Query("SELECT externalId FROM NotificationTargetEntity WHERE id = :id") + UUID getExternalIdByInternal(@Param("id") UUID internalId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java index 1381c43710..73a25b98c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTemplateRepository.java @@ -23,13 +23,14 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.NotificationTemplateEntity; import java.util.List; import java.util.UUID; @Repository -public interface NotificationTemplateRepository extends JpaRepository { +public interface NotificationTemplateRepository extends JpaRepository, ExportableEntityRepository { @Query("SELECT t FROM NotificationTemplateEntity t WHERE t.tenantId = :tenantId AND " + "t.notificationType IN :notificationTypes " + @@ -43,4 +44,11 @@ public interface NotificationTemplateRepository extends JpaRepository findByTenantId(UUID tenantId, Pageable pageable); + + @Query("SELECT externalId FROM NotificationTemplateEntity WHERE id = :id") + UUID getExternalIdByInternal(@Param("id") UUID internalId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index afc4b3661e..63a4ce0a77 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -160,6 +160,11 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId()))); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id) { + deleteWidgetsBundle(tenantId, (WidgetsBundleId) id); + } + @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9e219ed933..444c00ccf2 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -797,7 +797,9 @@ CREATE TABLE IF NOT EXISTS notification_target ( tenant_id UUID NOT NULL, name VARCHAR(255) NOT NULL, configuration VARCHAR(10000) NOT NULL, - CONSTRAINT uq_notification_target_name UNIQUE (tenant_id, name) + external_id UUID, + CONSTRAINT uq_notification_target_name UNIQUE (tenant_id, name), + CONSTRAINT uq_notification_target_external_id UNIQUE (tenant_id, external_id) ); CREATE TABLE IF NOT EXISTS notification_template ( @@ -807,7 +809,9 @@ CREATE TABLE IF NOT EXISTS notification_template ( name VARCHAR(255) NOT NULL, notification_type VARCHAR(50) NOT NULL, configuration VARCHAR(10000000) NOT NULL, - CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name) + external_id UUID, + CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name), + CONSTRAINT uq_notification_template_external_id UNIQUE (tenant_id, external_id) ); CREATE TABLE IF NOT EXISTS notification_rule ( @@ -820,7 +824,9 @@ CREATE TABLE IF NOT EXISTS notification_rule ( trigger_config VARCHAR(1000) NOT NULL, recipients_config VARCHAR(10000) NOT NULL, additional_config VARCHAR(255), - CONSTRAINT uq_notification_rule_name UNIQUE (tenant_id, name) + external_id UUID, + CONSTRAINT uq_notification_rule_name UNIQUE (tenant_id, name), + CONSTRAINT uq_notification_rule_external_id UNIQUE (tenant_id, external_id) ); CREATE TABLE IF NOT EXISTS notification_request ( From 97141efd5d8437e5e9ebb1412ebfffe34710cac7 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 12 May 2023 14:56:16 +0300 Subject: [PATCH 11/47] UI for notification configs VC --- ui-ngx/src/app/core/http/entity.service.ts | 8 ++++++++ .../vc/entity-types-version-create.component.html | 4 ++-- .../vc/entity-types-version-create.component.ts | 2 ++ .../vc/entity-types-version-load.component.html | 4 ++-- .../vc/entity-types-version-load.component.ts | 8 +++++++- ui-ngx/src/app/shared/models/entity-type.models.ts | 6 ++++++ ui-ngx/src/app/shared/models/notification.models.ts | 9 ++++----- ui-ngx/src/app/shared/models/vc.models.ts | 11 ++++++++++- ui-ngx/src/assets/locale/locale.constant-en_US.json | 8 +++++++- 9 files changed, 48 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 9c3570310e..5bdbacd1c5 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -422,6 +422,14 @@ export class EntityService { pageLink.sortOrder.property = 'name'; entitiesObservable = this.notificationService.getNotificationTargets(pageLink, subType as NotificationType, config); break; + case EntityType.NOTIFICATION_TEMPLATE: + pageLink.sortOrder.property = 'name'; + entitiesObservable = this.notificationService.getNotificationTemplates(pageLink, null, config); + break; + case EntityType.NOTIFICATION_RULE: + pageLink.sortOrder.property = 'name'; + entitiesObservable = this.notificationService.getNotificationRules(pageLink, config); + break; } return entitiesObservable; } diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html index bd714d9e33..519e87fb97 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html @@ -66,10 +66,10 @@ {{ 'version-control.export-credentials' | translate }} - + {{ 'version-control.export-attributes' | translate }} - + {{ 'version-control.export-relations' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts index b41e51c5c2..ac50042887 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.ts @@ -31,6 +31,7 @@ import { PageComponent } from '@shared/components/page.component'; import { EntityTypeVersionCreateConfig, exportableEntityTypes, + entityTypesWithNoRelatedData, SyncStrategy, syncStrategyTranslationMap } from '@shared/models/vc.models'; @@ -73,6 +74,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements syncStrategyTranslations = syncStrategyTranslationMap; entityTypes = EntityType; + entityTypesWithNoRelatedData = entityTypesWithNoRelatedData loading = true; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html index 8a3b23ea5e..b508320ad7 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.html @@ -66,10 +66,10 @@ {{ 'version-control.load-credentials' | translate }} - + {{ 'version-control.load-attributes' | translate }} - + {{ 'version-control.load-relations' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts index d4f28e3a7b..2fd891df23 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-load.component.ts @@ -28,7 +28,12 @@ import { Validators } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; -import { EntityTypeVersionLoadConfig, exportableEntityTypes, VersionCreationResult } from '@shared/models/vc.models'; +import { + EntityTypeVersionLoadConfig, + exportableEntityTypes, + entityTypesWithNoRelatedData, + VersionCreationResult +} from '@shared/models/vc.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; @@ -66,6 +71,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On public entityTypesVersionLoadFormGroup: UntypedFormGroup; entityTypes = EntityType; + entityTypesWithNoRelatedData = entityTypesWithNoRelatedData loading = true; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 062694cb43..1a5f187459 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -368,6 +368,8 @@ export const entityTypeTranslations = new Map, 'label'>{ +export interface NotificationRule extends Omit, 'label'>, ExportableEntity { tenantId: TenantId; templateId: NotificationTemplateId; triggerType: TriggerType; @@ -234,7 +233,7 @@ export interface NonConfirmedNotificationEscalation { targets: Array; } -export interface NotificationTarget extends Omit, 'label'>{ +export interface NotificationTarget extends Omit, 'label'>, ExportableEntity { tenantId: TenantId; configuration: NotificationTargetConfig; } @@ -279,7 +278,7 @@ export const NotificationTargetTypeTranslationMap = new Map, 'label'>{ +export interface NotificationTemplate extends Omit, 'label'>, ExportableEntity { tenantId: TenantId; notificationType: NotificationType; configuration: NotificationTemplateConfig; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 155a9c8633..32c86900ba 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -30,9 +30,18 @@ export const exportableEntityTypes: Array = [ EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.RULE_CHAIN, - EntityType.WIDGETS_BUNDLE + EntityType.WIDGETS_BUNDLE, + EntityType.NOTIFICATION_TEMPLATE, + EntityType.NOTIFICATION_TARGET, + EntityType.NOTIFICATION_RULE ]; +export const entityTypesWithNoRelatedData: Array = [ + EntityType.NOTIFICATION_TEMPLATE, + EntityType.NOTIFICATION_TARGET, + EntityType.NOTIFICATION_RULE +] + export interface VersionCreateConfig { saveRelations: boolean; saveAttributes: boolean; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bcaa4a183f..6b3110771c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2060,9 +2060,15 @@ "type-queue": "Queue", "type-notification": "Notification", "type-notification-rule": "Notification rule", + "type-notification-rules": "Notification rules", + "list-of-notification-rules": "{ count, plural, =1 {One notification rule} other {List of # notification rules} }", "type-notification-target": "Notification recipient", + "type-notification-targets": "Notification recipients", + "list-of-notification-targets": "{ count, plural, =1 {One notification recipient} other {List of # notification recipients} }", "type-notification-request": "Notification request", - "type-notification-template": "Notification template" + "type-notification-template": "Notification template", + "type-notification-templates": "Notification templates", + "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }" }, "entity-field": { "created-time": "Created time", From 7703af0d05dbd24b83ad55e2c663027090a12c6f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 15 May 2023 11:22:47 +0300 Subject: [PATCH 12/47] UI: Fixed control widget title change --- .../control/round-switch-widget-settings.component.ts | 4 ++-- .../control/slide-toggle-widget-settings.component.ts | 4 ++-- .../control/switch-control-widget-settings.component.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts index 2c470ea363..5b44d1b7f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts @@ -72,8 +72,8 @@ export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent protected prepareOutputSettings(settings: any): WidgetSettings { return { - title: settings.title, - ...settings.switchRpcSettings + ...settings.switchRpcSettings, + title: settings.title }; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts index 8315d7eaf4..e89d6a9d6b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts @@ -78,10 +78,10 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent protected prepareOutputSettings(settings: any): WidgetSettings { return { + ...settings.switchRpcSettings, title: settings.title, labelPosition: settings.labelPosition, - sliderColor: settings.sliderColor, - ...settings.switchRpcSettings + sliderColor: settings.sliderColor }; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts index a95cea4628..fd6ebec6cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts @@ -75,9 +75,9 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen protected prepareOutputSettings(settings: any): WidgetSettings { return { + ...settings.switchRpcSettings, title: settings.title, - showOnOffLabels: settings.showOnOffLabels, - ...settings.switchRpcSettings + showOnOffLabels: settings.showOnOffLabels }; } } From 8634916bb2ec1e3c3d55309c2cc2d27109a3a80a Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 15 May 2023 14:33:49 +0300 Subject: [PATCH 13/47] UI: Fixed api usage widget fill color --- .../home/pages/api-usage/api_usage_json.raw | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw index a4c9801720..769849cc9e 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw @@ -32,8 +32,8 @@ "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "usePostProcessing": true, + "postFuncBody": "return value ? value : 'ENABLED';" }, { "name": "jsExecutionLimit", @@ -72,7 +72,7 @@ "decimals": null, "funcBody": null, "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : '';" + "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" }, { "name": "jsExecutionApiState", @@ -190,8 +190,8 @@ "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "usePostProcessing": true, + "postFuncBody": "return value ? value : 'ENABLED';" }, { "name": "storageDataPointsLimit", @@ -230,7 +230,7 @@ "decimals": null, "funcBody": null, "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : '';" + "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" }, { "name": "dbApiState", @@ -348,8 +348,8 @@ "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "usePostProcessing": true, + "postFuncBody": "return value ? value : 'ENABLED';" }, { "name": "ruleEngineExecutionLimit", @@ -388,7 +388,7 @@ "decimals": null, "funcBody": null, "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : '';" + "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" }, { "name": "ruleEngineApiState", @@ -516,8 +516,8 @@ "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "usePostProcessing": true, + "postFuncBody": "return value ? value : 'ENABLED';" }, { "name": "transportMsgLimit", @@ -556,7 +556,7 @@ "decimals": null, "funcBody": null, "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : '';" + "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" }, { "name": "transportApiState", @@ -687,8 +687,8 @@ "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "usePostProcessing": true, + "postFuncBody": "return value ? value : 'ENABLED';" }, { "name": "createdAlarmsLimit", @@ -727,7 +727,7 @@ "decimals": null, "funcBody": null, "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : '';" + "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" }, { "name": "alarmApiState", From e3b9099bb16f780914aaf973af40e334020e9448 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 17 May 2023 20:43:33 +0200 Subject: [PATCH 14/47] fixed cookie deserialization vulnerability --- .../security/auth/oauth2/CookieUtils.java | 25 ++++++- ...th2AuthorizationRequestRepositoryTest.java | 66 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java index 429e185b3e..4deccb35a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java @@ -15,14 +15,20 @@ */ package org.thingsboard.server.service.security.auth.oauth2; +import lombok.extern.slf4j.Slf4j; import org.springframework.util.SerializationUtils; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; import java.util.Base64; import java.util.Optional; +@Slf4j public class CookieUtils { public static Optional getCookie(HttpServletRequest request, String name) { @@ -67,7 +73,22 @@ public class CookieUtils { } public static T deserialize(Cookie cookie, Class cls) { - return cls.cast(SerializationUtils.deserialize( - Base64.getUrlDecoder().decode(cookie.getValue()))); + byte[] decodedBytes = Base64.getUrlDecoder().decode(cookie.getValue()); + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(decodedBytes)) { + @Override + protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { + String name = desc.getName(); + if (!cls.getName().equals(name)) { + throw new ClassNotFoundException("Class not allowed for deserialization: " + name); + } + return super.resolveClass(desc); + } + }) { + + return cls.cast(ois.readObject()); + } catch (Exception e) { + log.debug("Failed to deserialize class from cookie.", e.getCause()); + return null; + } } } diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java new file mode 100644 index 0000000000..3691952b22 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepositoryTest.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import static org.junit.Assert.assertEquals; +import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME; + +public class HttpCookieOAuth2AuthorizationRequestRepositoryTest { + + private static final String SERIALIZED_ATTACK_STRING = + "rO0ABXNyAHVvcmcudGhpbmdzYm9hcmQuc2VydmVyLnNlcnZpY2Uuc2VjdXJpdHkuYXV0aC5vYXV0aDIuSHR0cENvb2tpZU9BdXRoMkF1dGhvcml6YXRpb25SZXF1ZXN0UmVwb3NpdG9yeVRlc3QkTWFsaWNpb3VzQ2xhc3MAAAAAAAAAAAIAAHhw"; + + private static int maliciousMethodInvocationCounter; + + @Before + public void resetInvocationCounter() { + maliciousMethodInvocationCounter = 0; + } + + @Test + public void whenLoadAuthorizationRequest_thenMaliciousMethodNotInvoked() { + HttpCookieOAuth2AuthorizationRequestRepository cookieRequestRepo = new HttpCookieOAuth2AuthorizationRequestRepository(); + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + Cookie cookie = new Cookie(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, SERIALIZED_ATTACK_STRING); + Mockito.when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + cookieRequestRepo.loadAuthorizationRequest(request); + + assertEquals(0, maliciousMethodInvocationCounter); + } + + private static class MaliciousClass implements Serializable { + private static final long serialVersionUID = 0L; + + public void maliciousMethod() { + maliciousMethodInvocationCounter++; + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + maliciousMethod(); + } + } +} From 733a2a03e4b8b7d1dfa4c9ff5d7f3951574e5ed7 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 19 May 2023 10:55:19 +0300 Subject: [PATCH 15/47] UI: Custom translate for alarm type --- .../home/components/alarm/alarm-details-dialog.component.ts | 4 +++- .../app/modules/home/components/alarm/alarm-table-config.ts | 3 ++- .../components/notification/notification.component.html | 2 +- .../shared/components/notification/notification.component.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts index cfd2251ea7..57578cae73 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts @@ -35,6 +35,7 @@ import { DatePipe } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; +import { UtilsService } from '@core/services/utils.service'; export interface AlarmDetailsDialogData { alarmId?: string; @@ -79,6 +80,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent, public fb: UntypedFormBuilder) { super(store, router, dialogRef); @@ -136,7 +138,7 @@ export class AlarmDetailsDialogComponent extends DialogComponent new EntityTableColumn('originatorName', 'alarm.originator', '25%', (entity) => entity.originatorName, entity => ({}), false)); this.columns.push( - new EntityTableColumn('type', 'alarm.type', '25%')); + new EntityTableColumn('type', 'alarm.type', '25%', + entity => this.utilsService.customTranslation(entity.type, entity.type))); this.columns.push( new EntityTableColumn('severity', 'alarm.severity', '25%', (entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)), diff --git a/ui-ngx/src/app/shared/components/notification/notification.component.html b/ui-ngx/src/app/shared/components/notification/notification.component.html index 6801ef0c20..a7da0c1865 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.html +++ b/ui-ngx/src/app/shared/components/notification/notification.component.html @@ -28,7 +28,7 @@
-
+