diff --git a/application/src/main/data/json/system/oauth2_config_templates/github_config.json b/application/src/main/data/json/system/oauth2_config_templates/github_config.json index 0a1ae5779b..439043d961 100644 --- a/application/src/main/data/json/system/oauth2_config_templates/github_config.json +++ b/application/src/main/data/json/system/oauth2_config_templates/github_config.json @@ -10,6 +10,7 @@ "mapperConfig": { "type": "GITHUB", "basic": { + "firstNameAttributeKey": "name", "tenantNameStrategy": "DOMAIN" } }, diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index 3d076ff812..da9a95b423 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -94,7 +94,8 @@ "name": "Device Profile Node", "debugMode": false, "configuration": { - "persistAlarmRulesState": false + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false } } ], diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index d35dadd16f..7f028932f2 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -544,7 +544,11 @@ public class ActorSystemContext { public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs); - getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); + if (delayInMs > 0) { + getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); + } else { + ctx.tell(msg); + } } } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index b02bc302de..c8bb083577 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -35,6 +35,7 @@ import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.EntityId; @@ -477,6 +478,16 @@ class DefaultTbContext implements TbContext { mainCtx.getRuleNodeStateService().removeByRuleNodeId(getTenantId(), getSelfId()); } + @Override + public void addProfileListener(Consumer listener) { + mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), listener); + } + + @Override + public void removeProfileListener() { + mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId()); + } + private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("ruleNodeId", ruleNodeId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index f55ac89ab5..d435f3e6ce 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -106,7 +106,6 @@ public class DeviceProfileController extends BaseController { DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile)); - deviceProfileCache.put(savedDeviceProfile); tbClusterService.onDeviceProfileChange(savedDeviceProfile, null); tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), savedDeviceProfile.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); @@ -132,7 +131,6 @@ public class DeviceProfileController extends BaseController { DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE); deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId); - deviceProfileCache.evict(deviceProfileId); tbClusterService.onDeviceProfileDelete(deviceProfile, null); tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED); diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 323eb19bcd..fc5e0a1426 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -36,6 +38,10 @@ import java.util.List; @RequestMapping("/api") @Slf4j public class OAuth2Controller extends BaseController { + + @Autowired + private OAuth2Configuration oAuth2Configuration; + @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) @ResponseBody public List getOAuth2Clients(HttpServletRequest request) throws ThingsboardException { @@ -70,4 +76,16 @@ public class OAuth2Controller extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") + @RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET) + @ResponseBody + public String getLoginProcessingUrl() throws ThingsboardException { + try { + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); + return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 7c23545374..83e3d98b1b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -92,6 +92,7 @@ public class TenantController extends BaseController { installScripts.createDefaultRuleChains(tenant.getId()); } tenantProfileCache.evict(tenant.getId()); + tbClusterService.onTenantChange(tenant, null); return tenant; } catch (Exception e) { throw handleException(e); @@ -105,9 +106,10 @@ public class TenantController extends BaseController { checkParameter("tenantId", strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); - checkTenantId(tenantId, Operation.DELETE); + Tenant tenant = checkTenantId(tenantId, Operation.DELETE); tenantService.deleteTenant(tenantId); tenantProfileCache.evict(tenantId); + tbClusterService.onTenantDelete(tenant, null); tbClusterService.onEntityStateChange(tenantId, tenantId, ComponentLifecycleEvent.DELETED); } catch (Exception e) { throw handleException(e); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 4c9d541ea6..0941fac725 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -96,6 +97,7 @@ public class TenantProfileController extends BaseController { tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile)); tenantProfileCache.put(tenantProfile); + tbClusterService.onTenantProfileChange(tenantProfile, null); tbClusterService.onEntityStateChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(), newTenantProfile ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); return tenantProfile; @@ -111,8 +113,9 @@ public class TenantProfileController extends BaseController { checkParameter("tenantProfileId", strTenantProfileId); try { TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); - checkTenantProfileId(tenantProfileId, Operation.DELETE); + TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE); tenantProfileService.deleteTenantProfile(getTenantId(), tenantProfileId); + tbClusterService.onTenantProfileDelete(profile, null); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index bc86857c4d..aded7868bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -170,7 +170,7 @@ public class DefaultDataUpdateService implements DataUpdateService { ruleChainService.saveRuleChainMetaData(tenant.getId(), md); } } catch (Exception e) { - log.error("Unable to update Tenant", e); + log.error("[{}] Unable to update Tenant: {}", tenant.getId(), tenant.getName(), e); } } }; diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java index b6af0d7496..6784405adb 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java @@ -21,14 +21,17 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; 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.TenantId; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; @Service @Slf4j @@ -40,6 +43,7 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache { private final ConcurrentMap deviceProfilesMap = new ConcurrentHashMap<>(); private final ConcurrentMap devicesMap = new ConcurrentHashMap<>(); + private final ConcurrentMap>> listeners = new ConcurrentHashMap<>(); public DefaultTbDeviceProfileCache(DeviceProfileService deviceProfileService, DeviceService deviceService) { this.deviceProfileService = deviceProfileService; @@ -50,19 +54,21 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache { public DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId) { DeviceProfile profile = deviceProfilesMap.get(deviceProfileId); if (profile == null) { - profile = deviceProfilesMap.get(deviceProfileId); - if (profile == null) { - deviceProfileFetchLock.lock(); - try { + deviceProfileFetchLock.lock(); + try { + profile = deviceProfilesMap.get(deviceProfileId); + if (profile == null) { profile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId); if (profile != null) { deviceProfilesMap.put(deviceProfileId, profile); + log.debug("[{}] Fetch device profile into cache: {}", profile.getId(), profile); } - } finally { - deviceProfileFetchLock.unlock(); } + } finally { + deviceProfileFetchLock.unlock(); } } + log.trace("[{}] Found device profile in cache: {}", deviceProfileId, profile); return profile; } @@ -85,12 +91,19 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache { public void put(DeviceProfile profile) { if (profile.getId() != null) { deviceProfilesMap.put(profile.getId(), profile); + log.debug("[{}] pushed device profile to cache: {}", profile.getId(), profile); + notifyListeners(profile); } } @Override - public void evict(DeviceProfileId profileId) { - deviceProfilesMap.remove(profileId); + public void evict(TenantId tenantId, DeviceProfileId profileId) { + DeviceProfile oldProfile = deviceProfilesMap.remove(profileId); + log.debug("[{}] evict device profile from cache: {}", profileId, oldProfile); + DeviceProfile newProfile = get(tenantId, profileId); + if (newProfile != null) { + notifyListeners(newProfile); + } } @Override @@ -98,4 +111,34 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache { devicesMap.remove(deviceId); } + @Override + public void addListener(TenantId tenantId, EntityId listenerId, Consumer listener) { + listeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, listener); + } + + @Override + public DeviceProfile find(DeviceProfileId deviceProfileId) { + return deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, deviceProfileId); + } + + @Override + public DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName) { + return deviceProfileService.findOrCreateDeviceProfile(tenantId, profileName); + } + + @Override + public void removeListener(TenantId tenantId, EntityId listenerId) { + ConcurrentMap> tenantListeners = listeners.get(tenantId); + if (tenantListeners != null) { + tenantListeners.remove(listenerId); + } + } + + private void notifyListeners(DeviceProfile profile) { + ConcurrentMap> tenantListeners = listeners.get(profile.getTenantId()); + if (tenantListeners != null) { + tenantListeners.forEach((id, listener) -> listener.accept(profile)); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java index 4260e36400..42c09c104c 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbTenantProfileCache.java @@ -55,17 +55,17 @@ public class DefaultTbTenantProfileCache implements TbTenantProfileCache { public TenantProfile get(TenantProfileId tenantProfileId) { TenantProfile profile = tenantProfilesMap.get(tenantProfileId); if (profile == null) { - profile = tenantProfilesMap.get(tenantProfileId); - if (profile == null) { - tenantProfileFetchLock.lock(); - try { + tenantProfileFetchLock.lock(); + try { + profile = tenantProfilesMap.get(tenantProfileId); + if (profile == null) { profile = tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenantProfileId); if (profile != null) { tenantProfilesMap.put(tenantProfileId, profile); } - } finally { - tenantProfileFetchLock.unlock(); } + } finally { + tenantProfileFetchLock.unlock(); } } return profile; diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java index ec19eb1da6..e65f297d53 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java @@ -19,13 +19,17 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.TenantId; public interface TbDeviceProfileCache extends RuleEngineDeviceProfileCache { void put(DeviceProfile profile); - void evict(DeviceProfileId id); + void evict(TenantId tenantId, DeviceProfileId id); void evict(DeviceId id); + DeviceProfile find(DeviceProfileId deviceProfileId); + + DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String deviceType); } diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java index d9f944012f..de69b5f51c 100644 --- a/application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java +++ b/application/src/main/java/org/thingsboard/server/service/profile/TbTenantProfileCache.java @@ -15,11 +15,7 @@ */ package org.thingsboard.server.service.profile; -import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; -import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 28cd998e46..9451b58e9f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -23,11 +23,15 @@ import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; 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.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -189,21 +193,51 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback) { - log.trace("[{}][{}] Processing device profile [{}] change event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName()); - TransportProtos.DeviceProfileUpdateMsg profileUpdateMsg = TransportProtos.DeviceProfileUpdateMsg.newBuilder() - .setData(ByteString.copyFrom(encodingService.encode(deviceProfile))).build(); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileUpdateMsg(profileUpdateMsg).build(); - broadcast(transportMsg); + onEntityChange(deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile, callback); + } + + @Override + public void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback) { + onEntityChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(), tenantProfile, callback); + } + + @Override + public void onTenantChange(Tenant tenant, TbQueueCallback callback) { + onEntityChange(TenantId.SYS_TENANT_ID, tenant.getId(), tenant, callback); + } + + @Override + public void onDeviceProfileDelete(DeviceProfile entity, TbQueueCallback callback) { + onEntityDelete(entity.getTenantId(), entity.getId(), entity.getName(), callback); } @Override - public void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback) { - log.trace("[{}][{}] Processing device profile [{}] delete event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName()); - TransportProtos.DeviceProfileDeleteMsg profileDeleteMsg = TransportProtos.DeviceProfileDeleteMsg.newBuilder() - .setProfileIdMSB(deviceProfile.getId().getId().getMostSignificantBits()) - .setProfileIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()) + public void onTenantProfileDelete(TenantProfile entity, TbQueueCallback callback) { + onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback); + } + + @Override + public void onTenantDelete(Tenant entity, TbQueueCallback callback) { + onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback); + } + + public void onEntityChange(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { + log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entity.getName()); + TransportProtos.EntityUpdateMsg entityUpdateMsg = TransportProtos.EntityUpdateMsg.newBuilder() + .setEntityType(entityid.getEntityType().name()) + .setData(ByteString.copyFrom(encodingService.encode(entity))).build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityUpdateMsg(entityUpdateMsg).build(); + broadcast(transportMsg); + } + + private void onEntityDelete(TenantId tenantId, EntityId entityId, String name, TbQueueCallback callback) { + log.trace("[{}][{}][{}] Processing [{}] delete event", tenantId, entityId.getEntityType(), entityId.getId(), name); + TransportProtos.EntityDeleteMsg entityDeleteMsg = TransportProtos.EntityDeleteMsg.newBuilder() + .setEntityType(entityId.getEntityType().name()) + .setEntityIdMSB(entityId.getId().getMostSignificantBits()) + .setEntityIdLSB(entityId.getId().getLeastSignificantBits()) .build(); - ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileDeleteMsg(profileDeleteMsg).build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setEntityDeleteMsg(entityDeleteMsg).build(); broadcast(transportMsg); } @@ -221,7 +255,8 @@ public class DefaultTbClusterService implements TbClusterService { byte[] msgBytes = encodingService.encode(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)); - if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)) { + if (msg.getEntityId().getEntityType().equals(EntityType.TENANT) + || msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java index cf212a06f5..838dbe0eef 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java @@ -17,7 +17,8 @@ package org.thingsboard.server.service.queue; import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -53,5 +54,13 @@ public interface TbClusterService { void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback); - void onDeviceProfileDelete(DeviceProfile deviceProfileId, TbQueueCallback callback); + void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback); + + void onTenantProfileChange(TenantProfile tenantProfile, TbQueueCallback callback); + + void onTenantProfileDelete(TenantProfile tenantProfile, TbQueueCallback callback); + + void onTenantChange(Tenant tenant, TbQueueCallback callback); + + void onTenantDelete(Tenant tenant, TbQueueCallback callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 3f6595f81e..0869dbc356 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -144,7 +144,7 @@ public abstract class AbstractConsumerService dashboardIdOpt = user.getAuthority() == Authority.TENANT_ADMIN ? getDashboardId(tenantId, oauth2User.getDefaultDashboardName()) : getDashboardId(tenantId, customerId, oauth2User.getDefaultDashboardName()); if (dashboardIdOpt.isPresent()) { - ObjectNode additionalInfo = objectMapper.createObjectNode(); additionalInfo.put("defaultDashboardFullscreen", oauth2User.isAlwaysFullScreen()); additionalInfo.put("defaultDashboardId", dashboardIdOpt.get().getId().toString()); - user.setAdditionalInfo(additionalInfo); } } + if (clientRegistration.getAdditionalInfo() != null && + clientRegistration.getAdditionalInfo().has("providerName")) { + additionalInfo.put("authProviderName", clientRegistration.getAdditionalInfo().get("providerName").asText()); + } + + user.setAdditionalInfo(additionalInfo); + user = userService.saveUser(user); - if (activateUser) { + if (config.isActivateUser()) { UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode("")); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java index 73da9e539f..bd6434f50a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.auth.oauth2; import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; @@ -29,11 +30,12 @@ import java.util.Map; public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { + OAuth2MapperConfig config = clientRegistration.getMapperConfig(); Map attributes = token.getPrincipal().getAttributes(); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index a85da830b0..0cd292f76f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.dao.oauth2.OAuth2User; @@ -38,9 +39,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { + OAuth2MapperConfig config = clientRegistration.getMapperConfig(); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser()); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration); } private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java index dcca2b71a1..f259720626 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java @@ -23,6 +23,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2User; @@ -45,12 +46,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private OAuth2Configuration oAuth2Configuration; @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) { + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { + OAuth2MapperConfig config = clientRegistration.getMapperConfig(); Map githubMapperConfig = oAuth2Configuration.getGithubMapper(); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); Map attributes = token.getPrincipal().getAttributes(); OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oAuth2User, config.isAllowUserCreation(), config.isActivateUser()); + return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration); } private synchronized String getEmail(String emailUrl, String oauth2Token) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java index 27b24043a5..4c9804930e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java @@ -16,9 +16,9 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.service.security.model.SecurityUser; public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config); + SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 5bb02b9a63..4840f03cf2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -74,7 +74,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS token.getPrincipal().getName()); OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType()); SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), - clientRegistration.getMapperConfig()); + clientRegistration); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); @@ -85,4 +85,4 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index c806757ddb..10360769a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -28,6 +28,7 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData; @@ -45,21 +46,19 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; -import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; @@ -70,6 +69,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.profile.TbTenantProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.state.DeviceStateService; @@ -90,9 +90,7 @@ public class DefaultTransportApiService implements TransportApiService { private static final ObjectMapper mapper = new ObjectMapper(); - //TODO: Constructor dependencies; - private final DeviceProfileService deviceProfileService; - private final TenantService tenantService; + private final TbDeviceProfileCache deviceProfileCache; private final TbTenantProfileCache tenantProfileCache; private final DeviceService deviceService; private final RelationService relationService; @@ -103,17 +101,15 @@ public class DefaultTransportApiService implements TransportApiService { private final DataDecodingEncodingService dataDecodingEncodingService; private final DeviceProvisionService deviceProvisionService; - private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); - public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService, + public DefaultTransportApiService(TbDeviceProfileCache deviceProfileCache, TbTenantProfileCache tenantProfileCache, DeviceService deviceService, RelationService relationService, DeviceCredentialsService deviceCredentialsService, DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, DeviceProvisionService deviceProvisionService) { - this.deviceProfileService = deviceProfileService; - this.tenantService = tenantService; + this.deviceProfileCache = deviceProfileCache; this.tenantProfileCache = tenantProfileCache; this.deviceService = deviceService; this.relationService = relationService; @@ -143,11 +139,8 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); - } else if (transportApiRequestMsg.hasGetTenantRoutingInfoRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()), - value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); - } else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) { - return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()), + } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getEntityProfileRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()), @@ -238,7 +231,7 @@ public class DefaultTransportApiService implements TransportApiService { device.setName(requestMsg.getDeviceName()); device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId()); - DeviceProfile deviceProfile = deviceProfileService.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); + DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); device.setDeviceProfileId(deviceProfile.getId()); device = deviceService.saveDevice(device); relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created")); @@ -258,7 +251,7 @@ public class DefaultTransportApiService implements TransportApiService { } GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() .setDeviceInfo(getDeviceInfoProto(device)); - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); if (deviceProfile != null) { builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); } else { @@ -320,23 +313,21 @@ public class DefaultTransportApiService implements TransportApiService { .build(); } - private ListenableFuture handle(GetTenantRoutingInfoRequestMsg requestMsg) { - TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); - - ListenableFuture tenantProfileFuture = Futures.immediateFuture(tenantProfileCache.get(tenantId)); - return Futures.transform(tenantProfileFuture, tenantProfile -> TransportApiResponseMsg.newBuilder() - .setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenantProfile.isIsolatedTbCore()) - .setIsolatedTbRuleEngine(tenantProfile.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService); - } - - private ListenableFuture handle(TransportProtos.GetDeviceProfileRequestMsg requestMsg) { - DeviceProfileId profileId = new DeviceProfileId(new UUID(requestMsg.getProfileIdMSB(), requestMsg.getProfileIdLSB())); - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, profileId); - return Futures.immediateFuture(TransportApiResponseMsg.newBuilder() - .setGetDeviceProfileResponseMsg( - TransportProtos.GetDeviceProfileResponseMsg.newBuilder() - .setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))) - .build()).build()); + private ListenableFuture handle(GetEntityProfileRequestMsg requestMsg) { + EntityType entityType = EntityType.valueOf(requestMsg.getEntityType()); + UUID entityUuid = new UUID(requestMsg.getEntityIdMSB(), requestMsg.getEntityIdLSB()); + ByteString data; + if (entityType.equals(EntityType.DEVICE_PROFILE)) { + DeviceProfileId deviceProfileId = new DeviceProfileId(entityUuid); + DeviceProfile deviceProfile = deviceProfileCache.find(deviceProfileId); + data = ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)); + } else if (entityType.equals(EntityType.TENANT)) { + TenantProfile tenantProfile = tenantProfileCache.get(new TenantId(entityUuid)); + data = ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile)); + } else { + throw new RuntimeException("Invalid entity profile request: " + entityType); + } + return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setEntityProfileResponseMsg(GetEntityProfileResponseMsg.newBuilder().setData(data).build()).build()); } private ListenableFuture getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) { @@ -348,7 +339,7 @@ public class DefaultTransportApiService implements TransportApiService { try { ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder(); builder.setDeviceInfo(getDeviceInfoProto(device)); - DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); if (deviceProfile != null) { builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); } else { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d0ddeb611a..deb28ffd99 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -502,7 +502,7 @@ js: remote: # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}" - # Maximum time in seconds for black listed function to stay in the list. + # Maximum time in seconds for black listed function to stay in 1:the list. max_black_list_duration_sec: "${REMOTE_JS_SANDBOX_MAX_BLACKLIST_DURATION_SEC:60}" stats: enabled: "${TB_JS_REMOTE_STATS_ENABLED:false}" @@ -512,10 +512,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java index d93401f5cc..4e932fad85 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java @@ -27,6 +27,6 @@ import java.util.Set; @NoArgsConstructor @AllArgsConstructor public class OAuth2ClientsDomainParams { - private Set domainInfos; - private Set clientRegistrations; -} \ No newline at end of file + private List domainInfos; + private List clientRegistrations; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java index ee20021aca..ad5d5305bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data.oauth2; import lombok.*; + +import java.util.List; import java.util.Set; @EqualsAndHashCode @@ -26,5 +28,5 @@ import java.util.Set; @AllArgsConstructor public class OAuth2ClientsParams { private boolean enabled; - private Set domainsParams; -} \ No newline at end of file + private List domainsParams; +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java index 3c79895c8b..3550bc44a9 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java @@ -50,4 +50,8 @@ public class TbRateLimits { return bucket.tryConsume(1); } + public boolean tryConsume(long number) { + return bucket.tryConsume(number); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java new file mode 100644 index 0000000000..dde1c6a620 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 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.queue.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'") +public @interface TbTransportComponent { +} diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 166f94b864..01d8ca4524 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -177,32 +177,26 @@ message GetOrCreateDeviceFromGatewayResponseMsg { bytes profileBody = 2; } -message GetTenantRoutingInfoRequestMsg { - int64 tenantIdMSB = 1; - int64 tenantIdLSB = 2; -} - -message GetTenantRoutingInfoResponseMsg { - bool isolatedTbCore = 1; - bool isolatedTbRuleEngine = 2; -} - -message GetDeviceProfileRequestMsg { - int64 profileIdMSB = 1; - int64 profileIdLSB = 2; +message GetEntityProfileRequestMsg { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; } -message GetDeviceProfileResponseMsg { - bytes data = 1; +message GetEntityProfileResponseMsg { + string entityType = 1; + bytes data = 2; } -message DeviceProfileUpdateMsg { - bytes data = 1; +message EntityUpdateMsg { + string entityType = 1; + bytes data = 2; } -message DeviceProfileDeleteMsg { - int64 profileIdMSB = 1; - int64 profileIdLSB = 2; +message EntityDeleteMsg { + string entityType = 1; + int64 entityIdMSB = 2; + int64 entityIdLSB = 3; } message SessionCloseNotificationProto { @@ -482,8 +476,7 @@ message TransportApiRequestMsg { ValidateDeviceTokenRequestMsg validateTokenRequestMsg = 1; ValidateDeviceX509CertRequestMsg validateX509CertRequestMsg = 2; GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3; - GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4; - GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5; + GetEntityProfileRequestMsg entityProfileRequestMsg = 4; ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7; } @@ -492,9 +485,8 @@ message TransportApiRequestMsg { message TransportApiResponseMsg { ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1; GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; - GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4; - GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5; - ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 6; + GetEntityProfileResponseMsg entityProfileResponseMsg = 3; + ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 4; } /* Messages that are handled by ThingsBoard Core Service */ @@ -535,7 +527,8 @@ message ToTransportMsg { AttributeUpdateNotificationMsg attributeUpdateNotification = 5; ToDeviceRpcRequestMsg toDeviceRequest = 6; ToServerRpcResponseMsg toServerResponse = 7; - DeviceProfileUpdateMsg deviceProfileUpdateMsg = 8; - DeviceProfileDeleteMsg deviceProfileDeleteMsg = 9; + /* For Tenant, TenantProfile and DeviceProfile */ + EntityUpdateMsg entityUpdateMsg = 8; + EntityDeleteMsg entityDeleteMsg = 9; ProvisionDeviceResponseMsg provisionResponse = 10; } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index fab0579bb2..3723b19f6d 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -263,12 +263,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { try { MqttTransportAdaptor payloadAdaptor = deviceSessionCtx.getPayloadAdaptor(); - if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) { - TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); - transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); - } else if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) { + if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) { TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); + } else if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) { + TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) { TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg); transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java similarity index 95% rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportProfileCache.java rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java index ee05e59010..d56c0291b7 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportProfileCache.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportDeviceProfileCache.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import java.util.Optional; -public interface TransportProfileCache { +public interface TransportDeviceProfileCache { DeviceProfile getOrCreate(DeviceProfileId id, ByteString profileBody); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 775a91f720..8f57db3897 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -20,11 +20,13 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.limits.TransportRateLimitType; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; @@ -47,7 +49,7 @@ import java.util.concurrent.ScheduledExecutorService; */ public interface TransportService { - GetTenantRoutingInfoResponseMsg getRoutingInfo(GetTenantRoutingInfoRequestMsg msg); + GetEntityProfileResponseMsg getRoutingInfo(GetEntityProfileRequestMsg msg); void process(DeviceTransportType transportType, ValidateDeviceTokenRequestMsg msg, TransportServiceCallback callback); @@ -64,12 +66,12 @@ public interface TransportService { void process(ProvisionDeviceRequestMsg msg, TransportServiceCallback callback); - void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback callback); - void onProfileUpdate(DeviceProfile deviceProfile); boolean checkLimits(SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback); + boolean checkLimits(SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback, int dataPoints, TransportRateLimitType... limits); + void process(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback callback); void process(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback callback); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java new file mode 100644 index 0000000000..1f6b31a356 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportTenantProfileCache.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport; + +import com.google.protobuf.ByteString; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; +import org.thingsboard.server.queue.discovery.TenantRoutingInfo; +import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; + +import java.util.Set; + +public interface TransportTenantProfileCache { + + TenantProfile get(TenantId tenantId); + + TenantProfileUpdateResult put(ByteString profileBody); + + boolean put(TenantId tenantId, TenantProfileId profileId); + + Set remove(TenantProfileId profileId); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitFactory.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitFactory.java new file mode 100644 index 0000000000..362502bf17 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitFactory.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.thingsboard.server.common.msg.tools.TbRateLimits; + +@Slf4j +@Component +public class DefaultTransportRateLimitFactory implements TransportRateLimitFactory { + + private static final DummyTransportRateLimit ALWAYS_TRUE = new DummyTransportRateLimit(); + + @Override + public TransportRateLimit create(TransportRateLimitType type, Object configuration) { + if (!StringUtils.isEmpty(configuration)) { + try { + return new SimpleTransportRateLimit(new TbRateLimits(configuration.toString()), configuration.toString()); + } catch (Exception e) { + log.warn("[{}] Failed to init rate limit with configuration: {}", type, configuration, e); + return ALWAYS_TRUE; + } + } else { + return ALWAYS_TRUE; + } + } + + @Override + public TransportRateLimit createDefault(TransportRateLimitType type) { + return ALWAYS_TRUE; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java new file mode 100644 index 0000000000..569f5446db --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java @@ -0,0 +1,133 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.TenantProfileData; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Service +@TbTransportComponent +@Slf4j +public class DefaultTransportRateLimitService implements TransportRateLimitService { + + private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>(); + + private final TransportRateLimitFactory rateLimitFactory; + private final TransportTenantProfileCache tenantProfileCache; + + public DefaultTransportRateLimitService(TransportRateLimitFactory rateLimitFactory, TransportTenantProfileCache tenantProfileCache) { + this.rateLimitFactory = rateLimitFactory; + this.tenantProfileCache = tenantProfileCache; + } + + @Override + public TransportRateLimitType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints, TransportRateLimitType... limits) { + TransportRateLimit[] tenantLimits = getTenantRateLimits(tenantId); + TransportRateLimit[] deviceLimits = getDeviceRateLimits(tenantId, deviceId); + for (TransportRateLimitType limitType : limits) { + TransportRateLimit rateLimit; + if (limitType.isTenantLevel()) { + rateLimit = tenantLimits[limitType.ordinal()]; + } else { + rateLimit = deviceLimits[limitType.ordinal()]; + } + if (!rateLimit.tryConsume(limitType.isMessageLevel() ? 1L : dataPoints)) { + return limitType; + } + } + return null; + } + + @Override + public void update(TenantProfileUpdateResult update) { + TransportRateLimit[] newLimits = createTransportRateLimits(update.getProfile()); + for (TenantId tenantId : update.getAffectedTenants()) { + mergeLimits(tenantId, newLimits); + } + } + + @Override + public void update(TenantId tenantId) { + mergeLimits(tenantId, fetchProfileAndInit(tenantId)); + } + + @Override + public void remove(TenantId tenantId) { + perTenantLimits.remove(tenantId); + } + + @Override + public void remove(DeviceId deviceId) { + perDeviceLimits.remove(deviceId); + } + + private void mergeLimits(TenantId tenantId, TransportRateLimit[] newRateLimits) { + TransportRateLimit[] oldRateLimits = perTenantLimits.get(tenantId); + if (oldRateLimits == null) { + perTenantLimits.put(tenantId, newRateLimits); + } else { + for (int i = 0; i < TransportRateLimitType.values().length; i++) { + TransportRateLimit newLimit = newRateLimits[i]; + TransportRateLimit oldLimit = oldRateLimits[i]; + if (newLimit != null && (oldLimit == null || !oldLimit.getConfiguration().equals(newLimit.getConfiguration()))) { + oldRateLimits[i] = newLimit; + } + } + } + } + + private TransportRateLimit[] fetchProfileAndInit(TenantId tenantId) { + return perTenantLimits.computeIfAbsent(tenantId, tmp -> createTransportRateLimits(tenantProfileCache.get(tenantId))); + } + + private TransportRateLimit[] createTransportRateLimits(TenantProfile tenantProfile) { + TenantProfileData profileData = tenantProfile.getProfileData(); + TransportRateLimit[] rateLimits = new TransportRateLimit[TransportRateLimitType.values().length]; + for (TransportRateLimitType type : TransportRateLimitType.values()) { + rateLimits[type.ordinal()] = rateLimitFactory.create(type, profileData.getProperties().get(type.getConfigurationKey())); + } + return rateLimits; + } + + private TransportRateLimit[] getTenantRateLimits(TenantId tenantId) { + TransportRateLimit[] limits = perTenantLimits.get(tenantId); + if (limits == null) { + limits = fetchProfileAndInit(tenantId); + perTenantLimits.put(tenantId, limits); + } + return limits; + } + + private TransportRateLimit[] getDeviceRateLimits(TenantId tenantId, DeviceId deviceId) { + TransportRateLimit[] limits = perDeviceLimits.get(deviceId); + if (limits == null) { + limits = fetchProfileAndInit(tenantId); + perDeviceLimits.put(deviceId, limits); + } + return limits; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java new file mode 100644 index 0000000000..a93487632f --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DummyTransportRateLimit.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public class DummyTransportRateLimit implements TransportRateLimit { + + @Override + public String getConfiguration() { + return ""; + } + + @Override + public boolean tryConsume(long number) { + return true; + } + + @Override + public boolean tryConsume() { + return true; + } + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java new file mode 100644 index 0000000000..3253272ded --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/SimpleTransportRateLimit.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.thingsboard.server.common.msg.tools.TbRateLimits; + +@RequiredArgsConstructor +public class SimpleTransportRateLimit implements TransportRateLimit { + + private final TbRateLimits rateLimit; + @Getter + private final String configuration; + + @Override + public boolean tryConsume() { + return rateLimit.tryConsume(); + } + + @Override + public boolean tryConsume(long number) { + return number <= 0 || rateLimit.tryConsume(number); + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java new file mode 100644 index 0000000000..a2eea81d3a --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimit.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public interface TransportRateLimit { + + String getConfiguration(); + + boolean tryConsume(); + + boolean tryConsume(long number); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitFactory.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitFactory.java new file mode 100644 index 0000000000..54d83610c2 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitFactory.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public interface TransportRateLimitFactory { + + TransportRateLimit create(TransportRateLimitType type, Object config); + + TransportRateLimit createDefault(TransportRateLimitType type); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java new file mode 100644 index 0000000000..a97fbfc61d --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitService.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; + +public interface TransportRateLimitService { + + TransportRateLimitType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints, TransportRateLimitType... limits); + + void update(TenantProfileUpdateResult update); + + void update(TenantId tenantId); + + void remove(TenantId tenantId); + + void remove(DeviceId deviceId); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitType.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitType.java new file mode 100644 index 0000000000..a3e6da6683 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/TransportRateLimitType.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.Getter; + +public enum TransportRateLimitType { + + TENANT_MAX_MSGS("transport.tenant.msg", true, true), + TENANT_TELEMETRY_MSGS("transport.tenant.telemetry", true, true), + TENANT_MAX_DATA_POINTS("transport.tenant.dataPoints", true, false), + DEVICE_MAX_MSGS("transport.device.msg", false, true), + DEVICE_TELEMETRY_MSGS("transport.device.telemetry", false, true), + DEVICE_MAX_DATA_POINTS("transport.device.dataPoints", false, false); + + @Getter + private final String configurationKey; + @Getter + private final boolean tenantLevel; + @Getter + private final boolean deviceLevel; + @Getter + private final boolean messageLevel; + @Getter + private final boolean dataPointLevel; + + TransportRateLimitType(String configurationKey, boolean tenantLevel, boolean messageLevel) { + this.configurationKey = configurationKey; + this.tenantLevel = tenantLevel; + this.deviceLevel = !tenantLevel; + this.messageLevel = messageLevel; + this.dataPointLevel = !messageLevel; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java new file mode 100644 index 0000000000..53950e6de8 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/profile/TenantProfileUpdateResult.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.Set; + +@Data +public class TenantProfileUpdateResult { + + private final TenantProfile profile; + private final Set affectedTenants; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java similarity index 87% rename from common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportProfileCache.java rename to common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java index 4d955de70c..b12aab1a8c 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportProfileCache.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java @@ -21,8 +21,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.transport.TransportProfileCache; +import org.thingsboard.server.common.transport.TransportDeviceProfileCache; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.queue.util.TbTransportComponent; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -30,14 +31,14 @@ import java.util.concurrent.ConcurrentMap; @Slf4j @Component -@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'") -public class DefaultTransportProfileCache implements TransportProfileCache { +@TbTransportComponent +public class DefaultTransportDeviceProfileCache implements TransportDeviceProfileCache { private final ConcurrentMap deviceProfiles = new ConcurrentHashMap<>(); private final DataDecodingEncodingService dataDecodingEncodingService; - public DefaultTransportProfileCache(DataDecodingEncodingService dataDecodingEncodingService) { + public DefaultTransportDeviceProfileCache(DataDecodingEncodingService dataDecodingEncodingService) { this.dataDecodingEncodingService = dataDecodingEncodingService; } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 47abc07286..3f37685383 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -29,25 +29,35 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.session.SessionMsgType; -import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; +import org.thingsboard.server.common.stats.MessagesStats; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.common.transport.SessionMsgListener; -import org.thingsboard.server.common.transport.TransportProfileCache; +import org.thingsboard.server.common.transport.TransportDeviceProfileCache; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.limits.TransportRateLimit; +import org.thingsboard.server.common.transport.limits.TransportRateLimitService; +import org.thingsboard.server.common.transport.limits.TransportRateLimitType; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; @@ -69,15 +79,14 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbTransportQueueFactory; -import org.thingsboard.server.common.stats.MessagesStats; -import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.queue.util.TbTransportComponent; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -95,15 +104,9 @@ import java.util.concurrent.atomic.AtomicInteger; */ @Slf4j @Service -@ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'") +@TbTransportComponent public class DefaultTransportService implements TransportService { - @Value("${transport.rate_limits.enabled}") - private boolean rateLimitEnabled; - @Value("${transport.rate_limits.tenant}") - private String perTenantLimitsConf; - @Value("${transport.rate_limits.device}") - private String perDevicesLimitsConf; @Value("${transport.sessions.inactivity_timeout}") private long sessionInactivityTimeout; @Value("${transport.sessions.report_timeout}") @@ -119,7 +122,10 @@ public class DefaultTransportService implements TransportService { private final PartitionService partitionService; private final TbServiceInfoProvider serviceInfoProvider; private final StatsFactory statsFactory; - private final TransportProfileCache transportProfileCache; + private final TransportDeviceProfileCache deviceProfileCache; + private final TransportTenantProfileCache tenantProfileCache; + private final TransportRateLimitService rateLimitService; + private final DataDecodingEncodingService dataDecodingEncodingService; protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate; protected TbQueueProducer> ruleEngineMsgProducer; @@ -132,14 +138,11 @@ public class DefaultTransportService implements TransportService { protected ScheduledExecutorService schedulerExecutor; protected ExecutorService transportCallbackExecutor; + private ExecutorService mainConsumerExecutor; private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final Map toServerRpcPendingMap = new ConcurrentHashMap<>(); - //TODO 3.2: @ybondarenko Implement cleanup of this maps. - private final ConcurrentMap perTenantLimits = new ConcurrentHashMap<>(); - private final ConcurrentMap perDeviceLimits = new ConcurrentHashMap<>(); - private ExecutorService mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); private volatile boolean stopped = false; public DefaultTransportService(TbServiceInfoProvider serviceInfoProvider, @@ -147,22 +150,22 @@ public class DefaultTransportService implements TransportService { TbQueueProducerProvider producerProvider, PartitionService partitionService, StatsFactory statsFactory, - TransportProfileCache transportProfileCache) { + TransportDeviceProfileCache deviceProfileCache, + TransportTenantProfileCache tenantProfileCache, + TransportRateLimitService rateLimitService, DataDecodingEncodingService dataDecodingEncodingService) { this.serviceInfoProvider = serviceInfoProvider; this.queueProvider = queueProvider; this.producerProvider = producerProvider; this.partitionService = partitionService; this.statsFactory = statsFactory; - this.transportProfileCache = transportProfileCache; + this.deviceProfileCache = deviceProfileCache; + this.tenantProfileCache = tenantProfileCache; + this.rateLimitService = rateLimitService; + this.dataDecodingEncodingService = dataDecodingEncodingService; } @PostConstruct public void init() { - if (rateLimitEnabled) { - //Just checking the configuration parameters - new TbRateLimits(perTenantLimitsConf); - new TbRateLimits(perDevicesLimitsConf); - } this.ruleEngineProducerStats = statsFactory.createMessagesStats(StatsType.RULE_ENGINE.getName() + ".producer"); this.tbCoreProducerStats = statsFactory.createMessagesStats(StatsType.CORE.getName() + ".producer"); this.transportApiStats = statsFactory.createMessagesStats(StatsType.TRANSPORT.getName() + ".producer"); @@ -177,6 +180,7 @@ public class DefaultTransportService implements TransportService { TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceInfoProvider.getServiceId()); transportNotificationsConsumer.subscribe(Collections.singleton(tpi)); transportApiRequestTemplate.init(); + mainConsumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("transport-consumer")); mainConsumerExecutor.execute(() -> { while (!stopped) { try { @@ -208,10 +212,6 @@ public class DefaultTransportService implements TransportService { @PreDestroy public void destroy() { - if (rateLimitEnabled) { - perTenantLimits.clear(); - perDeviceLimits.clear(); - } stopped = true; if (transportNotificationsConsumer != null) { @@ -232,7 +232,7 @@ public class DefaultTransportService implements TransportService { } @Override - public ScheduledExecutorService getSchedulerExecutor(){ + public ScheduledExecutorService getSchedulerExecutor() { return this.schedulerExecutor; } @@ -242,12 +242,12 @@ public class DefaultTransportService implements TransportService { } @Override - public TransportProtos.GetTenantRoutingInfoResponseMsg getRoutingInfo(TransportProtos.GetTenantRoutingInfoRequestMsg msg) { + public TransportProtos.GetEntityProfileResponseMsg getRoutingInfo(TransportProtos.GetEntityProfileRequestMsg msg) { TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetTenantRoutingInfoRequestMsg(msg).build()); + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setEntityProfileRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); - return response.getValue().getGetTenantRoutingInfoResponseMsg(); + return response.getValue().getEntityProfileResponseMsg(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -289,7 +289,7 @@ public class DefaultTransportService implements TransportService { result.deviceInfo(tdi); ByteString profileBody = msg.getProfileBody(); if (profileBody != null && !profileBody.isEmpty()) { - DeviceProfile profile = transportProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody); + DeviceProfile profile = deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody); if (transportType != DeviceTransportType.DEFAULT && profile != null && profile.getTransportType() != DeviceTransportType.DEFAULT && profile.getTransportType() != transportType) { log.debug("[{}] Device profile [{}] has different transport type: {}, expected: {}", tdi.getDeviceId(), tdi.getDeviceProfileId(), profile.getTransportType(), transportType); @@ -315,7 +315,7 @@ public class DefaultTransportService implements TransportService { result.deviceInfo(tdi); ByteString profileBody = msg.getProfileBody(); if (profileBody != null && !profileBody.isEmpty()) { - result.deviceProfile(transportProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); + result.deviceProfile(deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); } } return result.build(); @@ -339,8 +339,8 @@ public class DefaultTransportService implements TransportService { log.trace("Processing msg: {}", requestMsg); TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setProvisionDeviceRequestMsg(requestMsg).build()); ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> - tmp.getValue().getProvisionDeviceResponseMsg() - , MoreExecutors.directExecutor()); + tmp.getValue().getProvisionDeviceResponseMsg() + , MoreExecutors.directExecutor()); AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } @@ -364,7 +364,11 @@ public class DefaultTransportService implements TransportService { @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostTelemetryMsg msg, TransportServiceCallback callback) { - if (checkLimits(sessionInfo, msg, callback)) { + int dataPoints = 0; + for (TransportProtos.TsKvListProto tsKv : msg.getTsKvListList()) { + dataPoints += tsKv.getKvCount(); + } + if (checkLimits(sessionInfo, msg, callback, dataPoints, TELEMETRY)) { reportActivityInternal(sessionInfo); TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); @@ -385,7 +389,7 @@ public class DefaultTransportService implements TransportService { @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.PostAttributeMsg msg, TransportServiceCallback callback) { - if (checkLimits(sessionInfo, msg, callback)) { + if (checkLimits(sessionInfo, msg, callback, msg.getKvCount(), TELEMETRY)) { reportActivityInternal(sessionInfo); TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); @@ -575,38 +579,34 @@ public class DefaultTransportService implements TransportService { sessions.remove(toSessionId(sessionInfo)); } + private TransportRateLimitType[] DEFAULT = new TransportRateLimitType[]{TransportRateLimitType.TENANT_MAX_MSGS, TransportRateLimitType.DEVICE_MAX_MSGS}; + private TransportRateLimitType[] TELEMETRY = TransportRateLimitType.values(); + @Override public boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback) { + return checkLimits(sessionInfo, msg, callback, 0, DEFAULT); + } + + @Override + public boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback, int dataPoints, TransportRateLimitType... limits) { if (log.isTraceEnabled()) { log.trace("[{}] Processing msg: {}", toSessionId(sessionInfo), msg); } - if (!rateLimitEnabled) { - return true; - } TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); - TbRateLimits rateLimits = perTenantLimits.computeIfAbsent(tenantId, id -> new TbRateLimits(perTenantLimitsConf)); - if (!rateLimits.tryConsume()) { - if (callback != null) { - callback.onError(new TbRateLimitsException(EntityType.TENANT)); - } - if (log.isTraceEnabled()) { - log.trace("[{}][{}] Tenant level rate limit detected: {}", toSessionId(sessionInfo), tenantId, msg); - } - return false; - } DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); - rateLimits = perDeviceLimits.computeIfAbsent(deviceId, id -> new TbRateLimits(perDevicesLimitsConf)); - if (!rateLimits.tryConsume()) { + + TransportRateLimitType limit = rateLimitService.checkLimits(tenantId, deviceId, 0, limits); + if (limit == null) { + return true; + } else { if (callback != null) { - callback.onError(new TbRateLimitsException(EntityType.DEVICE)); + callback.onError(new TbRateLimitsException(limit.isTenantLevel() ? EntityType.TENANT : EntityType.DEVICE)); } if (log.isTraceEnabled()) { - log.trace("[{}][{}] Device level rate limit detected: {}", toSessionId(sessionInfo), deviceId, msg); + log.trace("[{}][{}] {} rateLimit detected: {}", toSessionId(sessionInfo), tenantId, limit, msg); } return false; } - - return true; } protected void processToTransportMsg(TransportProtos.ToTransportMsg toSessionMsg) { @@ -637,16 +637,40 @@ public class DefaultTransportService implements TransportService { deregisterSession(md.getSessionInfo()); } } else { - if (toSessionMsg.hasDeviceProfileUpdateMsg()) { - DeviceProfile deviceProfile = transportProfileCache.put(toSessionMsg.getDeviceProfileUpdateMsg().getData()); - if (deviceProfile != null) { - onProfileUpdate(deviceProfile); + if (toSessionMsg.hasEntityUpdateMsg()) { + TransportProtos.EntityUpdateMsg msg = toSessionMsg.getEntityUpdateMsg(); + EntityType entityType = EntityType.valueOf(msg.getEntityType()); + if (EntityType.DEVICE_PROFILE.equals(entityType)) { + DeviceProfile deviceProfile = deviceProfileCache.put(msg.getData()); + if (deviceProfile != null) { + onProfileUpdate(deviceProfile); + } + } else if (EntityType.TENANT_PROFILE.equals(entityType)) { + TenantProfileUpdateResult update = tenantProfileCache.put(msg.getData()); + rateLimitService.update(update); + } else if (EntityType.TENANT.equals(entityType)) { + Optional profileOpt = dataDecodingEncodingService.decode(msg.getData().toByteArray()); + if (profileOpt.isPresent()) { + Tenant tenant = profileOpt.get(); + boolean updated = tenantProfileCache.put(tenant.getId(), tenant.getTenantProfileId()); + if (updated) { + rateLimitService.update(tenant.getId()); + } + } + } + } else if (toSessionMsg.hasEntityDeleteMsg()) { + TransportProtos.EntityDeleteMsg msg = toSessionMsg.getEntityDeleteMsg(); + EntityType entityType = EntityType.valueOf(msg.getEntityType()); + UUID entityUuid = new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB()); + if (EntityType.DEVICE_PROFILE.equals(entityType)) { + deviceProfileCache.evict(new DeviceProfileId(new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB()))); + } else if (EntityType.TENANT_PROFILE.equals(entityType)) { + tenantProfileCache.remove(new TenantProfileId(entityUuid)); + } else if (EntityType.TENANT.equals(entityType)) { + rateLimitService.remove(new TenantId(entityUuid)); + } else if (EntityType.DEVICE.equals(entityType)) { + rateLimitService.remove(new DeviceId(entityUuid)); } - } else if (toSessionMsg.hasDeviceProfileDeleteMsg()) { - transportProfileCache.evict(new DeviceProfileId(new UUID( - toSessionMsg.getDeviceProfileDeleteMsg().getProfileIdMSB(), - toSessionMsg.getDeviceProfileDeleteMsg().getProfileIdLSB() - ))); } else { //TODO: should we notify the device actor about missed session? log.debug("[{}] Missing session.", sessionId); @@ -654,38 +678,6 @@ public class DefaultTransportService implements TransportService { } } - @Override - public void getDeviceProfile(DeviceProfileId deviceProfileId, TransportServiceCallback callback) { - DeviceProfile deviceProfile = transportProfileCache.get(deviceProfileId); - if (deviceProfile != null) { - callback.onSuccess(deviceProfile); - } else { - log.trace("Processing device profile request: [{}]", deviceProfileId); - TransportProtos.GetDeviceProfileRequestMsg msg = TransportProtos.GetDeviceProfileRequestMsg.newBuilder() - .setProfileIdMSB(deviceProfileId.getId().getMostSignificantBits()) - .setProfileIdLSB(deviceProfileId.getId().getLeastSignificantBits()) - .build(); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), - TransportApiRequestMsg.newBuilder().setGetDeviceProfileRequestMsg(msg).build()); - AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), - response -> { - ByteString devProfileBody = response.getValue().getGetDeviceProfileResponseMsg().getData(); - if (devProfileBody != null && !devProfileBody.isEmpty()) { - DeviceProfile profile = transportProfileCache.put(devProfileBody); - if (profile != null) { - callback.onSuccess(profile); - } else { - log.warn("Failed to decode device profile: {}", devProfileBody); - callback.onError(new IllegalArgumentException("Failed to decode device profile!")); - } - } else { - log.warn("Failed to find device profile: [{}]", deviceProfileId); - callback.onError(new IllegalArgumentException("Failed to find device profile!")); - } - }, callback::onError, transportCallbackExecutor); - } - } - @Override public void onProfileUpdate(DeviceProfile deviceProfile) { long deviceProfileIdMSB = deviceProfile.getId().getId().getMostSignificantBits(); @@ -750,7 +742,7 @@ public class DefaultTransportService implements TransportService { private RuleChainId resolveRuleChainId(TransportProtos.SessionInfoProto sessionInfo) { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); - DeviceProfile deviceProfile = transportProfileCache.get(deviceProfileId); + DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); RuleChainId ruleChainId; if (deviceProfile == null) { log.warn("[{}] Device profile is null!", deviceProfileId); @@ -779,7 +771,7 @@ public class DefaultTransportService implements TransportService { } } - private class StatsCallback implements TbQueueCallback { + private static class StatsCallback implements TbQueueCallback { private final TbQueueCallback callback; private final MessagesStats stats; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java new file mode 100644 index 0000000000..784f7b3ae8 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportTenantProfileCache.java @@ -0,0 +1,155 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.service; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; +import org.thingsboard.server.common.transport.profile.TenantProfileUpdateResult; +import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.discovery.TenantRoutingInfo; +import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Component +@TbTransportComponent +@Slf4j +public class DefaultTransportTenantProfileCache implements TransportTenantProfileCache { + + private final Lock tenantProfileFetchLock = new ReentrantLock(); + private final ConcurrentMap profiles = new ConcurrentHashMap<>(); + private final ConcurrentMap tenantIds = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantProfileIds = new ConcurrentHashMap<>(); + private final DataDecodingEncodingService dataDecodingEncodingService; + + private TransportService transportService; + + @Lazy + @Autowired + public void setTransportService(TransportService transportService) { + this.transportService = transportService; + } + + public DefaultTransportTenantProfileCache(DataDecodingEncodingService dataDecodingEncodingService) { + this.dataDecodingEncodingService = dataDecodingEncodingService; + } + + @Override + public TenantProfile get(TenantId tenantId) { + return getTenantProfile(tenantId); + } + + @Override + public TenantProfileUpdateResult put(ByteString profileBody) { + Optional profileOpt = dataDecodingEncodingService.decode(profileBody.toByteArray()); + if (profileOpt.isPresent()) { + TenantProfile newProfile = profileOpt.get(); + log.trace("[{}] put: {}", newProfile.getId(), newProfile); + return new TenantProfileUpdateResult(newProfile, tenantProfileIds.get(newProfile.getId())); + } else { + log.warn("Failed to decode profile: {}", profileBody.toString()); + return new TenantProfileUpdateResult(null, Collections.emptySet()); + } + } + + @Override + public boolean put(TenantId tenantId, TenantProfileId profileId) { + log.trace("[{}] put: {}", tenantId, profileId); + TenantProfileId oldProfileId = tenantIds.get(tenantId); + if (oldProfileId != null && !oldProfileId.equals(profileId)) { + tenantProfileIds.computeIfAbsent(oldProfileId, id -> ConcurrentHashMap.newKeySet()).remove(tenantId); + tenantIds.put(tenantId, profileId); + tenantProfileIds.computeIfAbsent(profileId, id -> ConcurrentHashMap.newKeySet()).add(tenantId); + return true; + } else { + return false; + } + } + + @Override + public Set remove(TenantProfileId profileId) { + Set tenants = tenantProfileIds.remove(profileId); + if (tenants != null) { + tenants.forEach(tenantIds::remove); + } + profiles.remove(profileId); + return tenants; + } + + private TenantProfile getTenantProfile(TenantId tenantId) { + TenantProfile profile = null; + TenantProfileId tenantProfileId = tenantIds.get(tenantId); + if (tenantProfileId != null) { + profile = profiles.get(tenantProfileId); + } + if (profile == null) { + tenantProfileFetchLock.lock(); + try { + tenantProfileId = tenantIds.get(tenantId); + if (tenantProfileId != null) { + profile = profiles.get(tenantProfileId); + } + if (profile == null) { + TransportProtos.GetEntityProfileRequestMsg msg = TransportProtos.GetEntityProfileRequestMsg.newBuilder() + .setEntityType(EntityType.TENANT.name()) + .setEntityIdMSB(tenantId.getId().getMostSignificantBits()) + .setEntityIdLSB(tenantId.getId().getLeastSignificantBits()) + .build(); + TransportProtos.GetEntityProfileResponseMsg routingInfo = transportService.getRoutingInfo(msg); + Optional profileOpt = dataDecodingEncodingService.decode(routingInfo.getData().toByteArray()); + if (profileOpt.isPresent()) { + profile = profileOpt.get(); + TenantProfile existingProfile = profiles.get(profile.getId()); + if (existingProfile != null) { + profile = existingProfile; + } else { + profiles.put(profile.getId(), profile); + } + tenantProfileIds.computeIfAbsent(profile.getId(), id -> ConcurrentHashMap.newKeySet()).add(tenantId); + tenantIds.put(tenantId, profile.getId()); + } else { + log.warn("[{}] Can't decode tenant profile: {}", tenantId, routingInfo.getData()); + throw new RuntimeException("Can't decode tenant profile!"); + } + } + } finally { + tenantProfileFetchLock.unlock(); + } + } + return profile; + } + + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java index f2534a64c9..55dee282ec 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/TransportTenantRoutingInfoService.java @@ -16,14 +16,11 @@ package org.thingsboard.server.common.transport.service; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; +import org.thingsboard.server.common.transport.TransportTenantProfileCache; import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @@ -32,21 +29,16 @@ import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; @ConditionalOnExpression("'${service.type:null}'=='tb-transport'") public class TransportTenantRoutingInfoService implements TenantRoutingInfoService { - private TransportService transportService; + private TransportTenantProfileCache tenantProfileCache; - @Lazy - @Autowired - public void setTransportService(TransportService transportService) { - this.transportService = transportService; + public TransportTenantRoutingInfoService(TransportTenantProfileCache tenantProfileCache) { + this.tenantProfileCache = tenantProfileCache; } @Override public TenantRoutingInfo getRoutingInfo(TenantId tenantId) { - GetTenantRoutingInfoRequestMsg msg = GetTenantRoutingInfoRequestMsg.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .build(); - GetTenantRoutingInfoResponseMsg routingInfo = transportService.getRoutingInfo(msg); - return new TenantRoutingInfo(tenantId, routingInfo.getIsolatedTbCore(), routingInfo.getIsolatedTbRuleEngine()); + TenantProfile profile = tenantProfileCache.get(tenantId); + return new TenantRoutingInfo(tenantId, profile.isIsolatedTbCore(), profile.isIsolatedTbRuleEngine()); } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java index 1b10cb5dc3..bf70fa5ef1 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/DataDecodingEncodingService.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.common.transport.util; -import org.thingsboard.server.common.msg.TbActorMsg; - import java.util.Optional; public interface DataDecodingEncodingService { diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java index a96a0d36c8..00b4f7e941 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -32,19 +32,19 @@ public class OAuth2Utils { } public static OAuth2ClientsParams toOAuth2Params(List extendedOAuth2ClientRegistrationInfos) { - Map> domainsByInfoId = new HashMap<>(); - Map infoById = new HashMap<>(); + Map> domainsByInfoId = new LinkedHashMap<>(); + Map infoById = new LinkedHashMap<>(); for (ExtendedOAuth2ClientRegistrationInfo extendedClientRegistrationInfo : extendedOAuth2ClientRegistrationInfos) { String domainName = extendedClientRegistrationInfo.getDomainName(); SchemeType domainScheme = extendedClientRegistrationInfo.getDomainScheme(); - domainsByInfoId.computeIfAbsent(extendedClientRegistrationInfo.getId(), key -> new HashSet<>()) + domainsByInfoId.computeIfAbsent(extendedClientRegistrationInfo.getId(), key -> new ArrayList<>()) .add(new DomainInfo(domainScheme, domainName)); infoById.put(extendedClientRegistrationInfo.getId(), extendedClientRegistrationInfo); } - Map, OAuth2ClientsDomainParams> domainParamsMap = new HashMap<>(); + Map, OAuth2ClientsDomainParams> domainParamsMap = new LinkedHashMap<>(); domainsByInfoId.forEach((clientRegistrationInfoId, domainInfos) -> { domainParamsMap.computeIfAbsent(domainInfos, - key -> new OAuth2ClientsDomainParams(key, new HashSet<>()) + key -> new OAuth2ClientsDomainParams(key, new ArrayList<>()) ) .getClientRegistrations() .add(toClientRegistrationDto(infoById.get(clientRegistrationInfoId))); @@ -52,7 +52,7 @@ public class OAuth2Utils { boolean enabled = extendedOAuth2ClientRegistrationInfos.stream() .map(OAuth2ClientRegistrationInfo::isEnabled) .findFirst().orElse(false); - return new OAuth2ClientsParams(enabled, new HashSet<>(domainParamsMap.values())); + return new OAuth2ClientsParams(enabled, new ArrayList<>(domainParamsMap.values())); } public static ClientRegistrationDto toClientRegistrationDto(OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java index b54505a3d7..a927f1e78c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.service; +import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.junit.After; import org.junit.Assert; @@ -29,7 +30,7 @@ import java.util.*; import java.util.stream.Collectors; public class BaseOAuth2ServiceTest extends AbstractServiceTest { - private static final OAuth2ClientsParams EMPTY_PARAMS = new OAuth2ClientsParams(false, new HashSet<>()); + private static final OAuth2ClientsParams EMPTY_PARAMS = new OAuth2ClientsParams(false, new ArrayList<>()); @Autowired protected OAuth2Service oAuth2Service; @@ -48,14 +49,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testSaveHttpAndMixedDomainsTogether() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto() @@ -67,14 +68,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testSaveHttpsAndMixedDomainsTogether() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(), DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto() @@ -131,20 +132,20 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { Assert.assertNotNull(foundClientsParams); Assert.assertEquals(clientsParams, foundClientsParams); - OAuth2ClientsParams newClientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams newClientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto() )) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("test-domain").scheme(SchemeType.MIXED).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto() )) .build() @@ -157,22 +158,22 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test public void testGetOAuth2Clients() { - Set firstGroup = Sets.newHashSet( + List firstGroup = Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto() ); - Set secondGroup = Sets.newHashSet( + List secondGroup = Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto() ); - Set thirdGroup = Sets.newHashSet( + List thirdGroup = Lists.newArrayList( validClientRegistrationDto() ); - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() @@ -180,14 +181,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { .clientRegistrations(firstGroup) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) .clientRegistrations(secondGroup) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() )) @@ -285,15 +286,15 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test public void testGetOAuth2ClientsForHttpAndHttps() { - Set firstGroup = Sets.newHashSet( + List firstGroup = Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto() ); - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build() @@ -335,25 +336,25 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test public void testGetDisabledOAuth2Clients() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto() )) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto() )) @@ -374,35 +375,35 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test public void testFindAllClientRegistrationInfos() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto() )) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto() )) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto() )) .build() @@ -423,35 +424,35 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test public void testFindClientRegistrationById() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Sets.newHashSet( + OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto() )) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto() )) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto() )) .build() @@ -466,14 +467,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { } private OAuth2ClientsParams createDefaultClientsParams() { - return new OAuth2ClientsParams(true, Sets.newHashSet( + return new OAuth2ClientsParams(true, Lists.newArrayList( OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto(), validClientRegistrationDto(), @@ -481,11 +482,11 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { )) .build(), OAuth2ClientsDomainParams.builder() - .domainInfos(Sets.newHashSet( + .domainInfos(Lists.newArrayList( DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) - .clientRegistrations(Sets.newHashSet( + .clientRegistrations(Lists.newArrayList( validClientRegistrationDto(), validClientRegistrationDto() )) diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index aef2cad684..289f73dc52 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -19,18 +19,40 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; -import io.netty.channel.*; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.mqtt.*; +import io.netty.handler.codec.mqtt.MqttDecoder; +import io.netty.handler.codec.mqtt.MqttEncoder; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.codec.mqtt.MqttSubscribeMessage; +import io.netty.handler.codec.mqtt.MqttSubscribePayload; +import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; +import io.netty.handler.codec.mqtt.MqttUnsubscribePayload; import io.netty.handler.ssl.SslContext; import io.netty.handler.timeout.IdleStateHandler; -import io.netty.util.collection.IntObjectHashMap; import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -41,11 +63,11 @@ import java.util.concurrent.atomic.AtomicInteger; final class MqttClientImpl implements MqttClient { private final Set serverSubscriptions = new HashSet<>(); - private final IntObjectHashMap pendingServerUnsubscribes = new IntObjectHashMap<>(); - private final IntObjectHashMap qos2PendingIncomingPublishes = new IntObjectHashMap<>(); - private final IntObjectHashMap pendingPublishes = new IntObjectHashMap<>(); + private final ConcurrentMap pendingServerUnsubscribes = new ConcurrentHashMap<>(); + private final ConcurrentMap qos2PendingIncomingPublishes = new ConcurrentHashMap<>(); + private final ConcurrentMap pendingPublishes = new ConcurrentHashMap<>(); private final HashMultimap subscriptions = HashMultimap.create(); - private final IntObjectHashMap pendingSubscriptions = new IntObjectHashMap<>(); + private final ConcurrentMap pendingSubscriptions = new ConcurrentHashMap<>(); private final Set pendingSubscribeTopics = new HashSet<>(); private final HashMultimap handlerToSubscribtion = HashMultimap.create(); private final AtomicInteger nextMessageId = new AtomicInteger(1); @@ -340,6 +362,7 @@ final class MqttClientImpl implements MqttClient { MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId()); MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload); MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.packetId(), future, payload.retain(), message, qos); + this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish); ChannelFuture channelFuture = this.sendAndFlushPacket(message); if (channelFuture != null) { @@ -350,10 +373,12 @@ final class MqttClientImpl implements MqttClient { } } if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) { + this.pendingPublishes.remove(pendingPublish.getMessageId()); pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 } else if (pendingPublish.isSent()) { - this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish); pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); + } else { + this.pendingPublishes.remove(pendingPublish.getMessageId()); } return future; } @@ -465,7 +490,7 @@ final class MqttClientImpl implements MqttClient { } } - IntObjectHashMap getPendingSubscriptions() { + ConcurrentMap getPendingSubscriptions() { return pendingSubscriptions; } @@ -485,15 +510,15 @@ final class MqttClientImpl implements MqttClient { return serverSubscriptions; } - IntObjectHashMap getPendingServerUnsubscribes() { + ConcurrentMap getPendingServerUnsubscribes() { return pendingServerUnsubscribes; } - IntObjectHashMap getPendingPublishes() { + ConcurrentMap getPendingPublishes() { return pendingPublishes; } - IntObjectHashMap getQos2PendingIncomingPublishes() { + ConcurrentMap getQos2PendingIncomingPublishes() { return qos2PendingIncomingPublishes; } diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index d2ba4d43b4..0a92d4b1a8 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -38,10 +38,18 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; @@ -50,6 +58,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -60,28 +69,41 @@ import org.thingsboard.server.common.data.id.AssetId; 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.EntityViewId; +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -325,6 +347,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getAssetInfoById(AssetId assetId) { + try { + ResponseEntity asset = restTemplate.getForEntity(baseURL + "/api/asset/info/{assetId}", AssetInfo.class, assetId.getId()); + return Optional.ofNullable(asset.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public Asset saveAsset(Asset asset) { return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody(); } @@ -390,6 +425,20 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return assets.getBody(); } + public PageData getTenantAssetInfos(PageLink pageLink, String assetType) { + Map params = new HashMap<>(); + params.put("type", assetType); + addPageLinkToParam(params, pageLink); + + ResponseEntity> assets = restTemplate.exchange( + baseURL + "/api/tenant/assetInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return assets.getBody(); + } + public Optional getTenantAsset(String assetName) { try { ResponseEntity asset = restTemplate.getForEntity(baseURL + "/api/tenant/assets?assetName={assetName}", Asset.class, assetName); @@ -419,6 +468,22 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return assets.getBody(); } + public PageData getCustomerAssetInfos(CustomerId customerId, PageLink pageLink, String assetType) { + Map params = new HashMap<>(); + params.put("customerId", customerId.getId().toString()); + params.put("type", assetType); + addPageLinkToParam(params, pageLink); + + ResponseEntity> assets = restTemplate.exchange( + baseURL + "/api/customer/{customerId}/assetInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params); + return assets.getBody(); + } + public List getAssetsByIds(List assetIds) { return restTemplate.exchange( baseURL + "/api/assets?assetIds={assetIds}", @@ -593,11 +658,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public Optional activateUser(UserId userId, String password) { + return activateUser(userId, password, true); + } + + public Optional activateUser(UserId userId, String password, boolean sendActivationMail) { ObjectNode activateRequest = objectMapper.createObjectNode(); activateRequest.put("activateToken", getActivateToken(userId)); activateRequest.put("password", password); try { - ResponseEntity jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate", activateRequest, JsonNode.class); + ResponseEntity jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate?sendActivationMail={sendActivationMail}", activateRequest, JsonNode.class, sendActivationMail); return Optional.ofNullable(jsonNode.getBody()); } catch (HttpClientErrorException exception) { if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { @@ -936,8 +1005,25 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getDeviceInfoById(DeviceId deviceId) { + try { + ResponseEntity device = restTemplate.getForEntity(baseURL + "/api/device/info/{deviceId}", DeviceInfo.class, deviceId); + return Optional.ofNullable(device.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public Device saveDevice(Device device) { - return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); + return saveDevice(device, null); + } + + public Device saveDevice(Device device, String accessToken) { + return restTemplate.postForEntity(baseURL + "/api/device?accessToken={accessToken}", device, Device.class, accessToken).getBody(); } public void deleteDevice(DeviceId deviceId) { @@ -1011,6 +1097,18 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } + public PageData getTenantDeviceInfos(String type, DeviceProfileId deviceProfileId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", type); + params.put("deviceProfileId", deviceProfileId != null ? deviceProfileId.toString() : null); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/deviceInfos?type={type}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public Optional getTenantDevice(String deviceName) { try { ResponseEntity device = restTemplate.getForEntity(baseURL + "/api/tenant/devices?deviceName={deviceName}", Device.class, deviceName); @@ -1036,6 +1134,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } + public PageData getCustomerDeviceInfos(CustomerId customerId, String deviceType, DeviceProfileId deviceProfileId, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId.toString()); + params.put("type", deviceType); + params.put("deviceProfileId", deviceProfileId != null ? deviceProfileId.toString() : null); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/devices?type={type}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public List getDevicesByIds(List deviceIds) { return restTemplate.exchange(baseURL + "/api/devices?deviceIds={deviceIds}", HttpMethod.GET, @@ -1074,6 +1185,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.delete(baseURL + "/api/customer/device/{deviceName}/claim", deviceName); } + public Device assignDeviceToTenant(TenantId tenantId, DeviceId deviceId) { + return restTemplate.postForEntity( + baseURL + "/api/tenant/{tenantId}/device/{deviceId}", + HttpEntity.EMPTY, Device.class, tenantId, deviceId).getBody(); + } + @Deprecated public Device createDevice(String name, String type) { Device device = new Device(); @@ -1138,6 +1255,91 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { customerId.toString(), deviceId.toString()).getBody(); } + public Optional getDeviceProfileById(DeviceProfileId deviceProfileId) { + try { + ResponseEntity deviceProfile = restTemplate.getForEntity(baseURL + "/api/deviceProfile/{deviceProfileId}", DeviceProfile.class, deviceProfileId); + return Optional.ofNullable(deviceProfile.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getDeviceProfileInfoById(DeviceProfileId deviceProfileId) { + try { + ResponseEntity deviceProfileInfo = restTemplate.getForEntity(baseURL + "/api/deviceProfileInfo/{deviceProfileId}", DeviceProfileInfo.class, deviceProfileId); + return Optional.ofNullable(deviceProfileInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public DeviceProfileInfo getDefaultDeviceProfileInfo() { + return restTemplate.getForEntity(baseURL + "/api/deviceProfileInfo/default", DeviceProfileInfo.class).getBody(); + } + + public DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { + return restTemplate.postForEntity(baseURL + "/api/deviceProfile", deviceProfile, DeviceProfile.class).getBody(); + } + + public void deleteDeviceProfile(DeviceProfileId deviceProfileId) { + restTemplate.delete(baseURL + "/api/deviceProfile/{deviceProfileId}", deviceProfileId); + } + + public DeviceProfile setDefaultDeviceProfile(DeviceProfileId deviceProfileId) { + return restTemplate.postForEntity( + baseURL + "/api/deviceProfile/{deviceProfileId}/default", + HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody(); + } + + public PageData getTenantDevices(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/deviceProfiles?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getDeviceProfileInfos(PageLink pageLink, DeviceTransportType deviceTransportType) { + Map params = new HashMap<>(); + params.put("deviceTransportType", deviceTransportType != null ? deviceTransportType.name() : null); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/deviceProfileInfos?deviceTransportType={deviceTransportType}&" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Long countEntitiesByQuery(EntityCountQuery query) { + return restTemplate.postForObject(baseURL + "/api/entitiesQuery/count", query, Long.class); + } + + public PageData findEntityDataByQuery(EntityDataQuery query) { + return restTemplate.exchange( + baseURL + "/api/entitiesQuery/find", + HttpMethod.POST, new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + + public PageData findAlarmDataByQuery(AlarmDataQuery query) { + return restTemplate.exchange( + baseURL + "/api/alarmsQuery/find", + HttpMethod.POST, new HttpEntity<>(query), + new ParameterizedTypeReference>() { + }).getBody(); + } + public void saveRelation(EntityRelation relation) { restTemplate.postForLocation(baseURL + "/api/relation", relation); } @@ -1313,6 +1515,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getEntityViewInfoById(EntityViewId entityViewId) { + try { + ResponseEntity entityView = restTemplate.getForEntity(baseURL + "/api/entityView/info/{entityViewId}", EntityViewInfo.class, entityViewId); + return Optional.ofNullable(entityView.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public EntityView saveEntityView(EntityView entityView) { return restTemplate.postForEntity(baseURL + "/api/entityView", entityView, EntityView.class).getBody(); } @@ -1373,6 +1588,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } + public PageData getCustomerEntityViewInfos(CustomerId customerId, String entityViewType, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("customerId", customerId.toString()); + params.put("type", entityViewType); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/customer/{customerId}/entityViewInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public PageData getTenantEntityViews(String entityViewType, PageLink pageLink) { Map params = new HashMap<>(); params.put("type", entityViewType); @@ -1385,6 +1613,18 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } + public PageData getTenantEntityViewInfos(String entityViewType, PageLink pageLink) { + Map params = new HashMap<>(); + params.put("type", entityViewType); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenant/entityViewInfos?type={type}&" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public List findByQuery(EntityViewSearchQuery query) { return restTemplate.exchange(baseURL + "/api/entityViews", HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference>() { }).getBody(); @@ -1437,8 +1677,41 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }, - params).getBody(); + }, params).getBody(); + } + + public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { + return restTemplate.postForEntity(baseURL + "/api/oauth2/config/template", clientRegistrationTemplate, OAuth2ClientRegistrationTemplate.class).getBody(); + } + + public void deleteClientRegistrationTemplate(OAuth2ClientRegistrationTemplateId oAuth2ClientRegistrationTemplateId) { + restTemplate.delete(baseURL + "/api/oauth2/config/template/{clientRegistrationTemplateId}", oAuth2ClientRegistrationTemplateId); + } + + public List getClientRegistrationTemplates() { + return restTemplate.exchange( + baseURL + "/api/oauth2/config/template", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public List getOAuth2Clients() { + return restTemplate.exchange( + baseURL + "/api/noauth/oauth2Clients", + HttpMethod.POST, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public OAuth2ClientsParams getCurrentOAuth2Params() { + return restTemplate.getForEntity(baseURL + "/api/oauth2/config", OAuth2ClientsParams.class).getBody(); + } + + public OAuth2ClientsParams saveOAuth2Params(OAuth2ClientsParams oauth2Params) { + return restTemplate.postForEntity(baseURL + "/api/oauth2/config", oauth2Params, OAuth2ClientsParams.class).getBody(); } public void handleOneWayDeviceRPCRequest(DeviceId deviceId, JsonNode requestBody) { @@ -1485,6 +1758,10 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/ruleChain", ruleChain, RuleChain.class).getBody(); } + public RuleChain saveRuleChain(DefaultRuleChainCreateRequest request) { + return restTemplate.postForEntity(baseURL + "/api/ruleChain/device/default", request, RuleChain.class).getBody(); + } + public Optional setRootRuleChain(RuleChainId ruleChainId) { try { ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/root", null, RuleChain.class, ruleChainId.getId()); @@ -1544,6 +1821,14 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public RuleChainData exportRuleChains(int limit) { + return restTemplate.getForEntity(baseURL + "/api/ruleChains/export?limit=" + limit, RuleChainData.class).getBody(); + } + + public void importRuleChains(RuleChainData ruleChainData, boolean overwrite) { + restTemplate.postForLocation(baseURL + "/api/ruleChains/import?overwrite=" + overwrite, ruleChainData); + } + public List getAttributeKeys(EntityId entityId) { return restTemplate.exchange( baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/keys/attributes", @@ -1805,6 +2090,19 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } + public Optional getTenantInfoById(TenantId tenantId) { + try { + ResponseEntity tenant = restTemplate.getForEntity(baseURL + "/api/tenant/info/{tenantId}", TenantInfo.class, tenantId); + return Optional.ofNullable(tenant.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public Tenant saveTenant(Tenant tenant) { return restTemplate.postForEntity(baseURL + "/api/tenant", tenant, Tenant.class).getBody(); } @@ -1824,6 +2122,81 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } + public PageData getTenantInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenantInfos?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Optional getTenantProfileById(TenantProfileId tenantProfileId) { + try { + ResponseEntity tenantProfile = restTemplate.getForEntity(baseURL + "/api/tenantProfile/{tenantProfileId}", TenantProfile.class, tenantProfileId); + return Optional.ofNullable(tenantProfile.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional getTenantProfileInfoById(TenantProfileId tenantProfileId) { + try { + ResponseEntity entityInfo = restTemplate.getForEntity(baseURL + "/api/tenantProfileInfo/{tenantProfileId}", EntityInfo.class, tenantProfileId); + return Optional.ofNullable(entityInfo.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public EntityInfo getDefaultTenantProfileInfo() { + return restTemplate.getForEntity(baseURL + "/api/tenantProfileInfo/default", EntityInfo.class).getBody(); + } + + public TenantProfile saveTenantProfile(TenantProfile tenantProfile) { + return restTemplate.postForEntity(baseURL + "/api/tenantProfile", tenantProfile, TenantProfile.class).getBody(); + } + + public void deleteTenantProfile(TenantProfileId tenantProfileId) { + restTemplate.delete(baseURL + "/api/tenantProfile/{tenantProfileId}", tenantProfileId); + } + + public TenantProfile setDefaultTenantProfile(TenantProfileId tenantProfileId) { + return restTemplate.exchange(baseURL + "/api/tenantProfile/{tenantProfileId}/default", HttpMethod.POST, HttpEntity.EMPTY, TenantProfile.class, tenantProfileId).getBody(); + } + + public PageData getTenantProfiles(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenantProfiles" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public PageData getTenantProfileInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/tenantProfileInfos" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public Optional getUserById(UserId userId) { try { ResponseEntity user = restTemplate.getForEntity(baseURL + "/api/user/{userId}", User.class, userId.getId()); @@ -1870,6 +2243,17 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.delete(baseURL + "/api/user/{userId}", userId.getId()); } + public PageData getUsers(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/users" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public PageData getTenantAdmins(TenantId tenantId, PageLink pageLink) { Map params = new HashMap<>(); params.put("tenantId", tenantId.getId().toString()); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java index c398131143..53e2c558f6 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineDeviceProfileCache.java @@ -18,8 +18,11 @@ package org.thingsboard.rule.engine.api; import org.thingsboard.server.common.data.DeviceProfile; 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.TenantId; +import java.util.function.Consumer; + /** * Created by ashvayka on 02.04.18. */ @@ -29,4 +32,8 @@ public interface RuleEngineDeviceProfileCache { DeviceProfile get(TenantId tenantId, DeviceId deviceId); + void addListener(TenantId tenantId, EntityId listenerId, Consumer listener); + + void removeListener(TenantId tenantId, EntityId listenerId); + } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index ab2e341e52..3d45fcf445 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -20,6 +20,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.EntityId; @@ -223,4 +224,8 @@ public interface TbContext { RuleNodeState saveRuleNodeState(RuleNodeState state); void clearRuleNodeStates(); + + void addProfileListener(Consumer listener); + + void removeProfileListener(); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java new file mode 100644 index 0000000000..7d510fe5fe --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.profile; + +public enum AlarmEvalResult { + + FALSE, NOT_YET_TRUE, TRUE; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index d9d08efbdc..8be2b81a59 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -123,17 +123,17 @@ class AlarmRuleState { } } - public boolean eval(DataSnapshot data) { + public AlarmEvalResult eval(DataSnapshot data) { boolean active = isActive(data.getTs()); switch (spec.getType()) { case SIMPLE: - return active && eval(alarmRule.getCondition(), data); + return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; case DURATION: return evalDuration(data, active); case REPEATING: return evalRepeating(data, active); default: - return false; + return AlarmEvalResult.FALSE; } } @@ -203,17 +203,17 @@ class AlarmRuleState { } } - private boolean evalRepeating(DataSnapshot data, boolean active) { + private AlarmEvalResult evalRepeating(DataSnapshot data, boolean active) { if (active && eval(alarmRule.getCondition(), data)) { state.setEventCount(state.getEventCount() + 1); updateFlag = true; - return state.getEventCount() >= requiredRepeats; + return state.getEventCount() >= requiredRepeats ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; } else { - return false; + return AlarmEvalResult.FALSE; } } - private boolean evalDuration(DataSnapshot data, boolean active) { + private AlarmEvalResult evalDuration(DataSnapshot data, boolean active) { if (active && eval(alarmRule.getCondition(), data)) { if (state.getLastEventTs() > 0) { if (data.getTs() > state.getLastEventTs()) { @@ -226,24 +226,28 @@ class AlarmRuleState { state.setDuration(0L); updateFlag = true; } - return state.getDuration() > requiredDurationInMs; + return state.getDuration() > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; } else { - return false; + return AlarmEvalResult.FALSE; } } - public boolean eval(long ts) { + public AlarmEvalResult eval(long ts) { switch (spec.getType()) { case SIMPLE: case REPEATING: - return false; + return AlarmEvalResult.NOT_YET_TRUE; case DURATION: if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) { long duration = state.getDuration() + (ts - state.getLastEventTs()); - return duration > requiredDurationInMs && isActive(ts); + if (isActive(ts)) { + return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; + } else { + return AlarmEvalResult.FALSE; + } } default: - return false; + return AlarmEvalResult.FALSE; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 5fb2c2957c..df46a4bf88 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -72,7 +72,7 @@ class AlarmState { return createOrClearAlarms(ctx, ts, null, AlarmRuleState::eval); } - public boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction evalFunction) { + public boolean createOrClearAlarms(TbContext ctx, T data, SnapshotUpdate update, BiFunction evalFunction) { boolean stateUpdate = false; AlarmSeverity resultSeverity = null; log.debug("[{}] processing update: {}", alarmDefinition.getId(), data); @@ -81,22 +81,28 @@ class AlarmState { log.debug("[{}][{}] Update is not valid for current rule state", alarmDefinition.getId(), state.getSeverity()); continue; } - boolean evalResult = evalFunction.apply(state, data); + AlarmEvalResult evalResult = evalFunction.apply(state, data); stateUpdate |= state.checkUpdate(); - if (evalResult) { + if (AlarmEvalResult.TRUE.equals(evalResult)) { resultSeverity = state.getSeverity(); break; + } else if (AlarmEvalResult.FALSE.equals(evalResult)) { + state.clear(); + stateUpdate |= state.checkUpdate(); } } if (resultSeverity != null) { - pushMsg(ctx, calculateAlarmResult(ctx, resultSeverity)); + TbAlarmResult result = calculateAlarmResult(ctx, resultSeverity); + if (result != null) { + pushMsg(ctx, result); + } } else if (currentAlarm != null && clearState != null) { if (!validateUpdate(update, clearState)) { log.debug("[{}] Update is not valid for current clear state", alarmDefinition.getId()); return stateUpdate; } - Boolean evalResult = evalFunction.apply(clearState, data); - if (evalResult) { + AlarmEvalResult evalResult = evalFunction.apply(clearState, data); + if (AlarmEvalResult.TRUE.equals(evalResult)) { stateUpdate |= clearState.checkUpdate(); for (AlarmRuleState state : createRulesSortedBySeverityDesc) { state.clear(); @@ -105,6 +111,9 @@ class AlarmState { ctx.getAlarmService().clearAlarm(ctx.getTenantId(), currentAlarm.getId(), JacksonUtil.OBJECT_MAPPER.createObjectNode(), System.currentTimeMillis()); pushMsg(ctx, new TbAlarmResult(false, false, true, currentAlarm)); currentAlarm = null; + } else if (AlarmEvalResult.FALSE.equals(evalResult)) { + clearState.clear(); + stateUpdate |= clearState.checkUpdate(); } } return stateUpdate; @@ -183,13 +192,18 @@ class AlarmState { // Maybe we should fetch alarm every time? currentAlarm.setEndTs(System.currentTimeMillis()); AlarmSeverity oldSeverity = currentAlarm.getSeverity(); - if (!oldSeverity.equals(severity)) { - currentAlarm.setSeverity(severity); - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - return new TbAlarmResult(false, false, true, false, currentAlarm); + // Skip update if severity is decreased. + if (severity.ordinal() <= oldSeverity.ordinal()) { + if (!oldSeverity.equals(severity)) { + currentAlarm.setSeverity(severity); + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + return new TbAlarmResult(false, false, true, false, currentAlarm); + } else { + currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); + return new TbAlarmResult(false, true, false, false, currentAlarm); + } } else { - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - return new TbAlarmResult(false, true, false, false, currentAlarm); + return null; } } else { currentAlarm = new Alarm(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index 4b0b87043a..b4bbf08323 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -39,6 +39,7 @@ import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.dao.util.mapping.JacksonUtil; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -57,16 +58,20 @@ import java.util.concurrent.TimeUnit; ) public class TbDeviceProfileNode implements TbNode { private static final String PERIODIC_MSG_TYPE = "TbDeviceProfilePeriodicMsg"; + private static final String PROFILE_UPDATE_MSG_TYPE = "TbDeviceProfileUpdateMsg"; private TbDeviceProfileNodeConfiguration config; private RuleEngineDeviceProfileCache cache; + private TbContext ctx; private final Map deviceStates = new ConcurrentHashMap<>(); @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbDeviceProfileNodeConfiguration.class); this.cache = ctx.getDeviceProfileCache(); + this.ctx = ctx; scheduleAlarmHarvesting(ctx); + ctx.addProfileListener(this::onProfileUpdate); if (config.isFetchAlarmRulesStateOnStart()) { log.info("[{}] Fetching alarm rule state", ctx.getSelfId()); int fetchCount = 0; @@ -95,15 +100,14 @@ public class TbDeviceProfileNode implements TbNode { } } - /** - * TODO: Dynamic values evaluation; - */ @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { EntityType originatorType = msg.getOriginator().getEntityType(); if (msg.getType().equals(PERIODIC_MSG_TYPE)) { scheduleAlarmHarvesting(ctx); harvestAlarms(ctx, System.currentTimeMillis()); + } else if (msg.getType().equals(PROFILE_UPDATE_MSG_TYPE)) { + updateProfile(ctx, new DeviceProfileId(UUID.fromString(msg.getData()))); } else { if (EntityType.DEVICE.equals(originatorType)) { DeviceId deviceId = new DeviceId(msg.getOriginator().getId()); @@ -119,36 +123,12 @@ public class TbDeviceProfileNode implements TbNode { ctx.tellFailure(msg, new IllegalStateException("Device profile for device [" + deviceId + "] not found!")); } } - } else if (EntityType.DEVICE_PROFILE.equals(originatorType)) { - log.info("[{}] Received device profile update notification: {}", ctx.getSelfId(), msg.getData()); - if (msg.getType().equals("ENTITY_UPDATED")) { - DeviceProfile deviceProfile = JacksonUtil.fromString(msg.getData(), DeviceProfile.class); - if (deviceProfile != null) { - for (DeviceState state : deviceStates.values()) { - if (deviceProfile.getId().equals(state.getProfileId())) { - state.updateProfile(ctx, deviceProfile); - } - } - } - } - ctx.tellSuccess(msg); } else { ctx.tellSuccess(msg); } } } - public void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) { - DeviceState deviceState = deviceStates.get(deviceId); - if (deviceState != null) { - DeviceProfileId currentProfileId = deviceState.getProfileId(); - Device device = JacksonUtil.fromString(deviceJson, Device.class); - if (!currentProfileId.equals(device.getDeviceProfileId())) { - deviceStates.remove(deviceId); - } - } - } - @Override public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { // Cleanup the cache for all entities that are no longer assigned to current server partitions @@ -157,6 +137,7 @@ public class TbDeviceProfileNode implements TbNode { @Override public void destroy() { + ctx.removeProfileListener(); deviceStates.clear(); } @@ -183,4 +164,33 @@ public class TbDeviceProfileNode implements TbNode { } } + protected void updateProfile(TbContext ctx, DeviceProfileId deviceProfileId) throws ExecutionException, InterruptedException { + DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceProfileId); + if (deviceProfile != null) { + log.debug("[{}] Received device profile update notification: {}", ctx.getSelfId(), deviceProfile); + for (DeviceState state : deviceStates.values()) { + if (deviceProfile.getId().equals(state.getProfileId())) { + state.updateProfile(ctx, deviceProfile); + } + } + } else { + log.debug("[{}] Received stale profile update notification: [{}]", ctx.getSelfId(), deviceProfileId); + } + } + + protected void onProfileUpdate(DeviceProfile profile) { + ctx.tellSelf(TbMsg.newMsg(PROFILE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, profile.getId().getId().toString()), 0L); + } + + protected void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) { + DeviceState deviceState = deviceStates.get(deviceId); + if (deviceState != null) { + DeviceProfileId currentProfileId = deviceState.getProfileId(); + Device device = JacksonUtil.fromString(deviceJson, Device.class); + if (!currentProfileId.equals(device.getDeviceProfileId())) { + deviceStates.remove(deviceId); + } + } + } + } diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 3f57b32fab..229196d325 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -49,10 +49,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 77d5f30fa7..6aaa42ca18 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -42,10 +42,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index f01b15c77a..f9341dfb86 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -71,10 +71,6 @@ transport: sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" - rate_limits: - enabled: "${TB_TRANSPORT_RATE_LIMITS_ENABLED:false}" - tenant: "${TB_TRANSPORT_RATE_LIMITS_TENANT:1000:1,20000:60}" - device: "${TB_TRANSPORT_RATE_LIMITS_DEVICE:10:1,300:60}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" diff --git a/ui-ngx/src/app/core/http/oauth2.service.ts b/ui-ngx/src/app/core/http/oauth2.service.ts index a8f890223d..2eff9fa339 100644 --- a/ui-ngx/src/app/core/http/oauth2.service.ts +++ b/ui-ngx/src/app/core/http/oauth2.service.ts @@ -41,4 +41,8 @@ export class OAuth2Service { return this.http.post('/api/oauth2/config', OAuth2Setting, defaultHttpOptionsFromConfig(config)); } + + public getLoginProcessingUrl(config?: RequestConfig): Observable { + return this.http.get(`/api/oauth2/loginProcessingUrl`, defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index e44c3c6ec2..4e98f375c2 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -191,6 +191,11 @@ export class MenuService { name: 'admin.security-settings', icon: 'security', path: '/settings/security-settings' + }, + { + name: 'admin.oauth2.oauth2', + icon: 'security', + path: '/settings/oauth2' } ] } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html index 47c9ef8645..64d40df581 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -88,14 +88,20 @@
-
+ + +
@@ -109,12 +115,5 @@ {{ 'alarm.clear' | translate }}
- -
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html index b7f0ff21fc..bb89ff479e 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html @@ -59,16 +59,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html index f817762f95..18f3e1a795 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html @@ -95,11 +95,6 @@ {{ 'alias.add' | translate }} - + diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index 072858455c..f815da49e7 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -44,16 +44,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html index 60fe5e30d7..4899a893e5 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html @@ -55,21 +55,22 @@ -
+
+ style="margin-bottom: 0;"> {{ 'dashboard.open-dashboard' | translate }} - + +
diff --git a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html index 8e2264fae1..fe48694983 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html @@ -26,7 +26,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html index f1e427445f..78cad36bc5 100644 --- a/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html @@ -33,11 +33,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html index 29ee7364eb..e4ee790806 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.html @@ -31,7 +31,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html index ab49dae445..717e8765de 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html @@ -55,16 +55,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html index 8aeacc51a2..b8d7103a3b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.html @@ -15,5 +15,5 @@ limitations under the License. --> -
diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss index 3f232f3ce2..aaa95f0f3f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.scss @@ -21,6 +21,11 @@ } &.required { color: #f44336; + padding: 0 4px; + } + &.nowrap { + white-space: nowrap; + overflow: hidden; } } } diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts index e6eb5955b0..5499cdc765 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-text.component.ts @@ -54,6 +54,9 @@ export class FilterTextComponent implements ControlValueAccessor, OnInit { @Input() addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); + @Input() + nowrap = false; + requiredClass = false; private filterText: string; diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html index a8d205479f..587504da45 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html @@ -46,12 +46,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html index 91bbc11425..c573267d2a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html @@ -88,11 +88,6 @@ {{ 'filter.add' | translate }} - + diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index f601814920..5a9f8858c3 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -79,12 +79,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html index 2a0cf0be24..bf38483244 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -65,16 +65,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html index e53ff360ca..a8fe3a1f46 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog.component.html @@ -43,16 +43,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index b5e4fd9795..fd5b5b17c2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

device-profile.add

@@ -106,28 +106,25 @@
-
-
- -
-
- - -
- - -
-
+
+ + + +
+ +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss index b8b811ca3d..d2a716bb50 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss @@ -28,7 +28,7 @@ display: flex; flex-direction: column; height: 100%; - padding: 24px 24px 8px !important; + padding: 0 !important; .mat-stepper-horizontal { display: flex; @@ -45,7 +45,7 @@ } } .mat-horizontal-content-container { - height: 350px; + height: 530px; max-height: 100%; width: 100%;; overflow-y: auto; diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html index 7be952c68d..493763756b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html @@ -108,17 +108,17 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html index 15d6173e0e..7c09f3ba1c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -15,25 +15,22 @@ limitations under the License. --> -
- +
- +
- +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss index 0b1ccabd65..733a80ada3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss @@ -22,13 +22,10 @@ } .tb-alarm-rule-condition { cursor: pointer; + min-width: 0; .tb-alarm-rule-condition-spec { - margin-top: 1em; - line-height: 1.8em; + opacity: 0.7; padding: 4px; - &.disabled { - opacity: 0.7; - } } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html index 3c49f61cf0..c7c77a7f3c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -16,36 +16,23 @@ -->
- + - - + - -
-
- - - - {{ 'action.edit' | translate }} - -
- +
+ + {{ alarmRuleFormGroup.get('alarmDetails').value ? ('device-profile.alarm-rule-details' | translate) + ': ' : ('device-profile.add-alarm-rule-details' | translate) }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss index a52e6f312e..f69af45170 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss @@ -14,31 +14,19 @@ * limitations under the License. */ :host { + min-width: 0; .row { margin-top: 1em; } .tb-alarm-rule-details { - a.mat-button { - &:hover, &:focus { - border-bottom: none; - } - } - .tb-alarm-rule-details-content { - min-height: 33px; - overflow: hidden; - white-space: pre; - line-height: 1.8em; - padding: 4px; - cursor: pointer; - &.collapsed { - max-height: 33px; - white-space: nowrap; - text-overflow: ellipsis; - } - &.disabled { - opacity: 0.7; - cursor: auto; - } + padding: 4px; + cursor: pointer; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + &.title { + opacity: 0.7; + overflow: visible; } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts index 4c86f7dfbd..1e7806b455 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -118,7 +118,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value, + readonly: this.disabled } }).afterClosed().subscribe((alarmDetails) => { if (isDefinedAndNotNull(alarmDetails)) { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html index abe511fc3f..64a808fe98 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html @@ -37,17 +37,17 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html index a9ea0eb8a4..c8991ccc65 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html @@ -15,19 +15,16 @@ limitations under the License. --> -
- - + {{('device-profile.schedule' | translate) + ': '}} + - + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss index 0849ab7f1d..ef5bb3ebad 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss @@ -21,14 +21,14 @@ } } .tb-alarm-rule-schedule { - line-height: 1.8em; padding: 4px; cursor: pointer; - &.disabled { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + &.title { opacity: 0.7; - } - .nowrap { - white-space: nowrap; + overflow: visible; } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts index 0d2e5a0fee..0b9ce26163 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts @@ -101,7 +101,7 @@ export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit for (const item of schedule.items) { if (item.enabled) { if (this.scheduleText.length) { - this.scheduleText += '
'; + this.scheduleText += ', '; } this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html index e1883256bb..5c992d9e04 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -19,7 +19,7 @@
-
+
alarm.severity - +
@@ -47,9 +47,9 @@ remove_circle_outline
-
+
device-profile.no-create-alarm-rules + class="tb-prompt required">device-profile.add-create-alarm-rule-prompt
-
+
device-profile.no-clear-alarm-rule
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts index 116d9a2a9c..cf4a55e6d8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts @@ -25,7 +25,7 @@ import { Validator, Validators } from '@angular/forms'; -import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models'; +import { AlarmRule, DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models'; import { MatDialog } from '@angular/material/dialog'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; @@ -92,7 +92,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit clearRule: [null], propagate: [null], propagateRelationTypes: [null] - }); + }, { validators: deviceProfileAlarmValidator }); this.alarmFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts index 632623115e..312dc5346d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts @@ -30,7 +30,7 @@ import { import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { DeviceProfileAlarm } from '@shared/models/device.models'; +import { DeviceProfileAlarm, deviceProfileAlarmValidator } from '@shared/models/device.models'; import { guid } from '@core/utils'; import { Subscription } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; @@ -141,7 +141,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni id: guid(), alarmType: '', createRules: { - empty: { + CRITICAL: { condition: { condition: [] } @@ -149,8 +149,11 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni } }; const alarmsArray = this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; - alarmsArray.push(this.fb.control(alarm, [Validators.required])); + alarmsArray.push(this.fb.control(alarm, [deviceProfileAlarmValidator])); this.deviceProfileAlarmsFormGroup.updateValueAndValidity(); + if (!this.deviceProfileAlarmsFormGroup.valid) { + this.updateModel(); + } } public validate(c: FormControl) { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html index cd039f28aa..9f4a4addf7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html @@ -38,16 +38,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts index bc46bee51d..a6017a6d70 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts @@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core'; export interface EditAlarmDetailsDialogData { alarmDetails: string; + readonly: boolean; } @Component({ @@ -57,6 +58,9 @@ export class EditAlarmDetailsDialogComponent extends DialogComponent -
+

{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}

@@ -37,11 +37,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 4fbc413d98..f74b19891e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -41,7 +41,7 @@
-
+
device-profile.name diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html index 79bf7983a7..75f75906d0 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - +

{{ (isAdd ? 'tenant-profile.add' : 'tenant-profile.edit' ) | translate }}

@@ -37,11 +37,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html index 2d637c4066..f1d6790ae8 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-dialog.component.html @@ -53,17 +53,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index a61b3a80c6..46a0d213a4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -147,16 +147,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html index d09b7aae24..8127b77e5d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html @@ -38,16 +38,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html index b150110fca..3a834621e6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html @@ -50,17 +50,16 @@
+ -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 26055c55b2..3e4b37dc79 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -29,7 +29,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetAction, WidgetContext } from '@home/models/widget-component.models'; -import { DataKey, Datasource, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models'; +import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models'; import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; @@ -371,7 +371,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.subscription.alarmSource.dataKeys.forEach((alarmDataKey) => { const dataKey: EntityColumn = deepClone(alarmDataKey) as EntityColumn; dataKey.entityKey = dataKeyToEntityKey(alarmDataKey); - dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label); + dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); + dataKey.title = dataKey.label; dataKey.def = 'def' + this.columns.length; const keySettings: TableWidgetDataKeySettings = dataKey.settings; if (dataKey.type === DataKeyType.alarm && !isDefined(keySettings.columnWidth)) { @@ -394,7 +395,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.displayedColumns.push(...this.columns.map(column => column.def)); } if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { - this.defaultSortOrder = this.settings.defaultSortOrder; + this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); } this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns); let sortColumn: EntityColumn; @@ -959,7 +960,7 @@ class AlarmsDatasource implements DataSource { } } } - alarm[dataKey.name] = value; + alarm[dataKey.label] = value; }); return alarm; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html index 2ee1222168..05aabbb7a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator-panel.component.html @@ -28,7 +28,6 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index dae33f3cfe..b9791b5ae9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -352,7 +352,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } dataKeys.push(dataKey); - dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label); + dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); + dataKey.title = dataKey.label; dataKey.def = 'def' + this.columns.length; const keySettings: TableWidgetDataKeySettings = dataKey.settings; if (dataKey.type === DataKeyType.entityField && @@ -374,7 +375,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { - this.defaultSortOrder = this.settings.defaultSortOrder; + this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); } this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 9e079bc247..1ab9ea6fc4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -189,7 +189,7 @@ export function getAlarmValue(alarm: AlarmDataInfo, key: EntityColumn) { if (alarmField) { return getDescendantProp(alarm, alarmField.value); } else { - return getDescendantProp(alarm, key.name); + return getDescendantProp(alarm, key.label); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index ebf5387862..1b820ab722 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -524,16 +524,22 @@ class TimeseriesDatasource implements DataSource { }); } - const rows: TimeseriesRow[] = []; - - for (const value of Object.values(rowsMap)) { - if (this.hideEmptyLines && isDefinedAndNotNull(value[1])) { - rows.push(value); - } else { - rows.push(value); + let rows: TimeseriesRow[] = []; + if (this.hideEmptyLines) { + for (const t of Object.keys(rowsMap)) { + let hideLine = true; + for (let c = 0; (c < data.length) && hideLine; c++) { + if (rowsMap[t][c + 1]) { + hideLine = false; + } + } + if (!hideLine) { + rows.push(rowsMap[t]); + } } + } else { + rows = Object.values(rowsMap); } - return rows; } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index ab63c71dda..0d224122d6 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -91,6 +91,12 @@
+
+ + +
{{ 'device.is-gateway' | translate }} @@ -151,28 +157,25 @@ -
-
- -
-
- - -
- - -
-
+
+ + + +
+ +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss index 1426e0f201..55878d638b 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -29,6 +29,7 @@ display: flex; flex-direction: column; height: 100%; + padding: 0 !important; .mat-stepper-horizontal { display: flex; @@ -45,7 +46,7 @@ } } .mat-horizontal-content-container { - height: 450px; + height: 530px; max-height: 100%; width: 100%;; overflow-y: auto; diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 5c59eb448b..c4f100b30f 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -42,6 +42,7 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; @Component({ selector: 'tb-device-wizard', @@ -103,6 +104,7 @@ export class DeviceWizardDialogComponent extends addProfileType: [0], deviceProfileId: [null, Validators.required], newDeviceProfileTitle: [{value: null, disabled: true}], + defaultRuleChainId: [{value: null, disabled: true}], description: [''] } ); @@ -114,6 +116,7 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup.get('deviceProfileId').enable(); this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators(null); this.deviceWizardFormGroup.get('newDeviceProfileTitle').disable(); + this.deviceWizardFormGroup.get('defaultRuleChainId').disable(); this.deviceWizardFormGroup.updateValueAndValidity(); this.createProfile = false; this.createTransportConfiguration = false; @@ -122,6 +125,7 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup.get('deviceProfileId').disable(); this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]); this.deviceWizardFormGroup.get('newDeviceProfileTitle').enable(); + this.deviceWizardFormGroup.get('defaultRuleChainId').enable(); this.deviceWizardFormGroup.updateValueAndValidity(); this.createProfile = true; this.createTransportConfiguration = this.deviceWizardFormGroup.get('transportType').value && @@ -274,6 +278,9 @@ export class DeviceWizardDialogComponent extends provisionConfiguration: deviceProvisionConfiguration } }; + if (this.deviceWizardFormGroup.get('defaultRuleChainId').value) { + deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceWizardFormGroup.get('defaultRuleChainId').value); + } return this.deviceProfileService.saveDeviceProfile(deviceProfile).pipe( map(profile => profile.id), tap((profileId) => { diff --git a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html index 0b33828bc4..2fcc346477 100644 --- a/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.html @@ -40,17 +40,17 @@
- +
diff --git a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html index 5c98c02fa1..18d71968fd 100644 --- a/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.html @@ -39,17 +39,17 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index af7dc98e19..7fefbfb521 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -14,8 +14,8 @@ /// limitations under the License. /// -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Injectable, NgModule } from '@angular/core'; +import { Resolve, RouterModule, Routes } from '@angular/router'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; @@ -23,6 +23,25 @@ import { Authority } from '@shared/models/authority.enum'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component'; +import { User } from '@shared/models/user.model'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { UserService } from '@core/http/user.service'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { OAuth2Service } from '@core/http/oauth2.service'; +import { UserProfileResolver } from '@home/pages/profile/profile-routing.module'; + +@Injectable() +export class OAuth2LoginProcessingUrlResolver implements Resolve { + + constructor(private oauth2Service: OAuth2Service) { + } + + resolve(): Observable { + return this.oauth2Service.getLoginProcessingUrl(); + } +} const routes: Routes = [ { @@ -90,6 +109,9 @@ const routes: Routes = [ label: 'admin.oauth2.oauth2', icon: 'security' } + }, + resolve: { + loginProcessingUrl: OAuth2LoginProcessingUrlResolver } } ] @@ -98,6 +120,9 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forChild(routes)], - exports: [RouterModule] + exports: [RouterModule], + providers: [ + OAuth2LoginProcessingUrlResolver + ] }) export class AdminRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index 58cbe536d7..ac26797899 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -43,6 +43,7 @@ import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { OAuth2Service } from '@core/http/oauth2.service'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'tb-oauth2-settings', @@ -87,7 +88,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha templateProvider = ['Custom']; + private loginProcessingUrl: string = this.route.snapshot.data.loginProcessingUrl; + constructor(protected store: Store, + private route: ActivatedRoute, private oauth2Service: OAuth2Service, private fb: FormBuilder, private dialogService: DialogService, @@ -130,7 +134,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha return this.oauth2SettingsForm.get('domainsParams') as FormArray; } - private formBasicGroup(mapperConfigBasic?: MapperConfigBasic): FormGroup { + private formBasicGroup(type: MapperConfigType, mapperConfigBasic?: MapperConfigBasic): FormGroup { let tenantNamePattern; if (mapperConfigBasic?.tenantNamePattern) { tenantNamePattern = mapperConfigBasic.tenantNamePattern; @@ -145,7 +149,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha tenantNamePattern: [tenantNamePattern, Validators.required], customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null], defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null], - alwaysFullScreen: [mapperConfigBasic?.alwaysFullScreen ? mapperConfigBasic.alwaysFullScreen : false] + alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] }); this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => { @@ -279,9 +283,12 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha clientRegistration?.userNameAttributeName ? clientRegistration.userNameAttributeName : 'email', Validators.required], mapperConfig: this.fb.group({ allowUserCreation: [ - clientRegistration?.mapperConfig?.allowUserCreation ? clientRegistration.mapperConfig.allowUserCreation : true + isDefinedAndNotNull(clientRegistration?.mapperConfig?.allowUserCreation) ? + clientRegistration.mapperConfig.allowUserCreation : true + ], + activateUser: [ + isDefinedAndNotNull(clientRegistration?.mapperConfig?.activateUser) ? clientRegistration.mapperConfig.activateUser : false ], - activateUser: [clientRegistration?.mapperConfig?.activateUser ? clientRegistration.mapperConfig.activateUser : false], type: [ clientRegistration?.mapperConfig?.type ? clientRegistration.mapperConfig.type : MapperConfigType.BASIC, Validators.required ] @@ -308,7 +315,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha return clientRegistrationFormGroup; } - private validateScope (control: AbstractControl): ValidationErrors | null { + private validateScope(control: AbstractControl): ValidationErrors | null { const scope: string[] = control.value; if (!scope || !scope.length) { return { @@ -347,7 +354,15 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); } else { mapperConfig.removeControl('custom'); - mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); + if (!mapperConfig.get('basic')) { + mapperConfig.addControl('basic', this.formBasicGroup(type, predefinedValue?.basic)); + } + if (type === MapperConfigType.GITHUB) { + mapperConfig.get('basic.emailAttributeKey').disable(); + mapperConfig.get('basic.emailAttributeKey').patchValue(null, {emitEvent: false}); + } else { + mapperConfig.get('basic.emailAttributeKey').enable(); + } } } @@ -490,7 +505,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha } else { protocol = domainInfo.scheme === DomainSchema.MIXED ? DomainSchema.HTTPS.toLowerCase() : domainInfo.scheme.toLowerCase(); } - return `${protocol}://${domainInfo.name}/login/oauth2/code/`; + return `${protocol}://${domainInfo.name}${this.loginProcessingUrl}`; } return ''; } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html index 614c63d980..fb0d17e73a 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html @@ -41,11 +41,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html index 082ff62633..80d5412226 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html @@ -152,17 +152,17 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html index 8c539a8cb8..0014fcb0cb 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html @@ -54,11 +54,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html index 0a20f6a48e..3097e0e303 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/make-dashboard-public-dialog.component.html @@ -54,7 +54,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html index cc27d98d9a..52788ca0e8 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html @@ -52,16 +52,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html index d75b4f41c3..7f4f486278 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html @@ -136,16 +136,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index d927e5222c..a803d35b1d 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -70,7 +70,7 @@ - diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html index 0c99fd7040..2936b60911 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -36,17 +36,17 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html index 9cd30ab768..926c4d7bcc 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html @@ -46,16 +46,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html index e8df333ecd..75ccbb764e 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-dialog.component.html @@ -40,16 +40,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html index 3016ab6d70..12de2706f5 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html @@ -39,16 +39,16 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html index 52ac55275c..a66a3ab995 100644 --- a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html @@ -41,11 +41,6 @@
- +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html index 8af9a379d9..0ae1e24a07 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html @@ -46,17 +46,17 @@
- +
diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html index 8e1b64381d..4de07b1f80 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.html @@ -34,7 +34,6 @@ diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html index 8dc4f5b1d2..9d5ee6c6ef 100644 --- a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html @@ -41,17 +41,16 @@
- +
diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html index 698ecb75ae..365445ed02 100644 --- a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html @@ -109,18 +109,17 @@ {{ 'rulenode.test' | translate }} - + diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index fb1c23e93a..0fc3d95086 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -97,10 +97,6 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte ngOnInit() { this.entitySelectFormGroup.get('entityType').valueChanges.subscribe( (value) => { - if(value === AliasEntityType.CURRENT_TENANT || value === AliasEntityType.CURRENT_USER || - value === AliasEntityType.CURRENT_USER_OWNER) { - this.modelValue.id = NULL_UUID; - } this.updateView(value, this.modelValue.id); } ); @@ -140,20 +136,23 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte } updateView(entityType: EntityType | AliasEntityType | null, entityId: string | null) { - if (this.modelValue.entityType !== entityType || - this.modelValue.id !== entityId) { - this.modelValue = { - entityType, - id: this.modelValue.entityType !== entityType ? null : entityId - }; - if (this.modelValue.entityType && (this.modelValue.id || - this.modelValue.entityType === AliasEntityType.CURRENT_TENANT || - this.modelValue.entityType === AliasEntityType.CURRENT_USER || - this.modelValue.entityType === AliasEntityType.CURRENT_USER_OWNER)) { - this.propagateChange(this.modelValue); - } else { - this.propagateChange(null); - } + if (this.modelValue.entityType !== entityType || this.modelValue.id !== entityId) { + this.modelValue = { + entityType, + id: this.modelValue.entityType !== entityType ? null : entityId + }; + + if (this.modelValue.entityType === AliasEntityType.CURRENT_TENANT + || this.modelValue.entityType === AliasEntityType.CURRENT_USER + || this.modelValue.entityType === AliasEntityType.CURRENT_USER_OWNER) { + this.modelValue.id = NULL_UUID; + } + + if (this.modelValue.entityType && this.modelValue.id) { + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } } } } diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 72e30462e5..5515ba055f 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -36,7 +36,6 @@ import { TranslateService } from '@ngx-translate/core'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { ResizeObserver } from '@juggle/resize-observer'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; -import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models'; @Component({ selector: 'tb-js-func', @@ -64,6 +63,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, private jsEditor: ace.Ace.Editor; private editorsResizeCaf: CancelAnimationFrame; private editorResize$: ResizeObserver; + private ignoreChange = false; toastTargetId = `jsFuncEditor-${guid()}`; @@ -154,8 +154,10 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, this.jsEditor.session.setUseWrapMode(true); this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); this.jsEditor.on('change', () => { - this.cleanupJsErrors(); - this.updateView(); + if (!this.ignoreChange) { + this.cleanupJsErrors(); + this.updateView(); + } }); if (this.editorCompleter) { this.jsEditor.completers = [this.editorCompleter, ...(this.jsEditor.completers || [])]; @@ -332,7 +334,9 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, writeValue(value: string): void { this.modelValue = value; if (this.jsEditor) { + this.ignoreChange = true; this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); + this.ignoreChange = false; } } diff --git a/ui-ngx/src/app/shared/components/json-content.component.ts b/ui-ngx/src/app/shared/components/json-content.component.ts index 242b4b6359..3339863cd2 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.ts +++ b/ui-ngx/src/app/shared/components/json-content.component.ts @@ -61,6 +61,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid private jsonEditor: ace.Ace.Editor; private editorsResizeCaf: CancelAnimationFrame; private editorResize$: ResizeObserver; + private ignoreChange = false; toastTargetId = `jsonContentEditor-${guid()}`; @@ -140,8 +141,13 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.jsonEditor.session.setUseWrapMode(true); this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); this.jsonEditor.on('change', () => { - this.cleanupJsonErrors(); - this.updateView(); + if (!this.ignoreChange) { + this.cleanupJsonErrors(); + this.updateView(); + } + }); + this.jsonEditor.on('blur', () => { + this.contentValid = !this.validateContent || this.doValidate(true); }); this.editorResize$ = new ResizeObserver(() => { this.onAceEditorResize(); @@ -210,34 +216,36 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.cleanupJsonErrors(); this.contentValid = true; this.propagateChange(this.contentBody); - this.contentValid = this.doValidate(); + this.contentValid = this.doValidate(true); this.propagateChange(this.contentBody); } } - private doValidate(): boolean { + private doValidate(showErrorToast = false): boolean { try { if (this.validateContent && this.contentType === ContentType.JSON) { JSON.parse(this.contentBody); } return true; } catch (ex) { - let errorInfo = 'Error:'; - if (ex.name) { - errorInfo += ' ' + ex.name + ':'; - } - if (ex.message) { - errorInfo += ' ' + ex.message; + if (showErrorToast) { + let errorInfo = 'Error:'; + if (ex.name) { + errorInfo += ' ' + ex.name + ':'; + } + if (ex.message) { + errorInfo += ' ' + ex.message; + } + this.store.dispatch(new ActionNotificationShow( + { + message: errorInfo, + type: 'error', + target: this.toastTargetId, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + this.errorShowed = true; } - this.store.dispatch(new ActionNotificationShow( - { - message: errorInfo, - type: 'error', - target: this.toastTargetId, - verticalPosition: 'bottom', - horizontalPosition: 'left' - })); - this.errorShowed = true; return false; } } @@ -256,8 +264,9 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.contentBody = value; this.contentValid = true; if (this.jsonEditor) { + this.ignoreChange = true; this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); - // this.jsonEditor. + this.ignoreChange = false; } } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index c462c150c0..398d5dd9d1 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -140,19 +140,18 @@
+ -
diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 9e92defa45..f6850dbc57 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -26,7 +26,7 @@ import { EntityInfoData } from '@shared/models/entity.models'; import { KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; import * as _moment from 'moment-timezone'; -import { AbstractControl, FormGroup } from '@angular/forms'; +import { AbstractControl, FormGroup, ValidationErrors } from '@angular/forms'; export enum DeviceProfileType { DEFAULT = 'DEFAULT' @@ -87,7 +87,7 @@ export const deviceProvisionTypeTranslationMap = new Map( [ @@ -303,6 +303,18 @@ export interface AlarmRule { schedule?: AlarmSchedule; } +export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null { + const alarmRule: AlarmRule = control.value; + return alarmRuleValid(alarmRule) ? null : {alarmRule: true}; +} + +function alarmRuleValid(alarmRule: AlarmRule): boolean { + if (!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length) { + return false; + } + return true; +} + export interface DeviceProfileAlarm { id: string; alarmType: string; @@ -312,6 +324,34 @@ export interface DeviceProfileAlarm { propagateRelationTypes?: Array; } +export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null { + const deviceProfileAlarm: DeviceProfileAlarm = control.value; + if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType && + deviceProfileAlarm.createRules) { + const severities = Object.keys(deviceProfileAlarm.createRules); + if (severities.length) { + let alarmRulesValid = true; + for (const severity of severities) { + const alarmRule = deviceProfileAlarm.createRules[severity]; + if (!alarmRuleValid(alarmRule)) { + alarmRulesValid = false; + break; + } + } + if (alarmRulesValid) { + if (deviceProfileAlarm.clearRule && !alarmRuleValid(deviceProfileAlarm.clearRule)) { + alarmRulesValid = false; + } + } + if (alarmRulesValid) { + return null; + } + } + } + return {deviceProfileAlarm: true}; +} + + export interface DeviceProfileData { configuration: DeviceProfileConfiguration; transportConfiguration: DeviceProfileTransportConfiguration; 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 612d7c9186..ff22370ba5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -912,6 +912,7 @@ "create-alarm-pattern": "Create {{alarmType}} alarm", "create-alarm-rules": "Create alarm rules", "no-create-alarm-rules": "No create conditions configured", + "add-create-alarm-rule-prompt": "Please add create alarm rule", "clear-alarm-rule": "Clear alarm rule", "no-clear-alarm-rule": "No clear condition configured", "add-create-alarm-rule": "Add create condition", @@ -927,6 +928,7 @@ "condition-duration-time-unit-required": "Time unit is required.", "advanced-settings": "Advanced settings", "alarm-rule-details": "Details", + "add-alarm-rule-details": "Add details", "propagate-alarm": "Propagate alarm", "alarm-rule-relation-types-list": "Relation types to propagate", "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", @@ -948,14 +950,14 @@ "condition-type": "Condition type", "condition-type-simple": "Simple", "condition-type-duration": "Duration", - "condition-during": "During {{during}}", + "condition-during": "During {{during}}", "condition-type-repeating": "Repeating", "condition-type-required": "Condition type is required.", "condition-repeating-value": "Count of events", "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-required": "Count of events is required.", - "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }", + "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }", "schedule-type": "Scheduler type", "schedule-type-required": "Scheduler type is required.", "schedule": "Schedule", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index c6c3b3af3a..190fbb1ba7 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -331,6 +331,9 @@ pre.tb-highlight { font-weight: 400; line-height: 18px; color: rgba(0, 0, 0, .38); + &.required { + color: rgb(221, 44, 0); + } } .tb-fullscreen { @@ -869,10 +872,7 @@ mat-label { } .mat-dialog-actions { margin-bottom: 0; - padding: 8px 8px 8px 16px; - button:last-of-type{ - margin-right: 20px; - } + padding: 8px; } } }