Browse Source

Merge branch 'develop/3.0' of https://github.com/thingsboard/thingsboard into map/3.0

pull/2738/head
Artem Halushko 6 years ago
parent
commit
9ad09bb5ac
  1. 4
      application/src/main/data/json/demo/dashboards/gateways.json
  2. 4
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  3. 38
      application/src/main/data/json/system/widget_bundles/gpio_widgets.json
  4. 13
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  5. 7
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  6. 33
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  7. 7
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  8. 6
      application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java
  9. 22
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
  10. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  12. 43
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java
  13. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java
  15. 34
      application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
  16. 43
      application/src/main/resources/thingsboard.yml
  17. 30
      application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java
  18. 63
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  19. 3
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java
  20. 4
      docker/haproxy/config/haproxy.cfg
  21. 8
      msa/js-executor/package-lock.json
  22. 12
      pom.xml
  23. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
  24. 64
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
  25. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
  26. 26
      ui-ngx/src/app/core/auth/auth.service.ts
  27. 14
      ui-ngx/src/app/core/guards/auth.guard.ts
  28. 4
      ui-ngx/src/app/core/services/utils.service.ts
  29. 2
      ui-ngx/src/app/core/utils.ts
  30. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  31. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  32. 6
      ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts
  33. 15
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  34. 8
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts
  35. 265
      ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts
  36. 5
      ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts
  37. 6
      ui-ngx/src/app/modules/login/pages/login/login.component.html
  38. 6
      ui-ngx/src/app/modules/login/pages/login/login.component.scss
  39. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  40. 17
      ui/src/app/api/user.service.js
  41. 3
      ui/src/app/locale/locale.constant-en_US.json
  42. 2
      ui/src/app/login/login.tpl.html
  43. 2
      ui/src/app/widget/lib/google-map.js

4
application/src/main/data/json/demo/dashboards/gateways.json

@ -5,7 +5,7 @@
"94715984-ae74-76e4-20b7-2f956b01ed80": {
"isSystemType": true,
"bundleAlias": "entity_admin_widgets",
"typeAlias": "device_admin_table2",
"typeAlias": "device_admin_table",
"type": "latest",
"title": "New widget",
"sizeX": 24,
@ -1264,4 +1264,4 @@
}
},
"name": "Gateways"
}
}

4
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

@ -6,7 +6,7 @@
},
"widgetTypes": [
{
"alias": "device_admin_table2",
"alias": "device_admin_table",
"name": "Device admin table",
"descriptor": {
"type": "latest",
@ -22,7 +22,7 @@
}
},
{
"alias": "device_admin_table",
"alias": "asset_admin_table",
"name": "Asset admin table",
"descriptor": {
"type": "latest",

38
application/src/main/data/json/system/widget_bundles/gpio_widgets.json

File diff suppressed because one or more lines are too long

13
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -79,11 +79,18 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
@Qualifier("oauth2AuthenticationSuccessHandler")
private AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler;
@Autowired(required = false)
@Qualifier("oauth2AuthenticationFailureHandler")
private AuthenticationFailureHandler oauth2AuthenticationFailureHandler;
@Autowired
@Qualifier("defaultAuthenticationSuccessHandler")
private AuthenticationSuccessHandler successHandler;
@Autowired private AuthenticationFailureHandler failureHandler;
@Autowired
@Qualifier("defaultAuthenticationFailureHandler")
private AuthenticationFailureHandler failureHandler;
@Autowired private RestAuthenticationProvider restAuthenticationProvider;
@Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
@Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
@ -204,11 +211,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
http.oauth2Login()
.loginPage("/oauth2Login")
.loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl())
.successHandler(oauth2AuthenticationSuccessHandler);
.successHandler(oauth2AuthenticationSuccessHandler)
.failureHandler(oauth2AuthenticationFailureHandler);
}
}
@Bean
@ConditionalOnMissingBean(CorsFilter.class)
public CorsFilter corsFilter(@Autowired MvcCorsProperties mvcCorsProperties) {

7
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -52,6 +52,7 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.utils.MiscUtils;
import ua_parser.Client;
import javax.servlet.http.HttpServletRequest;
@ -170,7 +171,7 @@ public class AuthController extends BaseController {
try {
String email = resetPasswordByEmailRequest.get("email").asText();
UserCredentials userCredentials = userService.requestPasswordReset(TenantId.SYS_TENANT_ID, email);
String baseUrl = constructBaseUrl(request);
String baseUrl = MiscUtils.constructBaseUrl(request);
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
userCredentials.getResetToken());
@ -218,7 +219,7 @@ public class AuthController extends BaseController {
User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
String baseUrl = constructBaseUrl(request);
String baseUrl = MiscUtils.constructBaseUrl(request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
@ -265,7 +266,7 @@ public class AuthController extends BaseController {
User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
String baseUrl = constructBaseUrl(request);
String baseUrl = MiscUtils.constructBaseUrl(request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
mailService.sendPasswordWasResetEmail(loginUrl, email);

33
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -567,39 +567,6 @@ public abstract class BaseController {
return ruleNode;
}
protected String constructBaseUrl(HttpServletRequest request) {
String scheme = request.getScheme();
String forwardedProto = request.getHeader("x-forwarded-proto");
if (forwardedProto != null) {
scheme = forwardedProto;
}
int serverPort = request.getServerPort();
if (request.getHeader("x-forwarded-port") != null) {
try {
serverPort = request.getIntHeader("x-forwarded-port");
} catch (NumberFormatException e) {
}
} else if (forwardedProto != null) {
switch (forwardedProto) {
case "http":
serverPort = 80;
break;
case "https":
serverPort = 443;
break;
}
}
String baseUrl = String.format("%s://%s:%d",
scheme,
request.getServerName(),
serverPort);
return baseUrl;
}
protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}

7
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -52,6 +52,7 @@ import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
@ -148,7 +149,7 @@ public class UserController extends BaseController {
if (sendEmail) {
SecurityUser authUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), savedUser.getId());
String baseUrl = constructBaseUrl(request);
String baseUrl = MiscUtils.constructBaseUrl(request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
String email = savedUser.getEmail();
@ -188,7 +189,7 @@ public class UserController extends BaseController {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId());
if (!userCredentials.isEnabled()) {
String baseUrl = constructBaseUrl(request);
String baseUrl = MiscUtils.constructBaseUrl(request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
mailService.sendActivationEmail(activateUrl, email);
@ -213,7 +214,7 @@ public class UserController extends BaseController {
SecurityUser authUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId());
if (!userCredentials.isEnabled()) {
String baseUrl = constructBaseUrl(request);
String baseUrl = MiscUtils.constructBaseUrl(request);
String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
userCredentials.getActivateToken());
return activateUrl;

6
application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java

@ -97,7 +97,11 @@ public class CassandraToSqlColumn {
public String getColumnValue(Row row) {
if (row.isNull(index)) {
return null;
if (this.type == CassandraToSqlColumnType.BOOLEAN) {
return Boolean.toString(false);
} else {
return null;
}
} else {
switch (this.type) {
case ID:

22
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Tenant;
@ -27,13 +28,16 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
@ -45,15 +49,21 @@ public abstract class AbstractOAuth2ClientMapper {
@Autowired
private UserService userService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private TenantService tenantService;
@Autowired
private CustomerService customerService;
@Autowired
private InstallScripts installScripts;
private final Lock userCreationLock = new ReentrantLock();
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation) {
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation, boolean activateUser) {
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
@ -83,7 +93,14 @@ public abstract class AbstractOAuth2ClientMapper {
user.setFirstName(oauth2User.getFirstName());
user.setLastName(oauth2User.getLastName());
user = userService.saveUser(user);
if (activateUser) {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId());
userService.activateUserCredentials(user.getTenantId(), userCredentials.getActivateToken(), passwordEncoder.encode(""));
}
}
} catch (Exception e) {
log.error("Can't get or create security user from oauth2 user", e);
throw new RuntimeException("Can't get or create security user from oauth2 user", e);
} finally {
userCreationLock.unlock();
}
@ -98,13 +115,14 @@ public abstract class AbstractOAuth2ClientMapper {
}
}
private TenantId getTenantId(String tenantName) {
private TenantId getTenantId(String tenantName) throws IOException {
List<Tenant> tenants = tenantService.findTenants(new PageLink(1, 0, tenantName)).getData();
Tenant tenant;
if (tenants == null || tenants.isEmpty()) {
tenant = new Tenant();
tenant.setTitle(tenantName);
tenant = tenantService.saveTenant(tenant);
installScripts.createDefaultRuleChains(tenant.getId());
} else {
tenant = tenants.get(0);
}

3
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java

@ -56,7 +56,8 @@ public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implemen
String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
oauth2User.setCustomerName(customerName);
}
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
}
private String getTenantName(Map<String, Object> attributes, OAuth2ClientMapperConfig config) {

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -38,7 +38,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
OAuth2User oauth2User = getOAuth2User(token, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
}
private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) {

43
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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 {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
String baseUrl = MiscUtils.constructBaseUrl(request);
getRedirectStrategy().sendRedirect(request, response, baseUrl + "/login?loginError=" +
URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8.toString()));
}
}

4
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -27,6 +27,7 @@ import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -65,6 +66,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
getRedirectStrategy().sendRedirect(request, response, "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
String baseUrl = MiscUtils.constructBaseUrl(request);
getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
}
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationFailureHandler.java

@ -26,7 +26,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Component(value = "defaultAuthenticationFailureHandler")
public class RestAwareAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final ThingsboardErrorResponseHandler errorResponseHandler;

34
application/src/main/java/org/thingsboard/server/utils/MiscUtils.java

@ -18,8 +18,8 @@ package org.thingsboard.server.utils;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.Charset;
import java.util.Random;
/**
@ -47,4 +47,36 @@ public class MiscUtils {
throw new IllegalArgumentException("Can't find hash function with name " + name);
}
}
public static String constructBaseUrl(HttpServletRequest request) {
String scheme = request.getScheme();
String forwardedProto = request.getHeader("x-forwarded-proto");
if (forwardedProto != null) {
scheme = forwardedProto;
}
int serverPort = request.getServerPort();
if (request.getHeader("x-forwarded-port") != null) {
try {
serverPort = request.getIntHeader("x-forwarded-port");
} catch (NumberFormatException e) {
}
} else if (forwardedProto != null) {
switch (forwardedProto) {
case "http":
serverPort = 80;
break;
case "https":
serverPort = 443;
break;
}
}
String baseUrl = String.format("%s://%s:%d",
scheme,
request.getServerName(),
serverPort);
return baseUrl;
}
}

43
application/src/main/resources/thingsboard.yml

@ -100,34 +100,54 @@ security:
basic:
enabled: "${SECURITY_BASIC_ENABLED:false}"
oauth2:
# Enable/disable OAuth 2 login functionality
# For details please refer to https://thingsboard.io/docs/user-guide/oauth-2-support/
enabled: "${SECURITY_OAUTH2_ENABLED:false}"
# Redirect URL where access code from external user management system will be processed
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
# List of SSO clients
clients:
default:
loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}" # Label that going to be show on login screen
loginButtonIcon: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_ICON:}" # Icon that going to be show on login screen. Material design icon ID (https://material.angularjs.org/latest/api/directive/mdIcon)
# Label that going to be show on login button - 'Login with {loginButtonLabel}'
loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}"
# Icon that going to be show on login button. Material design icon ID (https://material.angularjs.org/latest/api/directive/mdIcon)
loginButtonIcon: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_ICON:}"
clientName: "${SECURITY_OAUTH2_DEFAULT_CLIENT_NAME:ClientName}"
clientId: "${SECURITY_OAUTH2_DEFAULT_CLIENT_ID:}"
clientSecret: "${SECURITY_OAUTH2_DEFAULT_CLIENT_SECRET:}"
accessTokenUri: "${SECURITY_OAUTH2_DEFAULT_ACCESS_TOKEN_URI:}"
authorizationUri: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_URI:}"
scope: "${SECURITY_OAUTH2_DEFAULT_SCOPE:}"
redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}" # Must be in sync with security.oauth2.loginProcessingUrl
# Redirect URL that must be in sync with 'security.oauth2.loginProcessingUrl', but domain name added
redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}"
jwkSetUri: "${SECURITY_OAUTH2_DEFAULT_JWK_SET_URI:}"
authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}" # authorization_code, implicit, refresh_token or client_credentials
# 'authorization_code', 'implicit', 'refresh_token' or 'client_credentials'
authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}"
clientAuthenticationMethod: "${SECURITY_OAUTH2_DEFAULT_CLIENT_AUTHENTICATION_METHOD:post}" # basic or post
userInfoUri: "${SECURITY_OAUTH2_DEFAULT_USER_INFO_URI:}"
userNameAttributeName: "${SECURITY_OAUTH2_DEFAULT_USER_NAME_ATTRIBUTE_NAME:email}"
mapperConfig:
type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}" # basic or custom
# Allows to create user if it not exists
allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_ALLOW_USER_CREATION:true}"
# Allows user to setup ThingsBoard internal password and login over default Login window
activateUser: "${SECURITY_OAUTH2_DEFAULT_MAPPER_ACTIVATE_USER:false}"
# Mapper type of converter from external user into internal - 'basic' or 'custom'
type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}"
basic:
allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_ALLOW_USER_CREATION:true}" # Allows to create user if it not exists
emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}" # Attribute key to use as email for the user
# Key from attributes of external user object to use as email
emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}"
firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}"
lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}"
tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" # domain, email or custom
tenantNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_PATTERN:}" # %{attribute_key} as placeholder for attributes value by key
customerNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_PATTERN:}" # %{attribute_key} as placeholder for attributes value by key
# Strategy for generating Tenant from external user object - 'domain', 'email' or 'custom'
# 'domain' - name of the Tenant will be extracted as domain from the email of the user
# 'email' - name of the Tenant will email of the user
# 'custom' - please configure 'tenantNamePattern' for custom mapping
tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}"
# %{attribute_key} as placeholder for attribute value of attributes of external user object
tenantNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_PATTERN:}"
# If this field is not empty, user will be created as a user under defined Customer
# %{attribute_key} as placeholder for attribute value of attributes of external user object
customerNamePattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_PATTERN:}"
custom:
url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}"
username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}"
@ -620,8 +640,7 @@ queue:
notifications: "${TB_QUEUE_RABBIT_MQ_NOTIFICATIONS_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"
js-executor: "${TB_QUEUE_RABBIT_MQ_JE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"
partitions:
hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}"
virtual_nodes_size: "${TB_QUEUE_PARTITIONS_VIRTUAL_NODES_SIZE:16}"
hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256
transport_api:
requests_topic: "${TB_QUEUE_TRANSPORT_API_REQUEST_TOPIC:tb_transport.api.requests}"
responses_topic: "${TB_QUEUE_TRANSPORT_API_RESPONSE_TOPIC:tb_transport.api.responses}"

30
application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentHashParitionServiceTest.java → application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java

@ -26,14 +26,14 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.queue.discovery.ConsistentHashPartitionService;
import org.thingsboard.server.common.msg.queue.ServiceQueue;
import org.thingsboard.server.queue.discovery.HashPartitionService;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings;
import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration;
import java.util.ArrayList;
import java.util.Collections;
@ -48,18 +48,18 @@ import static org.mockito.Mockito.when;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class ConsistentHashParitionServiceTest {
public class HashPartitionServiceTest {
public static final int ITERATIONS = 1000000;
private ConsistentHashPartitionService clusterRoutingService;
public static final int SERVER_COUNT = 3;
private HashPartitionService clusterRoutingService;
private TbServiceInfoProvider discoveryService;
private TenantRoutingInfoService routingInfoService;
private ApplicationEventPublisher applicationEventPublisher;
private TbQueueRuleEngineSettings ruleEngineSettings;
private String hashFunctionName = "murmur3_128";
private Integer virtualNodesSize = 16;
private String hashFunctionName = "sha256";
@Before
@ -68,25 +68,28 @@ public class ConsistentHashParitionServiceTest {
applicationEventPublisher = mock(ApplicationEventPublisher.class);
routingInfoService = mock(TenantRoutingInfoService.class);
ruleEngineSettings = mock(TbQueueRuleEngineSettings.class);
clusterRoutingService = new ConsistentHashPartitionService(discoveryService,
clusterRoutingService = new HashPartitionService(discoveryService,
routingInfoService,
applicationEventPublisher,
ruleEngineSettings
);
when(ruleEngineSettings.getQueues()).thenReturn(Collections.emptyList());
ReflectionTestUtils.setField(clusterRoutingService, "coreTopic", "tb.core");
ReflectionTestUtils.setField(clusterRoutingService, "corePartitions", 3);
ReflectionTestUtils.setField(clusterRoutingService, "corePartitions", 10);
ReflectionTestUtils.setField(clusterRoutingService, "hashFunctionName", hashFunctionName);
ReflectionTestUtils.setField(clusterRoutingService, "virtualNodesSize", virtualNodesSize);
TransportProtos.ServiceInfo currentServer = TransportProtos.ServiceInfo.newBuilder()
.setServiceId("100.96.1.1")
.setServiceId("tb-core-0")
.setTenantIdMSB(TenantId.NULL_UUID.getMostSignificantBits())
.setTenantIdLSB(TenantId.NULL_UUID.getLeastSignificantBits())
.addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name()))
.build();
// when(discoveryService.getServiceInfo()).thenReturn(currentServer);
List<TransportProtos.ServiceInfo> otherServers = new ArrayList<>();
for (int i = 1; i < 30; i++) {
for (int i = 1; i < SERVER_COUNT; i++) {
otherServers.add(TransportProtos.ServiceInfo.newBuilder()
.setServiceId("100.96." + i * 2 + "." + i)
.setServiceId("tb-rule-" + i)
.setTenantIdMSB(TenantId.NULL_UUID.getMostSignificantBits())
.setTenantIdLSB(TenantId.NULL_UUID.getLeastSignificantBits())
.addAllServiceTypes(Collections.singletonList(ServiceType.TB_CORE.name()))
.build());
}
@ -116,12 +119,11 @@ public class ConsistentHashParitionServiceTest {
long end = System.currentTimeMillis();
double diff = (data.get(data.size() - 1).getValue() - data.get(0).getValue());
double diffPercent = (diff / ITERATIONS) * 100.0;
System.out.println("Size: " + virtualNodesSize + " Time: " + (end - start) + " Diff: " + diff + "(" + String.format("%f", diffPercent) + "%)");
System.out.println("Time: " + (end - start) + " Diff: " + diff + "(" + String.format("%f", diffPercent) + "%)");
Assert.assertTrue(diffPercent < 0.5);
for (Map.Entry<Integer, Integer> entry : data) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}

63
common/queue/src/main/java/org/thingsboard/server/queue/discovery/ConsistentHashPartitionService.java → common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.queue.discovery;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
@ -35,6 +36,7 @@ import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings;
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -48,7 +50,7 @@ import java.util.stream.Collectors;
@Service
@Slf4j
public class ConsistentHashPartitionService implements PartitionService {
public class HashPartitionService implements PartitionService {
@Value("${queue.core.topic}")
private String coreTopic;
@ -56,8 +58,6 @@ public class ConsistentHashPartitionService implements PartitionService {
private Integer corePartitions;
@Value("${queue.partitions.hash_function_name:murmur3_128}")
private String hashFunctionName;
@Value("${queue.partitions.virtual_nodes_size:16}")
private Integer virtualNodesSize;
private final ApplicationEventPublisher applicationEventPublisher;
private final TbServiceInfoProvider serviceInfoProvider;
@ -76,10 +76,10 @@ public class ConsistentHashPartitionService implements PartitionService {
private HashFunction hashFunction;
public ConsistentHashPartitionService(TbServiceInfoProvider serviceInfoProvider,
TenantRoutingInfoService tenantRoutingInfoService,
ApplicationEventPublisher applicationEventPublisher,
TbQueueRuleEngineSettings tbQueueRuleEngineSettings) {
public HashPartitionService(TbServiceInfoProvider serviceInfoProvider,
TenantRoutingInfoService tenantRoutingInfoService,
ApplicationEventPublisher applicationEventPublisher,
TbQueueRuleEngineSettings tbQueueRuleEngineSettings) {
this.serviceInfoProvider = serviceInfoProvider;
this.tenantRoutingInfoService = tenantRoutingInfoService;
this.applicationEventPublisher = applicationEventPublisher;
@ -128,20 +128,22 @@ public class ConsistentHashPartitionService implements PartitionService {
public void recalculatePartitions(ServiceInfo currentService, List<ServiceInfo> otherServices) {
logServiceInfo(currentService);
otherServices.forEach(this::logServiceInfo);
Map<ServiceQueueKey, ConsistentHashCircle<ServiceInfo>> circles = new HashMap<>();
addNode(circles, currentService);
Map<ServiceQueueKey, List<ServiceInfo>> queueServicesMap = new HashMap<>();
addNode(queueServicesMap, currentService);
for (ServiceInfo other : otherServices) {
addNode(circles, other);
addNode(queueServicesMap, other);
}
queueServicesMap.values().forEach(list -> list.sort((a, b) -> a.getServiceId().compareTo(b.getServiceId())));
ConcurrentMap<ServiceQueueKey, List<Integer>> oldPartitions = myPartitions;
TenantId myIsolatedOrSystemTenantId = getSystemOrIsolatedTenantId(currentService);
myPartitions = new ConcurrentHashMap<>();
partitionSizes.forEach((type, size) -> {
ServiceQueueKey myServiceQueueKey = new ServiceQueueKey(type, myIsolatedOrSystemTenantId);
partitionSizes.forEach((serviceQueue, size) -> {
ServiceQueueKey myServiceQueueKey = new ServiceQueueKey(serviceQueue, myIsolatedOrSystemTenantId);
for (int i = 0; i < size; i++) {
ServiceInfo serviceInfo = resolveByPartitionIdx(circles.get(myServiceQueueKey), i);
ServiceInfo serviceInfo = resolveByPartitionIdx(queueServicesMap.get(myServiceQueueKey), i);
if (currentService.equals(serviceInfo)) {
ServiceQueueKey serviceQueueKey = new ServiceQueueKey(type, getSystemOrIsolatedTenantId(serviceInfo));
ServiceQueueKey serviceQueueKey = new ServiceQueueKey(serviceQueue, getSystemOrIsolatedTenantId(serviceInfo));
myPartitions.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(i);
}
}
@ -293,7 +295,7 @@ public class ConsistentHashPartitionService implements PartitionService {
return new TenantId(new UUID(serviceInfo.getTenantIdMSB(), serviceInfo.getTenantIdLSB()));
}
private void addNode(Map<ServiceQueueKey, ConsistentHashCircle<ServiceInfo>> circles, ServiceInfo instance) {
private void addNode(Map<ServiceQueueKey, List<ServiceInfo>> queueServiceList, ServiceInfo instance) {
TenantId tenantId = getSystemOrIsolatedTenantId(instance);
for (String serviceTypeStr : instance.getServiceTypesList()) {
ServiceType serviceType = ServiceType.valueOf(serviceTypeStr.toUpperCase());
@ -302,34 +304,20 @@ public class ConsistentHashPartitionService implements PartitionService {
ServiceQueueKey serviceQueueKey = new ServiceQueueKey(new ServiceQueue(serviceType, queue.getName()), tenantId);
partitionSizes.put(new ServiceQueue(ServiceType.TB_RULE_ENGINE, queue.getName()), queue.getPartitions());
partitionTopics.put(new ServiceQueue(ServiceType.TB_RULE_ENGINE, queue.getName()), queue.getTopic());
for (int i = 0; i < virtualNodesSize; i++) {
circles.computeIfAbsent(serviceQueueKey, key -> new ConsistentHashCircle<>()).put(hash(instance, i).asLong(), instance);
}
queueServiceList.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(instance);
}
} else {
ServiceQueueKey serviceQueueKey = new ServiceQueueKey(new ServiceQueue(serviceType), tenantId);
for (int i = 0; i < virtualNodesSize; i++) {
circles.computeIfAbsent(serviceQueueKey, key -> new ConsistentHashCircle<>()).put(hash(instance, i).asLong(), instance);
}
queueServiceList.computeIfAbsent(serviceQueueKey, key -> new ArrayList<>()).add(instance);
}
}
}
private ServiceInfo resolveByPartitionIdx(ConsistentHashCircle<ServiceInfo> circle, Integer partitionIdx) {
if (circle == null || circle.isEmpty()) {
private ServiceInfo resolveByPartitionIdx(List<ServiceInfo> servers, Integer partitionIdx) {
if (servers == null || servers.isEmpty()) {
return null;
}
Long hash = hashFunction.newHasher().putInt(partitionIdx).hash().asLong();
if (!circle.containsKey(hash)) {
ConcurrentNavigableMap<Long, ServiceInfo> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ?
circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
private HashCode hash(ServiceInfo instance, int i) {
return hashFunction.newHasher().putString(instance.getServiceId(), StandardCharsets.UTF_8).putInt(i).hash();
return servers.get(partitionIdx % servers.size());
}
public static HashFunction forName(String name) {
@ -338,12 +326,11 @@ public class ConsistentHashPartitionService implements PartitionService {
return Hashing.murmur3_32();
case "murmur3_128":
return Hashing.murmur3_128();
case "crc32":
return Hashing.crc32();
case "md5":
return Hashing.md5();
case "sha256":
return Hashing.sha256();
default:
throw new IllegalArgumentException("Can't find hash function with name " + name);
}
}
}

3
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java

@ -20,13 +20,14 @@ import lombok.Data;
@Data
public class OAuth2ClientMapperConfig {
private boolean allowUserCreation;
private boolean activateUser;
private String type;
private BasicOAuth2ClientMapperConfig basic;
private CustomOAuth2ClientMapperConfig custom;
@Data
public static class BasicOAuth2ClientMapperConfig {
private boolean allowUserCreation;
private String emailAttributeKey;
private String firstNameAttributeKey;
private String lastNameAttributeKey;

4
docker/haproxy/config/haproxy.cfg

@ -58,7 +58,7 @@ frontend http-in
acl transport_http_acl path_beg /api/v1/
acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/
acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/
acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/
redirect scheme https if !letsencrypt_http_acl !transport_http_acl { env(FORCE_HTTPS_REDIRECT) -m str true }
@ -76,7 +76,7 @@ frontend https_in
reqadd X-Forwarded-Proto:\ https
acl transport_http_acl path_beg /api/v1/
acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/
acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/
use_backend tb-http-backend if transport_http_acl
use_backend tb-api-backend if tb_api_acl

8
msa/js-executor/package-lock.json

@ -1944,14 +1944,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1971,8 +1969,7 @@
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
@ -2120,7 +2117,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}

12
pom.xml

@ -472,11 +472,23 @@
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>${spring-security.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring-security.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java

@ -20,7 +20,6 @@ import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -78,7 +77,8 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(processEntityRelationAction(ctx, msg),
String relationType = processPattern(msg, config.getRelationType());
withCallback(processEntityRelationAction(ctx, msg, relationType),
filterResult -> ctx.tellNext(filterResult.getMsg(), filterResult.isResult() ? SUCCESS : FAILURE), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
@ -86,13 +86,13 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
public void destroy() {
}
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg) {
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer), MoreExecutors.directExecutor());
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg, String relationType) {
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer, relationType), ctx.getDbCallbackExecutor());
}
protected abstract boolean createEntityIfNotExists();
protected abstract ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer);
protected abstract ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType);
protected abstract C loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException;
@ -120,11 +120,11 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
if (EntitySearchDirection.FROM.name().equals(this.config.getDirection())) {
searchDirectionIds.setFromId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
searchDirectionIds.setToId(msg.getOriginator());
searchDirectionIds.setOrignatorDirectionFrom(false);
searchDirectionIds.setOriginatorDirectionFrom(false);
} else {
searchDirectionIds.setToId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
searchDirectionIds.setFromId(msg.getOriginator());
searchDirectionIds.setOrignatorDirectionFrom(true);
searchDirectionIds.setOriginatorDirectionFrom(true);
}
return searchDirectionIds;
}
@ -153,7 +153,7 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
protected static class SearchDirectionIds {
private EntityId fromId;
private EntityId toId;
private boolean orignatorDirectionFrom;
private boolean originatorDirectionFrom;
}
private static class EntityCacheLoader extends CacheLoader<EntityKey, EntityContainer> {

64
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -57,8 +56,6 @@ import java.util.List;
)
public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateRelationNodeConfiguration> {
private String relationType;
@Override
protected TbCreateRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbCreateRelationNodeConfiguration.class);
@ -70,8 +67,8 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateR
}
@Override
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entity) {
ListenableFuture<Boolean> future = createIfAbsent(ctx, msg, entity);
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entity, String relationType) {
ListenableFuture<Boolean> future = createIfAbsent(ctx, msg, entity, relationType);
return Futures.transform(future, result -> {
RelationContainer container = new RelationContainer();
if (result && config.isChangeOriginatorToRelatedEntity()) {
@ -82,16 +79,15 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateR
}
container.setResult(result);
return container;
}, MoreExecutors.directExecutor());
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> createIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
relationType = processPattern(msg, config.getRelationType());
private ListenableFuture<Boolean> createIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
ListenableFuture<Boolean> checkRelationFuture = Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON), result -> {
if (!result) {
if (config.isRemoveCurrentRelations()) {
return processDeleteRelations(ctx, processFindRelations(ctx, msg, sdId));
return processDeleteRelations(ctx, processFindRelations(ctx, msg, sdId, relationType));
}
return Futures.immediateFuture(false);
}
@ -100,14 +96,14 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateR
return Futures.transformAsync(checkRelationFuture, result -> {
if (!result) {
return processCreateRelation(ctx, entityContainer, sdId);
return processCreateRelation(ctx, entityContainer, sdId, relationType);
}
return Futures.immediateFuture(true);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<List<EntityRelation>> processFindRelations(TbContext ctx, TbMsg msg, SearchDirectionIds sdId) {
if (sdId.isOrignatorDirectionFrom()) {
private ListenableFuture<List<EntityRelation>> processFindRelations(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) {
if (sdId.isOriginatorDirectionFrom()) {
return ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), relationType, RelationTypeGroup.COMMON);
} else {
return ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), relationType, RelationTypeGroup.COMMON);
@ -121,91 +117,91 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateR
for (EntityRelation relation : entityRelations) {
list.add(ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation));
}
return Futures.transform(Futures.allAsList(list), result -> false, MoreExecutors.directExecutor());
return Futures.transform(Futures.allAsList(list), result -> false, ctx.getDbCallbackExecutor());
}
return Futures.immediateFuture(false);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
switch (entityContainer.getEntityType()) {
case ASSET:
return processAsset(ctx, entityContainer, sdId);
return processAsset(ctx, entityContainer, sdId, relationType);
case DEVICE:
return processDevice(ctx, entityContainer, sdId);
return processDevice(ctx, entityContainer, sdId, relationType);
case CUSTOMER:
return processCustomer(ctx, entityContainer, sdId);
return processCustomer(ctx, entityContainer, sdId, relationType);
case DASHBOARD:
return processDashboard(ctx, entityContainer, sdId);
return processDashboard(ctx, entityContainer, sdId, relationType);
case ENTITY_VIEW:
return processView(ctx, entityContainer, sdId);
return processView(ctx, entityContainer, sdId, relationType);
case TENANT:
return processTenant(ctx, entityContainer, sdId);
return processTenant(ctx, entityContainer, sdId, relationType);
}
return Futures.immediateFuture(true);
}
private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(entityContainer.getEntityId().getId())), entityView -> {
if (entityView != null) {
return processSave(ctx, sdId);
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())), device -> {
if (device != null) {
return processSave(ctx, sdId);
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, MoreExecutors.directExecutor());
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(entityContainer.getEntityId().getId())), asset -> {
if (asset != null) {
return processSave(ctx, sdId);
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), new CustomerId(entityContainer.getEntityId().getId())), customer -> {
if (customer != null) {
return processSave(ctx, sdId);
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), new DashboardId(entityContainer.getEntityId().getId())), dashboard -> {
if (dashboard != null) {
return processSave(ctx, sdId);
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), new TenantId(entityContainer.getEntityId().getId())), tenant -> {
if (tenant != null) {
return processSave(ctx, sdId);
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSave(TbContext ctx, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processSave(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON));
}

30
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -48,8 +47,6 @@ import java.util.List;
)
public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteRelationNodeConfiguration> {
private String relationType;
@Override
protected TbDeleteRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbDeleteRelationNodeConfiguration.class);
@ -61,21 +58,20 @@ public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteR
}
@Override
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg) {
return getRelationContainerListenableFuture(ctx, msg);
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg, String relationType) {
return getRelationContainerListenableFuture(ctx, msg, relationType);
}
@Override
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
return Futures.transform(processSingle(ctx, msg, entityContainer), result -> new RelationContainer(msg, result), MoreExecutors.directExecutor());
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
return Futures.transform(processSingle(ctx, msg, entityContainer, relationType), result -> new RelationContainer(msg, result), ctx.getDbCallbackExecutor());
}
private ListenableFuture<RelationContainer> getRelationContainerListenableFuture(TbContext ctx, TbMsg msg) {
relationType = processPattern(msg, config.getRelationType());
private ListenableFuture<RelationContainer> getRelationContainerListenableFuture(TbContext ctx, TbMsg msg, String relationType) {
if (config.isDeleteForSingleEntity()) {
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer), MoreExecutors.directExecutor());
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer, relationType), ctx.getDbCallbackExecutor());
} else {
return Futures.transform(processList(ctx, msg), result -> new RelationContainer(msg, result), MoreExecutors.directExecutor());
return Futures.transform(processList(ctx, msg), result -> new RelationContainer(msg, result), ctx.getDbCallbackExecutor());
}
}
@ -95,23 +91,23 @@ public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteR
}
}
return Futures.immediateFuture(true);
}, MoreExecutors.directExecutor());
}, ctx.getDbCallbackExecutor());
}
}, MoreExecutors.directExecutor());
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSingle(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
private ListenableFuture<Boolean> processSingle(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON),
result -> {
if (result) {
return processSingleDeleteRelation(ctx, sdId);
return processSingleDeleteRelation(ctx, sdId, relationType);
}
return Futures.immediateFuture(true);
}, MoreExecutors.directExecutor());
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSingleDeleteRelation(TbContext ctx, SearchDirectionIds sdId) {
private ListenableFuture<Boolean> processSingleDeleteRelation(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON);
}

26
ui-ngx/src/app/core/auth/auth.service.ts

@ -43,6 +43,8 @@ import { DashboardInfo } from '@shared/models/dashboard.models';
import { PageData } from '@app/shared/models/page/page-data';
import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
@Injectable({
providedIn: 'root'
@ -60,7 +62,8 @@ export class AuthService {
private utils: UtilsService,
private dashboardService: DashboardService,
private adminService: AdminService,
private translate: TranslateService
private translate: TranslateService,
private dialog: MatDialog
) {
}
@ -273,6 +276,7 @@ export class AuthService {
const refreshToken = this.utils.getQueryParam('refreshToken');
const username = this.utils.getQueryParam('username');
const password = this.utils.getQueryParam('password');
const loginError = this.utils.getQueryParam('loginError');
if (publicId) {
return this.publicLogin(publicId).pipe(
mergeMap((response) => {
@ -317,6 +321,10 @@ export class AuthService {
}
)
);
} else if (loginError) {
this.showLoginErrorDialog(loginError);
this.utils.updateQueryParam('loginError', null);
return throwError(Error());
}
return this.procceedJwtTokenValidate(doTokenRefresh);
} else {
@ -324,6 +332,22 @@ export class AuthService {
}
}
private showLoginErrorDialog(loginError: string) {
this.translate.get(['login.error', 'action.close']).subscribe(
(translations) => {
const dialogConfig: MatDialogConfig = {
disableClose: true,
data: {
title: translations['login.error'],
message: loginError,
ok: translations['action.close']
}
};
this.dialog.open(AlertDialogComponent, dialogConfig);
}
);
}
private procceedJwtTokenValidate(doTokenRefresh?: boolean): Observable<AuthPayload> {
const loadUserSubject = new ReplaySubject<AuthPayload>();
this.validateJwtToken(doTokenRefresh).subscribe(

14
ui-ngx/src/app/core/guards/auth.guard.ts

@ -84,15 +84,15 @@ export class AuthGuard implements CanActivate, CanActivateChild {
// this.authService.gotoDefaultPlace(false);
return of(this.authService.defaultUrl(false));
} else {
const tasks: Observable<any>[] = [];
if (path === 'login') {
tasks.push(this.authService.loadOAuth2Clients());
return forkJoin([this.authService.loadOAuth2Clients()]).pipe(
map(() => {
return true;
})
);
} else {
return of(true);
}
return forkJoin(tasks).pipe(
map(() => {
return true;
})
);
}
} else {
if (authState.authUser.isPublic) {

4
ui-ngx/src/app/core/services/utils.service.ts

@ -345,6 +345,10 @@ export class UtilsService {
return additionalDataKey;
}
public createLabelFromDatasource(datasource: Datasource, pattern: string): string {
return createLabelFromDatasource(datasource, pattern);
}
public generateColors(datasources: Array<Datasource>) {
let index = 0;
datasources.forEach((datasource) => {

2
ui-ngx/src/app/core/utils.ts

@ -460,7 +460,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin
return result;
}
export function createLabelFromDatasource(datasource: Datasource, pattern: string) {
export function createLabelFromDatasource(datasource: Datasource, pattern: string): string {
let label = pattern;
if (!datasource) {
return label;

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts

@ -100,6 +100,8 @@ export type MarkerSettings = {
tooltipFunction: GenericFunction;
labelFunction: GenericFunction;
markerImageFunction?: MarkerImageFunction;
markerOffsetX: number;
markerOffsetY: number;
}
export interface FormattedData {

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -97,7 +97,7 @@ export class MapWidgetController implements MapWidgetInterface {
const schema = initSchema();
addToSchema(schema, this.getProvidersSchema(mapProvider));
addGroupInfo(schema, 'Map Provider Settings');
addToSchema(schema, addCondition(commonMapSettingsSchema, 'model.provider !== "image-map"'));
addToSchema(schema, commonMapSettingsSchema);
addGroupInfo(schema, 'Common Map Settings');
addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon']));
addGroupInfo(schema, 'Polygon Settings');

6
ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts

@ -109,8 +109,10 @@ export function aspectCache(imageUrl: string): Observable<number> {
}
}
export type TranslateFunc = (key: string, defaultTranslation?: string) => string;
function parseTemplate(template: string, data: { $datasource?: Datasource, [key: string]: any },
translateFn?: (key: string) => string) {
translateFn?: TranslateFunc) {
let res = '';
try {
if (template.match(/<link-act/g)) {
@ -146,8 +148,6 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
return res;
}
type TranslateFunc = (key: string, defaultTranslation?: string) => string;
export const parseWithTranslation = {
translateFn: null,

15
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts

@ -18,10 +18,12 @@ import L, { LeafletMouseEvent } from 'leaflet';
import { FormattedData, MarkerSettings } from './map-models';
import { aspectCache, createTooltip, parseWithTranslation, safeExecute } from './maps-utils';
import tinycolor from 'tinycolor2';
import { isDefined } from '@core/utils';
export class Marker {
leafletMarker: L.Marker;
tooltipOffset: [number, number];
markerOffset: [number, number];
tooltip: L.Popup;
location: L.LatLngExpression;
data: FormattedData;
@ -34,9 +36,14 @@ export class Marker {
draggable: settings.draggableMarker
});
this.markerOffset = [
isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5,
isDefined(settings.markerOffsetY) ? settings.markerOffsetY : 1,
];
this.createMarkerIcon((iconInfo) => {
this.leafletMarker.setIcon(iconInfo.icon);
this.tooltipOffset = [0, -iconInfo.size[1] + 10];
this.tooltipOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10];
this.updateMarkerLabel(settings);
});
@ -95,7 +102,7 @@ export class Marker {
updateMarkerIcon(settings: MarkerSettings) {
this.createMarkerIcon((iconInfo) => {
this.leafletMarker.setIcon(iconInfo.icon);
this.tooltipOffset = [0, -iconInfo.size[1] + 10];
this.tooltipOffset = [0, -iconInfo.size[1] * this.markerOffset[1] + 10];
this.updateMarkerLabel(settings);
});
}
@ -129,7 +136,7 @@ export class Marker {
const icon = L.icon({
iconUrl: currentImage.url,
iconSize: [width, height],
iconAnchor: [width / 2, height],
iconAnchor: [width * this.markerOffset[0], height * this.markerOffset[1]],
popupAnchor: [0, -height]
});
const iconInfo = {
@ -151,7 +158,7 @@ export class Marker {
const icon = L.icon({
iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
iconSize: [21, 34],
iconAnchor: [10, 34],
iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
popupAnchor: [0, -34],
shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
shadowSize: [40, 37],

8
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts

@ -30,10 +30,11 @@ export class ImageMap extends LeafletMap {
width = 0;
height = 0;
imageUrl;
posFunction;
constructor($container: HTMLElement, options: UnitedMapSettings) {
super($container, options);
options.posFunction = parseFunction(options.posFunction, ['origXPos', 'origYPos']) as ((origXPos, origYPos) => { x, y });
this.posFunction = parseFunction(options.posFunction, ['origXPos', 'origYPos']) as ((origXPos, origYPos) => { x, y });
this.imageUrl = options.mapUrl;
aspectCache(this.imageUrl).subscribe(aspect => {
this.aspect = aspect;
@ -132,9 +133,10 @@ export class ImageMap extends LeafletMap {
convertPosition(expression): L.LatLng {
if (isNaN(expression[this.options.xPosKeyName]) || isNaN(expression[this.options.yPosKeyName])) return null;
Object.assign(expression, this.posFunction(expression[this.options.xPosKeyName], expression[this.options.yPosKeyName]))
return this.pointToLatLng(
expression[this.options.xPosKeyName] * this.width,
expression[this.options.yPosKeyName] * this.height);
expression.x * this.width,
expression.y * this.height);
}
pointToLatLng(x, y): L.LatLng {

265
ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts

@ -275,6 +275,16 @@ export const commonMapSettingsSchema =
type: 'string',
default: 'longitude'
},
xPosKeyName: {
title: 'X position key name',
type: 'string',
default: 'xPos'
},
yPosKeyName: {
title: 'Y position key name',
type: 'string',
default: 'yPos'
},
showLabel: {
title: 'Show label',
type: 'boolean',
@ -323,6 +333,21 @@ export const commonMapSettingsSchema =
title: 'Tooltip function: f(data, dsData, dsIndex)',
type: 'string'
},
posFunction: {
title: 'Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each',
type: 'string',
default: 'return {x: origXPos, y: origYPos};'
},
markerOffsetX: {
title: 'Marker X offset relative to position',
type: 'number',
default: 0.5
},
markerOffsetY: {
title: 'Marker Y offset relative to position',
type: 'number',
default: 1
},
color: {
title: 'Color',
type: 'string'
@ -366,14 +391,40 @@ export const commonMapSettingsSchema =
required: []
},
form: [
'defaultZoomLevel',
'useDefaultCenterPosition',
'defaultCenterPosition',
'fitMapBounds',
{
key: 'defaultZoomLevel',
condition: 'model.provider !== "image-map"'
},
{
key: 'useDefaultCenterPosition',
condition: 'model.provider !== "image-map"'
},
{
key: 'defaultCenterPosition',
condition: 'model.provider !== "image-map"'
},
{
key: 'fitMapBounds',
condition: 'model.provider !== "image-map"'
},
'draggableMarker',
'disableScrollZooming',
'latKeyName',
'lngKeyName',
{
key: 'latKeyName',
condition: 'model.provider !== "image-map"'
},
{
key: 'lngKeyName',
condition: 'model.provider !== "image-map"'
},
{
key: 'xPosKeyName',
condition: 'model.provider === "image-map"'
},
{
key: 'yPosKeyName',
condition: 'model.provider === "image-map"'
},
'showLabel',
'label',
'useLabelFunction',
@ -407,6 +458,19 @@ export const commonMapSettingsSchema =
key: 'tooltipFunction',
type: 'javascript'
},
{
key: 'markerOffsetX',
condition: 'model.provider === "image-map"'
},
{
key: 'markerOffsetY',
condition: 'model.provider === "image-map"'
},
{
key: 'posFunction',
type: 'javascript',
condition: 'model.provider === "image-map"'
},
{
key: 'color',
type: 'color'
@ -654,123 +718,6 @@ export const imageMapSettingsSchema =
title: 'Image URL source entity attribute',
type: 'string',
default: ''
},
disableScrollZooming: {
title: 'Disable scroll zooming',
type: 'boolean',
default: false
},
xPosKeyName: {
title: 'X position key name',
type: 'string',
default: 'xPos'
},
yPosKeyName: {
title: 'Y position key name',
type: 'string',
default: 'yPos'
},
showLabel: {
title: 'Show label',
type: 'boolean',
default: true
},
label: {
title: 'Label (pattern examples: \'${entityName}\', \'${entityName}: (Text ${keyName} units.)\' )',
type: 'string',
default: '${entityName}'
},
useLabelFunction: {
title: 'Use label function',
type: 'boolean',
default: false
},
labelFunction: {
title: 'Label function: f(data, dsData, dsIndex)',
type: 'string'
},
showTooltip: {
title: 'Show tooltip',
type: 'boolean',
default: true
},
showTooltipAction: {
title: 'Action for displaying the tooltip',
type: 'string',
default: 'click'
},
autocloseTooltip: {
title: 'Auto-close tooltips',
type: 'boolean',
default: true
},
tooltipPattern: {
title: 'Tooltip (for ex. \'Text ${keyName} units.\' or <link-act name=\'my-action\'>Link text</link-act>\')',
type: 'string',
default: '<b>${entityName}</b><br/><br/><b>X Pos:</b> ${xPos:2}<br/><b>Y Pos:</b> ${yPos:2}'
},
useTooltipFunction: {
title: 'Use tooltip function',
type: 'boolean',
default: false
},
tooltipFunction: {
title: 'Tooltip function: f(data, dsData, dsIndex)',
type: 'string'
},
color: {
title: 'Color',
type: 'string'
},
posFunction: {
title: 'Position conversion function: f(origXPos, origYPos), should return x,y coordinates as double from 0 to 1 each',
type: 'string',
default: 'return {x: origXPos, y: origYPos};'
},
markerOffsetX: {
title: 'Marker X offset relative to position',
type: 'number',
default: 0.5
},
markerOffsetY: {
title: 'Marker Y offset relative to position',
type: 'number',
default: 1
},
useColorFunction: {
title: 'Use color function',
type: 'boolean',
default: false
},
colorFunction: {
title: 'Color function: f(data, dsData, dsIndex)',
type: 'string'
},
markerImage: {
title: 'Custom marker image',
type: 'string'
},
markerImageSize: {
title: 'Custom marker image size (px)',
type: 'number',
default: 34
},
useMarkerImageFunction: {
title: 'Use marker image function',
type: 'boolean',
default: false
},
markerImageFunction: {
title: 'Marker image function: f(data, images, dsData, dsIndex)',
type: 'string'
},
markerImages: {
title: 'Marker images',
type: 'array',
items: {
title: 'Marker image',
type: 'string'
}
}
},
required: []
@ -781,77 +728,7 @@ export const imageMapSettingsSchema =
type: 'image'
},
'imageEntityAlias',
'imageUrlAttribute',
'disableScrollZooming',
'xPosKeyName',
'yPosKeyName',
'showLabel',
'label',
'useLabelFunction',
{
key: 'labelFunction',
type: 'javascript'
},
'showTooltip',
{
key: 'showTooltipAction',
type: 'rc-select',
multiple: false,
items: [
{
value: 'click',
label: 'Show tooltip on click (Default)'
},
{
value: 'hover',
label: 'Show tooltip on hover'
}
]
},
'autocloseTooltip',
{
key: 'tooltipPattern',
type: 'textarea'
},
'useTooltipFunction',
{
key: 'tooltipFunction',
type: 'javascript'
},
{
key: 'color',
type: 'color'
},
{
key: 'posFunction',
type: 'javascript'
},
'markerOffsetX',
'markerOffsetY',
'useColorFunction',
{
key: 'colorFunction',
type: 'javascript'
},
{
key: 'markerImage',
type: 'image'
},
'markerImageSize',
'useMarkerImageFunction',
{
key: 'markerImageFunction',
type: 'javascript'
},
{
key: 'markerImages',
items: [
{
key: 'markerImages[]',
type: 'image'
}
]
}
'imageUrlAttribute'
]
};

5
ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts

@ -199,9 +199,8 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
let stateName = dashboardState.name;
stateName = this.utils.customTranslation(stateName, stateName);
const params = this.stateObject[index].params;
const targetParams = params && params.targetEntityParamName ? params[params.targetEntityParamName] : params;
const entityName = targetParams && targetParams.entityName ? targetParams.entityName : '';
const entityLabel = targetParams && targetParams.entityLabel ? targetParams.entityLabel : '';
const entityName = params && params.entityName ? params.entityName : '';
const entityLabel = params && params.entityLabel ? params.entityLabel : '';
result = insertVariable(stateName, 'entityName', entityName);
result = insertVariable(result, 'entityLabel', entityLabel);
for (const prop of Object.keys(params)) {

6
ui-ngx/src/app/modules/login/pages/login/login.component.html

@ -56,10 +56,10 @@
<div class="line"><mat-divider></mat-divider></div>
</div>
<ng-container *ngFor="let oauth2Client of oauth2Clients">
<button mat-raised-button color="primary" class="centered" routerLink="{{ oauth2Client.url }}">
<mat-icon class="material-icons md-18" svgIcon="{{ oauth2Client.icon }}"></mat-icon>
<a mat-raised-button color="warn" class="centered" href="{{ oauth2Client.url }}">
<mat-icon svgIcon="{{ oauth2Client.icon }}"></mat-icon>
{{ 'login.login-with' | translate: {name: oauth2Client.name} }}
</button>
</a>
</ng-container>
</div>
</div>

6
ui-ngx/src/app/modules/login/pages/login/login.component.scss

@ -65,11 +65,17 @@
min-width: 20px;
}
a.centered {
&:hover {
border-bottom: 0px;
}
}
.centered ::ng-deep .mat-button-wrapper {
display: flex;
justify-content: center;
align-items: center;
text-transform: uppercase;
}
}
}

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

@ -1355,7 +1355,8 @@
"password-link-sent-message": "Password reset link was successfully sent!",
"email": "Email",
"login-with": "Login with {{name}}",
"or": "or"
"or": "or",
"error": "Login error"
},
"position": {
"top": "Top",

17
ui/src/app/api/user.service.js

@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
.name;
/*@ngInject*/
function UserService($http, $q, $rootScope, adminService, dashboardService, timeService, loginService, toast, store, jwtHelper, $translate, $state, $location) {
function UserService($http, $q, $rootScope, adminService, dashboardService, timeService, loginService, toast, store, jwtHelper, $translate, $state, $location, $mdDialog) {
var currentUser = null,
currentUserDetails = null,
lastPublicDashboardId = null,
@ -406,6 +406,10 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
}, function fail() {
deferred.reject();
});
} else if (locationSearch.loginError) {
showLoginErrorDialog(locationSearch.loginError);
$location.search('loginError', null);
deferred.reject();
} else {
procceedJwtTokenValidate();
}
@ -415,6 +419,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, time
return deferred.promise;
}
function showLoginErrorDialog(loginError) {
$translate(['login.error',
'action.close']).then(function (translations) {
var alert = $mdDialog.alert()
.title(translations['login.error'])
.htmlContent(loginError)
.ok(translations['action.close']);
$mdDialog.show(alert);
});
}
function loadIsUserTokenAccessEnabled() {
var deferred = $q.defer();
if (currentUser.authority === 'SYS_ADMIN' || currentUser.authority === 'TENANT_ADMIN') {

3
ui/src/app/locale/locale.constant-en_US.json

@ -1334,7 +1334,8 @@
"password-link-sent-message": "Password reset link was successfully sent!",
"email": "Email",
"login-with": "Login with {{name}}",
"or": "or"
"or": "or",
"error": "Login error"
},
"position": {
"top": "Top",

2
ui/src/app/login/login.tpl.html

@ -51,7 +51,7 @@
<div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
<div class="line"><md-divider></md-divider></div>
</div>
<md-button ng-repeat="oauth2Client in oauth2Clients" class="md-raised"
<md-button ng-repeat="oauth2Client in oauth2Clients" class="md-raised md-accent md-hue-2"
layout="row" layout-align="center center" ng-href="{{ oauth2Client.url }}" target="_self">
<md-icon class="material-icons md-18" md-svg-icon="{{ oauth2Client.icon }}"></md-icon>
{{ 'login.login-with' | translate: {name: oauth2Client.name} }}

2
ui/src/app/widget/lib/google-map.js

@ -94,7 +94,7 @@ export default class TbGoogleMap {
window[this.initMapFunctionName] = function() { // eslint-disable-line no-undef, angular/window-service
lazyLoad.load([ // eslint-disable-line no-undef
{ type: 'js', path: 'https://unpkg.com/@google/markerwithlabel@1.2.3/src/markerwithlabel.js' },
{ type: 'js', path: 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/markerclusterer.js' }
{ type: 'js', path: 'https://unpkg.com/@google/markerclustererplus@4.0.1/dist/markerclustererplus.min.js' }
]).then(
function success() {
gmGlobals.gmApiKeys[tbMap.apiKey].loaded = true;

Loading…
Cancel
Save