diff --git a/application/src/main/data/json/system/oauth2_config_templates/apple_config.json b/application/src/main/data/json/system/oauth2_config_templates/apple_config.json new file mode 100644 index 0000000000..a956920b6b --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/apple_config.json @@ -0,0 +1,24 @@ +{ + "providerId": "Apple", + "additionalInfo": null, + "accessTokenUri": "https://appleid.apple.com/auth/token", + "authorizationUri": "https://appleid.apple.com/auth/authorize?response_mode=form_post", + "scope": ["email","openid","name"], + "jwkSetUri": "https://appleid.apple.com/auth/keys", + "userInfoUri": null, + "clientAuthenticationMethod": "POST", + "userNameAttributeName": "email", + "mapperConfig": { + "type": "APPLE", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "firstName", + "lastNameAttributeKey": "lastName", + "tenantNameStrategy": "DOMAIN" + } + }, + "comment": null, + "loginButtonIcon": "apple-logo", + "loginButtonLabel": "Apple", + "helpLink": "https://developer.apple.com/sign-in-with-apple/get-started/" +} diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql index d57668de0d..8020046a7b 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration ( created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 13d39b0b2a..e28c5e37e1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -128,7 +128,7 @@ public class OtaPackageController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST) @ResponseBody - public OtaPackage saveOtaPackageData(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, + public OtaPackageInfo saveOtaPackageData(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, @RequestParam(required = false) String checksum, @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @RequestBody MultipartFile file) throws ThingsboardException { @@ -160,7 +160,7 @@ public class OtaPackageController extends BaseController { otaPackage.setContentType(file.getContentType()); otaPackage.setData(ByteBuffer.wrap(bytes)); otaPackage.setDataSize((long) bytes.length); - OtaPackage savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage); + OtaPackageInfo savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage); logEntityAction(savedOtaPackage.getId(), savedOtaPackage, null, ActionType.UPDATED, null); return savedOtaPackage; } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java new file mode 100644 index 0000000000..93da71169c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.dao.oauth2.OAuth2User; +import org.thingsboard.server.service.security.model.SecurityUser; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +@Service(value = "appleOAuth2ClientMapper") +@Slf4j +public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + + private static final String USER = "user"; + private static final String NAME = "name"; + private static final String FIRST_NAME = "firstName"; + private static final String LAST_NAME = "lastName"; + private static final String EMAIL = "email"; + + @Override + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + OAuth2MapperConfig config = registration.getMapperConfig(); + Map attributes = updateAttributesFromRequestParams(request, token.getPrincipal().getAttributes()); + String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); + + return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + } + + private static Map updateAttributesFromRequestParams(HttpServletRequest request, Map attributes) { + Map updated = attributes; + MultiValueMap params = toMultiMap(request.getParameterMap()); + String userValue = params.getFirst(USER); + if (StringUtils.hasText(userValue)) { + JsonNode user = null; + try { + user = JacksonUtil.toJsonNode(userValue); + } catch (Exception e) {} + if (user != null) { + updated = new HashMap<>(attributes); + if (user.has(NAME)) { + JsonNode name = user.get(NAME); + if (name.isObject()) { + JsonNode firstName = name.get(FIRST_NAME); + if (firstName != null && firstName.isTextual()) { + updated.put(FIRST_NAME, firstName.asText()); + } + JsonNode lastName = name.get(LAST_NAME); + if (lastName != null && lastName.isTextual()) { + updated.put(LAST_NAME, lastName.asText()); + } + } + } + if (user.has(EMAIL)) { + JsonNode email = user.get(EMAIL); + if (email != null && email.isTextual()) { + updated.put(EMAIL, email.asText()); + } + } + } + } + return updated; + } + + private static MultiValueMap toMultiMap(Map map) { + MultiValueMap params = new LinkedMultiValueMap<>(map.size()); + map.forEach((key, values) -> { + if (values.length > 0) { + for (String value : values) { + params.add(key, value); + } + } + }); + return params; + } +} 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 f5172b64e7..d2532d0240 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 @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; import java.util.Map; @Service(value = "basicOAuth2ClientMapper") @@ -30,7 +31,7 @@ import java.util.Map; public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { OAuth2MapperConfig config = registration.getMapperConfig(); Map attributes = token.getPrincipal().getAttributes(); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); 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 65ebb384de..778f7416ff 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 @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; + @Service(value = "customOAuth2ClientMapper") @Slf4j public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @@ -39,7 +41,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { OAuth2MapperConfig config = registration.getMapperConfig(); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); 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 d6260fe482..3810f36757 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 @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Map; import java.util.Optional; @@ -46,7 +47,7 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private OAuth2Configuration oAuth2Configuration; @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { OAuth2MapperConfig config = registration.getMapperConfig(); Map githubMapperConfig = oAuth2Configuration.getGithubMapper(); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); 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 280418d066..39957602d4 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 @@ -20,6 +20,8 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; + public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration); + SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java index 606b4f9b1d..df9e5e05ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java @@ -37,6 +37,10 @@ public class OAuth2ClientMapperProvider { @Qualifier("githubOAuth2ClientMapper") private OAuth2ClientMapper githubOAuth2ClientMapper; + @Autowired + @Qualifier("appleOAuth2ClientMapper") + private OAuth2ClientMapper appleOAuth2ClientMapper; + public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) { switch (oauth2MapperType) { case CUSTOM: @@ -45,6 +49,8 @@ public class OAuth2ClientMapperProvider { return basicOAuth2ClientMapper; case GITHUB: return githubOAuth2ClientMapper; + case APPLE: + return appleOAuth2ClientMapper; default: throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java index 0e413b4f22..95b4643f4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java @@ -36,7 +36,6 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @Component(value = "oauth2AuthenticationFailureHandler") -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; 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 227d733ebd..303a430e77 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 @@ -90,7 +90,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS token.getAuthorizedClientRegistrationId(), token.getPrincipal().getName()); OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType()); - SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), registration); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java index a11d1b624a..6aa341cef6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java @@ -141,10 +141,12 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); Assert.assertEquals(FILE_NAME, savedFirmware.getFileName()); Assert.assertEquals(CONTENT_TYPE, savedFirmware.getContentType()); + Assert.assertEquals(CHECKSUM_ALGORITHM, savedFirmware.getChecksumAlgorithm().name()); + Assert.assertEquals(CHECKSUM, savedFirmware.getChecksum()); } @Test @@ -189,11 +191,12 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); OtaPackage foundFirmware = doGet("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString(), OtaPackage.class); Assert.assertNotNull(foundFirmware); - Assert.assertEquals(savedFirmware, foundFirmware); + Assert.assertEquals(savedFirmware, new OtaPackageInfo(foundFirmware)); + Assert.assertEquals(DATA, foundFirmware.getData()); } @Test @@ -228,8 +231,8 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes if (i > 100) { MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); - otaPackages.add(new OtaPackageInfo(savedFirmware)); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + otaPackages.add(savedFirmware); } else { otaPackages.add(savedFirmwareInfo); } @@ -271,7 +274,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes if (i > 100) { MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); savedFirmwareInfo = new OtaPackageInfo(savedFirmware); otaPackagesWithData.add(savedFirmwareInfo); } @@ -318,11 +321,11 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes return doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class); } - protected OtaPackage savaData(String urlTemplate, MockMultipartFile content, String... params) throws Exception { + protected OtaPackageInfo savaData(String urlTemplate, MockMultipartFile content, String... params) throws Exception { MockMultipartHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.multipart(urlTemplate, params); postRequest.file(content); setJwtToken(postRequest); - return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackage.class); + return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackageInfo.class); } } diff --git a/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java b/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java index 31fae3d7a4..c37fbb1548 100644 --- a/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java +++ b/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java @@ -41,6 +41,7 @@ public class ActorSystemTest { public static final String ROOT_DISPATCHER = "root-dispatcher"; private static final int _100K = 100 * 1024; + public static final int TIMEOUT_AWAIT_MAX_SEC = 10; private volatile TbActorSystem actorSystem; private volatile ExecutorService submitPool; @@ -52,7 +53,7 @@ public class ActorSystemTest { parallelism = Math.max(2, cores / 2); TbActorSystemSettings settings = new TbActorSystemSettings(5, parallelism, 42); actorSystem = new DefaultTbActorSystem(settings); - submitPool = Executors.newWorkStealingPool(parallelism); + submitPool = Executors.newFixedThreadPool(parallelism); //order guaranteed } @After @@ -122,13 +123,23 @@ public class ActorSystemTest { ActorTestCtx testCtx1 = getActorTestCtx(1); ActorTestCtx testCtx2 = getActorTestCtx(1); TbActorId actorId = new TbEntityActorId(new DeviceId(UUID.randomUUID())); - submitPool.submit(() -> actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx1))); - submitPool.submit(() -> actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx2))); + final CountDownLatch initLatch = new CountDownLatch(1); + final CountDownLatch actorsReadyLatch = new CountDownLatch(2); + submitPool.submit(() -> { + actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx1, initLatch)); + actorsReadyLatch.countDown(); + }); + submitPool.submit(() -> { + actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx2, initLatch)); + actorsReadyLatch.countDown(); + }); + + initLatch.countDown(); //replacement for Thread.wait(500) in the SlowCreateActorCreator + Assert.assertTrue(actorsReadyLatch.await(TIMEOUT_AWAIT_MAX_SEC, TimeUnit.SECONDS)); - Thread.sleep(1000); actorSystem.tell(actorId, new IntTbActorMsg(42)); - Assert.assertTrue(testCtx1.getLatch().await(1, TimeUnit.SECONDS)); + Assert.assertTrue(testCtx1.getLatch().await(TIMEOUT_AWAIT_MAX_SEC, TimeUnit.SECONDS)); Assert.assertFalse(testCtx2.getLatch().await(1, TimeUnit.SECONDS)); } @@ -137,13 +148,21 @@ public class ActorSystemTest { actorSystem.createDispatcher(ROOT_DISPATCHER, Executors.newWorkStealingPool(parallelism)); ActorTestCtx testCtx = getActorTestCtx(1); TbActorId actorId = new TbEntityActorId(new DeviceId(UUID.randomUUID())); - for (int i = 0; i < 1000; i++) { - submitPool.submit(() -> actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx))); + final int actorsCount = 1000; + final CountDownLatch initLatch = new CountDownLatch(1); + final CountDownLatch actorsReadyLatch = new CountDownLatch(actorsCount); + for (int i = 0; i < actorsCount; i++) { + submitPool.submit(() -> { + actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx, initLatch)); + actorsReadyLatch.countDown(); + }); } - Thread.sleep(1000); + initLatch.countDown(); + Assert.assertTrue(actorsReadyLatch.await(TIMEOUT_AWAIT_MAX_SEC, TimeUnit.SECONDS)); + actorSystem.tell(actorId, new IntTbActorMsg(42)); - Assert.assertTrue(testCtx.getLatch().await(1, TimeUnit.SECONDS)); + Assert.assertTrue(testCtx.getLatch().await(TIMEOUT_AWAIT_MAX_SEC, TimeUnit.SECONDS)); //One for creation and one for message Assert.assertEquals(2, testCtx.getInvocationCount().get()); } diff --git a/common/actor/src/test/java/org/thingsboard/server/actors/SlowCreateActor.java b/common/actor/src/test/java/org/thingsboard/server/actors/SlowCreateActor.java index 50eb00a5ca..f14fe8461e 100644 --- a/common/actor/src/test/java/org/thingsboard/server/actors/SlowCreateActor.java +++ b/common/actor/src/test/java/org/thingsboard/server/actors/SlowCreateActor.java @@ -17,13 +17,18 @@ package org.thingsboard.server.actors; import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + @Slf4j public class SlowCreateActor extends TestRootActor { - public SlowCreateActor(TbActorId actorId, ActorTestCtx testCtx) { + public static final int TIMEOUT_AWAIT_MAX_MS = 5000; + + public SlowCreateActor(TbActorId actorId, ActorTestCtx testCtx, CountDownLatch initLatch) { super(actorId, testCtx); try { - Thread.sleep(500); + initLatch.await(TIMEOUT_AWAIT_MAX_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } @@ -34,10 +39,12 @@ public class SlowCreateActor extends TestRootActor { private final TbActorId actorId; private final ActorTestCtx testCtx; + private final CountDownLatch initLatch; - public SlowCreateActorCreator(TbActorId actorId, ActorTestCtx testCtx) { + public SlowCreateActorCreator(TbActorId actorId, ActorTestCtx testCtx, CountDownLatch initLatch) { this.actorId = actorId; this.testCtx = testCtx; + this.initLatch = initLatch; } @Override @@ -47,7 +54,7 @@ public class SlowCreateActor extends TestRootActor { @Override public TbActor createActor() { - return new SlowCreateActor(actorId, testCtx); + return new SlowCreateActor(actorId, testCtx, initLatch); } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index 09c9f084cb..f578cd15c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; @@ -31,5 +32,6 @@ public class AlarmRule implements Serializable { // Advanced @NoXss private String alarmDetails; + private DashboardId dashboardId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java index 3f91e14bd9..4811e153d9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.common.data.oauth2; public enum MapperType { - BASIC, CUSTOM, GITHUB; + BASIC, CUSTOM, GITHUB, APPLE; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java index ae965e327a..9be2957401 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.common.msg.session; public enum FeatureType { - ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION, FIRMWARE, SOFTWARE + ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java index 939197af80..5dbea04d59 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java @@ -30,10 +30,7 @@ public enum SessionMsgType { SESSION_OPEN, SESSION_CLOSE, - CLAIM_REQUEST(), - - GET_FIRMWARE_REQUEST, - GET_SOFTWARE_REQUEST; + CLAIM_REQUEST(); private final boolean requiresRulesProcessing; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index fe4300a421..bf694e00e6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -99,7 +99,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue @Override protected void doCommit() { - consumer.commitAsync(); + consumer.commitSync(); } @Override diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 33f122fa3f..95cdeada88 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.SessionMsgType; @@ -139,10 +138,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource { processExchangeGetRequest(exchange, featureType.get()); } else if (featureType.get() == FeatureType.ATTRIBUTES) { processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST); - } else if (featureType.get() == FeatureType.FIRMWARE) { - processRequest(exchange, SessionMsgType.GET_FIRMWARE_REQUEST); - } else if (featureType.get() == FeatureType.SOFTWARE) { - processRequest(exchange, SessionMsgType.GET_SOFTWARE_REQUEST); } else { log.trace("Invalid feature type parameter"); exchange.respond(CoAP.ResponseCode.BAD_REQUEST); @@ -349,12 +344,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource { coapTransportAdaptor.convertToGetAttributes(sessionId, request), new CoapNoOpCallback(exchange)); break; - case GET_FIRMWARE_REQUEST: - getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.FIRMWARE); - break; - case GET_SOFTWARE_REQUEST: - getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.SOFTWARE); - break; } } catch (AdaptorException e) { log.trace("[{}] Failed to decode message: ", sessionId, e); @@ -366,16 +355,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource { return new UUID(sessionInfoProto.getSessionIdMSB(), sessionInfoProto.getSessionIdLSB()); } - private void getOtaPackageCallback(TransportProtos.SessionInfoProto sessionInfo, CoapExchange exchange, OtaPackageType firmwareType) { - TransportProtos.GetOtaPackageRequestMsg requestMsg = TransportProtos.GetOtaPackageRequestMsg.newBuilder() - .setTenantIdMSB(sessionInfo.getTenantIdMSB()) - .setTenantIdLSB(sessionInfo.getTenantIdLSB()) - .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) - .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()) - .setType(firmwareType.name()).build(); - transportContext.getTransportService().process(sessionInfo, requestMsg, new OtaPackageCallback(exchange)); - } - private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) { tokenToObserveNotificationSeqMap.remove(token); return tokenToSessionInfoMap.remove(token); @@ -470,57 +449,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } - private class OtaPackageCallback implements TransportServiceCallback { - private final CoapExchange exchange; - - OtaPackageCallback(CoapExchange exchange) { - this.exchange = exchange; - } - - @Override - public void onSuccess(TransportProtos.GetOtaPackageResponseMsg msg) { - String title = exchange.getQueryParameter("title"); - String version = exchange.getQueryParameter("version"); - if (msg.getResponseStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) { - String firmwareId = new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB()).toString(); - if (msg.getTitle().equals(title) && msg.getVersion().equals(version)) { - String strChunkSize = exchange.getQueryParameter("size"); - String strChunk = exchange.getQueryParameter("chunk"); - int chunkSize = StringUtils.isEmpty(strChunkSize) ? 0 : Integer.parseInt(strChunkSize); - int chunk = StringUtils.isEmpty(strChunk) ? 0 : Integer.parseInt(strChunk); - exchange.respond(CoAP.ResponseCode.CONTENT, transportContext.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk)); - } - else if (firmwareId != null) { - sendOtaData(exchange, firmwareId); - } else { - exchange.respond(CoAP.ResponseCode.BAD_REQUEST); - } - } else { - exchange.respond(CoAP.ResponseCode.NOT_FOUND); - } - } - - @Override - public void onError(Throwable e) { - log.warn("Failed to process request", e); - exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); - } - } - - private void sendOtaData(CoapExchange exchange, String firmwareId) { - Response response = new Response(CoAP.ResponseCode.CONTENT); - byte[] fwData = transportContext.getOtaPackageDataCache().get(firmwareId); - if (fwData != null && fwData.length > 0) { - response.setPayload(fwData); - if (exchange.getRequestOptions().getBlock2() != null) { - int chunkSize = exchange.getRequestOptions().getBlock2().getSzx(); - boolean moreFlag = fwData.length > chunkSize; - response.getOptions().setBlock2(chunkSize, moreFlag, 0); - } - exchange.respond(response); - } - } - private static class CoapSessionListener implements SessionMsgListener { private final CoapTransportResource coapTransportResource; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index ce1618fe99..72be5e6f1e 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TbTransportService; import org.thingsboard.server.coapserver.CoapServerService; import org.thingsboard.server.coapserver.TbCoapServerComponent; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource; import javax.annotation.PostConstruct; @@ -59,6 +60,8 @@ public class CoapTransportService implements TbTransportService { efento.add(efentoMeasurementsTransportResource); coapServer.add(api); coapServer.add(efento); + coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.FIRMWARE)); + coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.SOFTWARE)); log.info("CoAP transport started!"); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java new file mode 100644 index 0000000000..60e8f55352 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.coap; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.network.Exchange; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.Resource; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.ota.OtaPackageType; +import org.thingsboard.server.common.data.security.DeviceTokenCredentials; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Slf4j +public class OtaPackageTransportResource extends AbstractCoapTransportResource { + private static final int ACCESS_TOKEN_POSITION = 2; + + private final OtaPackageType otaPackageType; + + public OtaPackageTransportResource(CoapTransportContext ctx, OtaPackageType otaPackageType) { + super(ctx, otaPackageType.getKeyPrefix()); + this.otaPackageType = otaPackageType; + } + + @Override + protected void processHandleGet(CoapExchange exchange) { + log.trace("Processing {}", exchange.advanced().getRequest()); + exchange.accept(); + Exchange advanced = exchange.advanced(); + Request request = advanced.getRequest(); + processAccessTokenRequest(exchange, request); + } + + @Override + protected void processHandlePost(CoapExchange exchange) { + exchange.respond(CoAP.ResponseCode.METHOD_NOT_ALLOWED); + } + + private void processAccessTokenRequest(CoapExchange exchange, Request request) { + Optional credentials = decodeCredentials(request); + if (credentials.isEmpty()) { + exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); + return; + } + transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), + new CoapDeviceAuthCallback(transportContext, exchange, (sessionInfo, deviceProfile) -> { + getOtaPackageCallback(sessionInfo, exchange, otaPackageType); + })); + } + + private void getOtaPackageCallback(TransportProtos.SessionInfoProto sessionInfo, CoapExchange exchange, OtaPackageType firmwareType) { + TransportProtos.GetOtaPackageRequestMsg requestMsg = TransportProtos.GetOtaPackageRequestMsg.newBuilder() + .setTenantIdMSB(sessionInfo.getTenantIdMSB()) + .setTenantIdLSB(sessionInfo.getTenantIdLSB()) + .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) + .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()) + .setType(firmwareType.name()).build(); + transportContext.getTransportService().process(sessionInfo, requestMsg, new OtaPackageCallback(exchange)); + } + + private Optional decodeCredentials(Request request) { + List uriPath = request.getOptions().getUriPath(); + if (uriPath.size() == ACCESS_TOKEN_POSITION) { + return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1))); + } else { + return Optional.empty(); + } + } + + @Override + public Resource getChild(String name) { + return this; + } + + private class OtaPackageCallback implements TransportServiceCallback { + private final CoapExchange exchange; + + OtaPackageCallback(CoapExchange exchange) { + this.exchange = exchange; + } + + @Override + public void onSuccess(TransportProtos.GetOtaPackageResponseMsg msg) { + String title = exchange.getQueryParameter("title"); + String version = exchange.getQueryParameter("version"); + if (msg.getResponseStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) { + String firmwareId = new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB()).toString(); + if ((title == null || msg.getTitle().equals(title)) && (version == null || msg.getVersion().equals(version))) { + String strChunkSize = exchange.getQueryParameter("size"); + String strChunk = exchange.getQueryParameter("chunk"); + int chunkSize = StringUtils.isEmpty(strChunkSize) ? 0 : Integer.parseInt(strChunkSize); + int chunk = StringUtils.isEmpty(strChunk) ? 0 : Integer.parseInt(strChunk); + respondOtaPackage(exchange, transportContext.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk)); + } else { + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } + } else { + exchange.respond(CoAP.ResponseCode.NOT_FOUND); + } + } + + @Override + public void onError(Throwable e) { + log.warn("Failed to process request", e); + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } + } + + private void respondOtaPackage(CoapExchange exchange, byte[] data) { + Response response = new Response(CoAP.ResponseCode.CONTENT); + if (data != null && data.length > 0) { + response.setPayload(data); + if (exchange.getRequestOptions().getBlock2() != null) { + int chunkSize = exchange.getRequestOptions().getBlock2().getSzx(); + boolean moreFlag = data.length > chunkSize; + response.getOptions().setBlock2(chunkSize, moreFlag, 0); + } + exchange.respond(response); + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java index edcaa794a6..ca32bad322 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java @@ -178,7 +178,7 @@ public class OAuth2RegistrationEntity extends BaseSqlEntity .activateUser(activateUser) .type(type) .basic( - (type == MapperType.BASIC || type == MapperType.GITHUB) ? + (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) ? OAuth2BasicMapperConfig.builder() .emailAttributeKey(emailAttributeKey) .firstNameAttributeKey(firstNameAttributeKey) diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index 83a134ea36..6d7dde1f21 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -377,9 +377,6 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se if (StringUtils.isEmpty(clientRegistration.getScope())) { throw new DataValidationException("Scope should be specified!"); } - if (StringUtils.isEmpty(clientRegistration.getUserInfoUri())) { - throw new DataValidationException("User info uri should be specified!"); - } if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) { throw new DataValidationException("User name attribute name should be specified!"); } diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 16522becde..ca7cd73604 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -387,7 +387,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration ( created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 88386a11dd..303bf6f0f9 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -424,7 +424,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration ( created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java index 7ae7d5989d..534883f337 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java @@ -200,7 +200,6 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message); this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish); - message.payload().retain(); channel.writeAndFlush(pubrecMessage); } 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 14a008634b..b716935b2a 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 @@ -2971,7 +2971,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/otaPackage", otaPackageInfo, OtaPackageInfo.class).getBody(); } - public OtaPackage saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception { + public OtaPackageInfo saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception { HttpHeaders header = new HttpHeaders(); header.setContentType(MediaType.MULTIPART_FORM_DATA); @@ -2993,7 +2993,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } return restTemplate.postForEntity( - baseURL + url, requestEntity, OtaPackage.class, params + baseURL + url, requestEntity, OtaPackageInfo.class, params ).getBody(); } 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 445b9653a2..614cfd4b9c 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 @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -269,16 +270,22 @@ class AlarmState { private JsonNode createDetails(AlarmRuleState ruleState) { JsonNode alarmDetails; String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); + DashboardId dashboardId = ruleState.getAlarmRule().getDashboardId(); - if (StringUtils.isNotEmpty(alarmDetailsStr)) { - for (var keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { - EntityKeyValue entityKeyValue = dataSnapshot.getValue(keyFilter.getKey()); - if (entityKeyValue != null) { - alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", keyFilter.getKey().getKey()), getValueAsString(entityKeyValue)); + if (StringUtils.isNotEmpty(alarmDetailsStr) || dashboardId != null) { + ObjectNode newDetails = JacksonUtil.newObjectNode(); + if (StringUtils.isNotEmpty(alarmDetailsStr)) { + for (var keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { + EntityKeyValue entityKeyValue = dataSnapshot.getValue(keyFilter.getKey()); + if (entityKeyValue != null) { + alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", keyFilter.getKey().getKey()), getValueAsString(entityKeyValue)); + } } + newDetails.put("data", alarmDetailsStr); + } + if (dashboardId != null) { + newDetails.put("dashboardId", dashboardId.getId().toString()); } - ObjectNode newDetails = JacksonUtil.newObjectNode(); - newDetails.put("data", alarmDetailsStr); alarmDetails = newDetails; } else if (currentAlarm != null) { alarmDetails = currentAlarm.getDetails(); diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 3ad4038066..39d51ced92 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -89,6 +89,13 @@ export class AppComponent implements OnInit { ) ); + this.matIconRegistry.addSvgIconLiteral( + 'apple-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + this.storageService.testLocalStorage(); this.setupTranslate(); diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 08119e6ce8..aeb0f9c286 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -98,24 +98,17 @@ export class EntityAliasesDialogComponent extends DialogComponent { if (widget.type === widgetType.rpc) { if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { - const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; - widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId]; - if (!widgetsTitleList) { - widgetsTitleList = []; - this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; - } - widgetsTitleList.push(widget.config.title); + this.addWidgetTitleToWidgetsMap(widget.config.targetDeviceAliasIds[0], widget.config.title); + } + } else if (widget.type === widgetType.alarm) { + if (widget.config.alarmSource) { + this.addWidgetTitleToWidgetsMap(widget.config.alarmSource.entityAliasId, widget.config.title); } } else { const datasources = this.utils.validateDatasources(widget.config.datasources); datasources.forEach((datasource) => { if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { - widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId]; - if (!widgetsTitleList) { - widgetsTitleList = []; - this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList; - } - widgetsTitleList.push(widget.config.title); + this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title); } }); } @@ -141,6 +134,15 @@ export class EntityAliasesDialogComponent extends DialogComponent = this.aliasToWidgetsMap[aliasId]; + if (!widgetsTitleList) { + widgetsTitleList = []; + this.aliasToWidgetsMap[aliasId] = widgetsTitleList; + } + widgetsTitleList.push(widgetTitle); + } + private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl { const aliasFormControl = this.fb.group({ id: [aliasId], diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index 58dea3b6f6..c48030050d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -63,7 +63,7 @@
- + +
{{'device-profile.mobile-dashboard-hint' | translate}}
{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}
+
+ + {{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }} + + +
{{'device-profile.alarm-rule-mobile-dashboard-hint' | 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 a0b8a83cd4..d973f261b7 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 @@ -18,16 +18,24 @@ .row { margin-top: 1em; } - .tb-alarm-rule-details { + .tb-alarm-rule-details, .tb-alarm-rule-dashboard { padding: 4px; - cursor: pointer; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; &.title { opacity: 0.7; overflow: visible; } } + .tb-alarm-rule-details { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + } + .tb-alarm-rule-dashboard { + &.dashboard { + width: 100%; + max-width: 350px; + } + } } 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 d664c5c2a4..76bfa7698e 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 @@ -34,6 +34,7 @@ import { EditAlarmDetailsDialogData } from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; import { EntityId } from '@shared/models/id/entity-id'; +import { DashboardId } from '@shared/models/id/dashboard-id'; @Component({ selector: 'tb-alarm-rule', @@ -92,7 +93,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat this.alarmRuleFormGroup = this.fb.group({ condition: [null, [Validators.required]], schedule: [null], - alarmDetails: [null] + alarmDetails: [null], + dashboardId: [null] }); this.alarmRuleFormGroup.valueChanges.subscribe(() => { this.updateModel(); @@ -110,7 +112,11 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat writeValue(value: AlarmRule): void { this.modelValue = value; - this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + const model = this.modelValue ? { + ...this.modelValue, + dashboardId: this.modelValue.dashboardId?.id + } : null; + this.alarmRuleFormGroup.reset(model || undefined, {emitEvent: false}); } public openEditDetailsDialog($event: Event) { @@ -143,7 +149,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat private updateModel() { const value = this.alarmRuleFormGroup.value; if (this.modelValue) { - this.modelValue = {...this.modelValue, ...value}; + this.modelValue = {...this.modelValue, ...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null}; this.propagateChange(this.modelValue); } } 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 5fa6c1acdb..83bba2a266 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 @@ -60,8 +60,9 @@ formControlName="defaultRuleChainId"> +
{{'device-profile.mobile-dashboard-hint' | translate}}
{{ column.title }} - + @@ -125,8 +125,8 @@ 'mat-selected': alarmsDatasource.isSelected(alarm), 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm), 'invisible': alarmsDatasource.dataLoading}" - *matRowDef="let alarm; columns: displayedColumns;" - [ngStyle]="rowStyle(alarm)" + *matRowDef="let alarm; columns: displayedColumns; let row = index" + [ngStyle]="rowStyle(alarm, row)" (click)="onRowClick($event, alarm)"> = []; + private cellStyleCache: Array = []; + private rowStyleCache: Array = []; + private settings: AlarmsTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; @@ -261,6 +273,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public onDataUpdated() { this.updateTitle(true); this.alarmsDatasource.updateAlarms(); + this.clearCache(); } public pageLinkSortDirection(): SortDirection { @@ -426,7 +439,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.displayedColumns.push('actions'); } - this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys); + this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys, this.ngZone); if (this.enableSelection) { this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => { const hideTitlePanel = selectionMode || this.textSearchMode; @@ -484,6 +497,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (this.actionCellDescriptors.length) { this.displayedColumns.push('actions'); } + this.clearCache(); } } as DisplayColumnsPanelData }, @@ -609,91 +623,100 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return widthStyle(columnWidth); } - public rowStyle(alarm: AlarmDataInfo): any { - let style: any = {}; - if (alarm) { - if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { + public rowStyle(alarm: AlarmDataInfo, row: number): any { + let res = this.rowStyleCache[row]; + if (!res) { + res = {}; + if (alarm && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { try { - style = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + res = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); } - if (Array.isArray(style)) { + if (Array.isArray(res)) { throw new TypeError(`Array instead of style object`); } } catch (e) { - style = {}; + res = {}; console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your row style function.`); } - } else { - style = {}; } + this.rowStyleCache[row] = res; } - return style; + return res; } - public cellStyle(alarm: AlarmDataInfo, key: EntityColumn): any { - let style: any = {}; - if (alarm && key) { - const styleInfo = this.stylesInfo[key.def]; - const value = getAlarmValue(alarm, key); - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - style = styleInfo.cellStyleFunction(value, alarm, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); - } - if (Array.isArray(style)) { - throw new TypeError(`Array instead of style object`); + public cellStyle(alarm: AlarmDataInfo, key: EntityColumn, row: number): any { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellStyleCache[index]; + if (!res) { + res = {}; + if (alarm && key) { + const styleInfo = this.stylesInfo[key.def]; + const value = getAlarmValue(alarm, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + res = styleInfo.cellStyleFunction(value, alarm, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); + } + if (Array.isArray(res)) { + throw new TypeError(`Array instead of style object`); + } + } catch (e) { + res = {}; + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + + `returns '${e}'. Please check your cell style function.`); } - } catch (e) { - style = {}; - console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + - `returns '${e}'. Please check your cell style function.`); + } else { + res = this.defaultStyle(key, value); } - } else { - style = this.defaultStyle(key, value); } + this.cellStyleCache[index] = res; } - if (!style.width) { + if (!res.width) { const columnWidth = this.columnWidth[key.def]; - style = {...style, ...widthStyle(columnWidth)}; + res = Object.assign(res, widthStyle(columnWidth)); } - return style; + return res; } - public cellContent(alarm: AlarmDataInfo, key: EntityColumn): SafeHtml { - if (alarm && key) { - const contentInfo = this.contentsInfo[key.def]; - const value = getAlarmValue(alarm, key); - let content = ''; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - content = contentInfo.cellContentFunction(value, alarm, this.ctx); - } catch (e) { - content = '' + value; + public cellContent(alarm: AlarmDataInfo, key: EntityColumn, row: number): SafeHtml { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellContentCache[index]; + if (isUndefined(res)) { + res = ''; + if (alarm && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getAlarmValue(alarm, key); + let content = ''; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + content = contentInfo.cellContentFunction(value, alarm, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); } - } else { - content = this.defaultContent(key, contentInfo, value); - } - if (!isDefined(content)) { - return ''; - - } else { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - return this.domSanitizer.bypassSecurityTrustHtml(content); - default: - return content; + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + res = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + default: + res = content; + } } } - - } else { - return ''; + this.cellContentCache[index] = res; } + return res; } public onRowClick($event: Event, alarm: AlarmDataInfo) { @@ -936,6 +959,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, isSorting(column: EntityColumn): boolean { return column.type === DataKeyType.alarm && column.name.startsWith('details.'); } + + private clearCache() { + this.cellContentCache.length = 0; + this.cellStyleCache.length = 0; + this.rowStyleCache.length = 0; + } } class AlarmsDatasource implements DataSource { @@ -957,7 +986,8 @@ class AlarmsDatasource implements DataSource { private appliedSortOrderLabel: string; constructor(private subscription: IWidgetSubscription, - private dataKeys: Array) { + private dataKeys: Array, + private ngZone: NgZone) { } connect(collectionViewer: CollectionViewer): Observable> { @@ -989,6 +1019,7 @@ class AlarmsDatasource implements DataSource { updateAlarms() { const subscriptionAlarms = this.subscription.alarms; let alarms = new Array(); + let isEmptySelection = false; subscriptionAlarms.data.forEach((alarmData) => { alarms.push(this.alarmDataToInfo(alarmData)); }); @@ -1001,7 +1032,7 @@ class AlarmsDatasource implements DataSource { const toRemove = this.selection.selected.filter(alarmId => alarmIds.indexOf(alarmId) === -1); this.selection.deselect(...toRemove); if (this.selection.isEmpty()) { - this.onSelectionModeChanged(false); + isEmptySelection = true; } } const alarmsPageData: PageData = { @@ -1010,9 +1041,14 @@ class AlarmsDatasource implements DataSource { totalElements: subscriptionAlarms.totalElements, hasNext: subscriptionAlarms.hasNext }; - this.alarmsSubject.next(alarms); - this.pageDataSubject.next(alarmsPageData); - this.dataLoading = false; + this.ngZone.run(() => { + if (isEmptySelection) { + this.onSelectionModeChanged(false); + } + this.alarmsSubject.next(alarms); + this.pageDataSubject.next(alarmsPageData); + this.dataLoading = false; + }); } private alarmDataToInfo(alarmData: AlarmData): AlarmDataInfo { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 680ed3ad97..ca93253ddc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -42,9 +42,9 @@ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> {{ column.title }} - + @@ -84,8 +84,8 @@ = []; + private cellStyleCache: Array = []; + private rowStyleCache: Array = []; + private settings: EntitiesTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; @@ -222,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public onDataUpdated() { this.updateTitle(true); this.entityDatasource.dataUpdated(); + this.clearCache(); } public pageLinkSortDirection(): SortDirection { @@ -415,8 +429,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if (this.actionCellDescriptors.length) { this.displayedColumns.push('actions'); } - this.entityDatasource = new EntityDatasource( - this.translate, dataKeys, this.subscription); + this.entityDatasource = new EntityDatasource(this.translate, dataKeys, this.subscription, this.ngZone); } private editColumnsToDisplay($event: Event) { @@ -460,6 +473,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if (this.actionCellDescriptors.length) { this.displayedColumns.push('actions'); } + this.clearCache(); } } as DisplayColumnsPanelData }, @@ -535,91 +549,98 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni return widthStyle(columnWidth); } - public rowStyle(entity: EntityData): any { - let style: any = {}; - if (entity) { - if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { + public rowStyle(entity: EntityData, row: number): any { + let res = this.rowStyleCache[row]; + if (!res) { + res = {}; + if (entity && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { try { - style = this.rowStylesInfo.rowStyleFunction(entity, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + res = this.rowStylesInfo.rowStyleFunction(entity, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); } - if (Array.isArray(style)) { + if (Array.isArray(res)) { throw new TypeError(`Array instead of style object`); } } catch (e) { - style = {}; + res = {}; console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your row style function.`); } - } else { - style = {}; } - } - return style; - } - - public cellStyle(entity: EntityData, key: EntityColumn): any { - let style: any = {}; - if (entity && key) { - const styleInfo = this.stylesInfo[key.def]; - const value = getEntityValue(entity, key); - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - style = styleInfo.cellStyleFunction(value, entity, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); - } - if (Array.isArray(style)) { - throw new TypeError(`Array instead of style object`); + this.rowStyleCache[row] = res; + } + return res; + } + + public cellStyle(entity: EntityData, key: EntityColumn, row: number): any { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellStyleCache[index]; + if (!res) { + res = {}; + if (entity && key) { + const styleInfo = this.stylesInfo[key.def]; + const value = getEntityValue(entity, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + res = styleInfo.cellStyleFunction(value, entity, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); + } + if (Array.isArray(res)) { + throw new TypeError(`Array instead of style object`); + } + } catch (e) { + res = {}; + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + + `returns '${e}'. Please check your cell style function.`); } - } catch (e) { - style = {}; - console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + - `returns '${e}'. Please check your cell style function.`); } - } else { - style = {}; + this.cellStyleCache[index] = res; } } - if (!style.width) { + if (!res.width) { const columnWidth = this.columnWidth[key.def]; - style = {...style, ...widthStyle(columnWidth)}; - } - return style; - } - - public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { - if (entity && key) { - const contentInfo = this.contentsInfo[key.def]; - const value = getEntityValue(entity, key); - let content: string; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - content = contentInfo.cellContentFunction(value, entity, this.ctx); - } catch (e) { - content = '' + value; + res = Object.assign(res, widthStyle(columnWidth)); + } + return res; + } + + public cellContent(entity: EntityData, key: EntityColumn, row: number): SafeHtml { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellContentCache[index]; + if (isUndefined(res)) { + res = ''; + if (entity && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getEntityValue(entity, key); + let content: string; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + content = contentInfo.cellContentFunction(value, entity, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); } - } else { - content = this.defaultContent(key, contentInfo, value); - } - if (!isDefined(content)) { - return ''; - - } else { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - return this.domSanitizer.bypassSecurityTrustHtml(content); - default: - return content; + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + res = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + default: + res = content; + } } } - - } else { - return ''; + this.cellContentCache[index] = res; } + return res; } private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { @@ -672,6 +693,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {entity}, entityLabel); } + + private clearCache() { + this.cellContentCache.length = 0; + this.cellStyleCache.length = 0; + this.rowStyleCache.length = 0; + } } class EntityDatasource implements DataSource { @@ -689,7 +716,8 @@ class EntityDatasource implements DataSource { constructor( private translate: TranslateService, private dataKeys: Array, - private subscription: IWidgetSubscription + private subscription: IWidgetSubscription, + private ngZone: NgZone ) { } @@ -732,9 +760,11 @@ class EntityDatasource implements DataSource { totalElements: datasourcesPageData.totalElements, hasNext: datasourcesPageData.hasNext }; - this.entitiesSubject.next(entities); - this.pageDataSubject.next(entitiesPageData); - this.dataLoading = false; + this.ngZone.run(() => { + this.entitiesSubject.next(entities); + this.pageDataSubject.next(entitiesPageData); + this.dataLoading = false; + }); } private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index a92b4e16bb..4524082e5b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -39,23 +39,23 @@ - +
Timestamp - + {{ h.dataKey.label }} - + @@ -93,8 +93,8 @@ -
= []; + private cellStyleCache: Array = []; + private rowStyleCache: Array = []; + private settings: TimeseriesTableWidgetSettings; private widgetConfig: WidgetConfig; private data: Array; @@ -194,6 +198,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI public onDataUpdated() { this.updateCurrentSourceData(); + this.clearCache(); } private initialize() { @@ -235,7 +240,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } public getTabLabel(source: TimeseriesTableSource){ - if(this.useEntityLabel){ + if (this.useEntityLabel) { return source.datasource.entityLabel || source.datasource.entityName; } else { return source.datasource.entityName; @@ -286,7 +291,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI if (this.actionCellDescriptors.length) { source.displayedColumns.push('actions'); } - const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe); + const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe, this.ngZone); tsDatasource.dataUpdated(this.data); this.sources.push(source); } @@ -337,6 +342,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI onSourceIndexChanged() { this.updateCurrentSourceData(); this.updateActiveEntityInfo(); + this.clearCache(); } private enterFilterMode() { @@ -378,6 +384,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI source.pageLink.sortOrder.property = sort.active; source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; source.timeseriesDatasource.loadRows(); + this.clearCache(); this.ctx.detectChanges(); } @@ -397,94 +404,109 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI return source.datasource.entityId; } - public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow): any { - let style: any = {}; - if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { - try { - const rowData = source.rowDataTemplate; - rowData.Timestamp = row[0]; - source.header.forEach((headerInfo) => { - rowData[headerInfo.dataKey.name] = row[headerInfo.index]; - }); - style = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); - } - if (Array.isArray(style)) { - throw new TypeError(`Array instead of style object`); - } - } catch (e) { - style = {}; - console.warn(`Row style function in widget ` + - `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`); - } - } - return style; - } - - public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): any { - let style: any = {}; - if (index > 0) { - const styleInfo = source.stylesInfo[index - 1]; - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): any { + let res = this.rowStyleCache[index]; + if (!res) { + res = {}; + if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { try { const rowData = source.rowDataTemplate; rowData.Timestamp = row[0]; source.header.forEach((headerInfo) => { rowData[headerInfo.dataKey.name] = row[headerInfo.index]; }); - style = styleInfo.cellStyleFunction(value, rowData, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + res = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); } - if (Array.isArray(style)) { + if (Array.isArray(res)) { throw new TypeError(`Array instead of style object`); } } catch (e) { - style = {}; - console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` + - `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`); + res = {}; + console.warn(`Row style function in widget ` + + `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`); } } + this.rowStyleCache[index] = res; } - return style; - } - - public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml { - if (index === 0) { - return row.formattedTs; - } else { - let content; - const contentInfo = source.contentsInfo[index - 1]; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - const rowData = source.rowDataTemplate; - rowData.Timestamp = row[0]; - source.header.forEach((headerInfo) => { - rowData[headerInfo.dataKey.name] = row[headerInfo.index]; - }); - content = contentInfo.cellContentFunction(value, rowData, this.ctx); - } catch (e) { - content = '' + value; + return res; + } + + public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): any { + const cacheIndex = rowIndex * (source.header.length + 1) + index; + let res = this.cellStyleCache[cacheIndex]; + if (!res) { + res = {}; + if (index > 0) { + const styleInfo = source.stylesInfo[index - 1]; + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; + }); + res = styleInfo.cellStyleFunction(value, rowData, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); + } + if (Array.isArray(res)) { + throw new TypeError(`Array instead of style object`); + } + } catch (e) { + res = {}; + console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` + + `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`); + } } - } else { - const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; - const units = contentInfo.units || this.ctx.widgetConfig.units; - content = this.ctx.utils.formatValue(value, decimals, units, true); } + this.cellStyleCache[cacheIndex] = res; + } + return res; + } - if (!isDefined(content)) { - return ''; + public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml { + const cacheIndex = rowIndex * (source.header.length + 1) + index ; + let res = this.cellContentCache[cacheIndex]; + if (isUndefined(res)) { + res = ''; + if (index === 0) { + res = row.formattedTs; } else { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - return this.domSanitizer.bypassSecurityTrustHtml(content); - default: - return content; + let content; + const contentInfo = source.contentsInfo[index - 1]; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; + }); + content = contentInfo.cellContentFunction(value, rowData, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; + const units = contentInfo.units || this.ctx.widgetConfig.units; + content = this.ctx.utils.formatValue(value, decimals, units, true); + } + + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + res = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + default: + res = content; + } } } + this.cellContentCache[cacheIndex] = res; } + return res; } public onRowClick($event: Event, row: TimeseriesRow) { @@ -530,6 +552,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private loadCurrentSourceRow() { this.sources[this.sourceIndex].timeseriesDatasource.loadRows(); + this.clearCache(); + } + + private clearCache() { + this.cellContentCache.length = 0; + this.cellStyleCache.length = 0; + this.rowStyleCache.length = 0; } } @@ -545,7 +574,8 @@ class TimeseriesDatasource implements DataSource { private source: TimeseriesTableSource, private hideEmptyLines: boolean, private dateFormatFilter: string, - private datePipe: DatePipe + private datePipe: DatePipe, + private ngZone: NgZone ) { this.source.timeseriesDatasource = this; } @@ -568,8 +598,10 @@ class TimeseriesDatasource implements DataSource { catchError(() => of(emptyPageData())), ).subscribe( (pageData) => { - this.rowsSubject.next(pageData.data); - this.pageDataSubject.next(pageData); + this.ngZone.run(() => { + this.rowsSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + }); } ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss index a269d569ae..0372773016 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss @@ -59,5 +59,13 @@ #widget-container { min-height: 0; min-width: 0; + + canvas { + user-select: none; + + &::selection, &::-moz-selection { + background-color: transparent; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index fea6e14291..dee17a000c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -759,7 +759,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement); this.widgetContext.$container.css('display', 'block'); - this.widgetContext.$container.css('user-select', 'none'); this.widgetContext.$container.attr('id', 'container'); if (this.widgetSizeDetected) { this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html index a983266434..43989d907a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -321,16 +321,13 @@ admin.oauth2.user-info-uri - + - - {{ 'admin.oauth2.user-info-uri-required' | translate }} - {{ 'admin.oauth2.uri-pattern-error' | translate }} 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 4a123c642b..bf5dc75023 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 @@ -311,8 +311,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha scope: this.fb.array(registration?.scope ? registration.scope : [], OAuth2SettingsComponent.validateScope), jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '', - [Validators.required, - Validators.pattern(this.URL_REGEXP)]], + [Validators.pattern(this.URL_REGEXP)]], clientAuthenticationMethod: [ registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, Validators.required], diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index 1ed9f784f1..b55498ed05 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -35,6 +35,7 @@ import { TranslateService } from '@ngx-translate/core'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { Subject } from 'rxjs'; import { OtaUpdateType } from '@shared/models/ota-package.models'; +import { distinctUntilChanged } from 'rxjs/operators'; @Component({ selector: 'tb-device', @@ -78,7 +79,7 @@ export class DeviceComponent extends EntityComponent { } buildForm(entity: DeviceInfo): FormGroup { - return this.fb.group( + const form = this.fb.group( { name: [entity ? entity.name : '', [Validators.required]], deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], @@ -95,6 +96,17 @@ export class DeviceComponent extends EntityComponent { ) } ); + form.get('deviceProfileId').valueChanges.pipe( + distinctUntilChanged((prev, curr) => prev?.id === curr?.id) + ).subscribe(profileId => { + if (profileId && this.isEdit) { + this.entityForm.patchValue({ + firmwareId: null, + softwareId: null + }, {emitEvent: false}); + } + }); + return form; } updateForm(entity: DeviceInfo) { @@ -156,10 +168,6 @@ export class DeviceComponent extends EntityComponent { this.entityForm.markAsDirty(); } } - this.entityForm.patchValue({ - firmwareId: null, - softwareId: null - }); } } } diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index 0f2662a251..6802768303 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -149,7 +149,7 @@ - + ota-update.direct-url-required diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts index 1182e6c40a..0c2d82e171 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts @@ -67,7 +67,7 @@ export class OtaUpdateComponent extends EntityComponent implements O this.entityForm.get('file').updateValueAndValidity({emitEvent: false}); } else { this.entityForm.get('file').clearValidators(); - this.entityForm.get('url').setValidators(Validators.required); + this.entityForm.get('url').setValidators([Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]); this.entityForm.get('file').updateValueAndValidity({emitEvent: false}); this.entityForm.get('url').updateValueAndValidity({emitEvent: false}); } @@ -172,6 +172,11 @@ export class OtaUpdateComponent extends EntityComponent implements O } prepareFormValue(formValue: any): any { + if (formValue.resource === 'url') { + delete formValue.file; + } else { + delete formValue.url; + } delete formValue.resource; delete formValue.generateChecksum; return super.prepareFormValue(formValue); diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html index cf36de3a7d..c1cef05057 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + + [matAutocomplete]="packageAutocomplete" + [matAutocompleteDisabled]="disabled">