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 index 09ce186811..321112c4eb 100644 --- a/application/src/main/data/upgrade/3.5.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.5.0/schema_update.sql @@ -14,4 +14,10 @@ -- limitations under the License. -- -ALTER TABLE notification_rule ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT true; +-- FIX DASHBOARD TEMPLATES AFTER ANGULAR MIGRATION TO VER.15 + +UPDATE dashboard SET configuration = REPLACE(configuration, 'mat-button mat-icon-button', 'mat-icon-button') + WHERE configuration like '%mat-button mat-icon-button%'; + +UPDATE widget_type SET descriptor = REPLACE(descriptor, 'mat-button mat-icon-button', 'mat-icon-button') + WHERE descriptor like '%mat-button mat-icon-button%'; diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql new file mode 100644 index 0000000000..3bc2c99168 --- /dev/null +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -0,0 +1,54 @@ +-- +-- 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. +-- + +ALTER TABLE notification_rule ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT true; + +-- 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/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 f3a3a2397c..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,16 +42,15 @@ 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; @@ -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/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index fc72c354ed..51d7435f0e 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -257,18 +257,19 @@ public class ThingsboardInstallService { case "3.5.0": log.info("Upgrading ThingsBoard from version 3.5.0 to 3.5.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.5.0"); - - entityDatabaseSchemaService.createOrUpdateViewsAndFunctions(); - entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); - log.info("Updating system data..."); - systemDataLoaderService.updateSystemWidgets(); - installScripts.loadSystemLwm2mResources(); + case "3.5.1": + log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.5.1"); + //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; - //TODO update CacheCleanupService on the next version upgrade default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); - } + entityDatabaseSchemaService.createOrUpdateViewsAndFunctions(); + entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry); + log.info("Updating system data..."); + systemDataLoaderService.updateSystemWidgets(); + installScripts.loadSystemLwm2mResources(); } log.info("Upgrade finished successfully!"); 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/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 38b3fc60e6..ed4bd64c8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -727,6 +727,27 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.5.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3005001)) { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.1", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + + try { + String[] entityNames = new String[]{"device"}; + for (String entityName : entityNames) { + conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN search_text CASCADE"); + } + } catch (Exception e) {} + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index 1e54d8d48f..278fc0adf5 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -84,7 +84,8 @@ public class DefaultCacheCleanupService implements CacheCleanupService { break; case "3.4.4": log.info("Clearing cache to upgrade from version 3.4.4 to 3.5.0"); - clearCacheByName("deviceProfiles"); + clearAll(); + break; default: //Do nothing, since cache cleanup is optional. } 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 a8e3caad1f..0cb9dc780b 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 @@ -64,8 +64,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 3e7f67e09b..9c57b3cc77 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 @@ -44,10 +44,10 @@ import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigg 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.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.notification.NotificationRuleProcessor; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.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/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 5cf051367a..b428e1cc05 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -16,8 +16,6 @@ package org.thingsboard.server.service.resource; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.model.DDFFileParser; -import org.eclipse.leshan.core.model.DefaultDDFFileValidator; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; @@ -53,11 +51,9 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService { private final ResourceService resourceService; - private final DDFFileParser ddfFileParser; public DefaultTbResourceService(ResourceService resourceService) { this.resourceService = resourceService; - this.ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); } @Override 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/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/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..c8f8f945f9 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.util.ThrowingRunnable; 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; @@ -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/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java index bb2e49bd62..4a07444bbb 100644 --- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; +import org.thingsboard.server.common.data.util.TbDDFFileParser; import org.thingsboard.server.dao.exception.DataValidationException; import java.io.ByteArrayInputStream; @@ -41,7 +42,7 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA @Slf4j public class LwM2mObjectModelUtils { - private static final DDFFileParser ddfFileParser = new DDFFileParser(new DefaultDDFFileValidator()); + private static final TbDDFFileParser ddfFileParser = new TbDDFFileParser(); public static void toLwm2mResource (TbResource resource) throws ThingsboardException { try { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f776887287..46418bb942 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -495,8 +495,8 @@ cache: timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_RULES_TTL:30}" maxSize: "${CACHE_SPECS_NOTIFICATION_RULES_MAX_SIZE:1000}" rateLimits: - timeToLiveInMinutes: "${CACHE_SPECS_RATE_LIMITS_TTL:60}" - maxSize: "${CACHE_SPECS_RATE_LIMITS_MAX_SIZE:100000}" + timeToLiveInMinutes: "${CACHE_SPECS_RATE_LIMITS_TTL:120}" + maxSize: "${CACHE_SPECS_RATE_LIMITS_MAX_SIZE:200000}" #Disable this because it is not required. spring.data.redis.repositories.enabled: false 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 85cc0a2478..455b897fcd 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 @@ -72,9 +72,9 @@ import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersion 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.dao.util.limits.LimitedApi; +import org.thingsboard.server.dao.util.limits.RateLimitService; import org.thingsboard.server.queue.notification.NotificationRuleProcessor; -import org.thingsboard.server.service.apiusage.limits.LimitedApi; -import org.thingsboard.server.service.apiusage.limits.RateLimitService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.ArrayList; @@ -480,7 +480,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(getMyNotifications(false, 100)).size().isZero(); createDevice("Device 1", "default", "111"); - await().atMost(15, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .untilAsserted(() -> { assertThat(getMyNotifications(false, 100)).size().isEqualTo(1); }); @@ -494,9 +494,10 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { rule.setEnabled(true); saveNotificationRule(rule); + TimeUnit.SECONDS.sleep(2); // for rule update event to reach rules cache createDevice("Device 3", "default", "333"); - await().atMost(15, TimeUnit.SECONDS) + await().atMost(30, TimeUnit.SECONDS) .untilAsserted(() -> { assertThat(getMyNotifications(false, 100)).size().isEqualTo(2); }); diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index e2c5985553..5c9d94cde2 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -45,6 +45,8 @@ import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -75,6 +77,32 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { "\n" + ""; + private static final String LWM2M_TEST_MODEL_WITH_XXE = " ]>" + + "\n" + + "\n" + + "My first resource\n" + + "\n" + + "0\n" + + "\n" + + "&ObjectVersion;\n" + + "Multiple\n" + + "Mandatory\n" + + "\n" + + "\n" + + "LWM2M\n" + + "RW\n" + + "Single\n" + + "Mandatory\n" + + "String\n" + + "0..255\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + private static final String DEFAULT_FILE_NAME = "test.jks"; private IdComparator idComparator = new IdComparator<>(); @@ -126,11 +154,11 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { loginTenantAdmin(); - Assert.assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId)); createResource("test", DEFAULT_FILE_NAME); - Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); try { assertThatThrownBy(() -> createResource("test1", 1 + DEFAULT_FILE_NAME)) @@ -145,19 +173,19 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { @Test public void sumDataSizeByTenantId() throws Exception { - Assert.assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId)); createResource("test", DEFAULT_FILE_NAME); - Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); int maxSumDataSize = 8; for (int i = 2; i <= maxSumDataSize; i++) { createResource("test" + i, i + DEFAULT_FILE_NAME); - Assert.assertEquals(i, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(i, resourceService.sumDataSizeByTenantId(tenantId)); } - Assert.assertEquals(maxSumDataSize, resourceService.sumDataSizeByTenantId(tenantId)); + assertEquals(maxSumDataSize, resourceService.sumDataSizeByTenantId(tenantId)); } private TbResource createResource(String title, String filename) throws Exception { @@ -184,16 +212,16 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(savedResource); Assert.assertNotNull(savedResource.getId()); Assert.assertTrue(savedResource.getCreatedTime() > 0); - Assert.assertEquals(resource.getTenantId(), savedResource.getTenantId()); - Assert.assertEquals(resource.getTitle(), savedResource.getTitle()); - Assert.assertEquals(resource.getResourceKey(), savedResource.getResourceKey()); - Assert.assertEquals(resource.getData(), savedResource.getData()); + assertEquals(resource.getTenantId(), savedResource.getTenantId()); + assertEquals(resource.getTitle(), savedResource.getTitle()); + assertEquals(resource.getResourceKey(), savedResource.getResourceKey()); + assertEquals(resource.getData(), savedResource.getData()); savedResource.setTitle("My new resource"); resourceService.save(savedResource); TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); - Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle()); + assertEquals(foundResource.getTitle(), savedResource.getTitle()); resourceService.delete(savedResource, null); } @@ -211,10 +239,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(savedResource); Assert.assertNotNull(savedResource.getId()); Assert.assertTrue(savedResource.getCreatedTime() > 0); - Assert.assertEquals(resource.getTenantId(), savedResource.getTenantId()); - Assert.assertEquals("My first resource id=0 v1.0", savedResource.getTitle()); - Assert.assertEquals("0_1.0", savedResource.getResourceKey()); - Assert.assertEquals(resource.getData(), savedResource.getData()); + assertEquals(resource.getTenantId(), savedResource.getTenantId()); + assertEquals("My first resource id=0 v1.0", savedResource.getTitle()); + assertEquals("0_1.0", savedResource.getResourceKey()); + assertEquals(resource.getData(), savedResource.getData()); resourceService.delete(savedResource, null); } @@ -228,7 +256,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setData("Test Data"); TbResource savedResource = resourceService.save(resource); - Assert.assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId()); + assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId()); resourceService.delete(savedResource, null); } @@ -285,6 +313,21 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { }); } + @Test + public void testSaveLwm2mTbResourceWithXXE() { + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setResourceType(ResourceType.LWM2M_MODEL); + resource.setFileName("xxe_test_model.xml"); + resource.setData(Base64.getEncoder().encodeToString(LWM2M_TEST_MODEL_WITH_XXE.getBytes())); + + DataValidationException thrown = assertThrows(DataValidationException.class, () -> { + resourceService.save(resource); + }); + assertEquals("Failed to parse file xxe_test_model.xml", thrown.getMessage()); + } + + @Test public void testFindResourceById() throws Exception { TbResource resource = new TbResource(); @@ -296,7 +339,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); Assert.assertNotNull(foundResource); - Assert.assertEquals(savedResource, foundResource); + assertEquals(savedResource, foundResource); resourceService.delete(savedResource, null); } @@ -312,7 +355,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { TbResource foundResource = resourceService.getResource(tenantId, savedResource.getResourceType(), savedResource.getResourceKey()); Assert.assertNotNull(foundResource); - Assert.assertEquals(savedResource, foundResource); + assertEquals(savedResource, foundResource); resourceService.delete(savedResource, null); } @@ -366,7 +409,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Collections.sort(resources, idComparator); Collections.sort(loadedResources, idComparator); - Assert.assertEquals(resources, loadedResources); + assertEquals(resources, loadedResources); resourceService.deleteResourcesByTenantId(tenantId); @@ -427,14 +470,14 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Collections.sort(resources, idComparator); Collections.sort(loadedResources, idComparator); - Assert.assertEquals(resources, loadedResources); + assertEquals(resources, loadedResources); resourceService.deleteResourcesByTenantId(tenantId); pageLink = new PageLink(100); pageData = resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); - Assert.assertEquals(pageData.getData().size(), 100); + assertEquals(pageData.getData().size(), 100); resourceService.deleteResourcesByTenantId(TenantId.SYS_TENANT_ID); 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(); + } + } +} 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/pom.xml b/common/data/pom.xml index 3257d4cf32..b596f01268 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -112,6 +112,11 @@ com.google.protobuf protobuf-java-util + + org.eclipse.leshan + leshan-core + compile + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java new file mode 100644 index 0000000000..b287956e4a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseDataWithAdditionalInfo.java @@ -0,0 +1,108 @@ +/** + * 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; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Created by ashvayka on 19.02.18. + */ +@Slf4j +public abstract class BaseDataWithAdditionalInfo extends BaseData implements HasAdditionalInfo { + + public static final ObjectMapper mapper = new ObjectMapper(); + @NoXss + private transient JsonNode additionalInfo; + @JsonIgnore + private byte[] additionalInfoBytes; + + public BaseDataWithAdditionalInfo() { + super(); + } + + public BaseDataWithAdditionalInfo(I id) { + super(id); + } + + public BaseDataWithAdditionalInfo(BaseDataWithAdditionalInfo baseData) { + super(baseData); + setAdditionalInfo(baseData.getAdditionalInfo()); + } + + @Override + public JsonNode getAdditionalInfo() { + return getJson(() -> additionalInfo, () -> additionalInfoBytes); + } + + public void setAdditionalInfo(JsonNode addInfo) { + setJson(addInfo, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + BaseDataWithAdditionalInfo that = (BaseDataWithAdditionalInfo) o; + return Arrays.equals(additionalInfoBytes, that.additionalInfoBytes); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), additionalInfoBytes); + } + + public static JsonNode getJson(Supplier jsonData, Supplier binaryData) { + JsonNode json = jsonData.get(); + if (json != null) { + return json; + } else { + byte[] data = binaryData.get(); + if (data != null) { + try { + return mapper.readTree(new ByteArrayInputStream(data)); + } catch (IOException e) { + log.warn("Can't deserialize json data: ", e); + return null; + } + } else { + return null; + } + } + } + + public static void setJson(JsonNode json, Consumer jsonConsumer, Consumer bytesConsumer) { + jsonConsumer.accept(json); + try { + bytesConsumer.accept(mapper.writeValueAsBytes(json)); + } catch (JsonProcessingException e) { + log.warn("Can't serialize json data: ", e); + } + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 27457bda34..45e4244bcb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -40,7 +40,7 @@ import java.util.Optional; @ApiModel @EqualsAndHashCode(callSuper = true) @Slf4j -public class Device extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity { +public class Device extends BaseDataWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity { private static final long serialVersionUID = 2807343040519543363L; @@ -201,11 +201,6 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen } } - @Override - public String getSearchText() { - return getName(); - } - @ApiModelProperty(position = 10, value = "JSON object with Ota Package Id.") public OtaPackageId getFirmwareId() { return firmwareId; 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/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 607ca7f639..5ca8dff863 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 @@ -61,6 +62,8 @@ public class NotificationRule extends BaseData implements Ha private NotificationRuleConfig additionalConfig; + private NotificationRuleId externalId; + public NotificationRule(NotificationRule other) { super(other); this.tenantId = other.tenantId; @@ -71,6 +74,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/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java new file mode 100644 index 0000000000..7c1b9307c5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TbDDFFileParser.java @@ -0,0 +1,272 @@ +/** + * 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.util; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.LwM2m; +import org.eclipse.leshan.core.model.DDFFileValidator; +import org.eclipse.leshan.core.model.DefaultDDFFileValidator; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.util.StringUtils; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class TbDDFFileParser { + private static final DDFFileValidator ddfFileValidator = new DefaultDDFFileValidator(); + + public List parse(InputStream inputStream, String streamName) + throws InvalidDDFFileException, IOException { + streamName = streamName == null ? "" : streamName; + + log.debug("Parsing DDF file {}", streamName); + + try { + // Parse XML file + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(inputStream); + + // Get DDF file validator + LwM2m.LwM2mVersion lwm2mVersion = null; + ddfFileValidator.validate(document); + + // Build list of ObjectModel + ArrayList objects = new ArrayList<>(); + NodeList nodeList = document.getDocumentElement().getElementsByTagName("Object"); + for (int i = 0; i < nodeList.getLength(); i++) { + objects.add(parseObject(nodeList.item(i), streamName, lwm2mVersion, true)); + } + return objects; + } catch (InvalidDDFFileException | SAXException e) { + throw new InvalidDDFFileException(e, "Invalid DDF file %s", streamName); + } + catch (ParserConfigurationException e) { + throw new IllegalStateException("Unable to create Document Builder", e); + } + } + + private ObjectModel parseObject(Node object, String streamName, LwM2m.LwM2mVersion schemaVersion, boolean validate) + throws InvalidDDFFileException { + + Node objectType = object.getAttributes().getNamedItem("ObjectType"); + if (validate && (objectType == null || !"MODefinition".equals(objectType.getTextContent()))) { + throw new InvalidDDFFileException( + "Object element in %s MUST have a ObjectType attribute equals to 'MODefinition'.", streamName); + } + + Integer id = null; + String name = null; + String description = null; + String version = ObjectModel.DEFAULT_VERSION; + Boolean multiple = null; + Boolean mandatory = null; + Map resources = new HashMap<>(); + String urn = null; + String description2 = null; + String lwm2mVersion = ObjectModel.DEFAULT_VERSION; + + for (int i = 0; i < object.getChildNodes().getLength(); i++) { + Node field = object.getChildNodes().item(i); + if (field.getNodeType() != Node.ELEMENT_NODE) + continue; + + switch (field.getNodeName()) { + case "ObjectID": + id = Integer.valueOf(field.getTextContent()); + break; + case "Name": + name = field.getTextContent(); + break; + case "Description1": + description = field.getTextContent(); + break; + case "ObjectVersion": + if (!StringUtils.isEmpty(field.getTextContent())) { + version = field.getTextContent(); + } + break; + case "MultipleInstances": + if ("Multiple".equals(field.getTextContent())) { + multiple = true; + } else if ("Single".equals(field.getTextContent())) { + multiple = false; + } + break; + case "Mandatory": + if ("Mandatory".equals(field.getTextContent())) { + mandatory = true; + } else if ("Optional".equals(field.getTextContent())) { + mandatory = false; + } + break; + case "Resources": + for (int j = 0; j < field.getChildNodes().getLength(); j++) { + Node item = field.getChildNodes().item(j); + if (item.getNodeType() != Node.ELEMENT_NODE) + continue; + + if (item.getNodeName().equals("Item")) { + ResourceModel resource = parseResource(item, streamName); + if (validate && resources.containsKey(resource.id)) { + throw new InvalidDDFFileException( + "Object %s in %s contains at least 2 resources with same id %s.", + id != null ? id : "", streamName, resource.id); + } else { + resources.put(resource.id, resource); + } + } + } + break; + case "ObjectURN": + urn = field.getTextContent(); + break; + case "LWM2MVersion": + if (!StringUtils.isEmpty(field.getTextContent())) { + lwm2mVersion = field.getTextContent(); + if (schemaVersion != null && !schemaVersion.toString().equals(lwm2mVersion)) { + throw new InvalidDDFFileException( + "LWM2MVersion is not consistent with xml shema(xsi:noNamespaceSchemaLocation) in %s : %s expected but was %s.", + streamName, schemaVersion, lwm2mVersion); + } + } + break; + case "Description2": + description2 = field.getTextContent(); + break; + default: + break; + } + } + + return new ObjectModel(id, name, description, version, multiple, mandatory, resources.values(), urn, + lwm2mVersion, description2); + + } + + private ResourceModel parseResource(Node item, String streamName) throws DOMException, InvalidDDFFileException { + + Integer id = Integer.valueOf(item.getAttributes().getNamedItem("ID").getTextContent()); + String name = null; + ResourceModel.Operations operations = null; + Boolean multiple = false; + Boolean mandatory = false; + ResourceModel.Type type = null; + String rangeEnumeration = null; + String units = null; + String description = null; + + for (int i = 0; i < item.getChildNodes().getLength(); i++) { + Node field = item.getChildNodes().item(i); + if (field.getNodeType() != Node.ELEMENT_NODE) + continue; + + switch (field.getNodeName()) { + case "Name": + name = field.getTextContent(); + break; + case "Operations": + String strOp = field.getTextContent(); + if (strOp != null && !strOp.isEmpty()) { + operations = ResourceModel.Operations.valueOf(strOp); + } else { + operations = ResourceModel.Operations.NONE; + } + break; + case "MultipleInstances": + if ("Multiple".equals(field.getTextContent())) { + multiple = true; + } else if ("Single".equals(field.getTextContent())) { + multiple = false; + } + break; + case "Mandatory": + if ("Mandatory".equals(field.getTextContent())) { + mandatory = true; + } else if ("Optional".equals(field.getTextContent())) { + mandatory = false; + } + break; + case "Type": + switch (field.getTextContent()) { + case "String": + type = ResourceModel.Type.STRING; + break; + case "Integer": + type = ResourceModel.Type.INTEGER; + break; + case "Float": + type = ResourceModel.Type.FLOAT; + break; + case "Boolean": + type = ResourceModel.Type.BOOLEAN; + break; + case "Opaque": + type = ResourceModel.Type.OPAQUE; + break; + case "Time": + type = ResourceModel.Type.TIME; + break; + case "Objlnk": + type = ResourceModel.Type.OBJLNK; + break; + case "Unsigned Integer": + type = ResourceModel.Type.UNSIGNED_INTEGER; + break; + case "Corelnk": + type = ResourceModel.Type.CORELINK; + break; + case "": + type = ResourceModel.Type.NONE; + break; + default: + break; + } + break; + case "RangeEnumeration": + rangeEnumeration = field.getTextContent(); + break; + case "Units": + units = field.getTextContent(); + break; + case "Description": + description = field.getTextContent(); + break; + default: + break; + } + } + return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description); + } +} \ No newline at end of file diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index 8c65c04939..ff2464f6d2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.codec.CodecException; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.util.TbDDFFileParser; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; @@ -142,9 +143,9 @@ public class LwM2mTransportServerHelper { .build(); } - public ObjectModel parseFromXmlToObjectModel(byte[] xmlByte, String streamName, DefaultDDFFileValidator ddfValidator) { + public ObjectModel parseFromXmlToObjectModel(byte[] xmlByte, String streamName) { try { - DDFFileParser ddfFileParser = new DDFFileParser(ddfValidator); + TbDDFFileParser ddfFileParser = new TbDDFFileParser(); return ddfFileParser.parse(new ByteArrayInputStream(xmlByte), streamName).get(0); } catch (IOException | InvalidDDFFileException e) { log.error("Could not parse the XML file [{}]", streamName, e); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index d649c06b07..3dd13cd171 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -154,8 +154,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { Optional tbResource = context.getTransportResourceCache().get(this.tenantId, LWM2M_MODEL, key); return tbResource.map(resource -> helper.parseFromXmlToObjectModel( Base64.getDecoder().decode(resource.getData()), - key + ".xml", - new DefaultDDFFileValidator())).orElse(null); + key + ".xml")).orElse(null); } } } 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/AbstractDeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java index 403cb04aef..31ed02914b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java @@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import org.thingsboard.server.dao.util.mapping.JsonStringType; @@ -47,7 +46,7 @@ import java.util.UUID; @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) }) @MappedSuperclass -public abstract class AbstractDeviceEntity extends BaseSqlEntity implements SearchTextEntity { +public abstract class AbstractDeviceEntity extends BaseSqlEntity { @Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY, columnDefinition = "uuid") private UUID tenantId; @@ -64,9 +63,6 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti @Column(name = ModelConstants.DEVICE_LABEL_PROPERTY) private String label; - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) - private String searchText; - @Type(type = "json") @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; @@ -131,23 +127,12 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti this.type = deviceEntity.getType(); this.name = deviceEntity.getName(); this.label = deviceEntity.getLabel(); - this.searchText = deviceEntity.getSearchText(); this.additionalInfo = deviceEntity.getAdditionalInfo(); this.firmwareId = deviceEntity.getFirmwareId(); this.softwareId = deviceEntity.getSoftwareId(); this.externalId = deviceEntity.getExternalId(); } - @Override - public String getSearchTextSource() { - return name; - } - - @Override - public void setSearchText(String searchText) { - this.searchText = searchText; - } - protected Device toDevice() { Device device = new Device(new DeviceId(getUuid())); device.setCreatedTime(createdTime); 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 c3bc8cce0a..15283c9fc1 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 @@ -72,6 +72,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) { @@ -85,6 +88,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) { @@ -98,6 +102,7 @@ public class NotificationRuleEntity extends BaseSqlEntity { this.triggerConfig = other.triggerConfig; this.recipientsConfig = other.recipientsConfig; this.additionalConfig = other.additionalConfig; + this.externalId = other.externalId; } @Override @@ -113,6 +118,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/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index d9e79dd594..8dfdaf8a3e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -348,7 +348,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) { - return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, getDetailsAsString(details))); + return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, details != null ? getDetailsAsString(details) : null)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index 89ed713975..56f6a7c647 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -35,27 +35,30 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, - @Param("searchText") String searchText, + @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :profileId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, @Param("profileId") UUID profileId, - @Param("searchText") String searchText, + @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM DeviceInfoEntity d " + "WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findDeviceInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, - @Param("searchText") String searchText, + @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId") @@ -63,14 +66,16 @@ public interface DeviceRepository extends JpaRepository, Exp Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -79,7 +84,8 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + "AND d.firmwareId = null " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndTypeAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, @Param("textSearch") String textSearch, @@ -88,7 +94,8 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + "AND d.softwareId = null " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndTypeAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, @Param("textSearch") String textSearch, @@ -109,7 +116,8 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -123,7 +131,7 @@ public interface DeviceRepository extends JpaRepository, Exp "AND ((:deviceType) IS NULL OR d.type = :deviceType) " + "AND (:deviceProfileId IS NULL OR d.deviceProfileId = uuid(:deviceProfileId)) " + "AND ((:filterByActive) IS FALSE OR d.active = :deviceActive) " + - "AND (LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + "OR LOWER(d.type) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + "OR LOWER(d.customerTitle) LIKE LOWER(CONCAT('%', :textSearch, '%')))") @@ -155,21 +163,23 @@ public interface DeviceRepository extends JpaRepository, Exp @Query("SELECT d FROM DeviceEntity d, RelationEntity re WHERE d.tenantId = :tenantId " + "AND d.id = re.toId AND re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, - @Param("searchText") String searchText, + @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM DeviceEntity d, RelationEntity re WHERE d.tenantId = :tenantId " + "AND d.id = re.toId AND re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + "AND (LOWER(d.name) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(d.label) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, - @Param("searchText") String searchText, + @Param("textSearch") String textSearch, Pageable pageable); /** diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index c40fd54bae..d1ea342bd2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.model.sql.DeviceEntity; -import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; @@ -56,7 +56,7 @@ import java.util.UUID; @Component @SqlDao @Slf4j -public class JpaDeviceDao extends JpaAbstractSearchTextDao implements DeviceDao { +public class JpaDeviceDao extends JpaAbstractDao implements DeviceDao { @Autowired private DeviceRepository deviceRepository; 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 af526b8d64..aa3258287e 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 @@ -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 2f2ffc4a43..1bdfe57bba 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/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 9d4ffd58b4..2135e98eed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.query; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; @@ -66,6 +67,7 @@ import java.util.stream.Collectors; @Slf4j public class DefaultEntityQueryRepository implements EntityQueryRepository { private static final Map entityTableMap = new HashMap<>(); + private static final Map entityNameColumns = new HashMap<>(); private static final String SELECT_PHONE = " CASE WHEN entity.entity_type = 'TENANT' THEN (select phone from tenant where id = entity_id)" + " WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id) END as phone"; private static final String SELECT_ZIP = " CASE WHEN entity.entity_type = 'TENANT' THEN (select zip from tenant where id = entity_id)" + @@ -242,6 +244,24 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { entityTableMap.put(EntityType.DEVICE_PROFILE, "device_profile"); entityTableMap.put(EntityType.ASSET_PROFILE, "asset_profile"); entityTableMap.put(EntityType.TENANT_PROFILE, "tenant_profile"); + + entityNameColumns.put(EntityType.DEVICE, "name"); + entityNameColumns.put(EntityType.CUSTOMER, "title"); + entityNameColumns.put(EntityType.DASHBOARD, "title"); + entityNameColumns.put(EntityType.RULE_CHAIN, "name"); + entityNameColumns.put(EntityType.RULE_NODE, "name"); + entityNameColumns.put(EntityType.OTA_PACKAGE, "title"); + entityNameColumns.put(EntityType.ASSET_PROFILE, "name"); + entityNameColumns.put(EntityType.ASSET, "name"); + entityNameColumns.put(EntityType.DEVICE_PROFILE, "name"); + entityNameColumns.put(EntityType.USER, "email"); + entityNameColumns.put(EntityType.TENANT_PROFILE, "name"); + entityNameColumns.put(EntityType.TENANT, "title"); + entityNameColumns.put(EntityType.WIDGETS_BUNDLE, "title"); + entityNameColumns.put(EntityType.ENTITY_VIEW, "name"); + entityNameColumns.put(EntityType.TB_RESOURCE, "search_text"); + entityNameColumns.put(EntityType.EDGE, "name"); + entityNameColumns.put(EntityType.QUEUE, "name"); } public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ @@ -807,33 +827,38 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { private String entityNameQuery(QueryContext ctx, EntityNameFilter filter) { ctx.addStringParameter("entity_filter_name_filter", filter.getEntityNameFilter()); - + String nameColumn = getNameColumn(filter.getEntityType()); if (filter.getEntityNameFilter().startsWith("%") || filter.getEntityNameFilter().endsWith("%")) { - return "lower(e.search_text) like lower(:entity_filter_name_filter)"; + return String.format("lower(e.%s) like lower(:entity_filter_name_filter)", nameColumn); } - return "lower(e.search_text) like lower(concat(:entity_filter_name_filter, '%%'))"; + return String.format("lower(e.%s) like lower(concat(:entity_filter_name_filter, '%%'))", nameColumn); } private String typeQuery(QueryContext ctx, EntityFilter filter) { List types; String name; + String nameColumn; switch (filter.getType()) { case ASSET_TYPE: types = ((AssetTypeFilter) filter).getAssetTypes(); name = ((AssetTypeFilter) filter).getAssetNameFilter(); + nameColumn = getNameColumn(EntityType.ASSET); break; case DEVICE_TYPE: types = ((DeviceTypeFilter) filter).getDeviceTypes(); name = ((DeviceTypeFilter) filter).getDeviceNameFilter(); + nameColumn = getNameColumn(EntityType.DEVICE); break; case ENTITY_VIEW_TYPE: types = ((EntityViewTypeFilter) filter).getEntityViewTypes(); name = ((EntityViewTypeFilter) filter).getEntityViewNameFilter(); + nameColumn = getNameColumn(EntityType.ENTITY_VIEW); break; case EDGE_TYPE: types = ((EdgeTypeFilter) filter).getEdgeTypes(); name = ((EdgeTypeFilter) filter).getEdgeNameFilter(); + nameColumn = getNameColumn(EntityType.EDGE); break; default: throw new RuntimeException("Not supported!"); @@ -843,14 +868,23 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { if (!StringUtils.isEmpty(name)) { ctx.addStringParameter("entity_filter_type_query_name", name); if (name.startsWith("%") || name.endsWith("%")) { - return typesFilter + " and lower(e.search_text) like lower(:entity_filter_type_query_name)"; + return typesFilter + " and lower(e." + nameColumn + ") like lower(:entity_filter_type_query_name)"; } - return typesFilter + " and lower(e.search_text) like lower(concat(:entity_filter_type_query_name, '%%'))"; + return typesFilter + " and lower(e." + nameColumn + ") like lower(concat(:entity_filter_type_query_name, '%%'))"; } else { return typesFilter; } } + private String getNameColumn(EntityType entityType) { + String nameColumn = entityNameColumns.get(entityType); + if (nameColumn == null) { + log.error("Name column is not defined in the entityNameColumns map for entity type {}.", entityType); + throw new RuntimeException("Name column is not defined for entity type: " + entityType); + } + return nameColumn; + } + public static EntityType resolveEntityType(EntityFilter entityFilter) { switch (entityFilter.getType()) { case SINGLE_ENTITY: 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 9fbf796eb0..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,24 +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.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; @@ -73,7 +70,6 @@ public abstract class AbstractBufferedRateExecutor perTenantLimits = new ConcurrentHashMap<>(); private final AtomicInteger printQueriesIdx = new AtomicInteger(0); @@ -81,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; @@ -102,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..918f16ef19 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:120}") int rateLimitsTtl, + @Value("${cache.rateLimits.maxSize:200000}") 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); } 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 ed1a124764..ad4770e04e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -340,7 +340,6 @@ CREATE TABLE IF NOT EXISTS device ( type varchar(255), name varchar(255), label varchar(255), - search_text varchar(255), tenant_id uuid, firmware_id uuid, software_id uuid, @@ -797,7 +796,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 +808,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 ( @@ -821,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 ( diff --git a/dao/src/main/resources/sql/schema-views-and-functions.sql b/dao/src/main/resources/sql/schema-views-and-functions.sql index 2de757be54..eaa09d78cc 100644 --- a/dao/src/main/resources/sql/schema-views-and-functions.sql +++ b/dao/src/main/resources/sql/schema-views-and-functions.sql @@ -226,7 +226,11 @@ BEGIN END IF; IF NOT(existing.cleared) THEN cleared = TRUE; - UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id; + IF a_details IS NULL THEN + UPDATE alarm a SET cleared = true, clear_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + ELSE + UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; END IF; SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text; diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index ea35933d4d..25231f822f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -202,7 +202,34 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Test public void testClearAlarmProcedure() { UUID tenantId = UUID.randomUUID(); - ; + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + long clearTs = System.currentTimeMillis(); + var details = JacksonUtil.newObjectNode().put("test", 123); + AlarmApiCallResult result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs, details); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCleared()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + assertEquals(details, result.getAlarm().getDetails()); + result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs + 1, JacksonUtil.newObjectNode()); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isCleared()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + } + + @Test + public void testClearAlarmWithoutDetailsProcedure() { + UUID tenantId = UUID.randomUUID(); UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); @@ -216,6 +243,7 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { assertEquals(afterSave, result.getAlarm()); assertEquals(clearTs, result.getAlarm().getClearTs()); assertTrue(result.getAlarm().isCleared()); + assertEquals(alarm.getDetails(), result.getAlarm().getDetails()); result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs + 1, JacksonUtil.newObjectNode()); assertNotNull(result); assertNotNull(result.getAlarm()); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 880143b58a..f04331e01e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -520,4 +520,11 @@ public class TestRestClient { .then() .statusCode(HTTP_OK); } + + public void setDevicePublic(DeviceId deviceId) { + given().spec(requestSpec) + .post("/api/customer/public/device/{deviceId}", deviceId.getId()) + .then() + .statusCode(HTTP_OK); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java index eb6776a4e4..4de6fbaf17 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java @@ -73,7 +73,7 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { private static final int WIDTH = 1680; private static final int HEIGHT = 1050; private static final String REMOTE_WEBDRIVER_HOST = "http://localhost:4444"; - protected final PageLink pageLink = new PageLink(10); + protected final PageLink pageLink = new PageLink(30); private final ContainerTestSuite instance = ContainerTestSuite.getInstance(); private JavascriptExecutor js; public static final long WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(10); @@ -253,6 +253,13 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { } } + public void deleteCustomerByName(String customerName) { + Customer customer = getCustomerByName(customerName); + if (customer != null) { + testRestClient.deleteCustomer(customer.getId()); + } + } + public void deleteDeviceById(DeviceId deviceId) { if (deviceId != null) { testRestClient.deleteDevice(deviceId); @@ -283,4 +290,19 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { testRestClient.deleteDevice(device.getId()); } } + + public void deleteDeviceProfileByTitle(String deviceProfileTitle) { + DeviceProfile deviceProfile = getDeviceProfileByName(deviceProfileTitle); + if (deviceProfile != null) { + testRestClient.deleteDeviseProfile(deviceProfile.getId()); + } + } + + public void assertInvisibilityOfElement(WebElement element) { + try { + new WebDriverWait(driver, duration).until(ExpectedConditions.invisibilityOf(element)); + } catch (WebDriverException e) { + fail("Element " + element.toString() + " stay visible"); + } + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageElements.java index 465c3d918e..09edf9e852 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/CustomerPageElements.java @@ -58,7 +58,7 @@ public class CustomerPageElements extends OtherPageElementsHelper { private static final String ASSIGNED_BTN = "//button[@type='submit']"; private static final String HIDE_HOME_DASHBOARD_TOOLBAR = "//mat-checkbox[@formcontrolname='homeDashboardHideToolbar']//label"; private static final String FILTER_BTN = "//tb-filters-edit"; - private static final String TIME_BTN = "//tb-timewindow"; + private static final String TIME_BTN = "//tb-timewindow[not(@hidelabel)]"; private static final String CUSTOMER_ICON_HEADER = "//tb-breadcrumb//span[contains(text(),'Customer %s')]"; private static final String CUSTOMER_USER_ICON_HEADER = "Users"; private static final String CUSTOMER_ASSETS_ICON_HEADER = "Assets"; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java index 030f7913c1..a4404858d9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageElements.java @@ -33,26 +33,26 @@ public class DevicePageElements extends OtherPageElementsHelper { private static final String CLOSE_DEVICE_DETAILS_VIEW = "//header//mat-icon[contains(text(),'close')]/parent::button"; private static final String SUBMIT_ASSIGN_TO_CUSTOMER_BTN = "//button[@type='submit']"; private static final String ADD_DEVICE_BTN = "//mat-icon[text() = 'insert_drive_file']/parent::button"; - private static final String CREATE_DEVICE_NAME_FIELD = "//tb-device-wizard//input[@formcontrolname='name']"; private static final String HEADER_NAME_VIEW = "//header//div[@class='tb-details-title']/span"; - private static final String DESCRIPTION_FIELD_CREATE_VIEW = "//tb-device-wizard//textarea[@formcontrolname='description']"; private static final String ADD_DEVICE_VIEW = "//tb-device-wizard"; private static final String DELETE_BTN_DETAILS_TAB = "//span[contains(text(),'Delete device')]/parent::button"; private static final String CHECKBOX_GATEWAY_EDIT = "//mat-checkbox[@formcontrolname='gateway']//label"; - private static final String CHECKBOX_GATEWAY_CREATE = "//tb-device-wizard//mat-checkbox[@formcontrolname='gateway']//label"; private static final String CHECKBOX_OVERWRITE_ACTIVITY_TIME_EDIT = "//mat-checkbox[@formcontrolname='overwriteActivityTime']//label"; - private static final String CHECKBOX_OVERWRITE_ACTIVITY_TIME_CREATE = "//tb-device-wizard//mat-checkbox[@formcontrolname='overwriteActivityTime']//label"; private static final String CHECKBOX_GATEWAY_DETAILS = "//mat-checkbox[@formcontrolname='gateway']//input"; private static final String CHECKBOX_GATEWAY_PAGE = DEVICE + "/ancestor::mat-row//mat-cell[contains(@class,'cdk-column-gateway')]//mat-icon[text() = 'check_box']"; private static final String CHECKBOX_OVERWRITE_ACTIVITY_TIME_DETAILS = "//mat-checkbox[@formcontrolname='overwriteActivityTime']//input"; private static final String CLEAR_PROFILE_FIELD_BTN = "//button[@aria-label='Clear']"; private static final String DEVICE_PROFILE_REDIRECTED_BTN = "//a[@aria-label='Open device profile']"; - private static final String DEVICE_LABEL_FIELD_CREATE = "//tb-device-wizard//input[@formcontrolname='label']"; private static final String DEVICE_LABEL_PAGE = DEVICE + "/ancestor::mat-row//mat-cell[contains(@class,'cdk-column-label')]/span"; private static final String DEVICE_CUSTOMER_PAGE = DEVICE + "/ancestor::mat-row//mat-cell[contains(@class,'cdk-column-customerTitle')]/span"; - private static final String CUSTOMER_OPTION_BNT = "//div[text() = 'Customer']/ancestor::mat-step-header"; - private static final String ASSIGN_ON_CUSTOMER_FIELD = "//input[@formcontrolname='entity']"; private static final String DEVICE_LABEL_EDIT = "//input[@formcontrolname='label']"; + private static final String DEVICE_DEVICE_PROFILE_PAGE = DEVICE + "/ancestor::mat-row//mat-cell[contains(@class,'cdk-column-deviceProfileName')]/span"; + protected static final String ASSIGN_BTN = ENTITY + "/ancestor::mat-row//mat-icon[contains(text(),'assignment_ind')]/ancestor::button"; + protected static final String UNASSIGN_BTN = ENTITY + "/ancestor::mat-row//mat-icon[contains(text(),' assignment_return')]/ancestor::button"; + protected static final String ASSIGN_BTN_DETAILS_TAB = "//span[contains(text(),'Assign to customer')]/parent::button"; + protected static final String UNASSIGN_BTN_DETAILS_TAB = "//span[contains(text(),'Unassign from customer')]/parent::button"; + protected static final String ASSIGNED_FIELD_DETAILS_TAB = "//mat-label[text() = 'Assigned to customer']/parent::label/parent::div/input"; + protected static final String ASSIGN_MARKED_DEVICE_BTN = "//mat-icon[text() = 'assignment_ind']/parent::button"; public WebElement device(String deviceName) { return waitUntilElementToBeClickable(String.format(DEVICE, deviceName)); @@ -90,18 +90,10 @@ public class DevicePageElements extends OtherPageElementsHelper { return waitUntilElementToBeClickable(ADD_DEVICE_BTN); } - public WebElement nameField() { - return waitUntilElementToBeClickable(CREATE_DEVICE_NAME_FIELD); - } - public WebElement headerNameView() { return waitUntilVisibilityOfElementLocated(HEADER_NAME_VIEW); } - public WebElement descriptionFieldCreateField() { - return waitUntilElementToBeClickable(DESCRIPTION_FIELD_CREATE_VIEW); - } - public WebElement addDeviceView() { return waitUntilPresenceOfElementLocated(ADD_DEVICE_VIEW); } @@ -114,18 +106,10 @@ public class DevicePageElements extends OtherPageElementsHelper { return waitUntilElementToBeClickable(CHECKBOX_GATEWAY_EDIT); } - public WebElement checkboxGatewayCreate() { - return waitUntilElementToBeClickable(CHECKBOX_GATEWAY_CREATE); - } - public WebElement checkboxOverwriteActivityTimeEdit() { return waitUntilElementToBeClickable(CHECKBOX_OVERWRITE_ACTIVITY_TIME_EDIT); } - public WebElement checkboxOverwriteActivityTimeCreate() { - return waitUntilElementToBeClickable(CHECKBOX_OVERWRITE_ACTIVITY_TIME_CREATE); - } - public WebElement checkboxGatewayDetailsTab() { return waitUntilPresenceOfElementLocated(CHECKBOX_GATEWAY_DETAILS); } @@ -146,22 +130,10 @@ public class DevicePageElements extends OtherPageElementsHelper { return waitUntilElementToBeClickable(DEVICE_PROFILE_REDIRECTED_BTN); } - public WebElement deviceLabelFieldCreate() { - return waitUntilElementToBeClickable(DEVICE_LABEL_FIELD_CREATE); - } - public WebElement deviceLabelOnPage(String deviceName) { return waitUntilVisibilityOfElementLocated(String.format(DEVICE_LABEL_PAGE, deviceName)); } - public WebElement customerOptionBtn() { - return waitUntilElementToBeClickable(CUSTOMER_OPTION_BNT); - } - - public WebElement assignOnCustomerField() { - return waitUntilElementToBeClickable(ASSIGN_ON_CUSTOMER_FIELD); - } - public WebElement deviceCustomerOnPage(String deviceName) { return waitUntilVisibilityOfElementLocated(String.format(DEVICE_CUSTOMER_PAGE, deviceName)); } @@ -173,4 +145,36 @@ public class DevicePageElements extends OtherPageElementsHelper { public WebElement deviceLabelDetailsField() { return waitUntilVisibilityOfElementLocated(DEVICE_LABEL_EDIT); } + + public WebElement deviceDeviceProfileOnPage(String deviceProfileTitle) { + return waitUntilVisibilityOfElementLocated(String.format(DEVICE_DEVICE_PROFILE_PAGE, deviceProfileTitle)); + } + + public WebElement assignBtn(String deviceName) { + return waitUntilElementToBeClickable(String.format(ASSIGN_BTN, deviceName)); + } + + public WebElement assignBtnVisible(String deviceName) { + return waitUntilVisibilityOfElementLocated(String.format(ASSIGN_BTN, deviceName)); + } + + public WebElement unassignBtn(String deviceName) { + return waitUntilElementToBeClickable(String.format(UNASSIGN_BTN, deviceName)); + } + + public WebElement assignBtnDetailsTab() { + return waitUntilElementToBeClickable(ASSIGN_BTN_DETAILS_TAB); + } + + public WebElement unassignBtnDetailsTab() { + return waitUntilElementToBeClickable(UNASSIGN_BTN_DETAILS_TAB); + } + + public WebElement assignFieldDetailsTab() { + return waitUntilVisibilityOfElementLocated(ASSIGNED_FIELD_DETAILS_TAB); + } + + public WebElement assignMarkedDeviceBtn() { + return waitUntilVisibilityOfElementLocated(ASSIGN_MARKED_DEVICE_BTN); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java index c6df90e0fa..3239bb5484 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/DevicePageHelper.java @@ -43,18 +43,6 @@ public class DevicePageHelper extends DevicePageElements { addDeviceBtn().click(); } - public void enterName(String deviceName) { - enterText(nameField(), deviceName); - } - - public void enterDescription(String description) { - enterText(descriptionFieldCreateField(), description); - } - - public void enterLabel(String label) { - enterText(deviceLabelFieldCreate(), label); - } - public void deleteDeviceByRightSideBtn(String deviceName) { deleteBtn(deviceName).click(); warningPopUpYesBtn().click(); @@ -87,10 +75,30 @@ public class DevicePageHelper extends DevicePageElements { entityFromDropdown(deviceProfileName).click(); } - public void assignOnCustomer(String customerTitle) { - customerOptionBtn().click(); - assignOnCustomerField().click(); - entityFromList(customerTitle).click(); - sleep(2); //waiting for the action to count + public void unassignedDeviceByRightSideBtn(String deviceName) { + unassignBtn(deviceName).click(); + warningPopUpYesBtn().click(); + } + + public void unassignedDeviceFromDetailsTab() { + unassignBtnDetailsTab().click(); + warningPopUpYesBtn().click(); + } + + public void selectDevices(String... deviceNames) { + for (String deviceName : deviceNames) { + checkBox(deviceName).click(); + } + } + + public void assignSelectedDevices(String... deviceNames) { + selectDevices(deviceNames); + assignMarkedDeviceBtn().click(); + } + + public void deleteSelectedDevices(String... deviceNames) { + selectDevices(deviceNames); + deleteSelectedBtn().click(); + warningPopUpYesBtn().click(); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/AssignDeviceTabElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/AssignDeviceTabElements.java new file mode 100644 index 0000000000..b688349230 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/AssignDeviceTabElements.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.msa.ui.tabs; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.thingsboard.server.msa.ui.base.AbstractBasePage; + +public class AssignDeviceTabElements extends AbstractBasePage { + public AssignDeviceTabElements(WebDriver driver) { + super(driver); + } + + private static final String ASSIGN_ON_CUSTOMER_FIELD = "//input[@formcontrolname='entity']"; + private static final String CUSTOMER_FROM_DROPDOWN = "//div[@role='listbox']/mat-option//span[contains(text(),'%s')]"; + private static final String ASSIGN_BTN = "//button[@type='submit']"; + + public WebElement assignOnCustomerField() { + return waitUntilElementToBeClickable(ASSIGN_ON_CUSTOMER_FIELD); + } + + public WebElement customerFromDropDown(String entityName) { + return waitUntilVisibilityOfElementLocated(String.format(CUSTOMER_FROM_DROPDOWN, entityName)); + } + + public WebElement assignBtn() { + return waitUntilElementToBeClickable(ASSIGN_BTN); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/AssignDeviceTabHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/AssignDeviceTabHelper.java new file mode 100644 index 0000000000..3176dfb0f7 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/AssignDeviceTabHelper.java @@ -0,0 +1,30 @@ +/** + * 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.msa.ui.tabs; + +import org.openqa.selenium.WebDriver; + +public class AssignDeviceTabHelper extends AssignDeviceTabElements { + public AssignDeviceTabHelper(WebDriver driver) { + super(driver); + } + + public void assignOnCustomer(String customerTitle) { + assignOnCustomerField().click(); + customerFromDropDown(customerTitle).click(); + assignBtn().click(); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/CreateDeviceTabElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/CreateDeviceTabElements.java new file mode 100644 index 0000000000..53bb1a72da --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/CreateDeviceTabElements.java @@ -0,0 +1,97 @@ +/** + * 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.msa.ui.tabs; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.thingsboard.server.msa.ui.base.AbstractBasePage; + +public class CreateDeviceTabElements extends AbstractBasePage { + public CreateDeviceTabElements(WebDriver driver) { + super(driver); + } + + private static final String CREATE_DEVICE_NAME_FIELD = "//tb-device-wizard//input[@formcontrolname='name']"; + private static final String CREATE_NEW_DEVICE_PROFILE_RADIO_BTN = "//span[text() = 'Create new device profile']/ancestor::mat-radio-button"; + private static final String SELECT_EXISTING_DEVICE_PROFILE_RADIO_BTN = "//span[text() = 'Select existing device profile']/ancestor::mat-radio-button"; + private static final String DEVICE_PROFILE_TITLE_FIELD = "//input[@formcontrolname='newDeviceProfileTitle']"; + private static final String ADD_BTN = "//span[text() = 'Add']"; + private static final String CLEAR_PROFILE_FIELD_BTN = "//button[@aria-label='Clear']"; + private static final String ENTITY_FROM_DROPDOWN = "//div[@role = 'listbox']//span[text() = '%s']"; + private static final String ASSIGN_ON_CUSTOMER_FIELD = "//input[@formcontrolname='entity']"; + private static final String CUSTOMER_OPTION_BNT = "//div[text() = 'Customer']/ancestor::mat-step-header"; + private static final String CUSTOMER_FROM_DROPDOWN = "//div[@role='listbox']/mat-option//span[contains(text(),'%s')]"; + private static final String DEVICE_LABEL_FIELD = "//tb-device-wizard//input[@formcontrolname='label']"; + private static final String CHECKBOX_GATEWAY = "//tb-device-wizard//mat-checkbox[@formcontrolname='gateway']//label"; + private static final String CHECKBOX_OVERWRITE_ACTIVITY_TIME = "//tb-device-wizard//mat-checkbox[@formcontrolname='overwriteActivityTime']//label"; + private static final String DESCRIPTION_FIELD = "//tb-device-wizard//textarea[@formcontrolname='description']"; + + public WebElement nameField() { + return waitUntilElementToBeClickable(CREATE_DEVICE_NAME_FIELD); + } + + public WebElement createNewDeviceProfileRadioBtn() { + return waitUntilElementToBeClickable(CREATE_NEW_DEVICE_PROFILE_RADIO_BTN); + } + + public WebElement selectExistingDeviceProfileRadioBtn() { + return waitUntilElementToBeClickable(SELECT_EXISTING_DEVICE_PROFILE_RADIO_BTN); + } + + public WebElement deviceProfileTitleField() { + return waitUntilElementToBeClickable(DEVICE_PROFILE_TITLE_FIELD); + } + + public WebElement addBtn() { + return waitUntilElementToBeClickable(ADD_BTN); + } + + public WebElement clearProfileFieldBtn() { + return waitUntilElementToBeClickable(CLEAR_PROFILE_FIELD_BTN); + } + + public WebElement entityFromDropdown(String customerTitle) { + return waitUntilElementToBeClickable(String.format(ENTITY_FROM_DROPDOWN, customerTitle)); + } + + public WebElement assignOnCustomerField() { + return waitUntilElementToBeClickable(ASSIGN_ON_CUSTOMER_FIELD); + } + + public WebElement customerOptionBtn() { + return waitUntilElementToBeClickable(CUSTOMER_OPTION_BNT); + } + + public WebElement customerFromDropDown(String entityName) { + return waitUntilVisibilityOfElementLocated(String.format(CUSTOMER_FROM_DROPDOWN, entityName)); + } + + public WebElement deviceLabelField() { + return waitUntilElementToBeClickable(DEVICE_LABEL_FIELD); + } + + public WebElement checkboxGateway() { + return waitUntilElementToBeClickable(CHECKBOX_GATEWAY); + } + + public WebElement checkboxOverwriteActivityTime() { + return waitUntilElementToBeClickable(CHECKBOX_OVERWRITE_ACTIVITY_TIME); + } + + public WebElement descriptionField() { + return waitUntilElementToBeClickable(DESCRIPTION_FIELD); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/CreateDeviceTabHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/CreateDeviceTabHelper.java new file mode 100644 index 0000000000..065f00cc08 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tabs/CreateDeviceTabHelper.java @@ -0,0 +1,58 @@ +/** + * 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.msa.ui.tabs; + +import org.openqa.selenium.WebDriver; + +public class CreateDeviceTabHelper extends CreateDeviceTabElements { + public CreateDeviceTabHelper(WebDriver driver) { + super(driver); + } + + public void enterName(String deviceName) { + enterText(nameField(), deviceName); + } + + public void createNewDeviceProfile(String deviceProfileTitle) { + if (!createNewDeviceProfileRadioBtn().getAttribute("class").contains("checked")) { + createNewDeviceProfileRadioBtn().click(); + } + deviceProfileTitleField().sendKeys(deviceProfileTitle); + } + + public void changeDeviceProfile(String deviceProfileName) { + if (!selectExistingDeviceProfileRadioBtn().getAttribute("class").contains("checked")) { + selectExistingDeviceProfileRadioBtn().click(); + } + clearProfileFieldBtn().click(); + entityFromDropdown(deviceProfileName).click(); + } + + public void assignOnCustomer(String customerTitle) { + customerOptionBtn().click(); + assignOnCustomerField().click(); + customerFromDropDown(customerTitle).click(); + sleep(2); //waiting for the action to count + } + + public void enterLabel(String label) { + enterText(deviceLabelField(), label); + } + + public void enterDescription(String description) { + enterText(descriptionField(), description); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java index 3cd46fefc9..5b4f1c0685 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AbstractDeviceTest.java @@ -22,24 +22,32 @@ import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.DevicePageHelper; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; import org.thingsboard.server.msa.ui.pages.SideBarMenuViewHelper; +import org.thingsboard.server.msa.ui.tabs.CreateDeviceTabHelper; @Epic("Device smoke tests") abstract public class AbstractDeviceTest extends AbstractDriverBaseTest { protected SideBarMenuViewHelper sideBarMenuView; protected DevicePageHelper devicePage; + protected CreateDeviceTabHelper createDeviceTab; protected String deviceName; + protected String deviceProfileTitle; @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); devicePage = new DevicePageHelper(driver); + createDeviceTab = new CreateDeviceTabHelper(driver); } @AfterMethod public void delete() { deleteDeviceByName(deviceName); deviceName = null; + if (deviceProfileTitle != null) { + deleteDeviceProfileByTitle(deviceProfileTitle); + deviceProfileTitle = null; + } } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java new file mode 100644 index 0000000000..8c0a45aa03 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/AssignToCustomerTest.java @@ -0,0 +1,193 @@ +/** + * 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.msa.ui.tests.devicessmoke; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import org.openqa.selenium.WebElement; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; +import org.thingsboard.server.msa.ui.tabs.AssignDeviceTabHelper; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; + +@Feature("Assign to customer") +public class AssignToCustomerTest extends AbstractDeviceTest { + + private AssignDeviceTabHelper assignDeviceTab; + private CustomerPageHelper customerPage; + private CustomerId customerId; + private Device device; + private Device device1; + private String customerName; + + @BeforeClass + public void create() { + assignDeviceTab = new AssignDeviceTabHelper(driver); + customerPage = new CustomerPageHelper(driver); + Customer customer = testRestClient.postCustomer(EntityPrototypes.defaultCustomerPrototype(ENTITY_NAME + random())); + customerId = customer.getId(); + customerName = customer.getName(); + device1 = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype("Device " + random())); + } + + @AfterClass + public void deleteCustomer() { + deleteCustomerById(customerId); + deleteCustomerByName("Public"); + deleteDeviceByName(device1.getName()); + } + + @BeforeMethod + public void createDevice() { + device = testRestClient.postDevice("", EntityPrototypes.defaultDevicePrototype(ENTITY_NAME)); + deviceName = device.getName(); + } + + @Test(groups = "smoke") + @Description("Assign to customer by right side of device btn") + public void assignToCustomerByRightSideBtn() { + sideBarMenuView.goToDevicesPage(); + devicePage.assignBtn(deviceName).click(); + assignDeviceTab.assignOnCustomer(customerName); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer added correctly").isEqualTo(customerName); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(customerName).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke") + @Description("Assign to customer by 'Assign to customer' btn on details tab") + public void assignToCustomerFromDetailsTab() { + sideBarMenuView.goToDevicesPage(); + devicePage.device(deviceName).click(); + devicePage.assignBtnDetailsTab().click(); + assignDeviceTab.assignOnCustomer(customerName); + String customerInAssignedField = devicePage.assignFieldDetailsTab().getAttribute("value"); + devicePage.closeDeviceDetailsViewBtn().click(); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer added correctly").isEqualTo(customerName); + assertThat(customerInAssignedField) + .as("Customer in details tab added correctly").isEqualTo(customerName); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(customerName).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke") + @Description("Assign marked device by btn on the top") + public void assignToCustomerMarkedDevice() { + sideBarMenuView.goToDevicesPage(); + devicePage.assignSelectedDevices(deviceName); + assignDeviceTab.assignOnCustomer(customerName); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer added correctly").isEqualTo(customerName); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(customerName).click(); + assertIsDisplayed(devicePage.device(deviceName)); + } + + @Test(groups = "smoke") + @Description("Unassign from customer by right side of device btn") + public void unassignedFromCustomerByRightSideBtn() { + device.setCustomerId(customerId); + testRestClient.postDevice("", device); + + sideBarMenuView.goToDevicesPage(); + WebElement element = devicePage.deviceCustomerOnPage(deviceName); + devicePage.unassignedDeviceByRightSideBtn(deviceName); + assertInvisibilityOfElement(element); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(customerName).click(); + devicePage.assertEntityIsNotPresent(deviceName); + } + + @Test(groups = "smoke") + @Description("Unassign from customer by 'Unassign from customer' btn on details tab") + public void unassignedFromCustomerFromDetailsTab() { + device.setCustomerId(customerId); + testRestClient.postDevice("", device); + + sideBarMenuView.goToDevicesPage(); + WebElement customerInColumn = devicePage.deviceCustomerOnPage(deviceName); + devicePage.device(deviceName).click(); + WebElement assignFieldDetailsTab = devicePage.assignFieldDetailsTab(); + devicePage.unassignedDeviceFromDetailsTab(); + assertInvisibilityOfElement(customerInColumn); + assertInvisibilityOfElement(assignFieldDetailsTab); + + devicePage.closeDeviceDetailsViewBtn().click(); + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(customerName).click(); + devicePage.assertEntityIsNotPresent(deviceName); + } + + @Test(groups = "smoke") + @Description("Can't assign device on several customer") + public void assignToSeveralCustomer() { + device.setCustomerId(customerId); + testRestClient.postDevice("", device); + sideBarMenuView.goToDevicesPage(); + + assertIsDisable(devicePage.assignBtnVisible(deviceName)); + } + + @Test(groups = "smoke") + @Description("Can't assign public device") + public void assignPublicDevice() { + testRestClient.setDevicePublic(device.getId()); + + sideBarMenuView.goToDevicesPage(); + assertIsDisable(devicePage.assignBtnVisible(deviceName)); + } + + @Test(groups = "smoke") + @Description("Assign several devices by btn on the top") + public void assignSeveralDevices() { + sideBarMenuView.goToDevicesPage(); + devicePage.assignSelectedDevices(deviceName, device1.getName()); + assignDeviceTab.assignOnCustomer(customerName); + assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); + assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) + .as("Customer added correctly").isEqualTo(customerName); + assertThat(devicePage.deviceCustomerOnPage(device1.getName()).getText()) + .as("Customer added correctly").isEqualTo(customerName); + + sideBarMenuView.customerBtn().click(); + customerPage.manageCustomersDevicesBtn(customerName).click(); + List.of(deviceName, device1.getName()). + forEach(d -> assertIsDisplayed(devicePage.device(d))); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java index 3e4ab48f5d..7f13614b37 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/CreateDeviceTest.java @@ -19,6 +19,7 @@ import io.qameta.allure.Description; import io.qameta.allure.Feature; import org.testng.annotations.Test; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.msa.ui.pages.ProfilesPageElements; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; import static org.assertj.core.api.Assertions.assertThat; @@ -39,8 +40,8 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.addBtn().click(); devicePage.refreshBtn().click(); assertIsDisplayed(devicePage.entity(deviceName)); @@ -53,9 +54,9 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.enterDescription(deviceName); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.enterDescription(deviceName); + createDeviceTab.addBtn().click(); devicePage.refreshBtn().click(); devicePage.entity(deviceName).click(); devicePage.setHeaderName(); @@ -70,8 +71,8 @@ public class CreateDeviceTest extends AbstractDeviceTest { public void createDeviceWithoutName() { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.nameField().click(); - devicePage.addBtnC().click(); + createDeviceTab.nameField().click(); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.addDeviceView()); assertThat(devicePage.errorMessage().getText()).as("Text of warning message").isEqualTo(NAME_IS_REQUIRED_MESSAGE); @@ -82,8 +83,8 @@ public class CreateDeviceTest extends AbstractDeviceTest { public void createDeviceWithOnlySpace() { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(" "); - devicePage.addBtnC().click(); + createDeviceTab.enterName(" "); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.warningMessage()); assertThat(devicePage.warningMessage().getText()).as("Text of warning message").isEqualTo(EMPTY_DEVICE_MESSAGE); @@ -98,8 +99,8 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.warningMessage()); assertThat(devicePage.warningMessage().getText()).as("Text of warning message").isEqualTo(SAME_NAME_WARNING_DEVICE_MESSAGE); @@ -113,8 +114,8 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.entity(deviceName)); } @@ -126,9 +127,9 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.clearProfileFieldBtn().click(); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.clearProfileFieldBtn().click(); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.errorMessage()); assertThat(devicePage.errorMessage().getText()).as("Text of warning message").isEqualTo(DEVICE_PROFILE_IS_REQUIRED_MESSAGE); @@ -142,9 +143,9 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.checkboxGatewayCreate().click(); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.checkboxGateway().click(); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.device(deviceName)); assertIsDisplayed(devicePage.checkboxGatewayPage(deviceName)); @@ -157,10 +158,10 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.checkboxGatewayCreate().click(); - devicePage.checkboxOverwriteActivityTimeCreate().click(); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.checkboxGateway().click(); + createDeviceTab.checkboxOverwriteActivityTime().click(); + createDeviceTab.addBtn().click(); devicePage.device(deviceName).click(); assertThat(devicePage.checkboxOverwriteActivityTimeDetails().getAttribute("class").contains("selected")) @@ -175,9 +176,9 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.enterLabel(deviceLabel); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.enterLabel(deviceLabel); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.deviceLabelOnPage(deviceName)); assertThat(devicePage.deviceLabelOnPage(deviceName).getText()).as("Label added correctly").isEqualTo(deviceLabel); @@ -191,9 +192,9 @@ public class CreateDeviceTest extends AbstractDeviceTest { sideBarMenuView.goToDevicesPage(); devicePage.openCreateDeviceView(); - devicePage.enterName(deviceName); - devicePage.assignOnCustomer(customer); - devicePage.addBtnC().click(); + createDeviceTab.enterName(deviceName); + createDeviceTab.assignOnCustomer(customer); + createDeviceTab.addBtn().click(); assertIsDisplayed(devicePage.deviceCustomerOnPage(deviceName)); assertThat(devicePage.deviceCustomerOnPage(deviceName).getText()) @@ -211,4 +212,42 @@ public class CreateDeviceTest extends AbstractDeviceTest { assertThat(urlContains(urlPath)).as("Redirected URL contains " + urlPath).isTrue(); } + + @Test(groups = "smoke") + @Description("Create new device profile from create device") + public void createNewDeviceProfile() { + ProfilesPageElements profilesPage = new ProfilesPageElements(driver); + deviceName = ENTITY_NAME + random(); + deviceProfileTitle = ENTITY_NAME + random(); + + sideBarMenuView.goToDevicesPage(); + devicePage.openCreateDeviceView(); + createDeviceTab.enterName(deviceName); + createDeviceTab.createNewDeviceProfile(deviceProfileTitle); + createDeviceTab.addBtn().click(); + devicePage.refreshBtn().click(); + String deviceProfileColumn = devicePage.deviceDeviceProfileOnPage(deviceName).getText(); + sideBarMenuView.openDeviceProfiles(); + + assertThat(deviceProfileColumn).as("Profile changed correctly").isEqualTo(deviceProfileTitle); + assertIsDisplayed(profilesPage.entity(deviceProfileTitle)); + } + + @Test(groups = "smoke") + @Description("Add device with changed device profile (from default to another)") + public void createDeviceWithChangedProfile() { + deviceName = ENTITY_NAME + random(); + deviceProfileTitle = ENTITY_NAME + random(); + testRestClient.postDeviceProfile(EntityPrototypes.defaultDeviceProfile(deviceProfileTitle)); + + sideBarMenuView.goToDevicesPage(); + devicePage.openCreateDeviceView(); + createDeviceTab.enterName(deviceName); + createDeviceTab.changeDeviceProfile(deviceProfileTitle); + createDeviceTab.addBtn().click(); + devicePage.refreshBtn().click(); + + assertThat(devicePage.deviceDeviceProfileOnPage(deviceName).getText()) + .as("Profile changed correctly").isEqualTo(deviceProfileTitle); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/DeleteSeveralDevicesTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/DeleteSeveralDevicesTest.java index 48df384c63..48b1921d0d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/DeleteSeveralDevicesTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/devicessmoke/DeleteSeveralDevicesTest.java @@ -23,6 +23,8 @@ import org.testng.annotations.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; @@ -51,11 +53,11 @@ public class DeleteSeveralDevicesTest extends AbstractDeviceTest { "that appears at the top") public void deleteSeveralDevicesByTopBtn() { sideBarMenuView.goToDevicesPage(); - devicePage.deleteSelected(2); + devicePage.deleteSelectedDevices(deviceName1, deviceName2); devicePage.refreshBtn().click(); - devicePage.assertEntityIsNotPresent(deviceName1); - devicePage.assertEntityIsNotPresent(deviceName2); + List.of(deviceName1, deviceName2) + .forEach(d -> devicePage.assertEntityIsNotPresent(d)); } @Test(groups = "smoke") @@ -76,9 +78,9 @@ public class DeleteSeveralDevicesTest extends AbstractDeviceTest { "that appears at the top without refresh") public void deleteSeveralWithoutRefresh() { sideBarMenuView.goToDevicesPage(); - devicePage.deleteSelected(2); + devicePage.deleteSelectedDevices(deviceName1, deviceName2); - devicePage.assertEntityIsNotPresent(deviceName1); - devicePage.assertEntityIsNotPresent(deviceName2); + List.of(deviceName1, deviceName2) + .forEach(d -> devicePage.assertEntityIsNotPresent(d)); } } diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 1fe43219a0..ba96ada39c 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.5.1-SNAPSHOT + 3.6.0-SNAPSHOT msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index c85ac88274..527d96766e 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -38,7 +38,7 @@ tb-postgres tb-cassandra /usr/share/${pkg.name} - 3.5.0 + 3.5.2 diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java index f8bf7089f3..e6b8377f51 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesByNameAndTypeLoader.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.EntityId; @@ -32,7 +33,7 @@ public class EntitiesByNameAndTypeLoader { EntityType.USER); public static EntityId findEntityId(TbContext ctx, EntityType entityType, String entityName) { - SearchTextBasedWithAdditionalInfo targetEntity; + BaseData targetEntity; switch (entityType) { case DEVICE: targetEntity = ctx.getDeviceService().findDeviceByTenantIdAndName(ctx.getTenantId(), entityName); diff --git a/ui-ngx/package.json b/ui-ngx/package.json index aa10ec6d8a..336c403f8a 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -14,17 +14,17 @@ }, "private": true, "dependencies": { - "@angular/animations": "^15.2.5", - "@angular/cdk": "^15.2.5", - "@angular/common": "^15.2.5", - "@angular/compiler": "^15.2.5", - "@angular/core": "^15.2.5", + "@angular/animations": "^15.2.9", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.2.9", + "@angular/compiler": "^15.2.9", + "@angular/core": "^15.2.9", "@angular/flex-layout": "^15.0.0-beta.42", - "@angular/forms": "^15.2.5", - "@angular/material": "^15.2.5", - "@angular/platform-browser": "^15.2.5", - "@angular/platform-browser-dynamic": "^15.2.5", - "@angular/router": "^15.2.5", + "@angular/forms": "^15.2.9", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.2.9", + "@angular/platform-browser-dynamic": "^15.2.9", + "@angular/router": "^15.2.9", "@auth0/angular-jwt": "^5.1.2", "@date-io/core": "1.3.7", "@date-io/date-fns": "1.3.7", @@ -108,16 +108,16 @@ }, "devDependencies": { "@angular-builders/custom-webpack": "~15.0.0", - "@angular-devkit/build-angular": "^15.2.4", + "@angular-devkit/build-angular": "^15.2.8", "@angular-eslint/builder": "15.2.1", "@angular-eslint/eslint-plugin": "15.2.1", "@angular-eslint/eslint-plugin-template": "15.2.1", "@angular-eslint/schematics": "15.2.1", "@angular-eslint/template-parser": "15.2.1", - "@angular/cli": "^15.2.4", - "@angular/compiler-cli": "^15.2.5", - "@angular/language-service": "^15.2.5", - "@ngtools/webpack": "^15.1.6", + "@angular/cli": "^15.2.8", + "@angular/compiler-cli": "^15.2.9", + "@angular/language-service": "^15.2.9", + "@ngtools/webpack": "15.2.1", "@types/ace-diff": "^2.1.1", "@types/canvas-gauges": "^2.1.4", "@types/flot": "^0.0.32", diff --git a/ui-ngx/patches/@angular+core+15.2.9.patch b/ui-ngx/patches/@angular+core+15.2.9.patch new file mode 100644 index 0000000000..3797e6b48c --- /dev/null +++ b/ui-ngx/patches/@angular+core+15.2.9.patch @@ -0,0 +1,20 @@ +diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs +index 3e93015..9efcb96 100755 +--- a/node_modules/@angular/core/fesm2020/core.mjs ++++ b/node_modules/@angular/core/fesm2020/core.mjs +@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) { + if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) { + matches || (matches = []); + if (isComponentDef(def)) { +- if (ngDevMode) { ++ // if (ngDevMode) { + assertTNodeType(tNode, 2 /* TNodeType.Element */, `"${tNode.value}" tags cannot be used as component hosts. ` + + `Please use a different tag to activate the ${stringify(def.type)} component.`); + if (isComponentHost(tNode)) { + throwMultipleComponentError(tNode, matches.find(isComponentDef).type, def.type); + } +- } ++ // } + // Components are inserted at the front of the matches array so that their lifecycle + // hooks run before any directive lifecycle hooks. This appears to be for ViewEngine + // compatibility. This logic doesn't make sense with host directives, because it diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 9c3570310e..ee3e232bba 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, subType as NotificationType, 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/core/http/notification.service.ts b/ui-ngx/src/app/core/http/notification.service.ts index a92007643a..ec64621aeb 100644 --- a/ui-ngx/src/app/core/http/notification.service.ts +++ b/ui-ngx/src/app/core/http/notification.service.ts @@ -35,7 +35,7 @@ import { SlackConversation } from '@shared/models/notification.models'; import { User } from '@shared/models/user.model'; -import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; +import { isNotEmptyStr } from '@core/utils'; @Injectable({ providedIn: 'root' @@ -169,7 +169,7 @@ export class NotificationService { public getNotificationTemplates(pageLink: PageLink, notificationTypes?: NotificationType, config?: RequestConfig): Observable> { let url = `/api/notification/templates${pageLink.toQuery()}`; - if (isDefinedAndNotNull(notificationTypes)) { + if (isNotEmptyStr(notificationTypes)) { url += `¬ificationTypes=${notificationTypes}`; } return this.http.get>(url, defaultHttpOptionsFromConfig(config)); diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 2b33057454..5f5f0f42df 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -280,7 +280,7 @@ export class WidgetService { this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); } - private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { + public deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); this.widgetsInfoInMemoryCache.delete(key); } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 6c36dde958..5a112f8336 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -30,7 +30,14 @@ import { WidgetLayout } from '@shared/models/dashboard.models'; import { isDefined, isString, isUndefined } from '@core/utils'; -import { Datasource, DatasourceType, Widget, WidgetConfig, widgetType } from '@app/shared/models/widget.models'; +import { + Datasource, + datasourcesHasOnlyComparisonAggregation, + DatasourceType, + Widget, + WidgetConfig, + widgetType +} from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { EntityId } from '@app/shared/models/id/entity-id'; @@ -227,7 +234,8 @@ export class DashboardUtilsService { } }); if (type === widgetType.latest) { - widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, this.timeService); + const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService); } if (type === widgetType.alarm) { if (!widgetConfig.alarmFilterConfig) { diff --git a/ui-ngx/src/app/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts index 6d664085b1..be0c24a958 100644 --- a/ui-ngx/src/app/core/services/dialog.service.ts +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -29,6 +29,7 @@ import { } from '@shared/components/dialog/material-icons-dialog.component'; import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component'; import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; @Injectable( @@ -78,6 +79,23 @@ export class DialogService { return dialogRef.afterClosed(); } + errorAlert(title: string, message: string, error: any, ok: string = null, fullscreen: boolean = false): Observable { + const dialogConfig: MatDialogConfig = { + disableClose: true, + data: { + title, + message, + error, + ok: ok || this.translate.instant('action.ok') + } + }; + if (fullscreen) { + dialogConfig.panelClass = ['tb-fullscreen-dialog']; + } + const dialogRef = this.dialog.open(ErrorAlertDialogComponent, dialogConfig); + return dialogRef.afterClosed(); + } + colorPicker(color: string): Observable { return this.dialog.open(ColorPickerDialogComponent, { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 813127cbc5..f82c6c3072 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -188,6 +188,10 @@ export class UtilsService { public processWidgetException(exception: any): ExceptionData { const data = this.parseException(exception, -6); + if (data.message?.startsWith('NG0')) { + data.message = `${this.translate.instant('widget.widget-template-error')}
+
${this.translate.instant('dialog.error-message-title')}

${data.message}`; + } if (this.widgetEditMode) { const message: WindowMessage = { type: 'widgetException', 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..0ad9b72aad 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/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 61dc5bf013..32ebb28ae9 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -139,7 +139,7 @@ - + diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index bce66fc3c1..fbdd0a07da 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { MatDialog } from '@angular/material/dialog'; @@ -32,7 +41,7 @@ import { Subscription } from 'rxjs'; templateUrl: './event-table.component.html', styleUrls: ['./event-table.component.scss'] }) -export class EventTableComponent implements OnInit, AfterViewInit { +export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { @Input() tenantId: string; 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.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 }} { 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}); + } } } @@ -203,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 ?? 1000, {emitEvent: false}); batchSizeField.setValidators([Validators.min(1), Validators.required]); batchSizeField.updateValueAndValidity({emitEvent: false}); this.hideBatchSize = true; 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..ef09a2b382 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..b6a394982c 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 @@ -29,6 +29,7 @@ import { } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; import { + entityTypesWithoutRelatedData, EntityTypeVersionCreateConfig, exportableEntityTypes, SyncStrategy, @@ -73,6 +74,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements syncStrategyTranslations = syncStrategyTranslationMap; entityTypes = EntityType; + entityTypesWithoutRelatedData = entityTypesWithoutRelatedData; 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..d179855a1a 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..5405436f35 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 @@ -18,22 +18,26 @@ import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } fro import { AbstractControl, ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - NG_VALIDATORS, - NG_VALUE_ACCESSOR, Validator, Validators } from '@angular/forms'; import { PageComponent } from '@shared/components/page.component'; -import { EntityTypeVersionLoadConfig, exportableEntityTypes, VersionCreationResult } from '@shared/models/vc.models'; +import { + entityTypesWithoutRelatedData, + EntityTypeVersionLoadConfig, + exportableEntityTypes +} from '@shared/models/vc.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox'; +import { MatCheckbox } from '@angular/material/checkbox'; import { TbPopoverService } from '@shared/components/popover.service'; import { RemoveOtherEntitiesConfirmComponent } from '@home/components/vc/remove-other-entities-confirm.component'; @@ -66,6 +70,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On public entityTypesVersionLoadFormGroup: UntypedFormGroup; entityTypes = EntityType; + entityTypesWithoutRelatedData = entityTypesWithoutRelatedData; loading = true; diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts index d9ab65813f..dc3e9c7ac2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts @@ -33,6 +33,8 @@ import { CustomDialogComponent, CustomDialogData } from '@home/components/widget/dialog/custom-dialog.component'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; export interface CustomDialogContainerData { controller: (instance: CustomDialogComponent) => void; @@ -54,6 +56,8 @@ export class CustomDialogContainerComponent extends DialogComponent, + private dialogService: DialogService, + private translate: TranslateService, @Inject(MAT_DIALOG_DATA) public data: CustomDialogContainerData) { super(store, router, dialogRef); let customDialogData: CustomDialogData = { @@ -72,7 +76,19 @@ export class CustomDialogContainerComponent extends DialogComponent item.unsubscribe()); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 7b63176203..64ef1a6303 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -36,7 +36,13 @@ import { Observable, of } from 'rxjs'; import { Polyline } from './polyline'; import { Polygon } from './polygon'; import { Circle } from './circle'; -import { createTooltip, entitiesParseName, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils'; +import { + createTooltip, + entitiesParseName, + isCutPolygon, + isJSON, + isValidLatLng +} from '@home/components/widget/lib/maps/maps-utils'; import { checkLngLat, createLoadingDiv } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { @@ -148,7 +154,8 @@ export default abstract class LeafletMap { if (isDefinedAndNotNull(markerColor) && tinycolor(markerColor).isValid()) { const parsedColor = tinycolor(markerColor); return L.divIcon({ - html: `
` + + html: `
` + `
` + childCount + '
', iconSize: new L.Point(40, 40), className: 'tb-cluster-marker-container' @@ -378,7 +385,8 @@ export default abstract class LeafletMap { }); }, }); - this.map.pm.Toolbar.changeControlOrder(['tbMarker', 'tbRectangle', 'tbPolygon', 'tbCircle', 'editMode', 'dragMode', 'tbCut', 'removalMode', 'rotateMode']); + this.map.pm.Toolbar.changeControlOrder(['tbMarker', 'tbRectangle', 'tbPolygon', 'tbCircle', + 'editMode', 'dragMode', 'tbCut', 'removalMode', 'rotateMode']); } this.map.pm.setLang('en', this.translateService.instant('widgets.maps'), 'en'); @@ -643,19 +651,19 @@ export default abstract class LeafletMap { } } - extractPosition(data: FormattedData): {x: number, y: number} { + extractPosition(data: FormattedData): {x: number; y: number} { if (!data) { return null; } const lat = data[this.options.latKeyName]; const lng = data[this.options.lngKeyName]; - if (!isDefinedAndNotNull(lat) || isString(lat) || isNaN(lat) || !isDefinedAndNotNull(lng) || isString(lng) || isNaN(lng)) { + if (!isValidLatLng(lat, lng)) { return null; } return {x: lat, y: lng}; } - positionToLatLng(position: {x: number, y: number}): L.LatLng { + positionToLatLng(position: {x: number; y: number}): L.LatLng { return L.latLng(position.x, position.y) as L.LatLng; } @@ -936,7 +944,7 @@ export default abstract class LeafletMap { return; } this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe(); - } + }; private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: Partial, updateBounds = true, callback?, snappable = false): Marker { @@ -1141,7 +1149,7 @@ export default abstract class LeafletMap { } } this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); - } + }; createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: Partial, updateBounds = true, snappable = false) { @@ -1218,7 +1226,7 @@ export default abstract class LeafletMap { const center = e.layer.getLatLng(); const radius = e.layer.getRadius(); this.saveLocation(data, this.convertCircleToCustomFormat(center, radius)).subscribe(() => {}); - } + }; updateCircle(circlesData: FormattedData[], updateBounds = true) { const toDelete = new Set(Array.from(this.circles.keys())); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index 4aad829eac..81005dee07 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -20,7 +20,7 @@ import { ShowTooltipAction, WidgetToolipSettings } from './map-models'; import { Datasource, FormattedData } from '@app/shared/models/widget.models'; -import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils'; +import { fillDataPattern, isDefinedAndNotNull, isString, processDataPattern, safeExecute } from '@core/utils'; import { parseWithTranslation } from '@home/components/widget/lib/maps/common-maps-utils'; export function createTooltip(target: L.Layer, @@ -110,3 +110,16 @@ export function entitiesParseName(entities: FormattedData[], labelSettings: labe } return entities; } + +export const isValidLatitude = (latitude: any): boolean => + isDefinedAndNotNull(latitude) && + !isString(latitude) && + !isNaN(latitude) && isFinite(latitude) && Math.abs(latitude) <= 90; + +export const isValidLongitude = (longitude: any): boolean => + isDefinedAndNotNull(longitude) && + !isString(longitude) && + !isNaN(longitude) && isFinite(longitude) && Math.abs(longitude) <= 180; + +export const isValidLatLng = (latitude: any, longitude: any): boolean => + isValidLatitude(latitude) && isValidLongitude(longitude); 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 }; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 50627212ee..9b73b5566d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -224,6 +224,11 @@ export class WidgetComponentService { ); } + public clearWidgetInfo(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): void { + this.dynamicComponentFactoryService.destroyDynamicComponentFactory(widgetInfo.componentFactory); + this.widgetService.deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); + } + private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { const widgetInfoSubject = new ReplaySubject(); const widgetInfo = this.widgetService.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index 0f8f6a9997..ce5a5a57e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -34,7 +34,7 @@
- Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}} +
{ + this.widgetContext.inited = true; // console.log(err); } ); @@ -739,9 +740,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.createDefaultSubscription().subscribe( () => { this.subscriptionInited = true; - this.configureDynamicWidgetComponent(); - initSubject.next(); - initSubject.complete(); + try { + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + } catch (err) { + initSubject.error(err); + } }, (err) => { this.subscriptionInited = true; @@ -751,9 +756,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } else { this.loadingData = false; this.subscriptionInited = true; - this.configureDynamicWidgetComponent(); - initSubject.next(); - initSubject.complete(); + try { + this.configureDynamicWidgetComponent(); + initSubject.next(); + initSubject.complete(); + } catch (err) { + initSubject.error(err); + } } return initSubject.asObservable(); } @@ -807,12 +816,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector); this.cd.detectChanges(); } catch (e) { - console.error(e); if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponentRef.destroy(); this.dynamicWidgetComponentRef = null; } this.widgetContentContainer.clear(); + this.handleWidgetException(e); + this.widgetComponentService.clearWidgetInfo(this.widgetInfo, this.widget.bundleAlias, this.widget.typeAlias, + this.widget.isSystemType); + throw e; } if (this.dynamicWidgetComponentRef) { 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", diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index b12a0dfa3a..0379c34620 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -489,9 +489,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe if (!this.gotError) { this.gotError = true; let errorInfo = 'Error:'; - if (details.name) { - errorInfo += ' ' + details.name + ':'; - } if (details.message) { errorInfo += ' ' + details.message; } diff --git a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html index c3c4b59871..de56079efd 100644 --- a/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/alert-dialog.component.html @@ -16,7 +16,7 @@ -->

{{data.title}}

-
+
diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html new file mode 100644 index 0000000000..8554a3d707 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.html @@ -0,0 +1,30 @@ + +

{{title}}

+
+
{{ message }}
+
dialog.error-message-title
+
{{ errorMessage }}
+ + {{ 'dialog.error-details-title' | translate }} + + +
+
+ +
diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss new file mode 100644 index 0000000000..3ca391dbcd --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.scss @@ -0,0 +1,37 @@ +/** + * 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. + */ +:host { + .mat-mdc-dialog-content { + padding: 0 24px 24px; + } + .tb-error-alert-dialog-content { + .error-message-title { + font-style: italic; + } + .error-message-content { + color: red; + } + .error-details-content { + display: block; + border: solid 1px #d3d3d3; + padding: 8px; + border-radius: 4px; + } + & > *:not(:last-child) { + padding-bottom: 16px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts new file mode 100644 index 0000000000..8ef9032421 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/error-alert-dialog.component.ts @@ -0,0 +1,49 @@ +/// +/// 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. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface ErrorAlertDialogData { + title: string; + message: string; + error: any; + ok: string; +} + +@Component({ + selector: 'tb-error-alert-dialog', + templateUrl: './error-alert-dialog.component.html', + styleUrls: ['./error-alert-dialog.component.scss'] +}) +export class ErrorAlertDialogComponent { + + title: string; + message: string; + errorMessage: string; + errorDetails?: string; + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ErrorAlertDialogData) { + this.title = this.data.title; + this.message = this.data.message; + this.errorMessage = this.data.error.message ? this.data.error.message : JSON.stringify(this.data.error); + if (this.data.error.stack) { + this.errorDetails = this.data.error.stack.replaceAll('\n', '
'); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/file-input.component.scss b/ui-ngx/src/app/shared/components/file-input.component.scss index 3426539be9..02924111b4 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.scss +++ b/ui-ngx/src/app/shared/components/file-input.component.scss @@ -87,7 +87,7 @@ $previewSize: 100px !default; button.browse-file { padding: 0; font-size: 16px; - span.mdc-button__label { + label { display: block; cursor: pointer; padding: 0 16px; diff --git a/ui-ngx/src/app/shared/components/image-input.component.scss b/ui-ngx/src/app/shared/components/image-input.component.scss index 0cdfce9e27..a776bc8cd7 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.scss +++ b/ui-ngx/src/app/shared/components/image-input.component.scss @@ -141,7 +141,7 @@ $previewSize: 96px !default; button.browse-file { padding: 0; font-size: 16px; - span.mdc-button__label { + label { display: block; cursor: pointer; padding: 0 16px; diff --git a/ui-ngx/src/app/shared/components/multiple-image-input.component.scss b/ui-ngx/src/app/shared/components/multiple-image-input.component.scss index c8c72a11ce..db097e06b1 100644 --- a/ui-ngx/src/app/shared/components/multiple-image-input.component.scss +++ b/ui-ngx/src/app/shared/components/multiple-image-input.component.scss @@ -157,7 +157,7 @@ $previewSize: 64px !default; button.browse-file { padding: 0; font-size: 16px; - span.mdc-button__label { + label { display: block; cursor: pointer; padding: 0 16px; 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..361d3589fb 100644 --- a/ui-ngx/src/app/shared/components/notification/notification.component.html +++ b/ui-ngx/src/app/shared/components/notification/notification.component.html @@ -28,8 +28,8 @@
-
-
+
+
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; enabled: boolean; templateId: NotificationTemplateId; @@ -235,7 +234,7 @@ export interface NonConfirmedNotificationEscalation { targets: Array; } -export interface NotificationTarget extends Omit, 'label'>{ +export interface NotificationTarget extends Omit, 'label'>, ExportableEntity { tenantId: TenantId; configuration: NotificationTargetConfig; } @@ -280,7 +279,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/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 3e87150f87..fd2b4f52fa 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -245,7 +245,8 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => { } }; -export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow => { +export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, + historyOnly: boolean, timeService: TimeService): Timewindow => { const model = defaultTimewindow(timeService); if (value) { model.hideInterval = value.hideInterval; @@ -316,6 +317,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO if (quickIntervalOnly) { model.realtime.realtimeType = RealtimeWindowType.INTERVAL; } + if (historyOnly) { + model.selectedTab = TimewindowType.HISTORY; + } return model; }; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 155a9c8633..d1886fb1aa 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 entityTypesWithoutRelatedData: Set = new Set([ + EntityType.NOTIFICATION_TEMPLATE, + EntityType.NOTIFICATION_TARGET, + EntityType.NOTIFICATION_RULE +]); + export interface VersionCreateConfig { saveRelations: boolean; saveAttributes: boolean; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 9bd9006399..60877bd20d 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -123,6 +123,7 @@ import { JsFuncComponent } from '@shared/components/js-func.component'; import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component'; import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component'; import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; @@ -305,6 +306,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSelectComponent, ConfirmDialogComponent, AlertDialogComponent, + ErrorAlertDialogComponent, TodoDialogComponent, ColorPickerDialogComponent, MaterialIconsDialogComponent, @@ -528,6 +530,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MarkdownModule, ConfirmDialogComponent, AlertDialogComponent, + ErrorAlertDialogComponent, TodoDialogComponent, ColorPickerDialogComponent, MaterialIconsDialogComponent, 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 fe56019c81..2fab5b1e85 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1793,7 +1793,9 @@ } }, "dialog": { - "close": "Close dialog" + "close": "Close dialog", + "error-message-title": "Error message:", + "error-details-title": "Error details" }, "direction": { "column": "Column", @@ -2065,9 +2067,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", @@ -4000,7 +4008,8 @@ "alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities", "search": "Search widget", "filter": "Widget filter type", - "loading-widgets": "Loading widgets..." + "loading-widgets": "Loading widgets...", + "widget-template-error": "Invalid widget HTML template." }, "widget-action": { "header-button": "Widget header button", @@ -4009,6 +4018,9 @@ "open-dashboard": "Navigate to other dashboard", "custom": "Custom action", "custom-pretty": "Custom action (with HTML template)", + "custom-pretty-error-title": "Custom dialog error", + "custom-pretty-template-error": "Invalid custom dialog template.", + "custom-pretty-controller-error": "Error occurred while evaluating custom dialog function.", "mobile-action": "Mobile action", "target-dashboard-state": "Target dashboard state", "target-dashboard-state-required": "Target dashboard state is required", 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..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,13 +55,18 @@ "continue": "Continuar", "discard-changes": "Cancelar cambios", "download": "Descargar", + "next": "Siguiente", "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", @@ -473,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}}", @@ -485,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", @@ -545,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", @@ -565,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", @@ -577,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": { @@ -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", @@ -693,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", @@ -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", @@ -771,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", @@ -780,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", @@ -811,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", @@ -828,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", @@ -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", @@ -993,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": { @@ -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", @@ -1086,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", @@ -1104,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.", @@ -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", @@ -1214,7 +1334,52 @@ "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", + "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 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.", + "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", @@ -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 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.", "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", @@ -1614,94 +1796,97 @@ "row": "Fila" }, "edge": { - "edge": "Borde", - "edge-instances": "Instancias de Borde", - "edge-file": "Archivo de borde", + "edge": "Edge", + "edge-instances": "Instancias de Edge", + "instances": "Instancias", + "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", - "rulechains": "Cadenas de regla de 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": { @@ -1709,11 +1894,12 @@ "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", "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", @@ -1735,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" }, @@ -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} }", @@ -1859,12 +2050,19 @@ "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-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", @@ -1917,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", @@ -1988,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", @@ -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", @@ -2827,26 +3300,27 @@ "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" }, "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" } 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 { diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 7764dcbe5d..8da9cb382a 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -31,12 +31,12 @@ "@angular-devkit/core" "15.2.1" rxjs "6.6.7" -"@angular-devkit/architect@0.1502.4": - version "0.1502.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1502.4.tgz#a2ceef215f75b03ec275f5ff68e87fc43e44b113" - integrity sha512-bDBcaRMBfXFfK9MpvfNO926F1rL0PEw+mveXxq3/SSql+1XP/hrc5TVGwnoim4g6DqsGmu9upS5DyJ6PnL/hHA== +"@angular-devkit/architect@0.1502.8": + version "0.1502.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1502.8.tgz#9fd3fd27b3a7fc5f8eb65c92500b4d9d15b879e8" + integrity sha512-rTltw2ABHrcKc8EGimALvXmrDTP5hlNbEy6nYolJoXEI9EwHgriWrVLVPs3OEF+/ed47dbJi9EGOXUOgzgpB5A== dependencies: - "@angular-devkit/core" "15.2.4" + "@angular-devkit/core" "15.2.8" rxjs "6.6.7" "@angular-devkit/build-angular@^15.0.0": @@ -107,15 +107,15 @@ optionalDependencies: esbuild "0.17.8" -"@angular-devkit/build-angular@^15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-15.2.4.tgz#ac9230fef0d57d1402796ec0e9be3b3afd90ee51" - integrity sha512-wt0S4oz0vxuW0/Ak5X0vQ7s7TSPynmktVNJblu9SFRgwCD3kplV2B693F+M6t8eLzSy0UCSbZp9h3Ae8gLEiEw== +"@angular-devkit/build-angular@^15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-15.2.8.tgz#5412125b810fee084eb8afc20b9911606ad66170" + integrity sha512-TGDnXhhOG6h6TOrWWzfnkha7wYBOXi7iJc1o1w1VKCayE3T6TZZdF847aK66vL9KG7AKYVdGhWEGw2WBHUBUpg== dependencies: "@ampproject/remapping" "2.2.0" - "@angular-devkit/architect" "0.1502.4" - "@angular-devkit/build-webpack" "0.1502.4" - "@angular-devkit/core" "15.2.4" + "@angular-devkit/architect" "0.1502.8" + "@angular-devkit/build-webpack" "0.1502.8" + "@angular-devkit/core" "15.2.8" "@babel/core" "7.20.12" "@babel/generator" "7.20.14" "@babel/helper-annotate-as-pure" "7.18.6" @@ -127,7 +127,7 @@ "@babel/runtime" "7.20.13" "@babel/template" "7.20.7" "@discoveryjs/json-ext" "0.5.7" - "@ngtools/webpack" "15.2.4" + "@ngtools/webpack" "15.2.8" ansi-colors "4.1.3" autoprefixer "10.4.13" babel-loader "9.1.2" @@ -183,12 +183,12 @@ "@angular-devkit/architect" "0.1502.1" rxjs "6.6.7" -"@angular-devkit/build-webpack@0.1502.4": - version "0.1502.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1502.4.tgz#0fa89e684692d2444d4e7996f1d2c974be2b6e2b" - integrity sha512-Bs/pxcY3517QAVyAalDxJgjc93KWQos+dFdgEQrKxj/VTs1BTYnLbb2M8Y7MoxVnfH4S+qqxGe5B57T+TlB3Eg== +"@angular-devkit/build-webpack@0.1502.8": + version "0.1502.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1502.8.tgz#1b375480deef1b0920e1a63d952795bd33bbfb38" + integrity sha512-jWtNv+S03FFLDe/C8SPCcRvkz3bSb2R+919IT086Q9axIPQ1VowOEwzt2k3qXPSSrC7GSYuASM+X92dB47NTQQ== dependencies: - "@angular-devkit/architect" "0.1502.4" + "@angular-devkit/architect" "0.1502.8" rxjs "6.6.7" "@angular-devkit/core@15.2.1", "@angular-devkit/core@^15.0.0": @@ -202,10 +202,10 @@ rxjs "6.6.7" source-map "0.7.4" -"@angular-devkit/core@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.2.4.tgz#f7696f09c66d01568a07f0e71672e887fdf57280" - integrity sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg== +"@angular-devkit/core@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.2.8.tgz#ff494ae7af137b0f0109deb8ee34f1550ed5cc1d" + integrity sha512-Lo4XrbDMtXarKnMrFgWLmQdSX+3QPNAg4otG8cmp/U4jJyjV4dAYKEAsb1sCNGUSM4h4v09EQU/5ugVjDU29lQ== dependencies: ajv "8.12.0" ajv-formats "2.1.1" @@ -213,12 +213,12 @@ rxjs "6.6.7" source-map "0.7.4" -"@angular-devkit/schematics@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.2.4.tgz#85129ebabcdb362f4b65a6e290bb2ae846f3d64c" - integrity sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw== +"@angular-devkit/schematics@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.2.8.tgz#c7dfc692e3f54e43085a8845d8c9f390a2519aa3" + integrity sha512-w6EUGC96kVsH9f8sEzajzbONMawezyVBiSo+JYp5r25rQArAz/a+KZntbuETWHQ0rQOEsKmUNKxwmr11BaptSQ== dependencies: - "@angular-devkit/core" "15.2.4" + "@angular-devkit/core" "15.2.8" jsonc-parser "3.2.0" magic-string "0.29.0" ora "5.4.1" @@ -281,31 +281,31 @@ "@angular-eslint/bundled-angular-compiler" "15.2.1" "@typescript-eslint/utils" "5.48.2" -"@angular/animations@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-15.2.5.tgz#d42e5f185ed3d8044d38b1c938c14044aa0345a8" - integrity sha512-areHbwTrJNEYWe+1xND7SEI8fY7Pi2wckuLtbGonjtoaAc8MPs0ov56TnpXHS7bIXKVszqUlBTBAElQSdtjCyw== +"@angular/animations@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-15.2.9.tgz#f0773d2071a5a17c03478d5838029b03bbab9a03" + integrity sha512-GQujLhI0cQFcl4Q8y0oSYKSRnW23GIeSL+Arl4eFufziJ9hGAAQNuesaNs/7i+9UlTHDMkPH3kd5ScXuYYz6wg== dependencies: tslib "^2.3.0" -"@angular/cdk@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-15.2.5.tgz#53208ea5e654b678af011b3ef6034e1382339630" - integrity sha512-6/vA4xATcx//AULwXnRRH0Of0aaNlfrFxdlhvRVfnfmxi/hZAkjjEtil/wv8uBw+dAwXBx5yZMsAol9nUFS/BQ== +"@angular/cdk@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-15.2.9.tgz#e22df07b296fec6dccf66d569c3acc3c504c2058" + integrity sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q== dependencies: tslib "^2.3.0" optionalDependencies: parse5 "^7.1.2" -"@angular/cli@^15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-15.2.4.tgz#5163efb3f3e5b2a8ab0598d46d4dc200a51e7197" - integrity sha512-nQFnrt9aWwqkyhJMaZvjHXXuxeR1pZNcpZEzTA5nXtpVeNs8U7EBFCyJ+cYecFX0LHe36SoxXcbfnNEKHt3NVQ== +"@angular/cli@^15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-15.2.8.tgz#612ffd69591aea0109db0a6dd8faec8044a4b80d" + integrity sha512-3VlTfm6DUZfFHBY43vQSAaqmFTxy3VtRd/iDBCHcEPhHwYLWBvNwReJuJfNja8O105QQ6DBiYVBExEBtPmjQ4w== dependencies: - "@angular-devkit/architect" "0.1502.4" - "@angular-devkit/core" "15.2.4" - "@angular-devkit/schematics" "15.2.4" - "@schematics/angular" "15.2.4" + "@angular-devkit/architect" "0.1502.8" + "@angular-devkit/core" "15.2.8" + "@angular-devkit/schematics" "15.2.8" + "@schematics/angular" "15.2.8" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.3" ini "3.0.1" @@ -321,17 +321,17 @@ symbol-observable "4.0.0" yargs "17.6.2" -"@angular/common@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-15.2.5.tgz#7ae960f17c260a92e939ad82fb115d908094aeda" - integrity sha512-6Wl1ak96NZvnL8p0eVsxHhaddv0/qYq2pQniKaKBfL9MVOQtAe07sPUDEZ6w0ApkmV63Giu4zFakaQMsmvxU0Q== +"@angular/common@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-15.2.9.tgz#5e1d47ce831935bcf545b172f88307aedacf1535" + integrity sha512-LM9/UHG2dRrOzlu2KovrFwWIziFMjRxHzSP3Igw6Symw/wIl0kXGq8Fn6RpFP78zmLqnv+IQOoRiby9MCXsI4g== dependencies: tslib "^2.3.0" -"@angular/compiler-cli@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-15.2.5.tgz#3c1882cc837a71fd0f93b880bfec6e851ec213e8" - integrity sha512-NNrnvL0JMg6dZMuTgBTb/IVIFJwy2my5XTNLngTNBfDCVu0SL1TKrYnpqp2qRi2ip9XdNqCtNF7JFDEklVbK2A== +"@angular/compiler-cli@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-15.2.9.tgz#d9e6013d6a8658e4a210aca7997e70d06f6976a8" + integrity sha512-zsbI8G2xHOeYWI0hjFzrI//ZhZV9il/uQW5dAimfwJp06KZDeXZ3PdwY9JQslf6F+saLwOObxy6QMrIVvfjy9w== dependencies: "@babel/core" "7.19.3" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -344,17 +344,17 @@ tslib "^2.3.0" yargs "^17.2.1" -"@angular/compiler@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-15.2.5.tgz#4902db0699fd9ff8af15a3cd0d8be9aacef09b7f" - integrity sha512-0mAFF6Ud4ojsb8qGLQCWYh/LgKKrnn1Fz788LzfYcLYhi7UZPiCFrQJrEcCXBKtQsk8sG335CA9Qk0I/qc35zg== +"@angular/compiler@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-15.2.9.tgz#3f55e206b0e380c28336d2a233b7132f21d72644" + integrity sha512-MoKugbjk+E0wRBj12uvIyDLELlVLonnqjA2+XiF+7FxALIeyds3/qQeEoMmYIqAbN3NnTT5pV92RxWwG4tHFwA== dependencies: tslib "^2.3.0" -"@angular/core@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-15.2.5.tgz#761d339551a3daf97d9b9786882a5dd42ee19445" - integrity sha512-GtmjJmwtzCuF4FDXIY+9UFMAcfPlJzJDBDF7mgEmD2YKf/HV5PSyU91lfv9yDPnAkdzlDAL9u4YxnGgoURY8/g== +"@angular/core@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-15.2.9.tgz#7cb12cc83fcc92f23196ceac82e07b67b2e02203" + integrity sha512-w46Z1yUXCQfKV7XfnamOoLA2VD0MVUUYVrUjO73mHSskDXSXxfZAEHO9kfUS71Cj35PvhP3mbkqWscpea2WeYg== dependencies: tslib "^2.3.0" @@ -365,22 +365,22 @@ dependencies: tslib "^2.3.0" -"@angular/forms@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-15.2.5.tgz#e40cf0bc27eae6e7ae1409ff618a3fe7cea19cfc" - integrity sha512-F4YLh0S1VyWp4IyfORTpzkZJcg7nB4q/wIirVZocR9sK3FJMEKYcAGwLbkWDYGZ66UCDw7EEqCTXkjr99/6sPg== +"@angular/forms@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-15.2.9.tgz#c3b4b0108f4eb4966ddc5a7ec9913c2ca2c94f00" + integrity sha512-sk0pC2EFi2Ohg5J0q0NYptbT+2WOkoiERSMYA39ncDvlSZBWsNlxpkbGUSck7NIxjK2QfcVN1ldGbHlZTFvtqg== dependencies: tslib "^2.3.0" -"@angular/language-service@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-15.2.5.tgz#fff42544e1dad609293a33ae93371a11e2df0c44" - integrity sha512-knXe4D0gc2KiU3Y3LZCRvcIkddxUMHI6/44/QAGZD3UDx6ZnpizS0eb8q8AO1pWiBTUY5ETUAlihaGWqDgQ+Wg== +"@angular/language-service@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-15.2.9.tgz#7a94e3394093a425c757f7b385b4a94edb09178a" + integrity sha512-B7lP4q/eHge2lZezOXS96EYzVf4stMCWfOnz7+pUUi0HbF+A5QCV65SWQddS/M+NM2jj8N2L/j+6UCH8lJjTQA== -"@angular/material@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-15.2.5.tgz#a0531b629334dfd2997dd01e549bd0c40680f4e9" - integrity sha512-Lk6l1VsBA2ActVN9YsX2W5lVFYHJEVwswbbUdBPjWaXJzp7TdgghTFczac7vsNA6y3DM3Rd+CvRsJPD2kK4g4A== +"@angular/material@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-15.2.9.tgz#d49f18660053bf9eae008e4738ebc65334a28491" + integrity sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ== dependencies: "@material/animation" "15.0.0-canary.684e33d25.0" "@material/auto-init" "15.0.0-canary.684e33d25.0" @@ -431,24 +431,24 @@ "@material/typography" "15.0.0-canary.684e33d25.0" tslib "^2.3.0" -"@angular/platform-browser-dynamic@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.5.tgz#f715dafd227606142295fd717a7c3fb0b67ae79f" - integrity sha512-NoGVeaR7K+RMcLpS2gI4hyMVeUqP057rw8Yfk15dHy3cM8icj5zVfyez3AADcO1XthNhE1sI1d+2LD4/GxwIKQ== +"@angular/platform-browser-dynamic@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.9.tgz#aa31ba63d535ee49fdf3a60fe771503565b4e3c9" + integrity sha512-ZIYDM6MShblb8OyV1m4+18lJJ2LCeICmeg2uSbpFYptYBSOClrTiYOOFVDJvn7HLvNzljLs16XPrgyaYVqNpcw== dependencies: tslib "^2.3.0" -"@angular/platform-browser@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-15.2.5.tgz#b5e1a4da920577d147f162463b2448588543a172" - integrity sha512-D19HL3UsvPX4s8yC4C4gphTNyQU75VuzcyGZ+6y1o1SQbVjF6UwSrztmb//4MCkpclb+avv669z5AEiTuBTnVQ== +"@angular/platform-browser@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-15.2.9.tgz#9150645843cc18b084fb5bf7025e6e320c2abe1e" + integrity sha512-ufCHeSX+U6d43YOMkn3igwfqtlozoCXADcbyfUEG8m2y9XASobqmCKvdSk/zfl62oyiA8msntWBJVBE2l4xKXg== dependencies: tslib "^2.3.0" -"@angular/router@^15.2.5": - version "15.2.5" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-15.2.5.tgz#879839960d9a3d52a20cdffc51062e5d288c3440" - integrity sha512-NSEfx2wgPCS8EdXLbKUjNCXC29cMqEGDUrSv8+P/O+tY21Szkqp6uzGTOZJuSTOoKQmDEw9CRmuwxMKm2l6y/Q== +"@angular/router@^15.2.9": + version "15.2.9" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-15.2.9.tgz#c3879be22bda236eacf97a18a1e8619b51a53d47" + integrity sha512-UCbh5DLSDhybv0xKYT7kGQMfOVdyhHIHOZz5EYVebbhste6S+W1LE57vTHq7QtxJsyKBa/WSkaUkCLXD6ntCAg== dependencies: tslib "^2.3.0" @@ -2619,15 +2619,15 @@ dependencies: tslib "^2.0.0" -"@ngtools/webpack@15.2.1", "@ngtools/webpack@^15.1.6": +"@ngtools/webpack@15.2.1": version "15.2.1" resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.1.tgz#439ac075b2dcb9f304f0b7009182cc5049c7988a" integrity sha512-YtA8rWAglPuf4CSStrFAxaprTSYE+DREGrJFc3WvZLcF5XrwVK+H4CC4Pmz07iYsG1TXShR4bWp1fbGw1cmBKw== -"@ngtools/webpack@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.4.tgz#bb8afe2800e79120985657180d2b33f64ecd763d" - integrity sha512-cQ7MsRoGJgPOVnpvFgWhygeSe6zJ0ITiUhjmmuOgpNDfYkrgYxN3Ot/qvQefFei+oGZ1JJ9bRb8lcPKL/apoBQ== +"@ngtools/webpack@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.8.tgz#df8fb9300ccf94cab8f8ad69fb16fd31181e6c82" + integrity sha512-BJexeT4FxMtToVBGa3wdl6rrkYXgilP0kkSH4Qzu4MPlLPbeBSr4XQalQriewlpC2uzG0r2SJfrAe2eDhtSykA== "@ngx-translate/core@^14.0.0": version "14.0.0" @@ -2733,13 +2733,13 @@ read-package-json-fast "^3.0.0" which "^3.0.0" -"@schematics/angular@15.2.4": - version "15.2.4" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-15.2.4.tgz#b61b9cb3becf601e904f60490846c1d95bd63700" - integrity sha512-P9axwKDXEDrGXYcoCuhwv4guhvtIjDCvyZMsQw8IMpfAacTYajka+T3mZrzyTe96Uhu0JwcAW5iUlOZAJhPEdQ== +"@schematics/angular@15.2.8": + version "15.2.8" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-15.2.8.tgz#d845903f1cc477d299f968eb5bc40a9855cfd911" + integrity sha512-F49IEzCFxQlpaMIgTO/wF1l/CLQKif7VaiDdyiTKOeT22IMmyd61FUmWDyZYfCBqMlvBmvDGx64HaHWes1HYCg== dependencies: - "@angular-devkit/core" "15.2.4" - "@angular-devkit/schematics" "15.2.4" + "@angular-devkit/core" "15.2.8" + "@angular-devkit/schematics" "15.2.8" jsonc-parser "3.2.0" "@socket.io/component-emitter@~3.1.0":