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

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

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

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.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Service(value = "basicOAuth2ClientMapper")
@ -30,7 +31,7 @@ import java.util.Map;
public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
OAuth2MapperConfig config = registration.getMapperConfig();
Map<String, Object> attributes = token.getPrincipal().getAttributes();
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.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
@Service(value = "customOAuth2ClientMapper")
@Slf4j
public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
@ -39,7 +41,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
OAuth2MapperConfig config = registration.getMapperConfig();
OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration);

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.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
@ -46,7 +47,7 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
private OAuth2Configuration oAuth2Configuration;
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
OAuth2MapperConfig config = registration.getMapperConfig();
Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
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.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
public interface OAuth2ClientMapper {
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration);
SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration);
}

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

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

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;
@Component(value = "oauth2AuthenticationFailureHandler")
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
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.getPrincipal().getName());
OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType());
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
registration);
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);

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

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

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

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

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;
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;
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,
CLAIM_REQUEST(),
GET_FIRMWARE_REQUEST,
GET_SOFTWARE_REQUEST;
CLAIM_REQUEST();
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
protected void doCommit() {
consumer.commitAsync();
consumer.commitSync();
}
@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.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.msg.session.SessionMsgType;
@ -139,10 +138,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
processExchangeGetRequest(exchange, featureType.get());
} else if (featureType.get() == FeatureType.ATTRIBUTES) {
processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST);
} else if (featureType.get() == FeatureType.FIRMWARE) {
processRequest(exchange, SessionMsgType.GET_FIRMWARE_REQUEST);
} else if (featureType.get() == FeatureType.SOFTWARE) {
processRequest(exchange, SessionMsgType.GET_SOFTWARE_REQUEST);
} else {
log.trace("Invalid feature type parameter");
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
@ -349,12 +344,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
coapTransportAdaptor.convertToGetAttributes(sessionId, request),
new CoapNoOpCallback(exchange));
break;
case GET_FIRMWARE_REQUEST:
getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.FIRMWARE);
break;
case GET_SOFTWARE_REQUEST:
getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.SOFTWARE);
break;
}
} catch (AdaptorException e) {
log.trace("[{}] Failed to decode message: ", sessionId, e);
@ -366,16 +355,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
return new UUID(sessionInfoProto.getSessionIdMSB(), sessionInfoProto.getSessionIdLSB());
}
private void getOtaPackageCallback(TransportProtos.SessionInfoProto sessionInfo, CoapExchange exchange, OtaPackageType firmwareType) {
TransportProtos.GetOtaPackageRequestMsg requestMsg = TransportProtos.GetOtaPackageRequestMsg.newBuilder()
.setTenantIdMSB(sessionInfo.getTenantIdMSB())
.setTenantIdLSB(sessionInfo.getTenantIdLSB())
.setDeviceIdMSB(sessionInfo.getDeviceIdMSB())
.setDeviceIdLSB(sessionInfo.getDeviceIdLSB())
.setType(firmwareType.name()).build();
transportContext.getTransportService().process(sessionInfo, requestMsg, new OtaPackageCallback(exchange));
}
private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) {
tokenToObserveNotificationSeqMap.remove(token);
return tokenToSessionInfoMap.remove(token);
@ -470,57 +449,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
}
private class OtaPackageCallback implements TransportServiceCallback<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 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.coapserver.CoapServerService;
import org.thingsboard.server.coapserver.TbCoapServerComponent;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource;
import javax.annotation.PostConstruct;
@ -59,6 +60,8 @@ public class CoapTransportService implements TbTransportService {
efento.add(efentoMeasurementsTransportResource);
coapServer.add(api);
coapServer.add(efento);
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.FIRMWARE));
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.SOFTWARE));
log.info("CoAP transport started!");
}

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)
.type(type)
.basic(
(type == MapperType.BASIC || type == MapperType.GITHUB) ?
(type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) ?
OAuth2BasicMapperConfig.builder()
.emailAttributeKey(emailAttributeKey)
.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())) {
throw new DataValidationException("Scope should be specified!");
}
if (StringUtils.isEmpty(clientRegistration.getUserInfoUri())) {
throw new DataValidationException("User info uri should be specified!");
}
if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) {
throw new DataValidationException("User name attribute name should be specified!");
}

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

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

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);
this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish);
message.payload().retain();
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();
}
public OtaPackage saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception {
public OtaPackageInfo saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception {
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.MULTIPART_FORM_DATA);
@ -2993,7 +2993,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
return restTemplate.postForEntity(
baseURL + url, requestEntity, OtaPackage.class, params
baseURL + url, requestEntity, OtaPackageInfo.class, params
).getBody();
}

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

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.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) => {
if (widget.type === widgetType.rpc) {
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId];
if (!widgetsTitleList) {
widgetsTitleList = [];
this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
}
widgetsTitleList.push(widget.config.title);
this.addWidgetTitleToWidgetsMap(widget.config.targetDeviceAliasIds[0], widget.config.title);
}
} else if (widget.type === widgetType.alarm) {
if (widget.config.alarmSource) {
this.addWidgetTitleToWidgetsMap(widget.config.alarmSource.entityAliasId, widget.config.title);
}
} else {
const datasources = this.utils.validateDatasources(widget.config.datasources);
datasources.forEach((datasource) => {
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) {
widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId];
if (!widgetsTitleList) {
widgetsTitleList = [];
this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList;
}
widgetsTitleList.push(widget.config.title);
this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title);
}
});
}
@ -141,6 +134,15 @@ export class EntityAliasesDialogComponent extends DialogComponent<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 {
const aliasFormControl = this.fb.group({
id: [aliasId],

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

@ -63,7 +63,7 @@
</mat-menu>
<div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
<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
[widget]="widget"
[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 index = row * this.entitiesTableConfig.columns.length + col;
let res = this.cellContentCache[index];
if (!res) {
if (isUndefined(res)) {
res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
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">
</tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete
placeholder="{{'device-profile.default-dashboard' | translate}}"
placeholder="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId">
<div tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</div>
</tb-dashboard-autocomplete>
<tb-queue-type-list
[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>
</button>
</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>

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

@ -18,16 +18,24 @@
.row {
margin-top: 1em;
}
.tb-alarm-rule-details {
.tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.title {
opacity: 0.7;
overflow: visible;
}
}
.tb-alarm-rule-details {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
.tb-alarm-rule-dashboard {
&.dashboard {
width: 100%;
max-width: 350px;
}
}
}

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

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

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

@ -60,8 +60,9 @@
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
<tb-dashboard-autocomplete
placeholder="{{'device-profile.default-dashboard' | translate}}"
placeholder="{{'device-profile.mobile-dashboard' | translate}}"
formControlName="defaultDashboardId">
<div tb-hint>{{'device-profile.mobile-dashboard-hint' | translate}}</div>
</tb-dashboard-autocomplete>
<tb-queue-type-list
[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)">
{{ column.title }}
</mat-header-cell>
<mat-cell *matCellDef="let alarm;"
[innerHTML]="cellContent(alarm, column)"
[ngStyle]="cellStyle(alarm, column)">
<mat-cell *matCellDef="let alarm; let row = index"
[innerHTML]="cellContent(alarm, column, row)"
[ngStyle]="cellStyle(alarm, column, row)">
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
@ -125,8 +125,8 @@
'mat-selected': alarmsDatasource.isSelected(alarm),
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
'invisible': alarmsDatasource.dataLoading}"
*matRowDef="let alarm; columns: displayedColumns;"
[ngStyle]="rowStyle(alarm)"
*matRowDef="let alarm; columns: displayedColumns; let row = index"
[ngStyle]="rowStyle(alarm, row)"
(click)="onRowClick($event, alarm)"></mat-row>
</table>
<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 { UtilsService } from '@core/services/utils.service';
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 { sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
@ -155,6 +163,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public actionCellDescriptors: AlarmWidgetActionDescriptor[] = [];
public alarmsDatasource: AlarmsDatasource;
private cellContentCache: Array<any> = [];
private cellStyleCache: Array<any> = [];
private rowStyleCache: Array<any> = [];
private settings: AlarmsTableWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
@ -261,6 +273,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public onDataUpdated() {
this.updateTitle(true);
this.alarmsDatasource.updateAlarms();
this.clearCache();
}
public pageLinkSortDirection(): SortDirection {
@ -426,7 +439,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.displayedColumns.push('actions');
}
this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys);
this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys, this.ngZone);
if (this.enableSelection) {
this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => {
const hideTitlePanel = selectionMode || this.textSearchMode;
@ -484,6 +497,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions');
}
this.clearCache();
}
} as DisplayColumnsPanelData
},
@ -609,91 +623,100 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
return widthStyle(columnWidth);
}
public rowStyle(alarm: AlarmDataInfo): any {
let style: any = {};
if (alarm) {
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
public rowStyle(alarm: AlarmDataInfo, row: number): any {
let res = this.rowStyleCache[row];
if (!res) {
res = {};
if (alarm && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
try {
style = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx);
if (!isObject(style)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
res = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx);
if (!isObject(res)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`);
}
if (Array.isArray(style)) {
if (Array.isArray(res)) {
throw new TypeError(`Array instead of style object`);
}
} catch (e) {
style = {};
res = {};
console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your row style function.`);
}
} else {
style = {};
}
this.rowStyleCache[row] = res;
}
return style;
return res;
}
public cellStyle(alarm: AlarmDataInfo, key: EntityColumn): any {
let style: any = {};
if (alarm && key) {
const styleInfo = this.stylesInfo[key.def];
const value = getAlarmValue(alarm, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
style = styleInfo.cellStyleFunction(value, alarm, this.ctx);
if (!isObject(style)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
}
if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`);
public cellStyle(alarm: AlarmDataInfo, key: EntityColumn, row: number): any {
const col = this.columns.indexOf(key);
const index = row * this.columns.length + col;
let res = this.cellStyleCache[index];
if (!res) {
res = {};
if (alarm && key) {
const styleInfo = this.stylesInfo[key.def];
const value = getAlarmValue(alarm, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
res = styleInfo.cellStyleFunction(value, alarm, this.ctx);
if (!isObject(res)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`);
}
if (Array.isArray(res)) {
throw new TypeError(`Array instead of style object`);
}
} catch (e) {
res = {};
console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your cell style function.`);
}
} catch (e) {
style = {};
console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your cell style function.`);
} else {
res = this.defaultStyle(key, value);
}
} else {
style = this.defaultStyle(key, value);
}
this.cellStyleCache[index] = res;
}
if (!style.width) {
if (!res.width) {
const columnWidth = this.columnWidth[key.def];
style = {...style, ...widthStyle(columnWidth)};
res = Object.assign(res, widthStyle(columnWidth));
}
return style;
return res;
}
public cellContent(alarm: AlarmDataInfo, key: EntityColumn): SafeHtml {
if (alarm && key) {
const contentInfo = this.contentsInfo[key.def];
const value = getAlarmValue(alarm, key);
let content = '';
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
content = contentInfo.cellContentFunction(value, alarm, this.ctx);
} catch (e) {
content = '' + value;
public cellContent(alarm: AlarmDataInfo, key: EntityColumn, row: number): SafeHtml {
const col = this.columns.indexOf(key);
const index = row * this.columns.length + col;
let res = this.cellContentCache[index];
if (isUndefined(res)) {
res = '';
if (alarm && key) {
const contentInfo = this.contentsInfo[key.def];
const value = getAlarmValue(alarm, key);
let content = '';
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
content = contentInfo.cellContentFunction(value, alarm, this.ctx);
} catch (e) {
content = '' + value;
}
} else {
content = this.defaultContent(key, contentInfo, value);
}
} else {
content = this.defaultContent(key, contentInfo, value);
}
if (!isDefined(content)) {
return '';
} else {
content = this.utils.customTranslation(content, content);
switch (typeof content) {
case 'string':
return this.domSanitizer.bypassSecurityTrustHtml(content);
default:
return content;
if (isDefined(content)) {
content = this.utils.customTranslation(content, content);
switch (typeof content) {
case 'string':
res = this.domSanitizer.bypassSecurityTrustHtml(content);
break;
default:
res = content;
}
}
}
} else {
return '';
this.cellContentCache[index] = res;
}
return res;
}
public onRowClick($event: Event, alarm: AlarmDataInfo) {
@ -936,6 +959,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
isSorting(column: EntityColumn): boolean {
return column.type === DataKeyType.alarm && column.name.startsWith('details.');
}
private clearCache() {
this.cellContentCache.length = 0;
this.cellStyleCache.length = 0;
this.rowStyleCache.length = 0;
}
}
class AlarmsDatasource implements DataSource<AlarmDataInfo> {
@ -957,7 +986,8 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
private appliedSortOrderLabel: string;
constructor(private subscription: IWidgetSubscription,
private dataKeys: Array<DataKey>) {
private dataKeys: Array<DataKey>,
private ngZone: NgZone) {
}
connect(collectionViewer: CollectionViewer): Observable<AlarmDataInfo[] | ReadonlyArray<AlarmDataInfo>> {
@ -989,6 +1019,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
updateAlarms() {
const subscriptionAlarms = this.subscription.alarms;
let alarms = new Array<AlarmDataInfo>();
let isEmptySelection = false;
subscriptionAlarms.data.forEach((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);
this.selection.deselect(...toRemove);
if (this.selection.isEmpty()) {
this.onSelectionModeChanged(false);
isEmptySelection = true;
}
}
const alarmsPageData: PageData<AlarmDataInfo> = {
@ -1010,9 +1041,14 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
totalElements: subscriptionAlarms.totalElements,
hasNext: subscriptionAlarms.hasNext
};
this.alarmsSubject.next(alarms);
this.pageDataSubject.next(alarmsPageData);
this.dataLoading = false;
this.ngZone.run(() => {
if (isEmptySelection) {
this.onSelectionModeChanged(false);
}
this.alarmsSubject.next(alarms);
this.pageDataSubject.next(alarmsPageData);
this.dataLoading = false;
});
}
private alarmDataToInfo(alarmData: AlarmData): AlarmDataInfo {

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>
<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-cell *matCellDef="let entity;"
[innerHTML]="cellContent(entity, column)"
[ngStyle]="cellStyle(entity, column)">
<mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column, row)"
[ngStyle]="cellStyle(entity, column, row)">
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
@ -84,8 +84,8 @@
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity),
'invisible': entityDatasource.dataLoading}"
*matRowDef="let entity; columns: displayedColumns;"
[ngStyle]="rowStyle(entity)"
*matRowDef="let entity; columns: displayedColumns; let row = index"
[ngStyle]="rowStyle(entity, row)"
(click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
</table>
<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 { UtilsService } from '@core/services/utils.service';
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 { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -131,6 +140,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public actionCellDescriptors: WidgetActionDescriptor[];
public entityDatasource: EntityDatasource;
private cellContentCache: Array<any> = [];
private cellStyleCache: Array<any> = [];
private rowStyleCache: Array<any> = [];
private settings: EntitiesTableWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
@ -222,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public onDataUpdated() {
this.updateTitle(true);
this.entityDatasource.dataUpdated();
this.clearCache();
}
public pageLinkSortDirection(): SortDirection {
@ -415,8 +429,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions');
}
this.entityDatasource = new EntityDatasource(
this.translate, dataKeys, this.subscription);
this.entityDatasource = new EntityDatasource(this.translate, dataKeys, this.subscription, this.ngZone);
}
private editColumnsToDisplay($event: Event) {
@ -460,6 +473,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions');
}
this.clearCache();
}
} as DisplayColumnsPanelData
},
@ -535,91 +549,98 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
return widthStyle(columnWidth);
}
public rowStyle(entity: EntityData): any {
let style: any = {};
if (entity) {
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
public rowStyle(entity: EntityData, row: number): any {
let res = this.rowStyleCache[row];
if (!res) {
res = {};
if (entity && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
try {
style = this.rowStylesInfo.rowStyleFunction(entity, this.ctx);
if (!isObject(style)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
res = this.rowStylesInfo.rowStyleFunction(entity, this.ctx);
if (!isObject(res)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`);
}
if (Array.isArray(style)) {
if (Array.isArray(res)) {
throw new TypeError(`Array instead of style object`);
}
} catch (e) {
style = {};
res = {};
console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your row style function.`);
}
} else {
style = {};
}
}
return style;
}
public cellStyle(entity: EntityData, key: EntityColumn): any {
let style: any = {};
if (entity && key) {
const styleInfo = this.stylesInfo[key.def];
const value = getEntityValue(entity, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
style = styleInfo.cellStyleFunction(value, entity, this.ctx);
if (!isObject(style)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
}
if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`);
this.rowStyleCache[row] = res;
}
return res;
}
public cellStyle(entity: EntityData, key: EntityColumn, row: number): any {
const col = this.columns.indexOf(key);
const index = row * this.columns.length + col;
let res = this.cellStyleCache[index];
if (!res) {
res = {};
if (entity && key) {
const styleInfo = this.stylesInfo[key.def];
const value = getEntityValue(entity, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
res = styleInfo.cellStyleFunction(value, entity, this.ctx);
if (!isObject(res)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`);
}
if (Array.isArray(res)) {
throw new TypeError(`Array instead of style object`);
}
} catch (e) {
res = {};
console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your cell style function.`);
}
} catch (e) {
style = {};
console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your cell style function.`);
}
} else {
style = {};
this.cellStyleCache[index] = res;
}
}
if (!style.width) {
if (!res.width) {
const columnWidth = this.columnWidth[key.def];
style = {...style, ...widthStyle(columnWidth)};
}
return style;
}
public cellContent(entity: EntityData, key: EntityColumn): SafeHtml {
if (entity && key) {
const contentInfo = this.contentsInfo[key.def];
const value = getEntityValue(entity, key);
let content: string;
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
content = contentInfo.cellContentFunction(value, entity, this.ctx);
} catch (e) {
content = '' + value;
res = Object.assign(res, widthStyle(columnWidth));
}
return res;
}
public cellContent(entity: EntityData, key: EntityColumn, row: number): SafeHtml {
const col = this.columns.indexOf(key);
const index = row * this.columns.length + col;
let res = this.cellContentCache[index];
if (isUndefined(res)) {
res = '';
if (entity && key) {
const contentInfo = this.contentsInfo[key.def];
const value = getEntityValue(entity, key);
let content: string;
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
content = contentInfo.cellContentFunction(value, entity, this.ctx);
} catch (e) {
content = '' + value;
}
} else {
content = this.defaultContent(key, contentInfo, value);
}
} else {
content = this.defaultContent(key, contentInfo, value);
}
if (!isDefined(content)) {
return '';
} else {
content = this.utils.customTranslation(content, content);
switch (typeof content) {
case 'string':
return this.domSanitizer.bypassSecurityTrustHtml(content);
default:
return content;
if (isDefined(content)) {
content = this.utils.customTranslation(content, content);
switch (typeof content) {
case 'string':
res = this.domSanitizer.bypassSecurityTrustHtml(content);
break;
default:
res = content;
}
}
}
} else {
return '';
this.cellContentCache[index] = res;
}
return res;
}
private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any {
@ -672,6 +693,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
}
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {entity}, entityLabel);
}
private clearCache() {
this.cellContentCache.length = 0;
this.cellStyleCache.length = 0;
this.rowStyleCache.length = 0;
}
}
class EntityDatasource implements DataSource<EntityData> {
@ -689,7 +716,8 @@ class EntityDatasource implements DataSource<EntityData> {
constructor(
private translate: TranslateService,
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,
hasNext: datasourcesPageData.hasNext
};
this.entitiesSubject.next(entities);
this.pageDataSubject.next(entitiesPageData);
this.dataLoading = false;
this.ngZone.run(() => {
this.entitiesSubject.next(entities);
this.pageDataSubject.next(entitiesPageData);
this.dataLoading = false;
});
}
private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData {

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

@ -39,23 +39,23 @@
</mat-toolbar>
<mat-tab-group [ngClass]="{'tb-headless': sources.length === 1}" fxFlex
[(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)">
<div fxFlex class="table-container">
<table mat-table [dataSource]="source.timeseriesDatasource" [trackBy]="trackByRowTimestamp"
matSort [matSortActive]="source.pageLink.sortOrder.property" [matSortDirection]="source.pageLink.sortDirection()" matSortDisableClear>
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'">
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
<mat-cell *matCellDef="let row;"
[innerHTML]="cellContent(source, 0, row, row[0])"
[ngStyle]="cellStyle(source, 0, row, row[0])">
<mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, 0, row, row[0], rowIndex)"
[ngStyle]="cellStyle(source, 0, row, row[0], rowIndex)">
</mat-cell>
</ng-container>
<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-cell *matCellDef="let row;"
[innerHTML]="cellContent(source, h.index, row, row[h.index])"
[ngStyle]="cellStyle(source, h.index, row, row[h.index])">
<mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, h.index, row, row[h.index], rowIndex)"
[ngStyle]="cellStyle(source, h.index, row, row[h.index], rowIndex)">
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
@ -93,8 +93,8 @@
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row *matRowDef="let row; columns: source.displayedColumns;"
[ngStyle]="rowStyle(source, row)"
<mat-row *matRowDef="let row; columns: source.displayedColumns; let rowIndex = index"
[ngStyle]="rowStyle(source, row, rowIndex)"
(click)="onRowClick($event, row)"></mat-row>
</table>
<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';
import { UtilsService } from '@core/services/utils.service';
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 { PageLink } from '@shared/models/page/page-link';
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 sourceIndex: number;
private cellContentCache: Array<any> = [];
private cellStyleCache: Array<any> = [];
private rowStyleCache: Array<any> = [];
private settings: TimeseriesTableWidgetSettings;
private widgetConfig: WidgetConfig;
private data: Array<DatasourceData>;
@ -194,6 +198,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public onDataUpdated() {
this.updateCurrentSourceData();
this.clearCache();
}
private initialize() {
@ -235,7 +240,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
}
public getTabLabel(source: TimeseriesTableSource){
if(this.useEntityLabel){
if (this.useEntityLabel) {
return source.datasource.entityLabel || source.datasource.entityName;
} else {
return source.datasource.entityName;
@ -286,7 +291,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
if (this.actionCellDescriptors.length) {
source.displayedColumns.push('actions');
}
const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe);
const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe, this.ngZone);
tsDatasource.dataUpdated(this.data);
this.sources.push(source);
}
@ -337,6 +342,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
onSourceIndexChanged() {
this.updateCurrentSourceData();
this.updateActiveEntityInfo();
this.clearCache();
}
private enterFilterMode() {
@ -378,6 +384,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
source.pageLink.sortOrder.property = sort.active;
source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()];
source.timeseriesDatasource.loadRows();
this.clearCache();
this.ctx.detectChanges();
}
@ -397,94 +404,109 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
return source.datasource.entityId;
}
public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow): any {
let style: any = {};
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
try {
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
});
style = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx);
if (!isObject(style)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
}
if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`);
}
} catch (e) {
style = {};
console.warn(`Row style function in widget ` +
`'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`);
}
}
return style;
}
public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): any {
let style: any = {};
if (index > 0) {
const styleInfo = source.stylesInfo[index - 1];
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): any {
let res = this.rowStyleCache[index];
if (!res) {
res = {};
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) {
try {
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
});
style = styleInfo.cellStyleFunction(value, rowData, this.ctx);
if (!isObject(style)) {
throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
res = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx);
if (!isObject(res)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`);
}
if (Array.isArray(style)) {
if (Array.isArray(res)) {
throw new TypeError(`Array instead of style object`);
}
} catch (e) {
style = {};
console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` +
`'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`);
res = {};
console.warn(`Row style function in widget ` +
`'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`);
}
}
this.rowStyleCache[index] = res;
}
return style;
}
public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml {
if (index === 0) {
return row.formattedTs;
} else {
let content;
const contentInfo = source.contentsInfo[index - 1];
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
});
content = contentInfo.cellContentFunction(value, rowData, this.ctx);
} catch (e) {
content = '' + value;
return res;
}
public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): any {
const cacheIndex = rowIndex * (source.header.length + 1) + index;
let res = this.cellStyleCache[cacheIndex];
if (!res) {
res = {};
if (index > 0) {
const styleInfo = source.stylesInfo[index - 1];
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
});
res = styleInfo.cellStyleFunction(value, rowData, this.ctx);
if (!isObject(res)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`);
}
if (Array.isArray(res)) {
throw new TypeError(`Array instead of style object`);
}
} catch (e) {
res = {};
console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` +
`'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`);
}
}
} else {
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
const units = contentInfo.units || this.ctx.widgetConfig.units;
content = this.ctx.utils.formatValue(value, decimals, units, true);
}
this.cellStyleCache[cacheIndex] = res;
}
return res;
}
if (!isDefined(content)) {
return '';
public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml {
const cacheIndex = rowIndex * (source.header.length + 1) + index ;
let res = this.cellContentCache[cacheIndex];
if (isUndefined(res)) {
res = '';
if (index === 0) {
res = row.formattedTs;
} else {
content = this.utils.customTranslation(content, content);
switch (typeof content) {
case 'string':
return this.domSanitizer.bypassSecurityTrustHtml(content);
default:
return content;
let content;
const contentInfo = source.contentsInfo[index - 1];
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
});
content = contentInfo.cellContentFunction(value, rowData, this.ctx);
} catch (e) {
content = '' + value;
}
} else {
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
const units = contentInfo.units || this.ctx.widgetConfig.units;
content = this.ctx.utils.formatValue(value, decimals, units, true);
}
if (isDefined(content)) {
content = this.utils.customTranslation(content, content);
switch (typeof content) {
case 'string':
res = this.domSanitizer.bypassSecurityTrustHtml(content);
break;
default:
res = content;
}
}
}
this.cellContentCache[cacheIndex] = res;
}
return res;
}
public onRowClick($event: Event, row: TimeseriesRow) {
@ -530,6 +552,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private loadCurrentSourceRow() {
this.sources[this.sourceIndex].timeseriesDatasource.loadRows();
this.clearCache();
}
private clearCache() {
this.cellContentCache.length = 0;
this.cellStyleCache.length = 0;
this.rowStyleCache.length = 0;
}
}
@ -545,7 +574,8 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
private source: TimeseriesTableSource,
private hideEmptyLines: boolean,
private dateFormatFilter: string,
private datePipe: DatePipe
private datePipe: DatePipe,
private ngZone: NgZone
) {
this.source.timeseriesDatasource = this;
}
@ -568,8 +598,10 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
catchError(() => of(emptyPageData<TimeseriesRow>())),
).subscribe(
(pageData) => {
this.rowsSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
this.ngZone.run(() => {
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 {
min-height: 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.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement);
this.widgetContext.$container.css('display', 'block');
this.widgetContext.$container.css('user-select', 'none');
this.widgetContext.$container.attr('id', 'container');
if (this.widgetSizeDetected) {
this.widgetContext.$container.css('height', this.widgetContext.height + 'px');

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-label translate>admin.oauth2.user-info-uri</mat-label>
<input matInput formControlName="userInfoUri" required>
<input matInput formControlName="userInfoUri">
<button mat-icon-button matSuffix
type="button"
(click)="toggleEditMode(registration, 'userInfoUri')"
*ngIf="!isCustomProvider(registration)">
<mat-icon class="material-icons">create</mat-icon>
</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')">
{{ 'admin.oauth2.uri-pattern-error' | translate }}
</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),
jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)],
userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '',
[Validators.required,
Validators.pattern(this.URL_REGEXP)]],
[Validators.pattern(this.URL_REGEXP)]],
clientAuthenticationMethod: [
registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST,
Validators.required],

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 { Subject } from 'rxjs';
import { OtaUpdateType } from '@shared/models/ota-package.models';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'tb-device',
@ -78,7 +79,7 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
}
buildForm(entity: DeviceInfo): FormGroup {
return this.fb.group(
const form = this.fb.group(
{
name: [entity ? entity.name : '', [Validators.required]],
deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]],
@ -95,6 +96,17 @@ export class DeviceComponent extends EntityComponent<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) {
@ -156,10 +168,6 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> {
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"
type="text"
[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
</mat-error>
</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});
} else {
this.entityForm.get('file').clearValidators();
this.entityForm.get('url').setValidators(Validators.required);
this.entityForm.get('url').setValidators([Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]);
this.entityForm.get('file').updateValueAndValidity({emitEvent: false});
this.entityForm.get('url').updateValueAndValidity({emitEvent: false});
}
@ -172,6 +172,11 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O
}
prepareFormValue(formValue: any): any {
if (formValue.resource === 'url') {
delete formValue.file;
} else {
delete formValue.url;
}
delete formValue.resource;
delete formValue.generateChecksum;
return super.prepareFormValue(formValue);

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

@ -15,7 +15,7 @@
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) }}"
#dashboardInput
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 { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FloatLabelType } from '@angular/material/form-field/form-field';
@Component({
selector: 'tb-dashboard-autocomplete',
@ -64,6 +65,9 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
@Input()
customerId: string;
@Input()
floatLabel: FloatLabelType = 'auto';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;

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

@ -21,7 +21,8 @@
formControlName="packageId"
(focusin)="onFocus()"
[required]="required"
[matAutocomplete]="packageAutocomplete">
[matAutocomplete]="packageAutocomplete"
[matAutocompleteDisabled]="disabled">
<button *ngIf="otaPackageFormGroup.get('packageId').value && !disabled"
type="button"
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() {
this.otaPackageFormGroup.get('packageId').patchValue('', {emitEvent: true});
this.otaPackageFormGroup.get('packageId').patchValue('', {emitEvent: false});
setTimeout(() => {
this.packageInput.nativeElement.blur();
this.packageInput.nativeElement.focus();

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

@ -428,6 +428,7 @@ export interface CustomTimeSchedulerItem{
export interface AlarmRule {
condition: AlarmCondition;
alarmDetails?: string;
dashboardId?: DashboardId;
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{
BASIC = 'BASIC',
CUSTOM = 'CUSTOM',
GITHUB = 'GITHUB'
GITHUB = 'GITHUB',
APPLE = 'APPLE'
}
export enum TenantNameStrategy{

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

@ -1070,7 +1070,8 @@
"profile-configuration": "Profile configuration",
"transport-configuration": "Transport configuration",
"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.",
"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.",
@ -1149,6 +1150,9 @@
"advanced-settings": "Advanced settings",
"alarm-rule-details": "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",
"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.",

Loading…
Cancel
Save