Browse Source

Merge remote-tracking branch 'origin/master' into feature/lwm2m-refactoring-downlink

pull/4760/head
Andrii Shvaika 5 years ago
parent
commit
015443d35b
  1. 24
      application/src/main/data/json/system/oauth2_config_templates/apple_config.json
  2. 2
      application/src/main/data/upgrade/3.2.2/schema_update.sql
  3. 4
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  4. 101
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java
  5. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
  10. 1
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  12. 19
      application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java
  13. 37
      common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java
  14. 15
      common/actor/src/test/java/org/thingsboard/server/actors/SlowCreateActor.java
  15. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java
  16. 2
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java
  17. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java
  18. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java
  19. 2
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
  20. 72
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  21. 3
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
  22. 144
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java
  23. 2
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java
  24. 3
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java
  25. 2
      dao/src/main/resources/sql/schema-entities-hsql.sql
  26. 2
      dao/src/main/resources/sql/schema-entities.sql
  27. 1
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
  28. 4
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  29. 21
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
  30. 7
      ui-ngx/src/app/app.component.ts
  31. 28
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  32. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  33. 2
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  34. 3
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  35. 12
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  36. 18
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss
  37. 12
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  38. 3
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  39. 10
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  40. 170
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  41. 10
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  42. 176
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  43. 18
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  44. 180
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  45. 8
      ui-ngx/src/app/modules/home/components/widget/widget.component.scss
  46. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  47. 5
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html
  48. 3
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts
  49. 18
      ui-ngx/src/app/modules/home/pages/device/device.component.ts
  50. 2
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html
  51. 7
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts
  52. 2
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html
  53. 4
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
  54. 3
      ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html
  55. 2
      ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts
  56. 1
      ui-ngx/src/app/shared/models/device.models.ts
  57. 3
      ui-ngx/src/app/shared/models/oauth2.models.ts
  58. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

24
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/"
}

2
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, created_time bigint NOT NULL,
additional_info varchar, additional_info varchar,
client_id varchar(255), client_id varchar(255),
client_secret varchar(255), client_secret varchar(2048),
authorization_uri varchar(255), authorization_uri varchar(255),
token_uri varchar(255), token_uri varchar(255),
scope varchar(255), scope varchar(255),

4
application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java

@ -128,7 +128,7 @@ public class OtaPackageController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST) @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST)
@ResponseBody @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(required = false) String checksum,
@RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr,
@RequestBody MultipartFile file) throws ThingsboardException { @RequestBody MultipartFile file) throws ThingsboardException {
@ -160,7 +160,7 @@ public class OtaPackageController extends BaseController {
otaPackage.setContentType(file.getContentType()); otaPackage.setContentType(file.getContentType());
otaPackage.setData(ByteBuffer.wrap(bytes)); otaPackage.setData(ByteBuffer.wrap(bytes));
otaPackage.setDataSize((long) bytes.length); otaPackage.setDataSize((long) bytes.length);
OtaPackage savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage); OtaPackageInfo savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage);
logEntityAction(savedOtaPackage.getId(), savedOtaPackage, null, ActionType.UPDATED, null); logEntityAction(savedOtaPackage.getId(), savedOtaPackage, null, ActionType.UPDATED, null);
return savedOtaPackage; return savedOtaPackage;
} catch (Exception e) { } catch (Exception e) {

101
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<String, Object> 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<String, Object> updateAttributesFromRequestParams(HttpServletRequest request, Map<String, Object> attributes) {
Map<String, Object> updated = attributes;
MultiValueMap<String, String> 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<String, String> toMultiMap(Map<String, String[]> map) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(map.size());
map.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
params.add(key, value);
}
}
});
return params;
}
}

3
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.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
import java.util.Map; import java.util.Map;
@Service(value = "basicOAuth2ClientMapper") @Service(value = "basicOAuth2ClientMapper")
@ -30,7 +31,7 @@ import java.util.Map;
public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
@Override @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(); OAuth2MapperConfig config = registration.getMapperConfig();
Map<String, Object> attributes = token.getPrincipal().getAttributes(); Map<String, Object> attributes = token.getPrincipal().getAttributes();
String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());

4
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.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
@Service(value = "customOAuth2ClientMapper") @Service(value = "customOAuth2ClientMapper")
@Slf4j @Slf4j
public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
@ -39,7 +41,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@Override @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(); OAuth2MapperConfig config = registration.getMapperConfig();
OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration);

3
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.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -46,7 +47,7 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private OAuth2Configuration oAuth2Configuration; private OAuth2Configuration oAuth2Configuration;
@Override @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(); OAuth2MapperConfig config = registration.getMapperConfig();
Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper(); Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);

4
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.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
public interface OAuth2ClientMapper { public interface OAuth2ClientMapper {
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration); SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration);
} }

6
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java

@ -37,6 +37,10 @@ public class OAuth2ClientMapperProvider {
@Qualifier("githubOAuth2ClientMapper") @Qualifier("githubOAuth2ClientMapper")
private OAuth2ClientMapper githubOAuth2ClientMapper; private OAuth2ClientMapper githubOAuth2ClientMapper;
@Autowired
@Qualifier("appleOAuth2ClientMapper")
private OAuth2ClientMapper appleOAuth2ClientMapper;
public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) { public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) {
switch (oauth2MapperType) { switch (oauth2MapperType) {
case CUSTOM: case CUSTOM:
@ -45,6 +49,8 @@ public class OAuth2ClientMapperProvider {
return basicOAuth2ClientMapper; return basicOAuth2ClientMapper;
case GITHUB: case GITHUB:
return githubOAuth2ClientMapper; return githubOAuth2ClientMapper;
case APPLE:
return appleOAuth2ClientMapper;
default: default:
throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!"); throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!");
} }

1
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; import java.nio.charset.StandardCharsets;
@Component(value = "oauth2AuthenticationFailureHandler") @Component(value = "oauth2AuthenticationFailureHandler")
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;

2
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.getAuthorizedClientRegistrationId(),
token.getPrincipal().getName()); token.getPrincipal().getName());
OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType()); 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); registration);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);

19
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()); 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(FILE_NAME, savedFirmware.getFileName());
Assert.assertEquals(CONTENT_TYPE, savedFirmware.getContentType()); Assert.assertEquals(CONTENT_TYPE, savedFirmware.getContentType());
Assert.assertEquals(CHECKSUM_ALGORITHM, savedFirmware.getChecksumAlgorithm().name());
Assert.assertEquals(CHECKSUM, savedFirmware.getChecksum());
} }
@Test @Test
@ -189,11 +191,12 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes
MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); 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); OtaPackage foundFirmware = doGet("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString(), OtaPackage.class);
Assert.assertNotNull(foundFirmware); Assert.assertNotNull(foundFirmware);
Assert.assertEquals(savedFirmware, foundFirmware); Assert.assertEquals(savedFirmware, new OtaPackageInfo(foundFirmware));
Assert.assertEquals(DATA, foundFirmware.getData());
} }
@Test @Test
@ -228,8 +231,8 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes
if (i > 100) { if (i > 100) {
MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); 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);
otaPackages.add(new OtaPackageInfo(savedFirmware)); otaPackages.add(savedFirmware);
} else { } else {
otaPackages.add(savedFirmwareInfo); otaPackages.add(savedFirmwareInfo);
} }
@ -271,7 +274,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes
if (i > 100) { if (i > 100) {
MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); 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); savedFirmwareInfo = new OtaPackageInfo(savedFirmware);
otaPackagesWithData.add(savedFirmwareInfo); otaPackagesWithData.add(savedFirmwareInfo);
} }
@ -318,11 +321,11 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes
return doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class); 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); MockMultipartHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.multipart(urlTemplate, params);
postRequest.file(content); postRequest.file(content);
setJwtToken(postRequest); setJwtToken(postRequest);
return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackage.class); return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackageInfo.class);
} }
} }

37
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"; public static final String ROOT_DISPATCHER = "root-dispatcher";
private static final int _100K = 100 * 1024; private static final int _100K = 100 * 1024;
public static final int TIMEOUT_AWAIT_MAX_SEC = 10;
private volatile TbActorSystem actorSystem; private volatile TbActorSystem actorSystem;
private volatile ExecutorService submitPool; private volatile ExecutorService submitPool;
@ -52,7 +53,7 @@ public class ActorSystemTest {
parallelism = Math.max(2, cores / 2); parallelism = Math.max(2, cores / 2);
TbActorSystemSettings settings = new TbActorSystemSettings(5, parallelism, 42); TbActorSystemSettings settings = new TbActorSystemSettings(5, parallelism, 42);
actorSystem = new DefaultTbActorSystem(settings); actorSystem = new DefaultTbActorSystem(settings);
submitPool = Executors.newWorkStealingPool(parallelism); submitPool = Executors.newFixedThreadPool(parallelism); //order guaranteed
} }
@After @After
@ -122,13 +123,23 @@ public class ActorSystemTest {
ActorTestCtx testCtx1 = getActorTestCtx(1); ActorTestCtx testCtx1 = getActorTestCtx(1);
ActorTestCtx testCtx2 = getActorTestCtx(1); ActorTestCtx testCtx2 = getActorTestCtx(1);
TbActorId actorId = new TbEntityActorId(new DeviceId(UUID.randomUUID())); TbActorId actorId = new TbEntityActorId(new DeviceId(UUID.randomUUID()));
submitPool.submit(() -> actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx1))); final CountDownLatch initLatch = new CountDownLatch(1);
submitPool.submit(() -> actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx2))); 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)); 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)); Assert.assertFalse(testCtx2.getLatch().await(1, TimeUnit.SECONDS));
} }
@ -137,13 +148,21 @@ public class ActorSystemTest {
actorSystem.createDispatcher(ROOT_DISPATCHER, Executors.newWorkStealingPool(parallelism)); actorSystem.createDispatcher(ROOT_DISPATCHER, Executors.newWorkStealingPool(parallelism));
ActorTestCtx testCtx = getActorTestCtx(1); ActorTestCtx testCtx = getActorTestCtx(1);
TbActorId actorId = new TbEntityActorId(new DeviceId(UUID.randomUUID())); TbActorId actorId = new TbEntityActorId(new DeviceId(UUID.randomUUID()));
for (int i = 0; i < 1000; i++) { final int actorsCount = 1000;
submitPool.submit(() -> actorSystem.createRootActor(ROOT_DISPATCHER, new SlowCreateActor.SlowCreateActorCreator(actorId, testCtx))); 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)); 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 //One for creation and one for message
Assert.assertEquals(2, testCtx.getInvocationCount().get()); Assert.assertEquals(2, testCtx.getInvocationCount().get());
} }

15
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 lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
public class SlowCreateActor extends TestRootActor { 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); super(actorId, testCtx);
try { try {
Thread.sleep(500); initLatch.await(TIMEOUT_AWAIT_MAX_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -34,10 +39,12 @@ public class SlowCreateActor extends TestRootActor {
private final TbActorId actorId; private final TbActorId actorId;
private final ActorTestCtx testCtx; 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.actorId = actorId;
this.testCtx = testCtx; this.testCtx = testCtx;
this.initLatch = initLatch;
} }
@Override @Override
@ -47,7 +54,7 @@ public class SlowCreateActor extends TestRootActor {
@Override @Override
public TbActor createActor() { public TbActor createActor() {
return new SlowCreateActor(actorId, testCtx); return new SlowCreateActor(actorId, testCtx, initLatch);
} }
} }
} }

2
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; package org.thingsboard.server.common.data.device.profile;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid; import javax.validation.Valid;
@ -31,5 +32,6 @@ public class AlarmRule implements Serializable {
// Advanced // Advanced
@NoXss @NoXss
private String alarmDetails; private String alarmDetails;
private DashboardId dashboardId;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.data.oauth2; package org.thingsboard.server.common.data.oauth2;
public enum MapperType { public enum MapperType {
BASIC, CUSTOM, GITHUB; BASIC, CUSTOM, GITHUB, APPLE;
} }

2
common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.msg.session; package org.thingsboard.server.common.msg.session;
public enum FeatureType { public enum FeatureType {
ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION, FIRMWARE, SOFTWARE ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION
} }

5
common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java

@ -30,10 +30,7 @@ public enum SessionMsgType {
SESSION_OPEN, SESSION_CLOSE, SESSION_OPEN, SESSION_CLOSE,
CLAIM_REQUEST(), CLAIM_REQUEST();
GET_FIRMWARE_REQUEST,
GET_SOFTWARE_REQUEST;
private final boolean requiresRulesProcessing; private final boolean requiresRulesProcessing;

2
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java

@ -99,7 +99,7 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
@Override @Override
protected void doCommit() { protected void doCommit() {
consumer.commitAsync(); consumer.commitSync();
} }
@Override @Override

72
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.JsonTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; 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.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.msg.session.SessionMsgType;
@ -139,10 +138,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
processExchangeGetRequest(exchange, featureType.get()); processExchangeGetRequest(exchange, featureType.get());
} else if (featureType.get() == FeatureType.ATTRIBUTES) { } else if (featureType.get() == FeatureType.ATTRIBUTES) {
processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST); 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 { } else {
log.trace("Invalid feature type parameter"); log.trace("Invalid feature type parameter");
exchange.respond(CoAP.ResponseCode.BAD_REQUEST); exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
@ -349,12 +344,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
coapTransportAdaptor.convertToGetAttributes(sessionId, request), coapTransportAdaptor.convertToGetAttributes(sessionId, request),
new CoapNoOpCallback(exchange)); new CoapNoOpCallback(exchange));
break; break;
case GET_FIRMWARE_REQUEST:
getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.FIRMWARE);
break;
case GET_SOFTWARE_REQUEST:
getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.SOFTWARE);
break;
} }
} catch (AdaptorException e) { } catch (AdaptorException e) {
log.trace("[{}] Failed to decode message: ", sessionId, 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()); 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) { private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) {
tokenToObserveNotificationSeqMap.remove(token); tokenToObserveNotificationSeqMap.remove(token);
return tokenToSessionInfoMap.remove(token); return tokenToSessionInfoMap.remove(token);
@ -470,57 +449,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
} }
} }
private class OtaPackageCallback implements TransportServiceCallback<TransportProtos.GetOtaPackageResponseMsg> {
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 static class CoapSessionListener implements SessionMsgListener {
private final CoapTransportResource coapTransportResource; private final CoapTransportResource coapTransportResource;

3
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.common.data.TbTransportService;
import org.thingsboard.server.coapserver.CoapServerService; import org.thingsboard.server.coapserver.CoapServerService;
import org.thingsboard.server.coapserver.TbCoapServerComponent; import org.thingsboard.server.coapserver.TbCoapServerComponent;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource; import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -59,6 +60,8 @@ public class CoapTransportService implements TbTransportService {
efento.add(efentoMeasurementsTransportResource); efento.add(efentoMeasurementsTransportResource);
coapServer.add(api); coapServer.add(api);
coapServer.add(efento); coapServer.add(efento);
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.FIRMWARE));
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.SOFTWARE));
log.info("CoAP transport started!"); log.info("CoAP transport started!");
} }

144
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<DeviceTokenCredentials> 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<DeviceTokenCredentials> decodeCredentials(Request request) {
List<String> 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<TransportProtos.GetOtaPackageResponseMsg> {
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);
}
}
}

2
dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java

@ -178,7 +178,7 @@ public class OAuth2RegistrationEntity extends BaseSqlEntity<OAuth2Registration>
.activateUser(activateUser) .activateUser(activateUser)
.type(type) .type(type)
.basic( .basic(
(type == MapperType.BASIC || type == MapperType.GITHUB) ? (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) ?
OAuth2BasicMapperConfig.builder() OAuth2BasicMapperConfig.builder()
.emailAttributeKey(emailAttributeKey) .emailAttributeKey(emailAttributeKey)
.firstNameAttributeKey(firstNameAttributeKey) .firstNameAttributeKey(firstNameAttributeKey)

3
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())) { if (StringUtils.isEmpty(clientRegistration.getScope())) {
throw new DataValidationException("Scope should be specified!"); 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())) { if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) {
throw new DataValidationException("User name attribute name should be specified!"); throw new DataValidationException("User name attribute name should be specified!");
} }

2
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, created_time bigint NOT NULL,
additional_info varchar, additional_info varchar,
client_id varchar(255), client_id varchar(255),
client_secret varchar(255), client_secret varchar(2048),
authorization_uri varchar(255), authorization_uri varchar(255),
token_uri varchar(255), token_uri varchar(255),
scope varchar(255), scope varchar(255),

2
dao/src/main/resources/sql/schema-entities.sql

@ -424,7 +424,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration (
created_time bigint NOT NULL, created_time bigint NOT NULL,
additional_info varchar, additional_info varchar,
client_id varchar(255), client_id varchar(255),
client_secret varchar(255), client_secret varchar(2048),
authorization_uri varchar(255), authorization_uri varchar(255),
token_uri varchar(255), token_uri varchar(255),
scope varchar(255), scope varchar(255),

1
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java

@ -200,7 +200,6 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message); MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message);
this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish); this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish);
message.payload().retain();
channel.writeAndFlush(pubrecMessage); channel.writeAndFlush(pubrecMessage);
} }

4
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(); 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(); HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.MULTIPART_FORM_DATA); header.setContentType(MediaType.MULTIPART_FORM_DATA);
@ -2993,7 +2993,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
} }
return restTemplate.postForEntity( return restTemplate.postForEntity(
baseURL + url, requestEntity, OtaPackage.class, params baseURL + url, requestEntity, OtaPackageInfo.class, params
).getBody(); ).getBody();
} }

21
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.AlarmConditionKeyType;
import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; 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.data.id.EntityId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -269,16 +270,22 @@ class AlarmState {
private JsonNode createDetails(AlarmRuleState ruleState) { private JsonNode createDetails(AlarmRuleState ruleState) {
JsonNode alarmDetails; JsonNode alarmDetails;
String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails();
DashboardId dashboardId = ruleState.getAlarmRule().getDashboardId();
if (StringUtils.isNotEmpty(alarmDetailsStr)) { if (StringUtils.isNotEmpty(alarmDetailsStr) || dashboardId != null) {
for (var keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { ObjectNode newDetails = JacksonUtil.newObjectNode();
EntityKeyValue entityKeyValue = dataSnapshot.getValue(keyFilter.getKey()); if (StringUtils.isNotEmpty(alarmDetailsStr)) {
if (entityKeyValue != null) { for (var keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) {
alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", keyFilter.getKey().getKey()), getValueAsString(entityKeyValue)); 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; alarmDetails = newDetails;
} else if (currentAlarm != null) { } else if (currentAlarm != null) {
alarmDetails = currentAlarm.getDetails(); alarmDetails = currentAlarm.getDetails();

7
ui-ngx/src/app/app.component.ts

@ -89,6 +89,13 @@ export class AppComponent implements OnInit {
) )
); );
this.matIconRegistry.addSvgIconLiteral(
'apple-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 256 315"><path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path></svg>'
)
);
this.storageService.testLocalStorage(); this.storageService.testLocalStorage();
this.setupTranslate(); this.setupTranslate();

28
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts

@ -98,24 +98,17 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
this.data.widgets.forEach((widget) => { this.data.widgets.forEach((widget) => {
if (widget.type === widgetType.rpc) { if (widget.type === widgetType.rpc) {
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; this.addWidgetTitleToWidgetsMap(widget.config.targetDeviceAliasIds[0], widget.config.title);
widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId]; }
if (!widgetsTitleList) { } else if (widget.type === widgetType.alarm) {
widgetsTitleList = []; if (widget.config.alarmSource) {
this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; this.addWidgetTitleToWidgetsMap(widget.config.alarmSource.entityAliasId, widget.config.title);
}
widgetsTitleList.push(widget.config.title);
} }
} else { } else {
const datasources = this.utils.validateDatasources(widget.config.datasources); const datasources = this.utils.validateDatasources(widget.config.datasources);
datasources.forEach((datasource) => { datasources.forEach((datasource) => {
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { if (datasource.type === DatasourceType.entity && datasource.entityAliasId) {
widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId]; this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title);
if (!widgetsTitleList) {
widgetsTitleList = [];
this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList;
}
widgetsTitleList.push(widget.config.title);
} }
}); });
} }
@ -141,6 +134,15 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
}); });
} }
private addWidgetTitleToWidgetsMap(aliasId: string, widgetTitle: string) {
let widgetsTitleList: Array<string> = this.aliasToWidgetsMap[aliasId];
if (!widgetsTitleList) {
widgetsTitleList = [];
this.aliasToWidgetsMap[aliasId] = widgetsTitleList;
}
widgetsTitleList.push(widgetTitle);
}
private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl { private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl {
const aliasFormControl = this.fb.group({ const aliasFormControl = this.fb.group({
id: [aliasId], id: [aliasId],

2
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -63,7 +63,7 @@
</mat-menu> </mat-menu>
<div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
<gridster #gridster id="gridster-child" [options]="gridsterOpts"> <gridster #gridster id="gridster-child" [options]="gridsterOpts">
<gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets"> <gridster-item [item]="widget" [ngClass]="{'tb-noselect': isEdit}" *ngFor="let widget of dashboardWidgets">
<tb-widget-container <tb-widget-container
[widget]="widget" [widget]="widget"
[dashboardWidgets]="dashboardWidgets" [dashboardWidgets]="dashboardWidgets"

2
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -528,7 +528,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
const col = this.entitiesTableConfig.columns.indexOf(column); const col = this.entitiesTableConfig.columns.indexOf(column);
const index = row * this.entitiesTableConfig.columns.length + col; const index = row * this.entitiesTableConfig.columns.length + col;
let res = this.cellContentCache[index]; let res = this.cellContentCache[index];
if (!res) { if (isUndefined(res)) {
res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
this.cellContentCache[index] = res; this.cellContentCache[index] = res;
} }

3
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -46,8 +46,9 @@
formControlName="defaultRuleChainId"> formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete> </tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete <tb-dashboard-autocomplete
placeholder="{{'device-profile.default-dashboard' | translate}}" placeholder="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId"> formControlName="defaultDashboardId">
<div tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</div>
</tb-dashboard-autocomplete> </tb-dashboard-autocomplete>
<tb-queue-type-list <tb-queue-type-list
[queueType]="serviceType" [queueType]="serviceType"

12
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -35,4 +35,16 @@
<mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon> <mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
</button> </button>
</div> </div>
<div *ngIf="!disabled || alarmRuleFormGroup.get('dashboardId').value" fxLayout="column" fxLayoutAlign="start start"
fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
<span class="tb-alarm-rule-dashboard title">
{{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }}
</span>
<tb-dashboard-autocomplete class="tb-alarm-rule-dashboard dashboard"
floatLabel="never"
placeholder="{{ 'device-profile.alarm-rule-no-mobile-dashboard' | translate }}"
formControlName="dashboardId">
<div tb-hint>{{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}}</div>
</tb-dashboard-autocomplete>
</div>
</div> </div>

18
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss

@ -18,16 +18,24 @@
.row { .row {
margin-top: 1em; margin-top: 1em;
} }
.tb-alarm-rule-details { .tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px; padding: 4px;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.title { &.title {
opacity: 0.7; opacity: 0.7;
overflow: visible; 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;
}
}
} }

12
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts

@ -34,6 +34,7 @@ import {
EditAlarmDetailsDialogData EditAlarmDetailsDialogData
} from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; } from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
@Component({ @Component({
selector: 'tb-alarm-rule', selector: 'tb-alarm-rule',
@ -92,7 +93,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
this.alarmRuleFormGroup = this.fb.group({ this.alarmRuleFormGroup = this.fb.group({
condition: [null, [Validators.required]], condition: [null, [Validators.required]],
schedule: [null], schedule: [null],
alarmDetails: [null] alarmDetails: [null],
dashboardId: [null]
}); });
this.alarmRuleFormGroup.valueChanges.subscribe(() => { this.alarmRuleFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
@ -110,7 +112,11 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
writeValue(value: AlarmRule): void { writeValue(value: AlarmRule): void {
this.modelValue = value; 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) { public openEditDetailsDialog($event: Event) {
@ -143,7 +149,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
private updateModel() { private updateModel() {
const value = this.alarmRuleFormGroup.value; const value = this.alarmRuleFormGroup.value;
if (this.modelValue) { 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); this.propagateChange(this.modelValue);
} }
} }

3
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -60,8 +60,9 @@
formControlName="defaultRuleChainId"> formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete> </tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete <tb-dashboard-autocomplete
placeholder="{{'device-profile.default-dashboard' | translate}}" placeholder="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId"> formControlName="defaultDashboardId">
<div tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</div>
</tb-dashboard-autocomplete> </tb-dashboard-autocomplete>
<tb-queue-type-list <tb-queue-type-list
[queueType]="serviceType" [queueType]="serviceType"

10
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html

@ -81,9 +81,9 @@
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header [disabled]="isSorting(column)"> <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header [disabled]="isSorting(column)">
{{ column.title }} {{ column.title }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let alarm;" <mat-cell *matCellDef="let alarm; let row = index"
[innerHTML]="cellContent(alarm, column)" [innerHTML]="cellContent(alarm, column, row)"
[ngStyle]="cellStyle(alarm, column)"> [ngStyle]="cellStyle(alarm, column, row)">
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction"> <ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
@ -125,8 +125,8 @@
'mat-selected': alarmsDatasource.isSelected(alarm), 'mat-selected': alarmsDatasource.isSelected(alarm),
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm), 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
'invisible': alarmsDatasource.dataLoading}" 'invisible': alarmsDatasource.dataLoading}"
*matRowDef="let alarm; columns: displayedColumns;" *matRowDef="let alarm; columns: displayedColumns; let row = index"
[ngStyle]="rowStyle(alarm)" [ngStyle]="rowStyle(alarm, row)"
(click)="onRowClick($event, alarm)"></mat-row> (click)="onRowClick($event, alarm)"></mat-row>
</table> </table>
<span [fxShow]="(alarmsDatasource.isEmpty() | async) && !alarmsDatasource.dataLoading" <span [fxShow]="(alarmsDatasource.isEmpty() | async) && !alarmsDatasource.dataLoading"

170
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -35,7 +35,15 @@ import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/wi
import { IWidgetSubscription } from '@core/api/widget-api.models'; import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber, isObject } from '@core/utils'; import {
createLabelFromDatasource,
deepClone,
hashCode,
isDefined,
isNumber,
isObject,
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { sortItems } from '@shared/models/page/page-link'; import { sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
@ -155,6 +163,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public actionCellDescriptors: AlarmWidgetActionDescriptor[] = []; public actionCellDescriptors: AlarmWidgetActionDescriptor[] = [];
public alarmsDatasource: AlarmsDatasource; public alarmsDatasource: AlarmsDatasource;
private cellContentCache: Array<any> = [];
private cellStyleCache: Array<any> = [];
private rowStyleCache: Array<any> = [];
private settings: AlarmsTableWidgetSettings; private settings: AlarmsTableWidgetSettings;
private widgetConfig: WidgetConfig; private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
@ -261,6 +273,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public onDataUpdated() { public onDataUpdated() {
this.updateTitle(true); this.updateTitle(true);
this.alarmsDatasource.updateAlarms(); this.alarmsDatasource.updateAlarms();
this.clearCache();
} }
public pageLinkSortDirection(): SortDirection { public pageLinkSortDirection(): SortDirection {
@ -426,7 +439,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');
} }
this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys); this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys, this.ngZone);
if (this.enableSelection) { if (this.enableSelection) {
this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => { this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => {
const hideTitlePanel = selectionMode || this.textSearchMode; const hideTitlePanel = selectionMode || this.textSearchMode;
@ -484,6 +497,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (this.actionCellDescriptors.length) { if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');
} }
this.clearCache();
} }
} as DisplayColumnsPanelData } as DisplayColumnsPanelData
}, },
@ -609,91 +623,100 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
return widthStyle(columnWidth); return widthStyle(columnWidth);
} }
public rowStyle(alarm: AlarmDataInfo): any { public rowStyle(alarm: AlarmDataInfo, row: number): any {
let style: any = {}; let res = this.rowStyleCache[row];
if (alarm) { if (!res) {
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { res = {};
if (alarm && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
try { try {
style = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx); res = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx);
if (!isObject(style)) { if (!isObject(res)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); 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`); throw new TypeError(`Array instead of style object`);
} }
} catch (e) { } catch (e) {
style = {}; res = {};
console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your row style function.`); `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 { public cellStyle(alarm: AlarmDataInfo, key: EntityColumn, row: number): any {
let style: any = {}; const col = this.columns.indexOf(key);
if (alarm && key) { const index = row * this.columns.length + col;
const styleInfo = this.stylesInfo[key.def]; let res = this.cellStyleCache[index];
const value = getAlarmValue(alarm, key); if (!res) {
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { res = {};
try { if (alarm && key) {
style = styleInfo.cellStyleFunction(value, alarm, this.ctx); const styleInfo = this.stylesInfo[key.def];
if (!isObject(style)) { const value = getAlarmValue(alarm, key);
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
} try {
if (Array.isArray(style)) { res = styleInfo.cellStyleFunction(value, alarm, this.ctx);
throw new TypeError(`Array instead of style object`); 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) { } else {
style = {}; res = this.defaultStyle(key, value);
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.defaultStyle(key, value);
} }
this.cellStyleCache[index] = res;
} }
if (!style.width) { if (!res.width) {
const columnWidth = this.columnWidth[key.def]; 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 { public cellContent(alarm: AlarmDataInfo, key: EntityColumn, row: number): SafeHtml {
if (alarm && key) { const col = this.columns.indexOf(key);
const contentInfo = this.contentsInfo[key.def]; const index = row * this.columns.length + col;
const value = getAlarmValue(alarm, key); let res = this.cellContentCache[index];
let content = ''; if (isUndefined(res)) {
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { res = '';
try { if (alarm && key) {
content = contentInfo.cellContentFunction(value, alarm, this.ctx); const contentInfo = this.contentsInfo[key.def];
} catch (e) { const value = getAlarmValue(alarm, key);
content = '' + value; 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)) { if (isDefined(content)) {
return ''; content = this.utils.customTranslation(content, content);
switch (typeof content) {
} else { case 'string':
content = this.utils.customTranslation(content, content); res = this.domSanitizer.bypassSecurityTrustHtml(content);
switch (typeof content) { break;
case 'string': default:
return this.domSanitizer.bypassSecurityTrustHtml(content); res = content;
default: }
return content;
} }
} }
this.cellContentCache[index] = res;
} else {
return '';
} }
return res;
} }
public onRowClick($event: Event, alarm: AlarmDataInfo) { public onRowClick($event: Event, alarm: AlarmDataInfo) {
@ -936,6 +959,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
isSorting(column: EntityColumn): boolean { isSorting(column: EntityColumn): boolean {
return column.type === DataKeyType.alarm && column.name.startsWith('details.'); 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<AlarmDataInfo> { class AlarmsDatasource implements DataSource<AlarmDataInfo> {
@ -957,7 +986,8 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
private appliedSortOrderLabel: string; private appliedSortOrderLabel: string;
constructor(private subscription: IWidgetSubscription, constructor(private subscription: IWidgetSubscription,
private dataKeys: Array<DataKey>) { private dataKeys: Array<DataKey>,
private ngZone: NgZone) {
} }
connect(collectionViewer: CollectionViewer): Observable<AlarmDataInfo[] | ReadonlyArray<AlarmDataInfo>> { connect(collectionViewer: CollectionViewer): Observable<AlarmDataInfo[] | ReadonlyArray<AlarmDataInfo>> {
@ -989,6 +1019,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
updateAlarms() { updateAlarms() {
const subscriptionAlarms = this.subscription.alarms; const subscriptionAlarms = this.subscription.alarms;
let alarms = new Array<AlarmDataInfo>(); let alarms = new Array<AlarmDataInfo>();
let isEmptySelection = false;
subscriptionAlarms.data.forEach((alarmData) => { subscriptionAlarms.data.forEach((alarmData) => {
alarms.push(this.alarmDataToInfo(alarmData)); alarms.push(this.alarmDataToInfo(alarmData));
}); });
@ -1001,7 +1032,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
const toRemove = this.selection.selected.filter(alarmId => alarmIds.indexOf(alarmId) === -1); const toRemove = this.selection.selected.filter(alarmId => alarmIds.indexOf(alarmId) === -1);
this.selection.deselect(...toRemove); this.selection.deselect(...toRemove);
if (this.selection.isEmpty()) { if (this.selection.isEmpty()) {
this.onSelectionModeChanged(false); isEmptySelection = true;
} }
} }
const alarmsPageData: PageData<AlarmDataInfo> = { const alarmsPageData: PageData<AlarmDataInfo> = {
@ -1010,9 +1041,14 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
totalElements: subscriptionAlarms.totalElements, totalElements: subscriptionAlarms.totalElements,
hasNext: subscriptionAlarms.hasNext hasNext: subscriptionAlarms.hasNext
}; };
this.alarmsSubject.next(alarms); this.ngZone.run(() => {
this.pageDataSubject.next(alarmsPageData); if (isEmptySelection) {
this.dataLoading = false; this.onSelectionModeChanged(false);
}
this.alarmsSubject.next(alarms);
this.pageDataSubject.next(alarmsPageData);
this.dataLoading = false;
});
} }
private alarmDataToInfo(alarmData: AlarmData): AlarmDataInfo { private alarmDataToInfo(alarmData: AlarmData): AlarmDataInfo {

10
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -42,9 +42,9 @@
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;"> <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell> <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let entity;" <mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column)" [innerHTML]="cellContent(entity, column, row)"
[ngStyle]="cellStyle(entity, column)"> [ngStyle]="cellStyle(entity, column, row)">
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction"> <ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
@ -84,8 +84,8 @@
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity), <mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity),
'invisible': entityDatasource.dataLoading}" 'invisible': entityDatasource.dataLoading}"
*matRowDef="let entity; columns: displayedColumns;" *matRowDef="let entity; columns: displayedColumns; let row = index"
[ngStyle]="rowStyle(entity)" [ngStyle]="rowStyle(entity, row)"
(click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row> (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
</table> </table>
<span [fxShow]="(entityDatasource.isEmpty() | async) && !entityDatasource.dataLoading" <span [fxShow]="(entityDatasource.isEmpty() | async) && !entityDatasource.dataLoading"

176
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -40,7 +40,16 @@ import {
import { IWidgetSubscription } from '@core/api/widget-api.models'; import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber, isObject } from '@core/utils'; import {
createLabelFromDatasource,
deepClone,
hashCode,
isDefined,
isNumber,
isObject,
isString,
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -131,6 +140,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public actionCellDescriptors: WidgetActionDescriptor[]; public actionCellDescriptors: WidgetActionDescriptor[];
public entityDatasource: EntityDatasource; public entityDatasource: EntityDatasource;
private cellContentCache: Array<any> = [];
private cellStyleCache: Array<any> = [];
private rowStyleCache: Array<any> = [];
private settings: EntitiesTableWidgetSettings; private settings: EntitiesTableWidgetSettings;
private widgetConfig: WidgetConfig; private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
@ -222,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public onDataUpdated() { public onDataUpdated() {
this.updateTitle(true); this.updateTitle(true);
this.entityDatasource.dataUpdated(); this.entityDatasource.dataUpdated();
this.clearCache();
} }
public pageLinkSortDirection(): SortDirection { public pageLinkSortDirection(): SortDirection {
@ -415,8 +429,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.actionCellDescriptors.length) { if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');
} }
this.entityDatasource = new EntityDatasource( this.entityDatasource = new EntityDatasource(this.translate, dataKeys, this.subscription, this.ngZone);
this.translate, dataKeys, this.subscription);
} }
private editColumnsToDisplay($event: Event) { private editColumnsToDisplay($event: Event) {
@ -460,6 +473,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.actionCellDescriptors.length) { if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');
} }
this.clearCache();
} }
} as DisplayColumnsPanelData } as DisplayColumnsPanelData
}, },
@ -535,91 +549,98 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
return widthStyle(columnWidth); return widthStyle(columnWidth);
} }
public rowStyle(entity: EntityData): any { public rowStyle(entity: EntityData, row: number): any {
let style: any = {}; let res = this.rowStyleCache[row];
if (entity) { if (!res) {
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { res = {};
if (entity && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
try { try {
style = this.rowStylesInfo.rowStyleFunction(entity, this.ctx); res = this.rowStylesInfo.rowStyleFunction(entity, this.ctx);
if (!isObject(style)) { if (!isObject(res)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); 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`); throw new TypeError(`Array instead of style object`);
} }
} catch (e) { } catch (e) {
style = {}; res = {};
console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your row style function.`); `returns '${e}'. Please check your row style function.`);
} }
} else {
style = {};
} }
} this.rowStyleCache[row] = res;
return style; }
} return res;
}
public cellStyle(entity: EntityData, key: EntityColumn): any {
let style: any = {}; public cellStyle(entity: EntityData, key: EntityColumn, row: number): any {
if (entity && key) { const col = this.columns.indexOf(key);
const styleInfo = this.stylesInfo[key.def]; const index = row * this.columns.length + col;
const value = getEntityValue(entity, key); let res = this.cellStyleCache[index];
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { if (!res) {
try { res = {};
style = styleInfo.cellStyleFunction(value, entity, this.ctx); if (entity && key) {
if (!isObject(style)) { const styleInfo = this.stylesInfo[key.def];
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); const value = getEntityValue(entity, key);
} if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
if (Array.isArray(style)) { try {
throw new TypeError(`Array instead of style object`); 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 { this.cellStyleCache[index] = res;
style = {};
} }
} }
if (!style.width) { if (!res.width) {
const columnWidth = this.columnWidth[key.def]; const columnWidth = this.columnWidth[key.def];
style = {...style, ...widthStyle(columnWidth)}; res = Object.assign(res, widthStyle(columnWidth));
} }
return style; return res;
} }
public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { public cellContent(entity: EntityData, key: EntityColumn, row: number): SafeHtml {
if (entity && key) { const col = this.columns.indexOf(key);
const contentInfo = this.contentsInfo[key.def]; const index = row * this.columns.length + col;
const value = getEntityValue(entity, key); let res = this.cellContentCache[index];
let content: string; if (isUndefined(res)) {
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { res = '';
try { if (entity && key) {
content = contentInfo.cellContentFunction(value, entity, this.ctx); const contentInfo = this.contentsInfo[key.def];
} catch (e) { const value = getEntityValue(entity, key);
content = '' + value; 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)) { if (isDefined(content)) {
return ''; content = this.utils.customTranslation(content, content);
switch (typeof content) {
} else { case 'string':
content = this.utils.customTranslation(content, content); res = this.domSanitizer.bypassSecurityTrustHtml(content);
switch (typeof content) { break;
case 'string': default:
return this.domSanitizer.bypassSecurityTrustHtml(content); res = content;
default: }
return content;
} }
} }
this.cellContentCache[index] = res;
} else {
return '';
} }
return res;
} }
private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { 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); 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<EntityData> { class EntityDatasource implements DataSource<EntityData> {
@ -689,7 +716,8 @@ class EntityDatasource implements DataSource<EntityData> {
constructor( constructor(
private translate: TranslateService, private translate: TranslateService,
private dataKeys: Array<DataKey>, private dataKeys: Array<DataKey>,
private subscription: IWidgetSubscription private subscription: IWidgetSubscription,
private ngZone: NgZone
) { ) {
} }
@ -732,9 +760,11 @@ class EntityDatasource implements DataSource<EntityData> {
totalElements: datasourcesPageData.totalElements, totalElements: datasourcesPageData.totalElements,
hasNext: datasourcesPageData.hasNext hasNext: datasourcesPageData.hasNext
}; };
this.entitiesSubject.next(entities); this.ngZone.run(() => {
this.pageDataSubject.next(entitiesPageData); this.entitiesSubject.next(entities);
this.dataLoading = false; this.pageDataSubject.next(entitiesPageData);
this.dataLoading = false;
});
} }
private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData { private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData {

18
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -39,23 +39,23 @@
</mat-toolbar> </mat-toolbar>
<mat-tab-group [ngClass]="{'tb-headless': sources.length === 1}" fxFlex <mat-tab-group [ngClass]="{'tb-headless': sources.length === 1}" fxFlex
[(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()">
<mat-tab *ngFor="let source of sources; trackBy: trackBySourcesIndex; let index = index;" label="{{getTabLabel(source)}}"> <mat-tab *ngFor="let source of sources; trackBy: trackBySourcesIndex; let index = index;" [label]="getTabLabel(source)">
<ng-template [ngIf]="isActiveTab(index)"> <ng-template [ngIf]="isActiveTab(index)">
<div fxFlex class="table-container"> <div fxFlex class="table-container">
<table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp" <table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"
matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear>
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'"> <ng-container *ngIf="showTimestamp" [matColumnDef]="'0'">
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
<mat-cell *matCellDef="let row;" <mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, 0, row, row[0])" [innerHTML]="cellContent(source, 0, row, row[0], rowIndex)"
[ngStyle]="cellStyle(source, 0, row, row[0])"> [ngStyle]="cellStyle(source, 0, row, row[0], rowIndex)">
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;"> <ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ h.dataKey.label }} </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header> {{ h.dataKey.label }} </mat-header-cell>
<mat-cell *matCellDef="let row;" <mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, h.index, row, row[h.index])" [innerHTML]="cellContent(source, h.index, row, row[h.index], rowIndex)"
[ngStyle]="cellStyle(source, h.index, row, row[h.index])"> [ngStyle]="cellStyle(source, h.index, row, row[h.index], rowIndex)">
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction"> <ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
@ -93,8 +93,8 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: enableStickyHeader"></mat-header-row> <mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row *matRowDef="let row; columns: source.displayedColumns;" <mat-row *matRowDef="let row; columns: source.displayedColumns; let rowIndex = index"
[ngStyle]="rowStyle(source, row)" [ngStyle]="rowStyle(source, row, rowIndex)"
(click)="onRowClick($event, row)"></mat-row> (click)="onRowClick($event, row)"></mat-row>
</table> </table>
<span [fxShow]="source.timeseriesDatasource.isEmpty() | async" <span [fxShow]="source.timeseriesDatasource.isEmpty() | async"

180
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -40,7 +40,7 @@ import {
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { hashCode, isDefined, isNumber, isObject } from '@core/utils'; import { hashCode, isDefined, isNumber, isObject, isUndefined } from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@ -120,6 +120,10 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public sources: TimeseriesTableSource[]; public sources: TimeseriesTableSource[];
public sourceIndex: number; public sourceIndex: number;
private cellContentCache: Array<any> = [];
private cellStyleCache: Array<any> = [];
private rowStyleCache: Array<any> = [];
private settings: TimeseriesTableWidgetSettings; private settings: TimeseriesTableWidgetSettings;
private widgetConfig: WidgetConfig; private widgetConfig: WidgetConfig;
private data: Array<DatasourceData>; private data: Array<DatasourceData>;
@ -194,6 +198,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public onDataUpdated() { public onDataUpdated() {
this.updateCurrentSourceData(); this.updateCurrentSourceData();
this.clearCache();
} }
private initialize() { private initialize() {
@ -235,7 +240,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
public getTabLabel(source: TimeseriesTableSource){ public getTabLabel(source: TimeseriesTableSource){
if(this.useEntityLabel){ if (this.useEntityLabel) {
return source.datasource.entityLabel || source.datasource.entityName; return source.datasource.entityLabel || source.datasource.entityName;
} else { } else {
return source.datasource.entityName; return source.datasource.entityName;
@ -286,7 +291,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
if (this.actionCellDescriptors.length) { if (this.actionCellDescriptors.length) {
source.displayedColumns.push('actions'); 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); tsDatasource.dataUpdated(this.data);
this.sources.push(source); this.sources.push(source);
} }
@ -337,6 +342,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
onSourceIndexChanged() { onSourceIndexChanged() {
this.updateCurrentSourceData(); this.updateCurrentSourceData();
this.updateActiveEntityInfo(); this.updateActiveEntityInfo();
this.clearCache();
} }
private enterFilterMode() { private enterFilterMode() {
@ -378,6 +384,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
source.pageLink.sortOrder.property = sort.active; source.pageLink.sortOrder.property = sort.active;
source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()];
source.timeseriesDatasource.loadRows(); source.timeseriesDatasource.loadRows();
this.clearCache();
this.ctx.detectChanges(); this.ctx.detectChanges();
} }
@ -397,94 +404,109 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
return source.datasource.entityId; return source.datasource.entityId;
} }
public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow): any { public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): any {
let style: any = {}; let res = this.rowStyleCache[index];
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { if (!res) {
try { res = {};
const rowData = source.rowDataTemplate; if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
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) {
try { try {
const rowData = source.rowDataTemplate; const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0]; rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => { source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index]; rowData[headerInfo.dataKey.name] = row[headerInfo.index];
}); });
style = styleInfo.cellStyleFunction(value, rowData, this.ctx); res = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx);
if (!isObject(style)) { if (!isObject(res)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); 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`); throw new TypeError(`Array instead of style object`);
} }
} catch (e) { } catch (e) {
style = {}; res = {};
console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` + console.warn(`Row style function in widget ` +
`'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`); `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`);
} }
} }
this.rowStyleCache[index] = res;
} }
return style; return res;
} }
public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml { public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): any {
if (index === 0) { const cacheIndex = rowIndex * (source.header.length + 1) + index;
return row.formattedTs; let res = this.cellStyleCache[cacheIndex];
} else { if (!res) {
let content; res = {};
const contentInfo = source.contentsInfo[index - 1]; if (index > 0) {
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { const styleInfo = source.stylesInfo[index - 1];
try { if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
const rowData = source.rowDataTemplate; try {
rowData.Timestamp = row[0]; const rowData = source.rowDataTemplate;
source.header.forEach((headerInfo) => { rowData.Timestamp = row[0];
rowData[headerInfo.dataKey.name] = row[headerInfo.index]; source.header.forEach((headerInfo) => {
}); rowData[headerInfo.dataKey.name] = row[headerInfo.index];
content = contentInfo.cellContentFunction(value, rowData, this.ctx); });
} catch (e) { res = styleInfo.cellStyleFunction(value, rowData, this.ctx);
content = '' + value; 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)) { public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml {
return ''; const cacheIndex = rowIndex * (source.header.length + 1) + index ;
let res = this.cellContentCache[cacheIndex];
if (isUndefined(res)) {
res = '';
if (index === 0) {
res = row.formattedTs;
} else { } else {
content = this.utils.customTranslation(content, content); let content;
switch (typeof content) { const contentInfo = source.contentsInfo[index - 1];
case 'string': if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
return this.domSanitizer.bypassSecurityTrustHtml(content); try {
default: const rowData = source.rowDataTemplate;
return content; 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) { public onRowClick($event: Event, row: TimeseriesRow) {
@ -530,6 +552,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private loadCurrentSourceRow() { private loadCurrentSourceRow() {
this.sources[this.sourceIndex].timeseriesDatasource.loadRows(); 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<TimeseriesRow> {
private source: TimeseriesTableSource, private source: TimeseriesTableSource,
private hideEmptyLines: boolean, private hideEmptyLines: boolean,
private dateFormatFilter: string, private dateFormatFilter: string,
private datePipe: DatePipe private datePipe: DatePipe,
private ngZone: NgZone
) { ) {
this.source.timeseriesDatasource = this; this.source.timeseriesDatasource = this;
} }
@ -568,8 +598,10 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
catchError(() => of(emptyPageData<TimeseriesRow>())), catchError(() => of(emptyPageData<TimeseriesRow>())),
).subscribe( ).subscribe(
(pageData) => { (pageData) => {
this.rowsSubject.next(pageData.data); this.ngZone.run(() => {
this.pageDataSubject.next(pageData); this.rowsSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
});
} }
); );
} }

8
ui-ngx/src/app/modules/home/components/widget/widget.component.scss

@ -59,5 +59,13 @@
#widget-container { #widget-container {
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
canvas {
user-select: none;
&::selection, &::-moz-selection {
background-color: transparent;
}
}
} }
} }

1
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.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance;
this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement); this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement);
this.widgetContext.$container.css('display', 'block'); this.widgetContext.$container.css('display', 'block');
this.widgetContext.$container.css('user-select', 'none');
this.widgetContext.$container.attr('id', 'container'); this.widgetContext.$container.attr('id', 'container');
if (this.widgetSizeDetected) { if (this.widgetSizeDetected) {
this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); this.widgetContext.$container.css('height', this.widgetContext.height + 'px');

5
ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html

@ -321,16 +321,13 @@
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.user-info-uri</mat-label> <mat-label translate>admin.oauth2.user-info-uri</mat-label>
<input matInput formControlName="userInfoUri" required> <input matInput formControlName="userInfoUri">
<button mat-icon-button matSuffix <button mat-icon-button matSuffix
type="button" type="button"
(click)="toggleEditMode(registration, 'userInfoUri')" (click)="toggleEditMode(registration, 'userInfoUri')"
*ngIf="!isCustomProvider(registration)"> *ngIf="!isCustomProvider(registration)">
<mat-icon class="material-icons">create</mat-icon> <mat-icon class="material-icons">create</mat-icon>
</button> </button>
<mat-error *ngIf="registration.get('userInfoUri').hasError('required')">
{{ 'admin.oauth2.user-info-uri-required' | translate }}
</mat-error>
<mat-error *ngIf="registration.get('userInfoUri').hasError('pattern')"> <mat-error *ngIf="registration.get('userInfoUri').hasError('pattern')">
{{ 'admin.oauth2.uri-pattern-error' | translate }} {{ 'admin.oauth2.uri-pattern-error' | translate }}
</mat-error> </mat-error>

3
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), scope: this.fb.array(registration?.scope ? registration.scope : [], OAuth2SettingsComponent.validateScope),
jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)],
userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '', userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '',
[Validators.required, [Validators.pattern(this.URL_REGEXP)]],
Validators.pattern(this.URL_REGEXP)]],
clientAuthenticationMethod: [ clientAuthenticationMethod: [
registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST,
Validators.required], Validators.required],

18
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 { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { OtaUpdateType } from '@shared/models/ota-package.models'; import { OtaUpdateType } from '@shared/models/ota-package.models';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-device', selector: 'tb-device',
@ -78,7 +79,7 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
} }
buildForm(entity: DeviceInfo): FormGroup { buildForm(entity: DeviceInfo): FormGroup {
return this.fb.group( const form = this.fb.group(
{ {
name: [entity ? entity.name : '', [Validators.required]], name: [entity ? entity.name : '', [Validators.required]],
deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]],
@ -95,6 +96,17 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
) )
} }
); );
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) { updateForm(entity: DeviceInfo) {
@ -156,10 +168,6 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
this.entityForm.markAsDirty(); this.entityForm.markAsDirty();
} }
} }
this.entityForm.patchValue({
firmwareId: null,
softwareId: null
});
} }
} }
} }

2
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html

@ -149,7 +149,7 @@
<input matInput formControlName="url" <input matInput formControlName="url"
type="text" type="text"
[required]="entityForm.get('resource').value === 'url'"> [required]="entityForm.get('resource').value === 'url'">
<mat-error *ngIf="entityForm.get('url').hasError('required')" translate> <mat-error *ngIf="entityForm.get('url').hasError('required') || entityForm.get('url').hasError('pattern')" translate>
ota-update.direct-url-required ota-update.direct-url-required
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>

7
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts

@ -67,7 +67,7 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
this.entityForm.get('file').updateValueAndValidity({emitEvent: false}); this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
} else { } else {
this.entityForm.get('file').clearValidators(); 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('file').updateValueAndValidity({emitEvent: false});
this.entityForm.get('url').updateValueAndValidity({emitEvent: false}); this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
} }
@ -172,6 +172,11 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
} }
prepareFormValue(formValue: any): any { prepareFormValue(formValue: any): any {
if (formValue.resource === 'url') {
delete formValue.file;
} else {
delete formValue.url;
}
delete formValue.resource; delete formValue.resource;
delete formValue.generateChecksum; delete formValue.generateChecksum;
return super.prepareFormValue(formValue); return super.prepareFormValue(formValue);

2
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="selectDashboardFormGroup" class="mat-block"> <mat-form-field [formGroup]="selectDashboardFormGroup" class="mat-block" [floatLabel]="floatLabel">
<input matInput type="text" placeholder="{{ placeholder || ('dashboard.dashboard' | translate) }}" <input matInput type="text" placeholder="{{ placeholder || ('dashboard.dashboard' | translate) }}"
#dashboardInput #dashboardInput
formControlName="dashboard" formControlName="dashboard"

4
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts

@ -29,6 +29,7 @@ import { getCurrentAuthUser } from '@app/core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FloatLabelType } from '@angular/material/form-field/form-field';
@Component({ @Component({
selector: 'tb-dashboard-autocomplete', selector: 'tb-dashboard-autocomplete',
@ -64,6 +65,9 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
@Input() @Input()
customerId: string; customerId: string;
@Input()
floatLabel: FloatLabelType = 'auto';
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue; return this.requiredValue;

3
ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.html

@ -21,7 +21,8 @@
formControlName="packageId" formControlName="packageId"
(focusin)="onFocus()" (focusin)="onFocus()"
[required]="required" [required]="required"
[matAutocomplete]="packageAutocomplete"> [matAutocomplete]="packageAutocomplete"
[matAutocompleteDisabled]="disabled">
<button *ngIf="otaPackageFormGroup.get('packageId').value && !disabled" <button *ngIf="otaPackageFormGroup.get('packageId').value && !disabled"
type="button" type="button"
matSuffix mat-button mat-icon-button aria-label="Clear" matSuffix mat-button mat-icon-button aria-label="Clear"

2
ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts

@ -222,7 +222,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
} }
clear() { clear() {
this.otaPackageFormGroup.get('packageId').patchValue('', {emitEvent: true}); this.otaPackageFormGroup.get('packageId').patchValue('', {emitEvent: false});
setTimeout(() => { setTimeout(() => {
this.packageInput.nativeElement.blur(); this.packageInput.nativeElement.blur();
this.packageInput.nativeElement.focus(); this.packageInput.nativeElement.focus();

1
ui-ngx/src/app/shared/models/device.models.ts

@ -428,6 +428,7 @@ export interface CustomTimeSchedulerItem{
export interface AlarmRule { export interface AlarmRule {
condition: AlarmCondition; condition: AlarmCondition;
alarmDetails?: string; alarmDetails?: string;
dashboardId?: DashboardId;
schedule?: AlarmSchedule; schedule?: AlarmSchedule;
} }

3
ui-ngx/src/app/shared/models/oauth2.models.ts

@ -54,7 +54,8 @@ export const domainSchemaTranslations = new Map<DomainSchema, string>(
export enum MapperConfigType{ export enum MapperConfigType{
BASIC = 'BASIC', BASIC = 'BASIC',
CUSTOM = 'CUSTOM', CUSTOM = 'CUSTOM',
GITHUB = 'GITHUB' GITHUB = 'GITHUB',
APPLE = 'APPLE'
} }
export enum TenantNameStrategy{ export enum TenantNameStrategy{

6
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1070,7 +1070,8 @@
"profile-configuration": "Profile configuration", "profile-configuration": "Profile configuration",
"transport-configuration": "Transport configuration", "transport-configuration": "Transport configuration",
"default-rule-chain": "Default rule chain", "default-rule-chain": "Default rule chain",
"default-dashboard": "Default dashboard", "mobile-dashboard": "Mobile dashboard",
"mobile-dashboard-hint": "Used by mobile application as a device details dashboard",
"select-queue-hint": "Select from a drop-down list or add a custom name.", "select-queue-hint": "Select from a drop-down list or add a custom name.",
"delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?",
"delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.", "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.",
@ -1149,6 +1150,9 @@
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
"alarm-rule-details": "Details", "alarm-rule-details": "Details",
"add-alarm-rule-details": "Add details", "add-alarm-rule-details": "Add details",
"alarm-rule-mobile-dashboard": "Mobile dashboard",
"alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard",
"alarm-rule-no-mobile-dashboard": "No dashboard selected",
"propagate-alarm": "Propagate alarm", "propagate-alarm": "Propagate alarm",
"alarm-rule-relation-types-list": "Relation types to propagate", "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.", "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.",

Loading…
Cancel
Save