Browse Source

Merge pull request #135 from thingsboard/master

Merge 1.2.3 to release branch
pull/144/head
Andrew Shvayka 10 years ago
committed by GitHub
parent
commit
71c4e0a48c
  1. 1
      .gitignore
  2. 14
      README.md
  3. 3
      application/.gitignore
  4. 2
      application/pom.xml
  5. 28
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  6. 45
      application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java
  7. 34
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  8. 7
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  9. 25
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  10. 16
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  11. 16
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  12. 69
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  13. 34
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java
  14. 64
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  15. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
  16. 96
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
  17. 12
      application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
  18. 42
      application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java
  19. 18
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  20. 22
      application/src/main/resources/thingsboard.yml
  21. 4
      application/src/main/scripts/windows/install.bat
  22. 4
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  23. 4
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  24. 2
      common/data/pom.xml
  25. 2
      common/message/pom.xml
  26. 2
      common/pom.xml
  27. 2
      common/transport/pom.xml
  28. 2
      dao/pom.xml
  29. 11
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
  30. 16
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java
  31. 2
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  32. 53
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  33. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  34. 7
      dao/src/main/resources/schema.cql
  35. 2
      dao/src/main/resources/system-data.cql
  36. 78
      dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java
  37. 2
      extensions-api/pom.xml
  38. 2
      extensions-core/pom.xml
  39. 2
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
  40. 7
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
  41. 4
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
  42. 2
      extensions/extension-kafka/pom.xml
  43. 2
      extensions/extension-rabbitmq/pom.xml
  44. 2
      extensions/extension-rest-api-call/pom.xml
  45. 7
      extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java
  46. 5
      extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java
  47. 21
      extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json
  48. 2
      extensions/pom.xml
  49. 2
      pom.xml
  50. 38
      tools/pom.xml
  51. 7
      tools/src/main/java/org/thingsboard/client/tools/RestClient.java
  52. 2
      transport/coap/pom.xml
  53. 2
      transport/http/pom.xml
  54. 2
      transport/mqtt/pom.xml
  55. 2
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  56. 4
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
  57. 2
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
  58. 2
      transport/pom.xml
  59. 4
      ui/package.json
  60. 2
      ui/pom.xml
  61. 87
      ui/src/app/api/customer.service.js
  62. 75
      ui/src/app/api/dashboard.service.js
  63. 4
      ui/src/app/api/datasource.service.js
  64. 110
      ui/src/app/api/device.service.js
  65. 14
      ui/src/app/api/login.service.js
  66. 670
      ui/src/app/api/subscription.js
  67. 10
      ui/src/app/api/telemetry-websocket.service.js
  68. 166
      ui/src/app/api/user.service.js
  69. 16
      ui/src/app/api/widget.service.js
  70. 5
      ui/src/app/app.config.js
  71. 2
      ui/src/app/app.js
  72. 50
      ui/src/app/app.run.js
  73. 168
      ui/src/app/common/utils.service.js
  74. 81
      ui/src/app/components/dashboard.directive.js
  75. 6
      ui/src/app/components/dashboard.tpl.html
  76. 2
      ui/src/app/components/device-filter.directive.js
  77. 131
      ui/src/app/components/grid.directive.js
  78. 6
      ui/src/app/components/grid.tpl.html
  79. 7
      ui/src/app/components/legend.directive.js
  80. 58
      ui/src/app/components/socialshare-panel.directive.js
  81. 62
      ui/src/app/components/socialshare-panel.tpl.html
  82. 18
      ui/src/app/components/widget-config.directive.js
  83. 4
      ui/src/app/components/widget-config.tpl.html
  84. 901
      ui/src/app/components/widget.controller.js
  85. 2
      ui/src/app/customer/customer-card.tpl.html
  86. 6
      ui/src/app/customer/customer-fieldset.tpl.html
  87. 40
      ui/src/app/customer/customer.controller.js
  88. 14
      ui/src/app/customer/customer.directive.js
  89. 6
      ui/src/app/dashboard/dashboard-card.tpl.html
  90. 39
      ui/src/app/dashboard/dashboard-fieldset.tpl.html
  91. 12
      ui/src/app/dashboard/dashboard-settings.controller.js
  92. 11
      ui/src/app/dashboard/dashboard-settings.tpl.html
  93. 89
      ui/src/app/dashboard/dashboard.controller.js
  94. 19
      ui/src/app/dashboard/dashboard.directive.js
  95. 2
      ui/src/app/dashboard/dashboard.routes.js
  96. 15
      ui/src/app/dashboard/dashboard.tpl.html
  97. 181
      ui/src/app/dashboard/dashboards.controller.js
  98. 3
      ui/src/app/dashboard/dashboards.tpl.html
  99. 1
      ui/src/app/dashboard/edit-widget.directive.js
  100. 1
      ui/src/app/dashboard/edit-widget.tpl.html

1
.gitignore

@ -1,3 +1,4 @@
*.toDelete
output/** output/**
*.class *.class
*~ *~

14
README.md

@ -10,16 +10,16 @@ Thingsboard is an open-source IoT platform for data collection, processing, visu
Thingsboard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs). Thingsboard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs).
## Sample Dashboards ## IoT use cases
[**Smart energy monitoring**](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github) [**Smart energy**](https://thingsboard.io/smart-energy/)
[![Smart energy monitoring demo](https://cloud.githubusercontent.com/assets/8308069/23790111/62c8a61e-0586-11e7-84eb-51febc54ec09.png "Smart energy monitoring demo")](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github) [![Smart energy monitoring demo](https://cloud.githubusercontent.com/assets/8308069/24495682/aebd45d0-153e-11e7-8de4-7360ed5b41ae.gif "Smart energy")](https://thingsboard.io/smart-energy/)
[**Silos monitoring**](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github) [**Smart farming**](https://thingsboard.io/smart-farming/)
[![Silos monitoring demo](https://cloud.githubusercontent.com/assets/8308069/23996135/00214844-0a55-11e7-9623-d1e3be0702ca.png "Silos monitoring demo")](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github) [![Smart farming](https://cloud.githubusercontent.com/assets/8308069/24496824/10dc1144-1542-11e7-8aa1-5d3a281d5a1a.gif "Smart farming")](https://thingsboard.io/smart-farming/)
[**Smart bus tracking**](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github) [**Fleet tracking**](https://thingsboard.io/fleet-tracking/)
[![Smart bus tracking demo](https://cloud.githubusercontent.com/assets/8308069/23790110/62c6ecde-0586-11e7-8249-19eafd5bf8cc.png "Smart bus tracking demo")](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github) [![Fleet tracking](https://cloud.githubusercontent.com/assets/8308069/24497169/3a1a61e0-1543-11e7-8d55-3c8a13f35634.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/)
## Getting Started ## Getting Started

3
application/.gitignore

@ -1 +1,2 @@
!bin/ !bin/
/bin/

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

28
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -51,13 +51,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg; import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg; import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
import java.util.HashMap; import java.util.*;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -205,25 +199,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) { void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
refreshAttributes(msg); refreshAttributes(msg);
Set<AttributeKey> keys = msg.getDeletedKeys();
if (attributeSubscriptions.size() > 0) { if (attributeSubscriptions.size() > 0) {
ToDeviceMsg notification = null; ToDeviceMsg notification = null;
if (msg.isDeleted()) { if (msg.isDeleted()) {
List<AttributeKey> sharedKeys = keys.stream() List<AttributeKey> sharedKeys = msg.getDeletedKeys().stream()
.filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
.collect(Collectors.toList()); .collect(Collectors.toList());
notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys)); notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys));
} else { } else {
List<AttributeKvEntry> attributes = keys.stream() if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) {
.filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues());
.map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey())) if (attributes.size() > 0) {
.filter(Optional::isPresent) notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
.map(Optional::get) } else {
.collect(Collectors.toList()); logger.debug("[{}] No public server side attributes changed!", deviceId);
if (attributes.size() > 0) { }
notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
} else {
logger.debug("[{}] No public server side attributes changed!", deviceId);
} }
} }
if (notification != null) { if (notification != null) {

45
application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2017 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.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import java.util.HashMap;
import java.util.Map;
/**
* Created by yyh on 2017/5/2.
* CORS configuration
*/
@Configuration
@ConfigurationProperties(prefix = "spring.mvc.cors")
public class MvcCorsProperties {
private Map<String, CorsConfiguration> mappings = new HashMap<>();
public MvcCorsProperties() {
}
public Map<String, CorsConfiguration> getMappings() {
return mappings;
}
public void setMappings(Map<String, CorsConfiguration> mappings) {
this.mappings = mappings;
}
}

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

@ -18,7 +18,9 @@ package org.thingsboard.server.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
@ -34,11 +36,15 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.*; import org.thingsboard.server.service.security.auth.jwt.*;
import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -56,6 +62,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
public static final String WEBJARS_ENTRY_POINT = "/webjars/**"; public static final String WEBJARS_ENTRY_POINT = "/webjars/**";
public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
@ -87,10 +94,18 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
return filter; return filter;
} }
@Bean
protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception {
RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean @Bean
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@ -135,6 +150,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().disable().frameOptions().disable() http.headers().cacheControl().disable().frameOptions().disable()
.and()
.cors()
.and() .and()
.csrf().disable() .csrf().disable()
.exceptionHandling() .exceptionHandling()
@ -146,6 +163,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
.antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.and() .and()
@ -156,8 +174,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler) .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
.and() .and()
.addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
} }
@Bean
@ConditionalOnMissingBean(CorsFilter.class)
public CorsFilter corsFilter(@Autowired MvcCorsProperties mvcCorsProperties) {
if (mvcCorsProperties.getMappings().size() == 0) {
return new CorsFilter(new UrlBasedCorsConfigurationSource());
} else {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.setCorsConfigurations(mvcCorsProperties.getMappings());
return new CorsFilter(source);
}
}
} }

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

@ -36,6 +36,7 @@ import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.service.mail.MailService; import org.thingsboard.server.service.mail.MailService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
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.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -167,7 +168,8 @@ public class AuthController extends BaseController {
String encodedPassword = passwordEncoder.encode(password); String encodedPassword = passwordEncoder.encode(password);
UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword); UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
User user = userService.findUserById(credentials.getUserId()); User user = userService.findUserById(credentials.getUserId());
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled()); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
String baseUrl = constructBaseUrl(request); String baseUrl = constructBaseUrl(request);
String loginUrl = String.format("%s/login", baseUrl); String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail(); String email = user.getEmail();
@ -201,7 +203,8 @@ public class AuthController extends BaseController {
userCredentials.setResetToken(null); userCredentials.setResetToken(null);
userCredentials = userService.saveUserCredentials(userCredentials); userCredentials = userService.saveUserCredentials(userCredentials);
User user = userService.findUserById(userCredentials.getUserId()); User user = userService.findUserById(userCredentials.getUserId());
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
String baseUrl = constructBaseUrl(request); String baseUrl = constructBaseUrl(request);
String loginUrl = String.format("%s/login", baseUrl); String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail(); String email = user.getEmail();

25
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -15,6 +15,9 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -42,6 +45,28 @@ public class CustomerController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET)
@ResponseBody
public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId);
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode infoObject = objectMapper.createObjectNode();
infoObject.put("title", customer.getTitle());
boolean isPublic = false;
if (customer.getAdditionalInfo() != null && customer.getAdditionalInfo().has("isPublic")) {
isPublic = customer.getAdditionalInfo().get("isPublic").asBoolean();
}
infoObject.put("isPublic", isPublic);
return infoObject;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer", method = RequestMethod.POST) @RequestMapping(value = "/customer", method = RequestMethod.POST)
@ResponseBody @ResponseBody

16
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -116,6 +117,21 @@ public class DashboardController extends BaseController {
} }
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST)
@ResponseBody
public Dashboard assignDashboardToPublicCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
checkParameter("dashboardId", strDashboardId);
try {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
@ResponseBody @ResponseBody

16
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -117,6 +118,21 @@ public class DeviceController extends BaseController {
} }
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public Device assignDeviceToPublicCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
checkParameter("deviceId", strDeviceId);
try {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId());
return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET) @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET)
@ResponseBody @ResponseBody

69
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java

@ -16,32 +16,40 @@
package org.thingsboard.server.service.security.auth.jwt; package org.thingsboard.server.service.security.auth.jwt;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.*;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
import java.util.UUID;
@Component @Component
public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;
private final UserService userService; private final UserService userService;
private final CustomerService customerService;
@Autowired @Autowired
public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) { public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) {
this.userService = userService; this.userService = userService;
this.customerService = customerService;
this.tokenFactory = tokenFactory; this.tokenFactory = tokenFactory;
} }
@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
Assert.notNull(authentication, "No authentication data provided"); Assert.notNull(authentication, "No authentication data provided");
RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken);
UserPrincipal principal = unsafeUser.getUserPrincipal();
SecurityUser securityUser;
if (principal.getType() == UserPrincipal.Type.USER_NAME) {
securityUser = authenticateByUserId(unsafeUser.getId());
} else {
securityUser = authenticateByPublicId(principal.getValue());
}
return new RefreshAuthenticationToken(securityUser);
}
User user = userService.findUserById(unsafeUser.getId()); private SecurityUser authenticateByUserId(UserId userId) {
User user = userService.findUserById(userId);
if (user == null) { if (user == null) {
throw new UsernameNotFoundException("User not found by refresh token"); throw new UsernameNotFoundException("User not found by refresh token");
} }
@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
return new RefreshAuthenticationToken(securityUser); SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
return securityUser;
}
private SecurityUser authenticateByPublicId(String publicId) {
CustomerId customerId;
try {
customerId = new CustomerId(UUID.fromString(publicId));
} catch (Exception e) {
throw new BadCredentialsException("Refresh token is not valid");
}
Customer publicCustomer = customerService.findCustomerById(customerId);
if (publicCustomer == null) {
throw new UsernameNotFoundException("Public entity not found by refresh token");
}
boolean isPublic = false;
if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) {
isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean();
}
if (!isPublic) {
throw new BadCredentialsException("Refresh token is not valid");
}
User user = new User(new UserId(UUIDBased.EMPTY));
user.setTenantId(publicCustomer.getTenantId());
user.setCustomerId(publicCustomer.getId());
user.setEmail(publicId);
user.setAuthority(Authority.CUSTOMER_USER);
user.setFirstName("Public");
user.setLastName("Public");
UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId);
SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
return securityUser;
} }
@Override @Override

34
application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2017 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.rest;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PublicLoginRequest {
private String publicId;
@JsonCreator
public PublicLoginRequest(@JsonProperty("publicId") String publicId) {
this.publicId = publicId;
}
public String getPublicId() {
return publicId;
}
}

64
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.util.UUID;
@Component @Component
public class RestAuthenticationProvider implements AuthenticationProvider { public class RestAuthenticationProvider implements AuthenticationProvider {
private final BCryptPasswordEncoder encoder; private final BCryptPasswordEncoder encoder;
private final UserService userService; private final UserService userService;
private final CustomerService customerService;
@Autowired @Autowired
public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) { public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) {
this.userService = userService; this.userService = userService;
this.customerService = customerService;
this.encoder = encoder; this.encoder = encoder;
} }
@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided"); Assert.notNull(authentication, "No authentication data provided");
String username = (String) authentication.getPrincipal(); Object principal = authentication.getPrincipal();
String password = (String) authentication.getCredentials(); if (!(principal instanceof UserPrincipal)) {
throw new BadCredentialsException("Authentication Failed. Bad user principal.");
}
UserPrincipal userPrincipal = (UserPrincipal) principal;
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials();
return authenticateByUsernameAndPassword(userPrincipal, username, password);
} else {
String publicId = userPrincipal.getValue();
return authenticateByPublicId(userPrincipal, publicId);
}
}
private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) {
User user = userService.findUserByEmail(username); User user = userService.findUserByEmail(username);
if (user == null) { if (user == null) {
throw new UsernameNotFoundException("User not found: " + username); throw new UsernameNotFoundException("User not found: " + username);
@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
}
private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
CustomerId customerId;
try {
customerId = new CustomerId(UUID.fromString(publicId));
} catch (Exception e) {
throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
}
Customer publicCustomer = customerService.findCustomerById(customerId);
if (publicCustomer == null) {
throw new UsernameNotFoundException("Public entity not found: " + publicId);
}
boolean isPublic = false;
if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) {
isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean();
}
if (!isPublic) {
throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
}
User user = new User(new UserId(UUIDBased.EMPTY));
user.setTenantId(publicCustomer.getTenantId());
user.setCustomerId(publicCustomer.getId());
user.setEmail(publicId);
user.setAuthority(Authority.CUSTOMER_USER);
user.setFirstName("Public");
user.setLastName("Public");
SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
} }

5
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java

@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.model.UserPrincipal;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
throw new AuthenticationServiceException("Username or Password not provided"); throw new AuthenticationServiceException("Username or Password not provided");
} }
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword());
return this.getAuthenticationManager().authenticate(token); return this.getAuthenticationManager().authenticate(token);
} }

96
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2017 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.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.model.UserPrincipal;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class);
private final AuthenticationSuccessHandler successHandler;
private final AuthenticationFailureHandler failureHandler;
private final ObjectMapper objectMapper;
public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
super(defaultProcessUrl);
this.successHandler = successHandler;
this.failureHandler = failureHandler;
this.objectMapper = mapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (!HttpMethod.POST.name().equals(request.getMethod())) {
if(logger.isDebugEnabled()) {
logger.debug("Authentication method not supported. Request method: " + request.getMethod());
}
throw new AuthMethodNotSupportedException("Authentication method not supported");
}
PublicLoginRequest loginRequest;
try {
loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class);
} catch (Exception e) {
throw new AuthenticationServiceException("Invalid public login request payload");
}
if (StringUtils.isBlank(loginRequest.getPublicId())) {
throw new AuthenticationServiceException("Public Id is not provided");
}
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "");
return this.getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
successHandler.onAuthenticationSuccess(request, response, authResult);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
}

12
application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java

@ -30,6 +30,7 @@ public class SecurityUser extends User {
private Collection<GrantedAuthority> authorities; private Collection<GrantedAuthority> authorities;
private boolean enabled; private boolean enabled;
private UserPrincipal userPrincipal;
public SecurityUser() { public SecurityUser() {
super(); super();
@ -39,9 +40,10 @@ public class SecurityUser extends User {
super(id); super(id);
} }
public SecurityUser(User user, boolean enabled) { public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) {
super(user); super(user);
this.enabled = enabled; this.enabled = enabled;
this.userPrincipal = userPrincipal;
} }
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
@ -57,8 +59,16 @@ public class SecurityUser extends User {
return enabled; return enabled;
} }
public UserPrincipal getUserPrincipal() {
return userPrincipal;
}
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
public void setUserPrincipal(UserPrincipal userPrincipal) {
this.userPrincipal = userPrincipal;
}
} }

42
application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2017 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.model;
public class UserPrincipal {
private final Type type;
private final String value;
public UserPrincipal(Type type, String value) {
this.type = type;
this.value = value;
}
public Type getType() {
return type;
}
public String getValue() {
return value;
}
public enum Type {
USER_NAME,
PUBLIC_ID
}
}

18
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -43,6 +44,7 @@ public class JwtTokenFactory {
private static final String FIRST_NAME = "firstName"; private static final String FIRST_NAME = "firstName";
private static final String LAST_NAME = "lastName"; private static final String LAST_NAME = "lastName";
private static final String ENABLED = "enabled"; private static final String ENABLED = "enabled";
private static final String IS_PUBLIC = "isPublic";
private static final String TENANT_ID = "tenantId"; private static final String TENANT_ID = "tenantId";
private static final String CUSTOMER_ID = "customerId"; private static final String CUSTOMER_ID = "customerId";
@ -63,12 +65,15 @@ public class JwtTokenFactory {
if (securityUser.getAuthority() == null) if (securityUser.getAuthority() == null)
throw new IllegalArgumentException("User doesn't have any privileges"); throw new IllegalArgumentException("User doesn't have any privileges");
Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); UserPrincipal principal = securityUser.getUserPrincipal();
String subject = principal.getValue();
Claims claims = Jwts.claims().setSubject(subject);
claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList())); claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(FIRST_NAME, securityUser.getFirstName()); claims.put(FIRST_NAME, securityUser.getFirstName());
claims.put(LAST_NAME, securityUser.getLastName()); claims.put(LAST_NAME, securityUser.getLastName());
claims.put(ENABLED, securityUser.isEnabled()); claims.put(ENABLED, securityUser.isEnabled());
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
if (securityUser.getTenantId() != null) { if (securityUser.getTenantId() != null) {
claims.put(TENANT_ID, securityUser.getTenantId().getId().toString()); claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
} }
@ -104,6 +109,9 @@ public class JwtTokenFactory {
securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class));
securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
securityUser.setUserPrincipal(principal);
String tenantId = claims.get(TENANT_ID, String.class); String tenantId = claims.get(TENANT_ID, String.class);
if (tenantId != null) { if (tenantId != null) {
securityUser.setTenantId(new TenantId(UUID.fromString(tenantId))); securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
@ -123,9 +131,11 @@ public class JwtTokenFactory {
DateTime currentTime = new DateTime(); DateTime currentTime = new DateTime();
Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); UserPrincipal principal = securityUser.getUserPrincipal();
Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name())); claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name()));
claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
String token = Jwts.builder() String token = Jwts.builder()
.setClaims(claims) .setClaims(claims)
@ -150,8 +160,10 @@ public class JwtTokenFactory {
if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) { if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
throw new IllegalArgumentException("Invalid Refresh Token scope"); throw new IllegalArgumentException("Invalid Refresh Token scope");
} }
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
securityUser.setEmail(subject); securityUser.setUserPrincipal(principal);
return securityUser; return securityUser;
} }

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

@ -188,3 +188,25 @@ cache:
updates: updates:
# Enable/disable updates checking. # Enable/disable updates checking.
enabled: "${UPDATES_ENABLED:true}" enabled: "${UPDATES_ENABLED:true}"
# spring CORS configuration
spring.mvc.cors:
mappings:
# Intercept path
"/api/auth/**":
#Comma-separated list of origins to allow. '*' allows all origins. When not set,CORS support is disabled.
allowed-origins: "*"
#Comma-separated list of methods to allow. '*' allows all methods.
allowed-methods: "POST,GET,OPTIONS"
#Comma-separated list of headers to allow in a request. '*' allows all headers.
allowed-headers: "*"
#How long, in seconds, the response from a pre-flight request can be cached by clients.
max-age: "1800"
#Set whether credentials are supported. When not set, credentials are not supported.
allow-credentials: "true"
"/api/v1/**":
allowed-origins: "*"
allowed-methods: "*"
allowed-headers: "*"
max-age: "1800"
allow-credentials: "true"

4
application/src/main/scripts/windows/install.bat

@ -2,9 +2,6 @@
setlocal ENABLEEXTENSIONS setlocal ENABLEEXTENSIONS
IF %PROCESSOR_ARCHITECTURE%==AMD64 GOTO CHECK_JAVA_64
IF %PROCESSOR_ARCHITECTURE%==x86 GOTO CHECK_JAVA_32
@ECHO Detecting Java version installed. @ECHO Detecting Java version installed.
:CHECK_JAVA_64 :CHECK_JAVA_64
@ECHO Detecting if it is 64 bit machine @ECHO Detecting if it is 64 bit machine
@ -36,7 +33,6 @@ if defined ValueName (
) )
IF NOT "%JRE_PATH2%" == "" GOTO JAVA_INSTALLED IF NOT "%JRE_PATH2%" == "" GOTO JAVA_INSTALLED
IF "%JRE_PATH2%" == "" GOTO JAVA_NOT_INSTALLED
:CHECK_JAVA_32 :CHECK_JAVA_32
@ECHO Detecting if it is 32 bit machine @ECHO Detecting if it is 32 bit machine

4
application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java

@ -288,7 +288,7 @@ public class CustomerControllerTest extends AbstractControllerTest {
for (int i=0;i<143;i++) { for (int i=0;i<143;i++) {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTenantId(tenantId); customer.setTenantId(tenantId);
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title1+suffix; String title = title1+suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
customer.setTitle(title); customer.setTitle(title);
@ -299,7 +299,7 @@ public class CustomerControllerTest extends AbstractControllerTest {
for (int i=0;i<175;i++) { for (int i=0;i<175;i++) {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTenantId(tenantId); customer.setTenantId(tenantId);
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title2+suffix; String title = title2+suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
customer.setTitle(title); customer.setTitle(title);

4
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -149,7 +149,7 @@ public class TenantControllerTest extends AbstractControllerTest {
List<Tenant> tenantsTitle1 = new ArrayList<>(); List<Tenant> tenantsTitle1 = new ArrayList<>();
for (int i=0;i<134;i++) { for (int i=0;i<134;i++) {
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title1+suffix; String title = title1+suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
tenant.setTitle(title); tenant.setTitle(title);
@ -159,7 +159,7 @@ public class TenantControllerTest extends AbstractControllerTest {
List<Tenant> tenantsTitle2 = new ArrayList<>(); List<Tenant> tenantsTitle2 = new ArrayList<>();
for (int i=0;i<127;i++) { for (int i=0;i<127;i++) {
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title2+suffix; String title = title2+suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
tenant.setTitle(title); tenant.setTitle(title);

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

11
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java

@ -16,12 +16,14 @@
package org.thingsboard.server.dao.customer; package org.thingsboard.server.dao.customer;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.model.CustomerEntity; import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.DeviceEntity;
/** /**
* The Interface CustomerDao. * The Interface CustomerDao.
@ -44,5 +46,14 @@ public interface CustomerDao extends Dao<CustomerEntity> {
* @return the list of customer objects * @return the list of customer objects
*/ */
List<CustomerEntity> findCustomersByTenantId(UUID tenantId, TextPageLink pageLink); List<CustomerEntity> findCustomersByTenantId(UUID tenantId, TextPageLink pageLink);
/**
* Find customers by tenantId and customer title.
*
* @param tenantId the tenantId
* @param title the customer title
* @return the optional customer object
*/
Optional<CustomerEntity> findCustomersByTenantIdAndTitle(UUID tenantId, String title);
} }

16
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java

@ -16,11 +16,18 @@
package org.thingsboard.server.dao.customer; package org.thingsboard.server.dao.customer;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import com.datastax.driver.core.querybuilder.Select;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
@ -60,4 +67,13 @@ public class CustomerDaoImpl extends AbstractSearchTextDao<CustomerEntity> imple
return customerEntities; return customerEntities;
} }
@Override
public Optional<CustomerEntity> findCustomersByTenantIdAndTitle(UUID tenantId, String title) {
Select select = select().from(CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME);
Select.Where query = select.where();
query.and(eq(CUSTOMER_TENANT_ID_PROPERTY, tenantId));
query.and(eq(CUSTOMER_TITLE_PROPERTY, title));
return Optional.ofNullable(findOneByStatement(query));
}
} }

2
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java

@ -28,6 +28,8 @@ public interface CustomerService {
public Customer saveCustomer(Customer customer); public Customer saveCustomer(Customer customer);
public void deleteCustomer(CustomerId customerId); public void deleteCustomer(CustomerId customerId);
public Customer findOrCreatePublicCustomer(TenantId tenantId);
public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink); public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink);

53
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -18,8 +18,12 @@ package org.thingsboard.server.dao.customer;
import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.convertDataList;
import static org.thingsboard.server.dao.DaoUtil.getData; import static org.thingsboard.server.dao.DaoUtil.getData;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
@ -46,6 +50,8 @@ import org.thingsboard.server.dao.service.Validator;
@Slf4j @Slf4j
public class CustomerServiceImpl implements CustomerService { public class CustomerServiceImpl implements CustomerService {
private static final String PUBLIC_CUSTOMER_TITLE = "Public";
@Autowired @Autowired
private CustomerDao customerDao; private CustomerDao customerDao;
@ -80,7 +86,7 @@ public class CustomerServiceImpl implements CustomerService {
@Override @Override
public void deleteCustomer(CustomerId customerId) { public void deleteCustomer(CustomerId customerId) {
log.trace("Executing deleteCustomer [{}]", customerId); log.trace("Executing deleteCustomer [{}]", customerId);
Validator.validateId(customerId, "Incorrect tenantId " + customerId); Validator.validateId(customerId, "Incorrect customerId " + customerId);
Customer customer = findCustomerById(customerId); Customer customer = findCustomerById(customerId);
if (customer == null) { if (customer == null) {
throw new IncorrectParameterException("Unable to delete non-existent customer."); throw new IncorrectParameterException("Unable to delete non-existent customer.");
@ -91,6 +97,27 @@ public class CustomerServiceImpl implements CustomerService {
customerDao.removeById(customerId.getId()); customerDao.removeById(customerId.getId());
} }
@Override
public Customer findOrCreatePublicCustomer(TenantId tenantId) {
log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
Validator.validateId(tenantId, "Incorrect customerId " + tenantId);
Optional<CustomerEntity> publicCustomerEntity = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE);
if (publicCustomerEntity.isPresent()) {
return getData(publicCustomerEntity.get());
} else {
Customer publicCustomer = new Customer();
publicCustomer.setTenantId(tenantId);
publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
try {
publicCustomer.setAdditionalInfo(new ObjectMapper().readValue("{ \"isPublic\": true }", JsonNode.class));
} catch (IOException e) {
throw new IncorrectParameterException("Unable to create public customer.", e);
}
CustomerEntity customerEntity = customerDao.save(publicCustomer);
return getData(customerEntity);
}
}
@Override @Override
public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) { public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) {
log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService {
private DataValidator<Customer> customerValidator = private DataValidator<Customer> customerValidator =
new DataValidator<Customer>() { new DataValidator<Customer>() {
@Override
protected void validateCreate(Customer customer) {
customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
c -> {
throw new DataValidationException("Customer with such title already exists!");
}
);
}
@Override
protected void validateUpdate(Customer customer) {
customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
c -> {
if (!c.getId().equals(customer.getUuidId())) {
throw new DataValidationException("Customer with such title already exists!");
}
}
);
}
@Override @Override
protected void validateDataImpl(Customer customer) { protected void validateDataImpl(Customer customer) {
if (StringUtils.isEmpty(customer.getTitle())) { if (StringUtils.isEmpty(customer.getTitle())) {
throw new DataValidationException("Customer title should be specified!"); throw new DataValidationException("Customer title should be specified!");
} }
if (customer.getTitle().equals(PUBLIC_CUSTOMER_TITLE)) {
throw new DataValidationException("'Public' title for customer is system reserved!");
}
if (!StringUtils.isEmpty(customer.getEmail())) { if (!StringUtils.isEmpty(customer.getEmail())) {
validateEmail(customer.getEmail()); validateEmail(customer.getEmail());
} }

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -111,6 +111,7 @@ public class ModelConstants {
public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text"; public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text";
public static final String CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME = "customer_by_tenant_and_title";
/** /**
* Cassandra device constants. * Cassandra device constants.

7
dao/src/main/resources/schema.cql

@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer (
PRIMARY KEY (id, tenant_id) PRIMARY KEY (id, tenant_id)
); );
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_title AS
SELECT *
from thingsboard.customer
WHERE tenant_id IS NOT NULL AND title IS NOT NULL AND id IS NOT NULL
PRIMARY KEY ( tenant_id, title, id )
WITH CLUSTERING ORDER BY ( title ASC, id DESC );
CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS
SELECT * SELECT *
from thingsboard.customer from thingsboard.customer

2
dao/src/main/resources/system-data.cql

File diff suppressed because one or more lines are too long

78
dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java

@ -34,11 +34,11 @@ import org.junit.Test;
import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.core.utils.UUIDs;
public class CustomerServiceImplTest extends AbstractServiceTest { public class CustomerServiceImplTest extends AbstractServiceTest {
private IdComparator<Customer> idComparator = new IdComparator<>(); private IdComparator<Customer> idComparator = new IdComparator<>();
private TenantId tenantId; private TenantId tenantId;
@Before @Before
public void before() { public void before() {
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
@ -59,23 +59,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
customer.setTenantId(tenantId); customer.setTenantId(tenantId);
customer.setTitle("My customer"); customer.setTitle("My customer");
Customer savedCustomer = customerService.saveCustomer(customer); Customer savedCustomer = customerService.saveCustomer(customer);
Assert.assertNotNull(savedCustomer); Assert.assertNotNull(savedCustomer);
Assert.assertNotNull(savedCustomer.getId()); Assert.assertNotNull(savedCustomer.getId());
Assert.assertTrue(savedCustomer.getCreatedTime() > 0); Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId()); Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId());
Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle()); Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
savedCustomer.setTitle("My new customer"); savedCustomer.setTitle("My new customer");
customerService.saveCustomer(savedCustomer); customerService.saveCustomer(savedCustomer);
Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId());
Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle()); Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle());
customerService.deleteCustomer(savedCustomer.getId()); customerService.deleteCustomer(savedCustomer.getId());
} }
@Test @Test
public void testFindCustomerById() { public void testFindCustomerById() {
Customer customer = new Customer(); Customer customer = new Customer();
@ -87,21 +87,21 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
Assert.assertEquals(savedCustomer, foundCustomer); Assert.assertEquals(savedCustomer, foundCustomer);
customerService.deleteCustomer(savedCustomer.getId()); customerService.deleteCustomer(savedCustomer.getId());
} }
@Test(expected = DataValidationException.class) @Test(expected = DataValidationException.class)
public void testSaveCustomerWithEmptyTitle() { public void testSaveCustomerWithEmptyTitle() {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTenantId(tenantId); customer.setTenantId(tenantId);
customerService.saveCustomer(customer); customerService.saveCustomer(customer);
} }
@Test(expected = DataValidationException.class) @Test(expected = DataValidationException.class)
public void testSaveCustomerWithEmptyTenant() { public void testSaveCustomerWithEmptyTenant() {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTitle("My customer"); customer.setTitle("My customer");
customerService.saveCustomer(customer); customerService.saveCustomer(customer);
} }
@Test(expected = DataValidationException.class) @Test(expected = DataValidationException.class)
public void testSaveCustomerWithInvalidTenant() { public void testSaveCustomerWithInvalidTenant() {
Customer customer = new Customer(); Customer customer = new Customer();
@ -109,7 +109,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
customer.setTenantId(new TenantId(UUIDs.timeBased())); customer.setTenantId(new TenantId(UUIDs.timeBased()));
customerService.saveCustomer(customer); customerService.saveCustomer(customer);
} }
@Test(expected = DataValidationException.class) @Test(expected = DataValidationException.class)
public void testSaveCustomerWithInvalidEmail() { public void testSaveCustomerWithInvalidEmail() {
Customer customer = new Customer(); Customer customer = new Customer();
@ -118,7 +118,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
customer.setEmail("invalid@mail"); customer.setEmail("invalid@mail");
customerService.saveCustomer(customer); customerService.saveCustomer(customer);
} }
@Test @Test
public void testDeleteCustomer() { public void testDeleteCustomer() {
Customer customer = new Customer(); Customer customer = new Customer();
@ -129,23 +129,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId());
Assert.assertNull(foundCustomer); Assert.assertNull(foundCustomer);
} }
@Test @Test
public void testFindCustomersByTenantId() { public void testFindCustomersByTenantId() {
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
tenant.setTitle("Test tenant"); tenant.setTitle("Test tenant");
tenant = tenantService.saveTenant(tenant); tenant = tenantService.saveTenant(tenant);
TenantId tenantId = tenant.getId(); TenantId tenantId = tenant.getId();
List<Customer> customers = new ArrayList<>(); List<Customer> customers = new ArrayList<>();
for (int i=0;i<135;i++) { for (int i = 0; i < 135; i++) {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTenantId(tenantId); customer.setTenantId(tenantId);
customer.setTitle("Customer"+i); customer.setTitle("Customer" + i);
customers.add(customerService.saveCustomer(customer)); customers.add(customerService.saveCustomer(customer));
} }
List<Customer> loadedCustomers = new ArrayList<>(); List<Customer> loadedCustomers = new ArrayList<>();
TextPageLink pageLink = new TextPageLink(23); TextPageLink pageLink = new TextPageLink(23);
TextPageData<Customer> pageData = null; TextPageData<Customer> pageData = null;
@ -156,47 +156,47 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
pageLink = pageData.getNextPageLink(); pageLink = pageData.getNextPageLink();
} }
} while (pageData.hasNext()); } while (pageData.hasNext());
Collections.sort(customers, idComparator); Collections.sort(customers, idComparator);
Collections.sort(loadedCustomers, idComparator); Collections.sort(loadedCustomers, idComparator);
Assert.assertEquals(customers, loadedCustomers); Assert.assertEquals(customers, loadedCustomers);
customerService.deleteCustomersByTenantId(tenantId); customerService.deleteCustomersByTenantId(tenantId);
pageLink = new TextPageLink(33); pageLink = new TextPageLink(33);
pageData = customerService.findCustomersByTenantId(tenantId, pageLink); pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());
Assert.assertTrue(pageData.getData().isEmpty()); Assert.assertTrue(pageData.getData().isEmpty());
tenantService.deleteTenant(tenantId); tenantService.deleteTenant(tenantId);
} }
@Test @Test
public void testFindCustomersByTenantIdAndTitle() { public void testFindCustomersByTenantIdAndTitle() {
String title1 = "Customer title 1"; String title1 = "Customer title 1";
List<Customer> customersTitle1 = new ArrayList<>(); List<Customer> customersTitle1 = new ArrayList<>();
for (int i=0;i<143;i++) { for (int i = 0; i < 143; i++) {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTenantId(tenantId); customer.setTenantId(tenantId);
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title1+suffix; String title = title1 + suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
customer.setTitle(title); customer.setTitle(title);
customersTitle1.add(customerService.saveCustomer(customer)); customersTitle1.add(customerService.saveCustomer(customer));
} }
String title2 = "Customer title 2"; String title2 = "Customer title 2";
List<Customer> customersTitle2 = new ArrayList<>(); List<Customer> customersTitle2 = new ArrayList<>();
for (int i=0;i<175;i++) { for (int i = 0; i < 175; i++) {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTenantId(tenantId); customer.setTenantId(tenantId);
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title2+suffix; String title = title2 + suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
customer.setTitle(title); customer.setTitle(title);
customersTitle2.add(customerService.saveCustomer(customer)); customersTitle2.add(customerService.saveCustomer(customer));
} }
List<Customer> loadedCustomersTitle1 = new ArrayList<>(); List<Customer> loadedCustomersTitle1 = new ArrayList<>();
TextPageLink pageLink = new TextPageLink(15, title1); TextPageLink pageLink = new TextPageLink(15, title1);
TextPageData<Customer> pageData = null; TextPageData<Customer> pageData = null;
@ -207,12 +207,12 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
pageLink = pageData.getNextPageLink(); pageLink = pageData.getNextPageLink();
} }
} while (pageData.hasNext()); } while (pageData.hasNext());
Collections.sort(customersTitle1, idComparator); Collections.sort(customersTitle1, idComparator);
Collections.sort(loadedCustomersTitle1, idComparator); Collections.sort(loadedCustomersTitle1, idComparator);
Assert.assertEquals(customersTitle1, loadedCustomersTitle1); Assert.assertEquals(customersTitle1, loadedCustomersTitle1);
List<Customer> loadedCustomersTitle2 = new ArrayList<>(); List<Customer> loadedCustomersTitle2 = new ArrayList<>();
pageLink = new TextPageLink(4, title2); pageLink = new TextPageLink(4, title2);
do { do {
@ -225,22 +225,22 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
Collections.sort(customersTitle2, idComparator); Collections.sort(customersTitle2, idComparator);
Collections.sort(loadedCustomersTitle2, idComparator); Collections.sort(loadedCustomersTitle2, idComparator);
Assert.assertEquals(customersTitle2, loadedCustomersTitle2); Assert.assertEquals(customersTitle2, loadedCustomersTitle2);
for (Customer customer : loadedCustomersTitle1) { for (Customer customer : loadedCustomersTitle1) {
customerService.deleteCustomer(customer.getId()); customerService.deleteCustomer(customer.getId());
} }
pageLink = new TextPageLink(4, title1); pageLink = new TextPageLink(4, title1);
pageData = customerService.findCustomersByTenantId(tenantId, pageLink); pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size()); Assert.assertEquals(0, pageData.getData().size());
for (Customer customer : loadedCustomersTitle2) { for (Customer customer : loadedCustomersTitle2) {
customerService.deleteCustomer(customer.getId()); customerService.deleteCustomer(customer.getId());
} }
pageLink = new TextPageLink(4, title2); pageLink = new TextPageLink(4, title2);
pageData = customerService.findCustomersByTenantId(tenantId, pageLink); pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
Assert.assertFalse(pageData.hasNext()); Assert.assertFalse(pageData.hasNext());

2
extensions-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
extensions-core/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.extensions.core.plugin.telemetry; package org.thingsboard.server.extensions.core.plugin.telemetry;
import com.sun.javafx.collections.MappingChange;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
@ -292,7 +291,6 @@ public class SubscriptionManager {
} else { } else {
log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress); log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress);
subscriptionIterator.remove(); subscriptionIterator.remove();
//TODO: onUpdate state of subscription by WsSessionId and other maps. //TODO: onUpdate state of subscription by WsSessionId and other maps.
} }
} }

7
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java

@ -139,7 +139,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
Map<String, List<TsData>> result = new LinkedHashMap<>(); Map<String, List<TsData>> result = new LinkedHashMap<>();
for (TsKvEntry entry : data) { for (TsKvEntry entry : data) {
result.put(entry.getKey(), data.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList())); List<TsData> vList = result.get(entry.getKey());
if (vList == null) {
vList = new ArrayList<>();
result.put(entry.getKey(), vList);
}
vList.add(new TsData(entry.getTs(), entry.getValueAsString()));
} }
msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK)); msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK));
} }

4
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java

@ -30,7 +30,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
import org.thingsboard.server.extensions.api.rules.RuleException; import org.thingsboard.server.extensions.api.rules.RuleException;
import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction; import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction;
import java.time.LocalDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
/** /**
@ -51,7 +51,7 @@ public class TimePlugin extends AbstractPlugin<TimePluginConfiguration> implemen
String reply; String reply;
if (!StringUtils.isEmpty(format)) { if (!StringUtils.isEmpty(format)) {
reply = "\"" + formatter.format(LocalDateTime.now()) + "\""; reply = "\"" + formatter.format(ZonedDateTime.now()) + "\"";
} else { } else {
reply = Long.toString(System.currentTimeMillis()); reply = Long.toString(System.currentTimeMillis());
} }

2
extensions/extension-kafka/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>extensions</artifactId> <artifactId>extensions</artifactId>
</parent> </parent>
<groupId>org.thingsboard.extensions</groupId> <groupId>org.thingsboard.extensions</groupId>

2
extensions/extension-rabbitmq/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>extensions</artifactId> <artifactId>extensions</artifactId>
</parent> </parent>
<groupId>org.thingsboard.extensions</groupId> <groupId>org.thingsboard.extensions</groupId>

2
extensions/extension-rest-api-call/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>extensions</artifactId> <artifactId>extensions</artifactId>
</parent> </parent>
<groupId>org.thingsboard.extensions</groupId> <groupId>org.thingsboard.extensions</groupId>

7
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java

@ -55,6 +55,13 @@ public class RestApiCallPlugin extends AbstractPlugin<RestApiCallPluginConfigura
this.headers.add(AUTHORIZATION_HEADER_NAME, String.format(AUTHORIZATION_HEADER_FORMAT, new String(token))); this.headers.add(AUTHORIZATION_HEADER_NAME, String.format(AUTHORIZATION_HEADER_FORMAT, new String(token)));
} }
if (configuration.getHeaders() != null) {
configuration.getHeaders().forEach(h -> {
log.debug("Adding header to request object. Key = {}, Value = {}", h.getKey(), h.getValue());
this.headers.add(h.getKey(), h.getValue());
});
}
init(); init();
} }

5
extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java

@ -16,6 +16,9 @@
package org.thingsboard.server.extensions.rest.plugin; package org.thingsboard.server.extensions.rest.plugin;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties;
import java.util.List;
@Data @Data
public class RestApiCallPluginConfiguration { public class RestApiCallPluginConfiguration {
@ -27,4 +30,6 @@ public class RestApiCallPluginConfiguration {
private String userName; private String userName;
private String password; private String password;
private List<KeyValuePluginProperties> headers;
} }

21
extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json

@ -30,6 +30,24 @@
"password": { "password": {
"title": "Password", "title": "Password",
"type": "string" "type": "string"
},
"headers": {
"title": "Request Headers",
"type": "array",
"items": {
"title": "Request Header",
"type": "object",
"properties": {
"key": {
"title": "Key",
"type": "string"
},
"value": {
"title": "Value",
"type": "string"
}
}
}
} }
}, },
"required": [ "required": [
@ -62,6 +80,7 @@
{ {
"key": "password", "key": "password",
"type": "password" "type": "password"
} },
"headers"
] ]
} }

2
extensions/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Thingsboard</name> <name>Thingsboard</name>

38
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
@ -54,40 +54,4 @@
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>org.thingsboard.client.tools.MqttStressTestTool</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

7
tools/src/main/java/org/thingsboard/client/tools/RestClient.java

@ -27,6 +27,7 @@ import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -76,6 +77,12 @@ public class RestClient implements ClientHttpRequestInterceptor {
return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
} }
public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
customerId.toString(), deviceId.toString()).getBody();
}
public DeviceCredentials getCredentials(DeviceId id) { public DeviceCredentials getCredentials(DeviceId id) {
return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody(); return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
} }

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -247,7 +247,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private MqttMessage createUnSubAckMessage(int msgId) { private MqttMessage createUnSubAckMessage(int msgId) {
MqttFixedHeader mqttFixedHeader = MqttFixedHeader mqttFixedHeader =
new MqttFixedHeader(SUBACK, false, AT_LEAST_ONCE, false, 0); new MqttFixedHeader(UNSUBACK, false, AT_LEAST_ONCE, false, 0);
MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader);
} }

4
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java

@ -40,6 +40,8 @@ import java.security.cert.CertificateException;
*/ */
public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> { public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
private final SessionMsgProcessor processor; private final SessionMsgProcessor processor;
private final DeviceService deviceService; private final DeviceService deviceService;
private final DeviceAuthService authService; private final DeviceAuthService authService;
@ -63,7 +65,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
sslHandler = sslHandlerProvider.getSslHandler(); sslHandler = sslHandlerProvider.getSslHandler();
pipeline.addLast(sslHandler); pipeline.addLast(sslHandler);
} }
pipeline.addLast("decoder", new MqttDecoder()); pipeline.addLast("decoder", new MqttDecoder(MAX_PAYLOAD_SIZE));
pipeline.addLast("encoder", MqttEncoder.INSTANCE); pipeline.addLast("encoder", MqttEncoder.INSTANCE);
MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, adaptor, sslHandler); MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, adaptor, sslHandler);

2
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java

@ -134,7 +134,7 @@ public class GatewaySessionCtx {
JsonObject jsonObj = json.getAsJsonObject(); JsonObject jsonObj = json.getAsJsonObject();
String deviceName = checkDeviceConnected(jsonObj.get("device").getAsString()); String deviceName = checkDeviceConnected(jsonObj.get("device").getAsString());
Integer requestId = jsonObj.get("id").getAsInt(); Integer requestId = jsonObj.get("id").getAsInt();
String data = jsonObj.get("data").getAsString(); String data = jsonObj.get("data").toString();
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data)))); new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

4
ui/package.json

@ -1,7 +1,7 @@
{ {
"name": "thingsboard", "name": "thingsboard",
"private": true, "private": true,
"version": "1.2.2", "version": "1.2.3",
"description": "Thingsboard UI", "description": "Thingsboard UI",
"licenses": [ "licenses": [
{ {
@ -33,6 +33,7 @@
"angular-messages": "1.5.8", "angular-messages": "1.5.8",
"angular-route": "1.5.8", "angular-route": "1.5.8",
"angular-sanitize": "1.5.8", "angular-sanitize": "1.5.8",
"angular-socialshare": "^2.3.8",
"angular-storage": "0.0.15", "angular-storage": "0.0.15",
"angular-touch": "1.5.8", "angular-touch": "1.5.8",
"angular-translate": "2.13.1", "angular-translate": "2.13.1",
@ -49,6 +50,7 @@
"clipboard": "^1.5.15", "clipboard": "^1.5.15",
"compass-sass-mixins": "^0.12.7", "compass-sass-mixins": "^0.12.7",
"flot": "git://github.com/flot/flot.git#0.9-work", "flot": "git://github.com/flot/flot.git#0.9-work",
"flot-curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.6.3", "font-awesome": "^4.6.3",
"javascript-detect-element-resize": "^0.5.3", "javascript-detect-element-resize": "^0.5.3",
"jquery": "^3.1.0", "jquery": "^3.1.0",

2
ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.2.2</version> <version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

87
ui/src/app/api/customer.service.js

@ -18,11 +18,14 @@ export default angular.module('thingsboard.api.customer', [])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function CustomerService($http, $q) { function CustomerService($http, $q, types) {
var service = { var service = {
getCustomers: getCustomers, getCustomers: getCustomers,
getCustomer: getCustomer, getCustomer: getCustomer,
getShortCustomerInfo: getShortCustomerInfo,
applyAssignedCustomersInfo: applyAssignedCustomersInfo,
applyAssignedCustomerInfo: applyAssignedCustomerInfo,
deleteCustomer: deleteCustomer, deleteCustomer: deleteCustomer,
saveCustomer: saveCustomer saveCustomer: saveCustomer
} }
@ -60,6 +63,88 @@ function CustomerService($http, $q) {
return deferred.promise; return deferred.promise;
} }
function getShortCustomerInfo(customerId) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/shortInfo';
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function applyAssignedCustomersInfo(items) {
var deferred = $q.defer();
var assignedCustomersMap = {};
function loadNextCustomerInfoOrComplete(i) {
i++;
if (i < items.length) {
loadNextCustomerInfo(i);
} else {
deferred.resolve(items);
}
}
function loadNextCustomerInfo(i) {
var item = items[i];
item.assignedCustomer = {};
if (item.customerId && item.customerId.id != types.id.nullUid) {
item.assignedCustomer.id = item.customerId.id;
var assignedCustomer = assignedCustomersMap[item.customerId.id];
if (assignedCustomer){
item.assignedCustomer = assignedCustomer;
loadNextCustomerInfoOrComplete(i);
} else {
getShortCustomerInfo(item.customerId.id).then(
function success(info) {
assignedCustomer = {
id: item.customerId.id,
title: info.title,
isPublic: info.isPublic
};
assignedCustomersMap[assignedCustomer.id] = assignedCustomer;
item.assignedCustomer = assignedCustomer;
loadNextCustomerInfoOrComplete(i);
},
function fail() {
loadNextCustomerInfoOrComplete(i);
}
);
}
} else {
loadNextCustomerInfoOrComplete(i);
}
}
if (items.length > 0) {
loadNextCustomerInfo(0);
} else {
deferred.resolve(items);
}
return deferred.promise;
}
function applyAssignedCustomerInfo(items, customerId) {
var deferred = $q.defer();
getShortCustomerInfo(customerId).then(
function success(info) {
var assignedCustomer = {
id: customerId,
title: info.title,
isPublic: info.isPublic
}
items.forEach(function(item) {
item.assignedCustomer = assignedCustomer;
});
deferred.resolve(items);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function saveCustomer(customer) { function saveCustomer(customer) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/customer'; var url = '/api/customer';

75
ui/src/app/api/dashboard.service.js

@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', [])
.factory('dashboardService', DashboardService).name; .factory('dashboardService', DashboardService).name;
/*@ngInject*/ /*@ngInject*/
function DashboardService($http, $q) { function DashboardService($http, $q, $location, customerService) {
var service = { var service = {
assignDashboardToCustomer: assignDashboardToCustomer, assignDashboardToCustomer: assignDashboardToCustomer,
@ -27,7 +27,9 @@ function DashboardService($http, $q) {
getTenantDashboards: getTenantDashboards, getTenantDashboards: getTenantDashboards,
deleteDashboard: deleteDashboard, deleteDashboard: deleteDashboard,
saveDashboard: saveDashboard, saveDashboard: saveDashboard,
unassignDashboardFromCustomer: unassignDashboardFromCustomer unassignDashboardFromCustomer: unassignDashboardFromCustomer,
makeDashboardPublic: makeDashboardPublic,
getPublicDashboardLink: getPublicDashboardLink
} }
return service; return service;
@ -45,7 +47,15 @@ function DashboardService($http, $q) {
url += '&textOffset=' + pageLink.textOffset; url += '&textOffset=' + pageLink.textOffset;
} }
$http.get(url, null).then(function success(response) { $http.get(url, null).then(function success(response) {
deferred.resolve(response.data); customerService.applyAssignedCustomersInfo(response.data.data).then(
function success(data) {
response.data.data = data;
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
}); });
@ -65,7 +75,15 @@ function DashboardService($http, $q) {
url += '&textOffset=' + pageLink.textOffset; url += '&textOffset=' + pageLink.textOffset;
} }
$http.get(url, null).then(function success(response) { $http.get(url, null).then(function success(response) {
deferred.resolve(response.data); customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
function success(data) {
response.data.data = data;
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
}); });
@ -92,8 +110,8 @@ function DashboardService($http, $q) {
var url = '/api/dashboard/' + dashboardId; var url = '/api/dashboard/' + dashboardId;
$http.get(url, null).then(function success(response) { $http.get(url, null).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -103,8 +121,8 @@ function DashboardService($http, $q) {
var url = '/api/dashboard'; var url = '/api/dashboard';
$http.post(url, dashboard).then(function success(response) { $http.post(url, dashboard).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -114,8 +132,8 @@ function DashboardService($http, $q) {
var url = '/api/dashboard/' + dashboardId; var url = '/api/dashboard/' + dashboardId;
$http.delete(url).then(function success() { $http.delete(url).then(function success() {
deferred.resolve(); deferred.resolve();
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -123,10 +141,10 @@ function DashboardService($http, $q) {
function assignDashboardToCustomer(customerId, dashboardId) { function assignDashboardToCustomer(customerId, dashboardId) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId;
$http.post(url, null).then(function success() { $http.post(url, null).then(function success(response) {
deferred.resolve(); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -134,12 +152,33 @@ function DashboardService($http, $q) {
function unassignDashboardFromCustomer(dashboardId) { function unassignDashboardFromCustomer(dashboardId) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/customer/dashboard/' + dashboardId; var url = '/api/customer/dashboard/' + dashboardId;
$http.delete(url).then(function success() { $http.delete(url).then(function success(response) {
deferred.resolve(); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
});
return deferred.promise;
}
function makeDashboardPublic(dashboardId) {
var deferred = $q.defer();
var url = '/api/customer/public/dashboard/' + dashboardId;
$http.post(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
function getPublicDashboardLink(dashboard) {
var url = $location.protocol() + '://' + $location.host();
var port = $location.port();
if (port != 80 && port != 443) {
url += ":" + port;
}
url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.customerId.id;
return url;
}
} }

4
ui/src/app/api/datasource.service.js

@ -58,10 +58,10 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t
var datasourceSubscription = { var datasourceSubscription = {
datasourceType: datasource.type, datasourceType: datasource.type,
dataKeys: subscriptionDataKeys, dataKeys: subscriptionDataKeys,
type: listener.widget.type type: listener.subscriptionType
}; };
if (listener.widget.type === types.widgetType.timeseries.value) { if (listener.subscriptionType === types.widgetType.timeseries.value) {
datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow); datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow);
} }
if (datasourceSubscription.datasourceType === types.datasourceType.device) { if (datasourceSubscription.datasourceType === types.datasourceType.device) {

110
ui/src/app/api/device.service.js

@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) { function DeviceService($http, $q, $filter, userService, customerService, telemetryWebsocketService, types) {
var deviceAttributesSubscriptionMap = {}; var deviceAttributesSubscriptionMap = {};
@ -33,6 +33,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
getDevices: getDevices, getDevices: getDevices,
processDeviceAliases: processDeviceAliases, processDeviceAliases: processDeviceAliases,
checkDeviceAlias: checkDeviceAlias, checkDeviceAlias: checkDeviceAlias,
fetchAliasDeviceByNameFilter: fetchAliasDeviceByNameFilter,
getDeviceCredentials: getDeviceCredentials, getDeviceCredentials: getDeviceCredentials,
getDeviceKeys: getDeviceKeys, getDeviceKeys: getDeviceKeys,
getDeviceTimeseriesValues: getDeviceTimeseriesValues, getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@ -40,6 +41,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
saveDevice: saveDevice, saveDevice: saveDevice,
saveDeviceCredentials: saveDeviceCredentials, saveDeviceCredentials: saveDeviceCredentials,
unassignDeviceFromCustomer: unassignDeviceFromCustomer, unassignDeviceFromCustomer: unassignDeviceFromCustomer,
makeDevicePublic: makeDevicePublic,
getDeviceAttributes: getDeviceAttributes, getDeviceAttributes: getDeviceAttributes,
subscribeForDeviceAttributes: subscribeForDeviceAttributes, subscribeForDeviceAttributes: subscribeForDeviceAttributes,
unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes, unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes,
@ -51,7 +53,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
return service; return service;
function getTenantDevices(pageLink) { function getTenantDevices(pageLink, applyCustomersInfo, config) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/tenant/devices?limit=' + pageLink.limit; var url = '/api/tenant/devices?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) { if (angular.isDefined(pageLink.textSearch)) {
@ -63,15 +65,27 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
if (angular.isDefined(pageLink.textOffset)) { if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset; url += '&textOffset=' + pageLink.textOffset;
} }
$http.get(url, null).then(function success(response) { $http.get(url, config).then(function success(response) {
deferred.resolve(response.data); if (applyCustomersInfo) {
customerService.applyAssignedCustomersInfo(response.data.data).then(
function success(data) {
response.data.data = data;
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(response.data);
}
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
function getCustomerDevices(customerId, pageLink) { function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit; var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) { if (angular.isDefined(pageLink.textSearch)) {
@ -83,18 +97,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
if (angular.isDefined(pageLink.textOffset)) { if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset; url += '&textOffset=' + pageLink.textOffset;
} }
$http.get(url, null).then(function success(response) { $http.get(url, config).then(function success(response) {
deferred.resolve(response.data); if (applyCustomersInfo) {
customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
function success(data) {
response.data.data = data;
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(response.data);
}
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
function getDevice(deviceId, ignoreErrors) { function getDevice(deviceId, ignoreErrors, config) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device/' + deviceId; var url = '/api/device/' + deviceId;
$http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) { if (!config) {
config = {};
}
config = Object.assign(config, { ignoreErrors: ignoreErrors });
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail(response) {
deferred.reject(response.data); deferred.reject(response.data);
@ -102,7 +133,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
return deferred.promise; return deferred.promise;
} }
function getDevices(deviceIds) { function getDevices(deviceIds, config) {
var deferred = $q.defer(); var deferred = $q.defer();
var ids = ''; var ids = '';
for (var i=0;i<deviceIds.length;i++) { for (var i=0;i<deviceIds.length;i++) {
@ -112,7 +143,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
ids += deviceIds[i]; ids += deviceIds[i];
} }
var url = '/api/devices?deviceIds=' + ids; var url = '/api/devices?deviceIds=' + ids;
$http.get(url, null).then(function success(response) { $http.get(url, config).then(function success(response) {
var devices = response.data; var devices = response.data;
devices.sort(function (device1, device2) { devices.sort(function (device1, device2) {
var id1 = device1.id.id; var id1 = device1.id.id;
@ -128,16 +159,16 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
return deferred.promise; return deferred.promise;
} }
function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) { function fetchAliasDeviceByNameFilter(deviceNameFilter, limit, applyCustomersInfo, config) {
var deferred = $q.defer(); var deferred = $q.defer();
var user = userService.getCurrentUser(); var user = userService.getCurrentUser();
var promise; var promise;
var pageLink = {limit: limit, textSearch: deviceNameFilter}; var pageLink = {limit: limit, textSearch: deviceNameFilter};
if (user.authority === 'CUSTOMER_USER') { if (user.authority === 'CUSTOMER_USER') {
var customerId = user.customerId; var customerId = user.customerId;
promise = getCustomerDevices(customerId, pageLink); promise = getCustomerDevices(customerId, pageLink, applyCustomersInfo, config);
} else { } else {
promise = getTenantDevices(pageLink); promise = getTenantDevices(pageLink, applyCustomersInfo, config);
} }
promise.then( promise.then(
function success(result) { function success(result) {
@ -194,7 +225,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var deviceFilter = deviceAlias.deviceFilter; var deviceFilter = deviceAlias.deviceFilter;
if (deviceFilter.useFilter) { if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter; var deviceNameFilter = deviceFilter.deviceNameFilter;
fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then( fetchAliasDeviceByNameFilter(deviceNameFilter, 100, false).then(
function(devices) { function(devices) {
if (devices && devices != null) { if (devices && devices != null) {
var resolvedAlias = {alias: alias, deviceId: devices[0].id.id}; var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
@ -276,7 +307,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var promise; var promise;
if (deviceFilter.useFilter) { if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter; var deviceNameFilter = deviceFilter.deviceNameFilter;
promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1); promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1, false);
} else { } else {
var deviceList = deviceFilter.deviceList; var deviceList = deviceFilter.deviceList;
promise = getDevices(deviceList); promise = getDevices(deviceList);
@ -301,8 +332,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device'; var url = '/api/device';
$http.post(url, device).then(function success(response) { $http.post(url, device).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -312,8 +343,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/' + deviceId; var url = '/api/device/' + deviceId;
$http.delete(url).then(function success() { $http.delete(url).then(function success() {
deferred.resolve(); deferred.resolve();
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -323,8 +354,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/' + deviceId + '/credentials'; var url = '/api/device/' + deviceId + '/credentials';
$http.get(url, null).then(function success(response) { $http.get(url, null).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -334,8 +365,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/credentials'; var url = '/api/device/credentials';
$http.post(url, deviceCredentials).then(function success(response) { $http.post(url, deviceCredentials).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -343,10 +374,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
function assignDeviceToCustomer(customerId, deviceId) { function assignDeviceToCustomer(customerId, deviceId) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/device/' + deviceId; var url = '/api/customer/' + customerId + '/device/' + deviceId;
$http.post(url, null).then(function success() { $http.post(url, null).then(function success(response) {
deferred.resolve(); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -354,10 +385,21 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
function unassignDeviceFromCustomer(deviceId) { function unassignDeviceFromCustomer(deviceId) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/customer/device/' + deviceId; var url = '/api/customer/device/' + deviceId;
$http.delete(url).then(function success() { $http.delete(url).then(function success(response) {
deferred.resolve(); deferred.resolve(response.data);
}, function fail(response) { }, function fail() {
deferred.reject(response.data); deferred.reject();
});
return deferred.promise;
}
function makeDevicePublic(deviceId) {
var deferred = $q.defer();
var url = '/api/customer/public/device/' + deviceId;
$http.post(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
}); });
return deferred.promise; return deferred.promise;
} }
@ -425,7 +467,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
} }
} }
function getDeviceAttributes(deviceId, attributeScope, query, successCallback) { function getDeviceAttributes(deviceId, attributeScope, query, successCallback, config) {
var deferred = $q.defer(); var deferred = $q.defer();
var subscriptionId = deviceId + attributeScope; var subscriptionId = deviceId + attributeScope;
var das = deviceAttributesSubscriptionMap[subscriptionId]; var das = deviceAttributesSubscriptionMap[subscriptionId];
@ -445,7 +487,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
} }
} else { } else {
var url = '/api/plugins/telemetry/' + deviceId + '/values/attributes/' + attributeScope; var url = '/api/plugins/telemetry/' + deviceId + '/values/attributes/' + attributeScope;
$http.get(url, null).then(function success(response) { $http.get(url, config).then(function success(response) {
processDeviceAttributes(response.data, query, deferred, successCallback); processDeviceAttributes(response.data, query, deferred, successCallback);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();

14
ui/src/app/api/login.service.js

@ -25,6 +25,7 @@ function LoginService($http, $q) {
changePassword: changePassword, changePassword: changePassword,
hasUser: hasUser, hasUser: hasUser,
login: login, login: login,
publicLogin: publicLogin,
resetPassword: resetPassword, resetPassword: resetPassword,
sendResetPasswordLink: sendResetPasswordLink, sendResetPasswordLink: sendResetPasswordLink,
} }
@ -49,6 +50,19 @@ function LoginService($http, $q) {
return deferred.promise; return deferred.promise;
} }
function publicLogin(publicId) {
var deferred = $q.defer();
var pubilcLoginRequest = {
publicId: publicId
};
$http.post('/api/auth/login/public', pubilcLoginRequest).then(function success(response) {
deferred.resolve(response);
}, function fail(response) {
deferred.reject(response);
});
return deferred.promise;
}
function sendResetPasswordLink(email) { function sendResetPasswordLink(email) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/noauth/resetPasswordByEmail?email=' + email; var url = '/api/noauth/resetPasswordByEmail?email=' + email;

670
ui/src/app/api/subscription.js

@ -0,0 +1,670 @@
/*
* Copyright © 2016-2017 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.
*/
/*
options = {
type,
targetDeviceAliasIds, // RPC
targetDeviceIds, // RPC
datasources,
timeWindowConfig,
useDashboardTimewindow,
legendConfig,
decimals,
units,
callbacks
}
*/
export default class Subscription {
constructor(subscriptionContext, options) {
this.ctx = subscriptionContext;
this.type = options.type;
this.callbacks = options.callbacks;
this.id = this.ctx.utils.guid();
this.cafs = {};
this.registrations = [];
if (this.type === this.ctx.types.widgetType.rpc.value) {
this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || function(){};
this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || function(){};
this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || function(){};
this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || function(){};
this.targetDeviceAliasIds = options.targetDeviceAliasIds;
this.targetDeviceIds = options.targetDeviceIds;
this.targetDeviceAliasId = null;
this.targetDeviceId = null;
this.rpcRejection = null;
this.rpcErrorText = null;
this.rpcEnabled = false;
this.executingRpcRequest = false;
this.executingPromises = [];
this.initRpc();
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){};
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
this.datasources = options.datasources;
this.datasourceListeners = [];
this.data = [];
this.hiddenData = [];
this.originalTimewindow = null;
this.timeWindow = {
stDiff: this.ctx.stDiff
}
this.useDashboardTimewindow = options.useDashboardTimewindow;
if (this.useDashboardTimewindow) {
this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
} else {
this.timeWindowConfig = angular.copy(options.timeWindowConfig);
}
this.subscriptionTimewindow = null;
this.units = options.units || '';
this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2;
this.loadingData = false;
if (options.legendConfig) {
this.legendConfig = options.legendConfig;
this.legendData = {
keys: [],
data: []
};
this.displayLegend = true;
} else {
this.displayLegend = false;
}
this.caulculateLegendData = this.displayLegend &&
this.type === this.ctx.types.widgetType.timeseries.value &&
(this.legendConfig.showMin === true ||
this.legendConfig.showMax === true ||
this.legendConfig.showAvg === true ||
this.legendConfig.showTotal === true);
this.initDataSubscription();
}
}
initDataSubscription() {
var dataIndex = 0;
for (var i = 0; i < this.datasources.length; i++) {
var datasource = this.datasources[i];
for (var a = 0; a < datasource.dataKeys.length; a++) {
var dataKey = datasource.dataKeys[a];
dataKey.pattern = angular.copy(dataKey.label);
var datasourceData = {
datasource: datasource,
dataKey: dataKey,
data: []
};
this.data.push(datasourceData);
this.hiddenData.push({data: []});
if (this.displayLegend) {
var legendKey = {
dataKey: dataKey,
dataIndex: dataIndex++
};
this.legendData.keys.push(legendKey);
var legendKeyData = {
min: null,
max: null,
avg: null,
total: null,
hidden: false
};
this.legendData.data.push(legendKeyData);
}
}
}
var subscription = this;
var registration;
if (this.displayLegend) {
this.legendData.keys = this.ctx.$filter('orderBy')(this.legendData.keys, '+label');
registration = this.ctx.$scope.$watch(
function() {
return subscription.legendData.data;
},
function (newValue, oldValue) {
for(var i = 0; i < newValue.length; i++) {
if(newValue[i].hidden != oldValue[i].hidden) {
subscription.updateDataVisibility(i);
}
}
}, true);
this.registrations.push(registration);
}
if (this.type === this.ctx.types.widgetType.timeseries.value) {
if (this.useDashboardTimewindow) {
registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
subscription.unsubscribe();
subscription.subscribe();
}
});
this.registrations.push(registration);
} else {
this.startWatchingTimewindow();
}
}
registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
subscription.checkSubscriptions();
});
this.registrations.push(registration);
}
startWatchingTimewindow() {
var subscription = this;
this.timeWindowWatchRegistration = this.ctx.$scope.$watch(function () {
return subscription.timeWindowConfig;
}, function (newTimewindow, prevTimewindow) {
if (!angular.equals(newTimewindow, prevTimewindow)) {
subscription.unsubscribe();
subscription.subscribe();
}
}, true);
this.registrations.push(this.timeWindowWatchRegistration);
}
stopWatchingTimewindow() {
if (this.timeWindowWatchRegistration) {
this.timeWindowWatchRegistration();
var index = this.registrations.indexOf(this.timeWindowWatchRegistration);
if (index > -1) {
this.registrations.splice(index, 1);
}
}
}
initRpc() {
if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) {
this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId;
}
var subscription = this;
var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
var deviceId = null;
if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) {
deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId;
}
if (!angular.equals(deviceId, subscription.targetDeviceId)) {
subscription.targetDeviceId = deviceId;
if (subscription.targetDeviceId) {
subscription.rpcEnabled = true;
} else {
subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false;
}
subscription.callbacks.rpcStateChanged(subscription);
}
});
this.registrations.push(registration);
} else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
this.targetDeviceId = this.targetDeviceIds[0];
}
if (this.targetDeviceId) {
this.rpcEnabled = true;
} else {
this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
}
this.callbacks.rpcStateChanged(this);
}
clearRpcError() {
this.rpcRejection = null;
this.rpcErrorText = null;
this.callbacks.onRpcErrorCleared(this);
}
sendOneWayCommand(method, params, timeout) {
return this.sendCommand(true, method, params, timeout);
}
sendTwoWayCommand(method, params, timeout) {
return this.sendCommand(false, method, params, timeout);
}
sendCommand(oneWayElseTwoWay, method, params, timeout) {
if (!this.rpcEnabled) {
return this.ctx.$q.reject();
}
if (this.rpcRejection && this.rpcRejection.status !== 408) {
this.rpcRejection = null;
this.rpcErrorText = null;
this.callbacks.onRpcErrorCleared(this);
}
var subscription = this;
var requestBody = {
method: method,
params: params
};
if (timeout && timeout > 0) {
requestBody.timeout = timeout;
}
var deferred = this.ctx.$q.defer();
this.executingRpcRequest = true;
this.callbacks.rpcStateChanged(this);
if (this.ctx.$scope.widgetEditMode) {
this.ctx.$timeout(function() {
subscription.executingRpcRequest = false;
subscription.callbacks.rpcStateChanged(subscription);
if (oneWayElseTwoWay) {
deferred.resolve();
} else {
deferred.resolve(requestBody);
}
}, 500);
} else {
this.executingPromises.push(deferred.promise);
var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand;
targetSendFunction(this.targetDeviceId, requestBody).then(
function success(responseBody) {
subscription.rpcRejection = null;
subscription.rpcErrorText = null;
var index = subscription.executingPromises.indexOf(deferred.promise);
if (index >= 0) {
subscription.executingPromises.splice( index, 1 );
}
subscription.executingRpcRequest = subscription.executingPromises.length > 0;
subscription.callbacks.onRpcSuccess(subscription);
deferred.resolve(responseBody);
},
function fail(rejection) {
var index = subscription.executingPromises.indexOf(deferred.promise);
if (index >= 0) {
subscription.executingPromises.splice( index, 1 );
}
subscription.executingRpcRequest = subscription.executingPromises.length > 0;
subscription.callbacks.rpcStateChanged(subscription);
if (!subscription.executingRpcRequest || rejection.status === 408) {
subscription.rpcRejection = rejection;
if (rejection.status === 408) {
subscription.rpcErrorText = 'Device is offline.';
} else {
subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
if (rejection.data && rejection.data.length > 0) {
subscription.rpcErrorText += '</br>';
subscription.rpcErrorText += rejection.data;
}
}
subscription.callbacks.onRpcFailed(subscription);
}
deferred.reject(rejection);
}
);
}
return deferred.promise;
}
updateDataVisibility(index) {
var hidden = this.legendData.data[index].hidden;
if (hidden) {
this.hiddenData[index].data = this.data[index].data;
this.data[index].data = [];
} else {
this.data[index].data = this.hiddenData[index].data;
this.hiddenData[index].data = [];
}
this.onDataUpdated();
}
onDataUpdated(apply) {
if (this.cafs['dataUpdated']) {
this.cafs['dataUpdated']();
this.cafs['dataUpdated'] = null;
}
var subscription = this;
this.cafs['dataUpdated'] = this.ctx.tbRaf(function() {
try {
subscription.callbacks.onDataUpdated(subscription, apply);
} catch (e) {
subscription.callbacks.onDataUpdateError(subscription, e);
}
});
if (apply) {
this.ctx.$scope.$digest();
}
}
updateTimewindowConfig(newTimewindow) {
this.timeWindowConfig = newTimewindow;
}
onResetTimewindow() {
if (this.useDashboardTimewindow) {
this.ctx.dashboardTimewindowApi.onResetTimewindow();
} else {
if (this.originalTimewindow) {
this.stopWatchingTimewindow();
this.timeWindowConfig = angular.copy(this.originalTimewindow);
this.originalTimewindow = null;
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
this.unsubscribe();
this.subscribe();
this.startWatchingTimewindow();
}
}
}
onUpdateTimewindow(startTimeMs, endTimeMs) {
if (this.useDashboardTimewindow) {
this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
} else {
this.stopWatchingTimewindow();
if (!this.originalTimewindow) {
this.originalTimewindow = angular.copy(this.timeWindowConfig);
}
this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs);
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
this.unsubscribe();
this.subscribe();
this.startWatchingTimewindow();
}
}
notifyDataLoading() {
this.loadingData = true;
this.callbacks.dataLoading(this);
}
notifyDataLoaded() {
this.loadingData = false;
this.callbacks.dataLoading(this);
}
updateTimewindow() {
this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
if (this.subscriptionTimewindow.realtimeWindowMs) {
this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff;
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
} else if (this.subscriptionTimewindow.fixedWindow) {
this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs;
}
}
updateRealtimeSubscription(subscriptionTimewindow) {
if (subscriptionTimewindow) {
this.subscriptionTimewindow = subscriptionTimewindow;
} else {
this.subscriptionTimewindow =
this.ctx.timeService.createSubscriptionTimewindow(
this.timeWindowConfig,
this.timeWindow.stDiff);
}
this.updateTimewindow();
return this.subscriptionTimewindow;
}
dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
this.notifyDataLoaded();
var update = true;
var currentData;
if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) {
currentData = this.hiddenData[datasourceIndex + dataKeyIndex];
} else {
currentData = this.data[datasourceIndex + dataKeyIndex];
}
if (this.type === this.ctx.types.widgetType.latest.value) {
var prevData = currentData.data;
if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {
var prevValue = prevData[0][1];
if (prevValue === sourceData.data[0][1]) {
update = false;
}
}
}
if (update) {
if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
this.updateTimewindow();
}
currentData.data = sourceData.data;
if (this.caulculateLegendData) {
this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
}
this.onDataUpdated(apply);
}
}
updateLegend(dataIndex, data, apply) {
var legendKeyData = this.legendData.data[dataIndex];
if (this.legendConfig.showMin) {
legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units);
}
if (this.legendConfig.showMax) {
legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units);
}
if (this.legendConfig.showAvg) {
legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units);
}
if (this.legendConfig.showTotal) {
legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units);
}
this.callbacks.legendDataUpdated(this, apply !== false);
}
subscribe() {
if (this.type === this.ctx.types.widgetType.rpc.value) {
return;
}
this.notifyDataLoading();
if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
var index = 0;
for (var i = 0; i < this.datasources.length; i++) {
var datasource = this.datasources[i];
if (angular.isFunction(datasource))
continue;
var deviceId = null;
if (datasource.type === this.ctx.types.datasourceType.device) {
var aliasName = null;
var deviceName = null;
if (datasource.deviceId) {
deviceId = datasource.deviceId;
datasource.name = datasource.deviceName;
aliasName = datasource.deviceName;
deviceName = datasource.deviceName;
} else if (datasource.deviceAliasId && this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
deviceId = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
datasource.name = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
aliasName = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
deviceName = '';
var devicesInfo = this.ctx.aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
for (var d = 0; d < devicesInfo.length; d++) {
if (devicesInfo[d].id === deviceId) {
deviceName = devicesInfo[d].name;
break;
}
}
}
} else {
datasource.name = datasource.name || this.ctx.types.datasourceType.function;
}
for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, deviceName, aliasName);
}
var subscription = this;
var listener = {
subscriptionType: this.type,
subscriptionTimewindow: this.subscriptionTimewindow,
datasource: datasource,
deviceId: deviceId,
dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
},
updateRealtimeSubscription: function () {
this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
return this.subscriptionTimewindow;
},
setRealtimeSubscription: function (subscriptionTimewindow) {
subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
},
datasourceIndex: index
};
for (var a = 0; a < datasource.dataKeys.length; a++) {
this.data[index + a].data = [];
}
index += datasource.dataKeys.length;
this.datasourceListeners.push(listener);
this.ctx.datasourceService.subscribeToDatasource(listener);
}
}
unsubscribe() {
if (this.type !== this.ctx.types.widgetType.rpc.value) {
for (var i = 0; i < this.datasourceListeners.length; i++) {
var listener = this.datasourceListeners[i];
this.ctx.datasourceService.unsubscribeFromDatasource(listener);
}
this.datasourceListeners = [];
}
}
checkSubscriptions() {
var subscriptionsChanged = false;
for (var i = 0; i < this.datasourceListeners.length; i++) {
var listener = this.datasourceListeners[i];
var deviceId = null;
var aliasName = null;
if (listener.datasource.type === this.ctx.types.datasourceType.device) {
if (listener.datasource.deviceAliasId &&
this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
deviceId = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
aliasName = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
}
if (!angular.equals(deviceId, listener.deviceId) ||
!angular.equals(aliasName, listener.datasource.name)) {
subscriptionsChanged = true;
break;
}
}
}
if (subscriptionsChanged) {
this.unsubscribe();
this.subscribe();
}
}
destroy() {
this.unsubscribe();
for (var cafId in this.cafs) {
if (this.cafs[cafId]) {
this.cafs[cafId]();
this.cafs[cafId] = null;
}
}
this.registrations.forEach(function (registration) {
registration();
});
this.registrations = [];
}
}
const varsRegex = /\$\{([^\}]*)\}/g;
function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) {
var pattern = dataKey.pattern;
var label = dataKey.pattern;
var match = varsRegex.exec(pattern);
while (match !== null) {
var variable = match[0];
var variableName = match[1];
if (variableName === 'dsName') {
label = label.split(variable).join(dsName);
} else if (variableName === 'deviceName') {
label = label.split(variable).join(deviceName);
} else if (variableName === 'aliasName') {
label = label.split(variable).join(aliasName);
}
match = varsRegex.exec(pattern);
}
dataKey.label = label;
}
function calculateMin(data) {
if (data.length > 0) {
var result = Number(data[0][1]);
for (var i=1;i<data.length;i++) {
result = Math.min(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateMax(data) {
if (data.length > 0) {
var result = Number(data[0][1]);
for (var i=1;i<data.length;i++) {
result = Math.max(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateAvg(data) {
if (data.length > 0) {
return calculateTotal(data)/data.length;
} else {
return null;
}
}
function calculateTotal(data) {
if (data.length > 0) {
var result = 0;
for (var i = 0; i < data.length; i++) {
result += Number(data[i][1]);
}
return result;
} else {
return null;
}
}

10
ui/src/app/api/telemetry-websocket.service.js

@ -45,15 +45,21 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
socketCloseTimer, socketCloseTimer,
reconnectTimer; reconnectTimer;
var port = location.port;
if (location.protocol === "https:") { if (location.protocol === "https:") {
if (!port) {
port = "443";
}
telemetryUri = "wss:"; telemetryUri = "wss:";
} else { } else {
if (!port) {
port = "80";
}
telemetryUri = "ws:"; telemetryUri = "ws:";
} }
telemetryUri += "//" + location.hostname + ":" + location.port; telemetryUri += "//" + location.hostname + ":" + port;
telemetryUri += "/api/ws/plugins/telemetry"; telemetryUri += "/api/ws/plugins/telemetry";
var service = { var service = {
subscribe: subscribe, subscribe: subscribe,
unsubscribe: unsubscribe unsubscribe: unsubscribe

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

@ -22,9 +22,10 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
.name; .name;
/*@ngInject*/ /*@ngInject*/
function UserService($http, $q, $rootScope, adminService, dashboardService, toast, store, jwtHelper, $translate, $state) { function UserService($http, $q, $rootScope, adminService, dashboardService, loginService, toast, store, jwtHelper, $translate, $state, $location) {
var currentUser = null, var currentUser = null,
currentUserDetails = null, currentUserDetails = null,
lastPublicDashboardId = null,
allowedDashboardIds = [], allowedDashboardIds = [],
userLoaded = false; userLoaded = false;
@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
var service = { var service = {
deleteUser: deleteUser, deleteUser: deleteUser,
getAuthority: getAuthority, getAuthority: getAuthority,
isPublic: isPublic,
getPublicId: getPublicId,
parsePublicId: parsePublicId,
isAuthenticated: isAuthenticated, isAuthenticated: isAuthenticated,
getCurrentUser: getCurrentUser, getCurrentUser: getCurrentUser,
getCustomerUsers: getCustomerUsers, getCustomerUsers: getCustomerUsers,
@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
updateAuthorizationHeader: updateAuthorizationHeader, updateAuthorizationHeader: updateAuthorizationHeader,
gotoDefaultPlace: gotoDefaultPlace, gotoDefaultPlace: gotoDefaultPlace,
forceDefaultPlace: forceDefaultPlace, forceDefaultPlace: forceDefaultPlace,
logout: logout updateLastPublicDashboardId: updateLastPublicDashboardId,
logout: logout,
reloadUser: reloadUser
} }
loadUser(true).then(function success() { reloadUser();
notifyUserLoaded();
}, function fail() {
notifyUserLoaded();
});
return service; return service;
function updateAndValidateToken(token, prefix) { function reloadUser() {
userLoaded = false;
loadUser(true).then(function success() {
notifyUserLoaded();
}, function fail() {
notifyUserLoaded();
});
}
function updateAndValidateToken(token, prefix, notify) {
var valid = false; var valid = false;
var tokenData = jwtHelper.decodeToken(token); var tokenData = jwtHelper.decodeToken(token);
var issuedAt = tokenData.iat; var issuedAt = tokenData.iat;
@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
valid = true; valid = true;
} }
} }
if (!valid) { if (!valid && notify) {
$rootScope.$broadcast('unauthenticated'); $rootScope.$broadcast('unauthenticated');
} }
} }
@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) { function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
currentUser = null; currentUser = null;
currentUserDetails = null; currentUserDetails = null;
lastPublicDashboardId = null;
allowedDashboardIds = []; allowedDashboardIds = [];
if (!jwtToken) { if (!jwtToken) {
clearTokenData(); clearTokenData();
@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
$rootScope.$broadcast('unauthenticated', doLogout); $rootScope.$broadcast('unauthenticated', doLogout);
} }
} else { } else {
updateAndValidateToken(jwtToken, 'jwt_token'); updateAndValidateToken(jwtToken, 'jwt_token', true);
updateAndValidateToken(refreshToken, 'refresh_token'); updateAndValidateToken(refreshToken, 'refresh_token', true);
if (notify) { if (notify) {
loadUser(false).then(function success() { loadUser(false).then(function success() {
$rootScope.$broadcast('authenticated'); $rootScope.$broadcast('authenticated');
@ -213,13 +225,58 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
} }
} }
function isPublic() {
if (currentUser) {
return currentUser.isPublic;
} else {
return false;
}
}
function getPublicId() {
if (isPublic()) {
return currentUser.sub;
} else {
return null;
}
}
function parsePublicId() {
var token = getJwtToken();
if (token) {
var tokenData = jwtHelper.decodeToken(token);
if (tokenData && tokenData.isPublic) {
return tokenData.sub;
}
}
return null;
}
function isUserLoaded() { function isUserLoaded() {
return userLoaded; return userLoaded;
} }
function loadUser(doTokenRefresh) { function loadUser(doTokenRefresh) {
var deferred = $q.defer(); var deferred = $q.defer();
if (!currentUser) {
function fetchAllowedDashboardIds() {
var pageLink = {limit: 100};
dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
function success(result) {
var dashboards = result.data;
for (var d=0;d<dashboards.length;d++) {
allowedDashboardIds.push(dashboards[d].id.id);
}
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
}
function procceedJwtTokenValidate() {
validateJwtToken(doTokenRefresh).then(function success() { validateJwtToken(doTokenRefresh).then(function success() {
var jwtToken = store.get('jwt_token'); var jwtToken = store.get('jwt_token');
currentUser = jwtHelper.decodeToken(jwtToken); currentUser = jwtHelper.decodeToken(jwtToken);
@ -228,29 +285,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
} else if (currentUser) { } else if (currentUser) {
currentUser.authority = "ANONYMOUS"; currentUser.authority = "ANONYMOUS";
} }
if (currentUser.userId) { if (currentUser.isPublic) {
$rootScope.forceFullscreen = true;
fetchAllowedDashboardIds();
} else if (currentUser.userId) {
getUser(currentUser.userId).then( getUser(currentUser.userId).then(
function success(user) { function success(user) {
currentUserDetails = user; currentUserDetails = user;
$rootScope.forceFullscreen = false; $rootScope.forceFullscreen = false;
if (currentUserDetails.additionalInfo && if (userForceFullscreen()) {
currentUserDetails.additionalInfo.defaultDashboardFullscreen) { $rootScope.forceFullscreen = true;
$rootScope.forceFullscreen = currentUserDetails.additionalInfo.defaultDashboardFullscreen === true;
} }
if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') { if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
var pageLink = {limit: 100}; fetchAllowedDashboardIds();
dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
function success(result) {
var dashboards = result.data;
for (var d=0;d<dashboards.length;d++) {
allowedDashboardIds.push(dashboards[d].id.id);
}
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
} else { } else {
deferred.resolve(); deferred.resolve();
} }
@ -265,6 +312,23 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
}); });
}
if (!currentUser) {
var locationSearch = $location.search();
if (locationSearch.publicId) {
loginService.publicLogin(locationSearch.publicId).then(function success(response) {
var token = response.data.token;
var refreshToken = response.data.refreshToken;
updateAndValidateToken(token, 'jwt_token', false);
updateAndValidateToken(refreshToken, 'refresh_token', false);
procceedJwtTokenValidate();
}, function fail() {
deferred.reject();
});
} else {
procceedJwtTokenValidate();
}
} else { } else {
deferred.resolve(); deferred.resolve();
} }
@ -373,17 +437,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
function forceDefaultPlace(to, params) { function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) { if (currentUser && isAuthenticated()) {
if (currentUser.authority === 'CUSTOMER_USER') { if (currentUser.authority === 'CUSTOMER_USER') {
if (currentUserDetails && if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) {
currentUserDetails.additionalInfo && if (to.name === 'home.profile') {
currentUserDetails.additionalInfo.defaultDashboardId) { if (userHasProfile()) {
if ($rootScope.forceFullscreen) {
if (to.name === 'home.profile') {
return false;
} else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
return false; return false;
} else { } else {
return true; return true;
} }
} else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
return false;
} else {
return true;
} }
} }
} }
@ -395,11 +459,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
if (currentUser && isAuthenticated()) { if (currentUser && isAuthenticated()) {
var place = 'home.links'; var place = 'home.links';
if (currentUser.authority === 'CUSTOMER_USER') { if (currentUser.authority === 'CUSTOMER_USER') {
if (currentUserDetails && if (userHasDefaultDashboard()) {
currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardId) {
place = 'home.dashboards.dashboard'; place = 'home.dashboards.dashboard';
params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId}; params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
} else if (isPublic()) {
place = 'home.dashboards.dashboard';
params = {dashboardId: lastPublicDashboardId};
} }
} else if (currentUser.authority === 'SYS_ADMIN') { } else if (currentUser.authority === 'SYS_ADMIN') {
adminService.checkUpdates().then( adminService.checkUpdates().then(
@ -416,4 +481,27 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
} }
} }
function userHasDefaultDashboard() {
return currentUserDetails &&
currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardId;
}
function userForceFullscreen() {
return (currentUser && currentUser.isPublic) ||
(currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardFullscreen &&
currentUserDetails.additionalInfo.defaultDashboardFullscreen === true);
}
function userHasProfile() {
return currentUser && !currentUser.isPublic;
}
function updateLastPublicDashboardId(dashboardId) {
if (isPublic()) {
lastPublicDashboardId = dashboardId;
}
}
} }

16
ui/src/app/api/widget.service.js

@ -17,7 +17,8 @@ import $ from 'jquery';
import moment from 'moment'; import moment from 'moment';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import thinsboardLedLight from '../components/led-light.directive'; import thingsboardLedLight from '../components/led-light.directive';
import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
import TbFlot from '../widget/lib/flot-widget'; import TbFlot from '../widget/lib/flot-widget';
import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
@ -31,7 +32,8 @@ import cssjs from '../../vendor/css.js/css';
import thingsboardTypes from '../common/types.constant'; import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service'; import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thinsboardLedLight, thingsboardTypes, thingsboardUtils]) export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
thingsboardTypes, thingsboardUtils])
.factory('widgetService', WidgetService) .factory('widgetService', WidgetService)
.name; .name;
@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
' }\n\n' + ' }\n\n' +
' self.useCustomDatasources = function() {\n\n' +
' }\n\n' +
' self.onResize = function() {\n\n' + ' self.onResize = function() {\n\n' +
' }\n\n' + ' }\n\n' +
@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
} }
if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) {
result.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
} else {
result.useCustomDatasources = false;
}
return result; return result;
} catch (e) { } catch (e) {
utils.processWidgetException(e); utils.processWidgetException(e);
@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if (widgetType.dataKeySettingsSchema) { if (widgetType.dataKeySettingsSchema) {
widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
} }
widgetInfo.useCustomDatasources = widgetType.useCustomDatasources;
putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
deferred.resolve(widgetInfo); deferred.resolve(widgetInfo);

5
ui/src/app/app.config.js

@ -16,6 +16,7 @@
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
import UrlHandler from './url.handler'; import UrlHandler from './url.handler';
import addLocaleKorean from './locale/locale.constant-ko'; import addLocaleKorean from './locale/locale.constant-ko';
import addLocaleChinese from './locale/locale.constant-zh';
/* eslint-disable import/no-unresolved, import/default */ /* eslint-disable import/no-unresolved, import/default */
@ -50,11 +51,15 @@ export default function AppConfig($provide,
$translateProvider.addInterpolation('$translateMessageFormatInterpolation'); $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
addLocaleKorean(locales); addLocaleKorean(locales);
addLocaleChinese(locales);
var $window = angular.injector(['ng']).get('$window'); var $window = angular.injector(['ng']).get('$window');
var lang = $window.navigator.language || $window.navigator.userLanguage; var lang = $window.navigator.language || $window.navigator.userLanguage;
if (lang === 'ko') { if (lang === 'ko') {
$translateProvider.useSanitizeValueStrategy(null); $translateProvider.useSanitizeValueStrategy(null);
$translateProvider.preferredLanguage('ko_KR'); $translateProvider.preferredLanguage('ko_KR');
} else if (lang === 'zh') {
$translateProvider.useSanitizeValueStrategy(null);
$translateProvider.preferredLanguage('zh_CN');
} }
for (var langKey in locales) { for (var langKey in locales) {

2
ui/src/app/app.js

@ -19,6 +19,7 @@ import angular from 'angular';
import ngMaterial from 'angular-material'; import ngMaterial from 'angular-material';
import ngMdIcons from 'angular-material-icons'; import ngMdIcons from 'angular-material-icons';
import ngCookies from 'angular-cookies'; import ngCookies from 'angular-cookies';
import angularSocialshare from 'angular-socialshare';
import 'angular-translate'; import 'angular-translate';
import 'angular-translate-loader-static-files'; import 'angular-translate-loader-static-files';
import 'angular-translate-storage-local'; import 'angular-translate-storage-local';
@ -82,6 +83,7 @@ angular.module('thingsboard', [
ngMaterial, ngMaterial,
ngMdIcons, ngMdIcons,
ngCookies, ngCookies,
angularSocialshare,
'pascalprecht.translate', 'pascalprecht.translate',
'mdColorPicker', 'mdColorPicker',
mdPickers, mdPickers,

50
ui/src/app/app.run.js

@ -55,8 +55,39 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
}); });
$rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
function waitForUserLoaded() {
if ($rootScope.userLoadedHandle) {
$rootScope.userLoadedHandle();
}
$rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
$rootScope.userLoadedHandle();
$state.go(to.name, params);
});
}
function reloadUserFromPublicId() {
userService.setUserFromJwtToken(null, null, false);
waitForUserLoaded();
userService.reloadUser();
}
var locationSearch = $location.search();
var publicId = locationSearch.publicId;
if (userService.isUserLoaded() === true) { if (userService.isUserLoaded() === true) {
if (userService.isAuthenticated()) { if (userService.isAuthenticated()) {
if (userService.isPublic()) {
if (userService.parsePublicId() !== publicId) {
evt.preventDefault();
if (publicId && publicId.length > 0) {
reloadUserFromPublicId();
} else {
userService.logout();
}
return;
}
}
if (userService.forceDefaultPlace(to, params)) { if (userService.forceDefaultPlace(to, params)) {
evt.preventDefault(); evt.preventDefault();
gotoDefaultPlace(params); gotoDefaultPlace(params);
@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
} }
} }
} else { } else {
if (to.module === 'private') { if (publicId && publicId.length > 0) {
evt.preventDefault();
reloadUserFromPublicId();
} else if (to.module === 'private') {
evt.preventDefault(); evt.preventDefault();
if (to.url === '/home' || to.url === '/') { if (to.url === '/home' || to.url === '/') {
$state.go('login', params); $state.go('login', params);
@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
} }
} else { } else {
evt.preventDefault(); evt.preventDefault();
if ($rootScope.userLoadedHandle) { waitForUserLoaded();
$rootScope.userLoadedHandle();
}
$rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
$rootScope.userLoadedHandle();
$state.go(to.name, params);
});
} }
}) })
$rootScope.pageTitle = 'Thingsboard'; $rootScope.pageTitle = 'Thingsboard';
$rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to) { $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) {
if (userService.isPublic() && to.name === 'home.dashboards.dashboard') {
$location.search('publicId', userService.getPublicId());
userService.updateLastPublicDashboardId(params.dashboardId);
}
if (angular.isDefined(to.data.pageTitle)) { if (angular.isDefined(to.data.pageTitle)) {
$translate(to.data.pageTitle).then(function (translation) { $translate(to.data.pageTitle).then(function (translation) {
$rootScope.pageTitle = 'Thingsboard | ' + translation; $rootScope.pageTitle = 'Thingsboard | ' + translation;

168
ui/src/app/common/utils.service.js

@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function Utils($mdColorPalette, $rootScope, $window, types) { function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
var predefinedFunctions = {}, var predefinedFunctions = {},
predefinedFunctionsList = [], predefinedFunctionsList = [],
@ -104,7 +104,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
parseException: parseException, parseException: parseException,
processWidgetException: processWidgetException, processWidgetException: processWidgetException,
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities filterSearchTextEntities: filterSearchTextEntities,
guid: guid,
createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
isLocalUrl: isLocalUrl
} }
return service; return service;
@ -276,4 +279,165 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
deferred.resolve(response); deferred.resolve(response);
} }
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
function genNextColor(datasources) {
var index = 0;
if (datasources) {
for (var i = 0; i < datasources.length; i++) {
var datasource = datasources[i];
index += datasource.dataKeys.length;
}
}
return getMaterialColor(index);
}
/*var defaultDataKey = {
name: 'f(x)',
type: types.dataKeyType.function,
label: 'Sin',
color: getMaterialColor(0),
funcBody: getPredefinedFunctionBody('Sin'),
settings: {},
_hash: Math.random()
};
var defaultDatasource = {
type: types.datasourceType.function,
name: types.datasourceType.function,
dataKeys: [angular.copy(defaultDataKey)]
};*/
function createKey(keyInfo, type, datasources) {
var dataKey = {
name: keyInfo.name,
type: type,
label: keyInfo.label || keyInfo.name,
color: genNextColor(datasources),
funcBody: keyInfo.funcBody,
settings: {},
_hash: Math.random()
}
return dataKey;
}
function createDatasourceKeys(keyInfos, type, datasource, datasources) {
for (var i=0;i<keyInfos.length;i++) {
var keyInfo = keyInfos[i];
var dataKey = createKey(keyInfo, type, datasources);
datasource.dataKeys.push(dataKey);
}
}
function createDatasourceFromSubscription(subscriptionInfo, datasources, device) {
var datasource;
if (subscriptionInfo.type === types.datasourceType.device) {
datasource = {
type: subscriptionInfo.type,
deviceName: device.name,
name: device.name,
deviceId: device.id.id,
dataKeys: []
}
} else if (subscriptionInfo.type === types.datasourceType.function) {
datasource = {
type: subscriptionInfo.type,
name: subscriptionInfo.name || types.datasourceType.function,
dataKeys: []
}
}
datasources.push(datasource);
if (subscriptionInfo.timeseries) {
createDatasourceKeys(subscriptionInfo.timeseries, types.dataKeyType.timeseries, datasource, datasources);
}
if (subscriptionInfo.attributes) {
createDatasourceKeys(subscriptionInfo.attributes, types.dataKeyType.attribute, datasource, datasources);
}
if (subscriptionInfo.functions) {
createDatasourceKeys(subscriptionInfo.functions, types.dataKeyType.function, datasource, datasources);
}
}
function processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred) {
if (index < subscriptionsInfo.length) {
var subscriptionInfo = subscriptionsInfo[index];
if (subscriptionInfo.type === types.datasourceType.device) {
if (subscriptionInfo.deviceId) {
deviceService.getDevice(subscriptionInfo.deviceId, true, {ignoreLoading: true}).then(
function success(device) {
createDatasourceFromSubscription(subscriptionInfo, datasources, device);
index++;
processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
},
function fail() {
index++;
processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
}
);
} else if (subscriptionInfo.deviceName || subscriptionInfo.deviceNamePrefix
|| subscriptionInfo.deviceIds) {
var promise;
if (subscriptionInfo.deviceName) {
promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceName, 1, false, {ignoreLoading: true});
} else if (subscriptionInfo.deviceNamePrefix) {
promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceNamePrefix, 100, false, {ignoreLoading: true});
} else if (subscriptionInfo.deviceIds) {
promise = deviceService.getDevices(subscriptionInfo.deviceIds, {ignoreLoading: true});
}
promise.then(
function success(devices) {
if (devices && devices.length > 0) {
for (var i = 0; i < devices.length; i++) {
var device = devices[i];
createDatasourceFromSubscription(subscriptionInfo, datasources, device);
}
}
index++;
processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
},
function fail() {
index++;
processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
}
)
} else {
index++;
processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
}
} else if (subscriptionInfo.type === types.datasourceType.function) {
createDatasourceFromSubscription(subscriptionInfo, datasources);
index++;
processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
}
} else {
deferred.resolve(datasources);
}
}
function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) {
var deferred = $q.defer();
var datasources = [];
processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred);
return deferred.promise;
}
function isLocalUrl(url) {
var parser = document.createElement('a'); //eslint-disable-line
parser.href = url;
var host = parser.hostname;
if (host === "localhost" || host === "127.0.0.1") {
return true;
} else {
return false;
}
}
} }

81
ui/src/app/components/dashboard.directive.js

@ -87,8 +87,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
var highlightedMode = false; var highlightedMode = false;
var highlightedWidget = null; var highlightedWidget = null;
var selectedWidget = null; var selectedWidget = null;
var mouseDownWidget = -1;
var widgetMouseMoved = false;
var gridsterParent = $('#gridster-parent', $element); var gridsterParent = $('#gridster-parent', $element);
var gridsterElement = angular.element($('#gridster-child', gridsterParent)); var gridsterElement = angular.element($('#gridster-child', gridsterParent));
@ -152,9 +150,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
vm.resetHighlight = resetHighlight; vm.resetHighlight = resetHighlight;
vm.onWidgetFullscreenChanged = onWidgetFullscreenChanged; vm.onWidgetFullscreenChanged = onWidgetFullscreenChanged;
vm.widgetMouseDown = widgetMouseDown; vm.widgetMouseDown = widgetMouseDown;
vm.widgetMouseMove = widgetMouseMove; vm.widgetClicked = widgetClicked;
vm.widgetMouseUp = widgetMouseUp;
vm.widgetSizeX = widgetSizeX; vm.widgetSizeX = widgetSizeX;
vm.widgetSizeY = widgetSizeY; vm.widgetSizeY = widgetSizeY;
@ -184,23 +182,23 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
vm.dashboardTimewindowApi = { vm.dashboardTimewindowApi = {
onResetTimewindow: function() { onResetTimewindow: function() {
if (vm.originalDashboardTimewindow) { $timeout(function() {
vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); if (vm.originalDashboardTimewindow) {
vm.originalDashboardTimewindow = null; vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
} vm.originalDashboardTimewindow = null;
}
}, 0);
}, },
onUpdateTimewindow: function(startTimeMs, endTimeMs) { onUpdateTimewindow: function(startTimeMs, endTimeMs) {
if (!vm.originalDashboardTimewindow) { if (!vm.originalDashboardTimewindow) {
vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow); vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow);
} }
vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs); $timeout(function() {
vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs);
}, 0);
} }
}; };
//$element[0].onmousemove=function(){
// widgetMouseMove();
// }
//TODO: widgets visibility //TODO: widgets visibility
/*gridsterParent.scroll(function () { /*gridsterParent.scroll(function () {
updateVisibleRect(); updateVisibleRect();
@ -350,7 +348,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
} }
function loadDashboard() { function loadDashboard() {
resetWidgetClick();
$timeout(function () { $timeout(function () {
if (vm.loadWidgets) { if (vm.loadWidgets) {
var promise = vm.loadWidgets(); var promise = vm.loadWidgets();
@ -434,42 +431,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
return gridsterElement && gridsterElement[0] === gridster.$element[0]; return gridsterElement && gridsterElement[0] === gridster.$element[0];
} }
function resetWidgetClick () {
mouseDownWidget = -1;
widgetMouseMoved = false;
}
function onWidgetFullscreenChanged(expanded, widget) { function onWidgetFullscreenChanged(expanded, widget) {
vm.isWidgetExpanded = expanded; vm.isWidgetExpanded = expanded;
$scope.$broadcast('onWidgetFullscreenChanged', vm.isWidgetExpanded, widget); $scope.$broadcast('onWidgetFullscreenChanged', vm.isWidgetExpanded, widget);
} }
function widgetMouseDown ($event, widget) { function widgetMouseDown ($event, widget) {
mouseDownWidget = widget;
widgetMouseMoved = false;
if (vm.onWidgetMouseDown) { if (vm.onWidgetMouseDown) {
vm.onWidgetMouseDown({event: $event, widget: widget}); vm.onWidgetMouseDown({event: $event, widget: widget});
} }
} }
function widgetMouseMove () {
if (mouseDownWidget) {
widgetMouseMoved = true;
}
}
function widgetMouseUp ($event, widget) {
$timeout(function () {
if (!widgetMouseMoved && mouseDownWidget) {
if (widget === mouseDownWidget) {
widgetClicked($event, widget);
}
}
mouseDownWidget = null;
widgetMouseMoved = false;
}, 0);
}
function widgetClicked ($event, widget) { function widgetClicked ($event, widget) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -518,7 +490,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
} }
function editWidget ($event, widget) { function editWidget ($event, widget) {
resetWidgetClick();
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -528,7 +499,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
} }
function exportWidget ($event, widget) { function exportWidget ($event, widget) {
resetWidgetClick();
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -538,7 +508,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
} }
function removeWidget($event, widget) { function removeWidget($event, widget) {
resetWidgetClick();
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -548,27 +517,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
} }
function highlightWidget(widget, delay) { function highlightWidget(widget, delay) {
highlightedMode = true; if (!highlightedMode || highlightedWidget != widget) {
highlightedWidget = widget; highlightedMode = true;
if (vm.gridster) { highlightedWidget = widget;
var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; scrollToWidget(widget, delay);
if (item) {
var height = $(item).outerHeight(true);
var rectHeight = gridsterParent.height();
var offset = (rectHeight - height) / 2;
var scrollTop = item.offsetTop;
if (offset > 0) {
scrollTop -= offset;
}
gridsterParent.animate({
scrollTop: scrollTop
}, delay);
}
} }
} }
function selectWidget(widget, delay) { function selectWidget(widget, delay) {
selectedWidget = widget; if (selectedWidget != widget) {
selectedWidget = widget;
scrollToWidget(widget, delay);
}
}
function scrollToWidget(widget, delay) {
if (vm.gridster) { if (vm.gridster) {
var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)];
if (item) { if (item) {

6
ui/src/app/components/dashboard.tpl.html

@ -37,9 +37,7 @@
'tb-not-highlighted': vm.isNotHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget),
'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}" 'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}"
tb-mousedown="vm.widgetMouseDown($event, widget)" tb-mousedown="vm.widgetMouseDown($event, widget)"
tb-mousemove="vm.widgetMouseMove($event, widget)" ng-click="vm.widgetClicked($event, widget)"
tb-mouseup="vm.widgetMouseUp($event, widget)"
ng-click=""
tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)" tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)"
ng-style="{cursor: 'pointer', ng-style="{cursor: 'pointer',
color: vm.widgetColor(widget), color: vm.widgetColor(widget),
@ -49,7 +47,7 @@
<span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span> <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span>
<tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div> </div>
<div class="tb-widget-actions" layout="row" layout-align="start center"> <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()">
<md-button id="expand-button" <md-button id="expand-button"
ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)" ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
aria-label="{{ 'fullscreen.fullscreen' | translate }}" aria-label="{{ 'fullscreen.fullscreen' | translate }}"

2
ui/src/app/components/device-filter.directive.js

@ -41,7 +41,7 @@ function DeviceFilter($compile, $templateCache, $q, deviceService) {
var deferred = $q.defer(); var deferred = $q.defer();
deviceService.getTenantDevices(pageLink).then(function success(result) { deviceService.getTenantDevices(pageLink, false).then(function success(result) {
deferred.resolve(result.data); deferred.resolve(result.data);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();

131
ui/src/app/components/grid.directive.js

@ -26,6 +26,7 @@ import gridTemplate from './grid.tpl.html';
export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav]) export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav])
.directive('tbGrid', Grid) .directive('tbGrid', Grid)
.controller('ItemCardController', ItemCardController)
.directive('tbGridCardContent', GridCardContent) .directive('tbGridCardContent', GridCardContent)
.filter('range', RangeFilter) .filter('range', RangeFilter)
.name; .name;
@ -44,14 +45,52 @@ function RangeFilter() {
} }
/*@ngInject*/ /*@ngInject*/
function GridCardContent($compile) { function ItemCardController() {
var vm = this; //eslint-disable-line
}
/*@ngInject*/
function GridCardContent($compile, $controller) {
var linker = function(scope, element) { var linker = function(scope, element) {
var controllerInstance = null;
scope.$watch('itemTemplate', scope.$watch('itemTemplate',
function(value) { function() {
element.html(value); initContent();
$compile(element.contents())(scope); }
);
scope.$watch('itemController',
function() {
initContent();
} }
); );
scope.$watch('parentCtl',
function() {
controllerInstance.parentCtl = scope.parentCtl;
}
);
scope.$watch('item',
function() {
controllerInstance.item = scope.item;
}
);
function initContent() {
if (scope.itemTemplate && scope.itemController && !controllerInstance) {
element.html(scope.itemTemplate);
var locals = {};
angular.extend(locals, {$scope: scope, $element: element});
var controller = $controller(scope.itemController, locals, true, 'vm');
controller.instance = controller();
controllerInstance = controller.instance;
controllerInstance.item = scope.item;
controllerInstance.parentCtl = scope.parentCtl;
$compile(element.contents())(scope);
}
}
}; };
return { return {
@ -61,6 +100,7 @@ function GridCardContent($compile) {
parentCtl: "=parentCtl", parentCtl: "=parentCtl",
gridCtl: "=gridCtl", gridCtl: "=gridCtl",
itemTemplate: "=itemTemplate", itemTemplate: "=itemTemplate",
itemController: "=itemController",
item: "=item" item: "=item"
} }
}; };
@ -171,26 +211,31 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
vm.items.pending = true; vm.items.pending = true;
promise.then( promise.then(
function success(items) { function success(items) {
vm.items.data = vm.items.data.concat(items.data); if (vm.items.reloadPending) {
var startIndex = vm.items.data.length - items.data.length; vm.items.pending = false;
var endIndex = vm.items.data.length; reload();
for (var i = startIndex; i < endIndex; i++) { } else {
var item = vm.items.data[i]; vm.items.data = vm.items.data.concat(items.data);
item.index = i; var startIndex = vm.items.data.length - items.data.length;
var row = Math.floor(i / vm.columns); var endIndex = vm.items.data.length;
var itemRow = vm.items.rowData[row]; for (var i = startIndex; i < endIndex; i++) {
if (!itemRow) { var item = vm.items.data[i];
itemRow = []; item.index = i;
vm.items.rowData.push(itemRow); var row = Math.floor(i / vm.columns);
var itemRow = vm.items.rowData[row];
if (!itemRow) {
itemRow = [];
vm.items.rowData.push(itemRow);
}
itemRow.push(item);
} }
itemRow.push(item); vm.items.nextPageLink = items.nextPageLink;
} vm.items.hasNext = items.hasNext;
vm.items.nextPageLink = items.nextPageLink; if (vm.items.hasNext) {
vm.items.hasNext = items.hasNext; vm.items.nextPageLink.limit = pageSize;
if (vm.items.hasNext) { }
vm.items.nextPageLink.limit = pageSize; vm.items.pending = false;
} }
vm.items.pending = false;
}, },
function fail() { function fail() {
vm.items.hasNext = false; vm.items.hasNext = false;
@ -291,6 +336,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
} else if (vm.config.itemCardTemplateUrl) { } else if (vm.config.itemCardTemplateUrl) {
vm.itemCardTemplate = $templateCache.get(vm.config.itemCardTemplateUrl); vm.itemCardTemplate = $templateCache.get(vm.config.itemCardTemplateUrl);
} }
if (vm.config.itemCardController) {
vm.itemCardController = vm.config.itemCardController;
} else {
vm.itemCardController = 'ItemCardController';
}
vm.parentCtl = vm.config.parentCtl || vm; vm.parentCtl = vm.config.parentCtl || vm;
@ -380,26 +430,35 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
} }
$scope.$on('searchTextUpdated', function () { $scope.$on('searchTextUpdated', function () {
vm.items = { reload();
data: [],
rowData: [],
nextPageLink: {
limit: pageSize,
textSearch: $scope.searchConfig.searchText
},
selections: {},
selectedCount: 0,
hasNext: true,
pending: false
};
vm.detailsConfig.isDetailsOpen = false;
vm.itemRows.getItemAtIndex(pageSize);
}); });
vm.onGridInited(vm); vm.onGridInited(vm);
vm.itemRows.getItemAtIndex(pageSize); vm.itemRows.getItemAtIndex(pageSize);
function reload() {
if (vm.items && vm.items.pending) {
vm.items.reloadPending = true;
} else {
vm.items = {
data: [],
rowData: [],
nextPageLink: {
limit: pageSize,
textSearch: $scope.searchConfig.searchText
},
selections: {},
selectedCount: 0,
hasNext: true,
pending: false
};
vm.detailsConfig.isDetailsOpen = false;
vm.items.reloadPending = false;
vm.itemRows.getItemAtIndex(pageSize);
}
}
function refreshList() { function refreshList() {
$state.go($state.current, vm.refreshParamsFunc(), {reload: true}); $state.go($state.current, vm.refreshParamsFunc(), {reload: true});
} }

6
ui/src/app/components/grid.tpl.html

@ -43,13 +43,13 @@
</md-card-title> </md-card-title>
</section> </section>
<md-card-content flex> <md-card-content flex>
<tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content> <tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
</md-card-content> </md-card-content>
<md-card-actions layout="row" layout-align="end end"> <md-card-actions layout="row" layout-align="end end">
<md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList" <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}"> ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
{{ action.details() }} {{ action.details( rowItem[n] ) }}
</md-tooltip> </md-tooltip>
<ng-md-icon icon="{{action.icon}}"></ng-md-icon> <ng-md-icon icon="{{action.icon}}"></ng-md-icon>
</md-button> </md-button>
@ -62,7 +62,7 @@
</div> </div>
<tb-details-sidenav <tb-details-sidenav
header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}" header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}"
header-subtitle="{{vm.itemDetailsText()}}" header-subtitle="{{vm.itemDetailsText(vm.operatingItem())}}"
is-read-only="vm.isDetailsReadOnly(vm.operatingItem())" is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
is-open="vm.detailsConfig.isDetailsOpen" is-open="vm.detailsConfig.isDetailsOpen"
is-edit="vm.detailsConfig.isDetailsEditMode" is-edit="vm.detailsConfig.isDetailsEditMode"

7
ui/src/app/components/legend.directive.js

@ -44,15 +44,8 @@ function Legend($compile, $templateCache, types) {
scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value || scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
scope.legendConfig.position === types.position.top.value; scope.legendConfig.position === types.position.top.value;
scope.$on('legendDataUpdated', function (event, apply) {
if (apply) {
scope.$digest();
}
});
scope.toggleHideData = function(index) { scope.toggleHideData = function(index) {
scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden; scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
scope.$emit('legendDataHiddenChanged', index);
} }
$compile(element.contents())(scope); $compile(element.contents())(scope);

58
ui/src/app/components/socialshare-panel.directive.js

@ -0,0 +1,58 @@
/*
* Copyright © 2016-2017 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import socialsharePanelTemplate from './socialshare-panel.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
export default angular.module('thingsboard.directives.socialsharePanel', [])
.directive('tbSocialSharePanel', SocialsharePanel)
.name;
/*@ngInject*/
function SocialsharePanel() {
return {
restrict: "E",
scope: true,
bindToController: {
shareTitle: '@',
shareText: '@',
shareLink: '@',
shareHashTags: '@'
},
controller: SocialsharePanelController,
controllerAs: 'vm',
templateUrl: socialsharePanelTemplate
};
}
/*@ngInject*/
function SocialsharePanelController(utils) {
let vm = this;
vm.isShareLinkLocal = function() {
if (vm.shareLink && vm.shareLink.length > 0) {
return utils.isLocalUrl(vm.shareLink);
} else {
return true;
}
}
}

62
ui/src/app/components/socialshare-panel.tpl.html

@ -0,0 +1,62 @@
<!--
Copyright © 2016-2017 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.
-->
<div layout="row" ng-show="!vm.isShareLinkLocal()">
<md-button class="md-icon-button md-raised md-primary"
socialshare
socialshare-provider="facebook"
socialshare-title="{{ vm.shareTitle }}"
socialshare-text="{{ vm.shareText }}"
socialshare-url="{{ vm.shareLink }}">
<ng-md-icon icon="facebook" aria-label="Facebook"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.share-via' | translate:{provider:'Facebook'} }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button md-raised md-primary"
socialshare
socialshare-provider="twitter"
socialshare-text="{{ vm.shareTitle }}"
socialshare-hashtags="{{ vm.shareHashTags }}"
socialshare-url="{{ vm.shareLink }}">
<ng-md-icon icon="twitter" aria-label="Twitter"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.share-via' | translate:{provider:'Twitter'} }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button md-raised md-primary"
socialshare
socialshare-provider="linkedin"
socialshare-text="{{ vm.shareTitle }}"
socialshare-url="{{ vm.shareLink }}">
<ng-md-icon icon="linkedin" aria-label="Linkedin"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.share-via' | translate:{provider:'Linkedin'} }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button md-raised md-primary"
socialshare
socialshare-provider="reddit"
socialshare-text="{{ vm.shareTitle }}"
socialshare-url="{{ vm.shareLink }}">
<md-icon md-svg-icon="mdi:reddit" aria-label="Reddit"></md-icon>
<md-tooltip md-direction="top">
{{ 'action.share-via' | translate:{provider:'Reddit'} }}
</md-tooltip>
</md-button>
</div>

18
ui/src/app/components/widget-config.directive.js

@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.forceExpandDatasources = false; scope.forceExpandDatasources = false;
} }
if (angular.isUndefined(scope.isDataEnabled)) {
scope.isDataEnabled = true;
}
scope.currentSettingsSchema = {}; scope.currentSettingsSchema = {};
scope.currentSettings = angular.copy(scope.emptySettingsSchema); scope.currentSettings = angular.copy(scope.emptySettingsSchema);
@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ? scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ?
ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value; ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
scope.legendConfig = ngModelCtrl.$viewValue.legendConfig; scope.legendConfig = ngModelCtrl.$viewValue.legendConfig;
if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) { if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
&& scope.isDataEnabled) {
if (scope.datasources) { if (scope.datasources) {
scope.datasources.splice(0, scope.datasources.length); scope.datasources.splice(0, scope.datasources.length);
} else { } else {
@ -119,7 +124,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]}); scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
} }
} }
} else if (scope.widgetType === types.widgetType.rpc.value) { } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) { if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0]; var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
if (scope.deviceAliases[aliasId]) { if (scope.deviceAliases[aliasId]) {
@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
var valid; var valid;
if (scope.widgetType === types.widgetType.rpc.value) { if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0; valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
} else if (scope.widgetType !== types.widgetType.static.value) { } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
valid = value && value.datasources && value.datasources.length > 0; valid = value && value.datasources && value.datasources.length > 0;
ngModelCtrl.$setValidity('datasources', valid); ngModelCtrl.$setValidity('datasources', valid);
} }
@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.$watch('datasources', function () { scope.$watch('datasources', function () {
if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value
&& scope.widgetType !== types.widgetType.static.value) { && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
if (value.datasources) { if (value.datasources) {
value.datasources.splice(0, value.datasources.length); value.datasources.splice(0, value.datasources.length);
@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}, true); }, true);
scope.$watch('targetDeviceAlias.value', function () { scope.$watch('targetDeviceAlias.value', function () {
if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value) { if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
if (scope.targetDeviceAlias.value) { if (scope.targetDeviceAlias.value) {
value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
@ -359,6 +364,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
require: "^ngModel", require: "^ngModel",
scope: { scope: {
forceExpandDatasources: '=?', forceExpandDatasources: '=?',
isDataEnabled: '=?',
widgetType: '=', widgetType: '=',
widgetSettingsSchema: '=', widgetSettingsSchema: '=',
datakeySettingsSchema: '=', datakeySettingsSchema: '=',

4
ui/src/app/components/widget-config.tpl.html

@ -31,7 +31,7 @@
</section> </section>
</div> </div>
<v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value"> ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled">
<v-pane id="datasources-pane" expanded="true"> <v-pane id="datasources-pane" expanded="true">
<v-pane-header> <v-pane-header>
{{ 'widget-config.datasources' | translate }} {{ 'widget-config.datasources' | translate }}
@ -96,7 +96,7 @@
</v-pane> </v-pane>
</v-accordion> </v-accordion>
<v-accordion id="target-devices-accordion" control="targetDevicesAccordion" class="vAccordion--default" <v-accordion id="target-devices-accordion" control="targetDevicesAccordion" class="vAccordion--default"
ng-show="widgetType === types.widgetType.rpc.value"> ng-show="widgetType === types.widgetType.rpc.value && isDataEnabled">
<v-pane id="target-devices-pane" expanded="true"> <v-pane id="target-devices-pane" expanded="true">
<v-pane-header> <v-pane-header>
{{ 'widget-config.target-device' | translate }} {{ 'widget-config.target-device' | translate }}

901
ui/src/app/components/widget.controller.js

@ -15,6 +15,7 @@
*/ */
import $ from 'jquery'; import $ from 'jquery';
import 'javascript-detect-element-resize/detect-element-resize'; import 'javascript-detect-element-resize/detect-element-resize';
import Subscription from '../api/subscription';
/* eslint-disable angular/angularelement */ /* eslint-disable angular/angularelement */
@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.rpcErrorText = null; $scope.rpcErrorText = null;
$scope.rpcEnabled = false; $scope.rpcEnabled = false;
$scope.executingRpcRequest = false; $scope.executingRpcRequest = false;
$scope.executingPromises = [];
var gridsterItemInited = false; var gridsterItemInited = false;
var datasourceListeners = [];
var targetDeviceAliasId = null;
var targetDeviceId = null;
var originalTimewindow = null;
var subscriptionTimewindow = null;
var cafs = {}; var cafs = {};
var varsRegex = /\$\{([^\}]*)\}/g;
/* /*
* data = array of datasourceData * data = array of datasourceData
* datasourceData = { * datasourceData = {
@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
* dataKey, { name, config } * dataKey, { name, config }
* data = array of [time, value] * data = array of [time, value]
* } * }
*
*
*/ */
var widgetContext = { var widgetContext = {
@ -71,22 +62,70 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
settings: widget.config.settings, settings: widget.config.settings,
units: widget.config.units || '', units: widget.config.units || '',
decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2, decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
datasources: angular.copy(widget.config.datasources), subscriptions: {},
data: [], defaultSubscription: null,
hiddenData: [],
timeWindow: {
stDiff: stDiff
},
timewindowFunctions: { timewindowFunctions: {
onUpdateTimewindow: onUpdateTimewindow, onUpdateTimewindow: function(startTimeMs, endTimeMs) {
onResetTimewindow: onResetTimewindow if (widgetContext.defaultSubscription) {
widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs);
}
},
onResetTimewindow: function() {
if (widgetContext.defaultSubscription) {
widgetContext.defaultSubscription.onResetTimewindow();
}
}
},
subscriptionApi: {
createSubscription: function(options, subscribe) {
return createSubscription(options, subscribe);
},
// type: "timeseries" or "latest" or "rpc"
/* devicesSubscriptionInfo = [
{
deviceId: ""
deviceName: ""
timeseries: [{ name: "", label: "" }, ..]
attributes: [{ name: "", label: "" }, ..]
}
..
]*/
// options = {
// timeWindowConfig,
// useDashboardTimewindow,
// legendConfig,
// decimals,
// units,
// callbacks [ onDataUpdated(subscription, apply) ]
// }
//
createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe);
},
removeSubscription: function(id) {
var subscription = widgetContext.subscriptions[id];
if (subscription) {
subscription.destroy();
delete widgetContext.subscriptions[id];
}
}
}, },
controlApi: { controlApi: {
sendOneWayCommand: function(method, params, timeout) { sendOneWayCommand: function(method, params, timeout) {
return sendCommand(true, method, params, timeout); if (widgetContext.defaultSubscription) {
return widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout);
}
return null;
}, },
sendTwoWayCommand: function(method, params, timeout) { sendTwoWayCommand: function(method, params, timeout) {
return sendCommand(false, method, params, timeout); if (widgetContext.defaultSubscription) {
return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout);
}
return null;
} }
}, },
utils: { utils: {
@ -94,7 +133,27 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} }
}; };
var subscriptionContext = {
$scope: $scope,
$q: $q,
$filter: $filter,
$timeout: $timeout,
tbRaf: tbRaf,
timeService: timeService,
deviceService: deviceService,
datasourceService: datasourceService,
utils: utils,
widgetUtils: widgetContext.utils,
dashboardTimewindowApi: dashboardTimewindowApi,
types: types,
stDiff: stDiff,
aliasesInfo: aliasesInfo
};
var widgetTypeInstance; var widgetTypeInstance;
vm.useCustomDatasources = false;
try { try {
widgetTypeInstance = new widgetType(widgetContext); widgetTypeInstance = new widgetType(widgetContext);
} catch (e) { } catch (e) {
@ -119,19 +178,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
if (!widgetTypeInstance.onDestroy) { if (!widgetTypeInstance.onDestroy) {
widgetTypeInstance.onDestroy = function() {}; widgetTypeInstance.onDestroy = function() {};
} }
if (widgetTypeInstance.useCustomDatasources) {
//var bounds = {top: 0, left: 0, bottom: 0, right: 0}; vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
//TODO: widgets visibility
/*var visible = false;*/
$scope.clearRpcError = function() {
$scope.rpcRejection = null;
$scope.rpcErrorText = null;
} }
vm.gridsterItemInitialized = gridsterItemInitialized;
//TODO: widgets visibility //TODO: widgets visibility
//var bounds = {top: 0, left: 0, bottom: 0, right: 0};
/*var visible = false;*/
/*vm.visibleRectChanged = visibleRectChanged; /*vm.visibleRectChanged = visibleRectChanged;
function visibleRectChanged(newVisibleRect) { function visibleRectChanged(newVisibleRect) {
@ -139,23 +193,216 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
updateVisibility(); updateVisibility();
}*/ }*/
$scope.clearRpcError = function() {
if (widgetContext.defaultSubscription) {
widgetContext.defaultSubscription.clearRpcError();
}
}
vm.gridsterItemInitialized = gridsterItemInitialized;
initialize(); initialize();
function handleWidgetException(e) {
$log.error(e); /*
$scope.widgetErrorData = utils.processWidgetException(e); options = {
type,
targetDeviceAliasIds, // RPC
targetDeviceIds, // RPC
datasources,
timeWindowConfig,
useDashboardTimewindow,
legendConfig,
decimals,
units,
callbacks
}
*/
function createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
var deferred = $q.defer();
options.type = type;
if (useDefaultComponents) {
defaultComponentsOptions(options);
} else {
if (!options.timeWindowConfig) {
options.useDashboardTimewindow = true;
}
}
utils.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then(
function (datasources) {
options.datasources = datasources;
var subscription = createSubscription(options, subscribe);
if (useDefaultComponents) {
defaultSubscriptionOptions(subscription, options);
}
deferred.resolve(subscription);
}
);
return deferred.promise;
}
function createSubscription(options, subscribe) {
options.dashboardTimewindow = dashboardTimewindow;
var subscription =
new Subscription(subscriptionContext, options);
widgetContext.subscriptions[subscription.id] = subscription;
if (subscribe) {
subscription.subscribe();
}
return subscription;
} }
function notifyDataLoaded() { function defaultComponentsOptions(options) {
if ($scope.loadingData === true) { options.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
? widget.config.useDashboardTimewindow : true;
options.timeWindowConfig = options.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;
options.legendConfig = null;
if ($scope.displayLegend) {
options.legendConfig = $scope.legendConfig;
}
options.decimals = widgetContext.decimals;
options.units = widgetContext.units;
options.callbacks = {
onDataUpdated: function() {
widgetTypeInstance.onDataUpdated();
},
onDataUpdateError: function(subscription, e) {
handleWidgetException(e);
},
dataLoading: function(subscription) {
if ($scope.loadingData !== subscription.loadingData) {
$scope.loadingData = subscription.loadingData;
}
},
legendDataUpdated: function(subscription, apply) {
if (apply) {
$scope.$digest();
}
},
timeWindowUpdated: function(subscription, timeWindowConfig) {
widget.config.timewindow = timeWindowConfig;
$scope.$apply();
}
}
}
function defaultSubscriptionOptions(subscription, options) {
if (!options.useDashboardTimewindow) {
$scope.$watch(function () {
return widget.config.timewindow;
}, function (newTimewindow, prevTimewindow) {
if (!angular.equals(newTimewindow, prevTimewindow)) {
subscription.updateTimewindowConfig(widget.config.timewindow);
}
});
}
if ($scope.displayLegend) {
$scope.legendData = subscription.legendData;
}
}
function createDefaultSubscription() {
var subscription;
var options;
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
options = {
type: widget.type,
datasources: angular.copy(widget.config.datasources)
};
defaultComponentsOptions(options);
subscription = createSubscription(options);
defaultSubscriptionOptions(subscription, options);
// backward compatibility
widgetContext.datasources = subscription.datasources;
widgetContext.data = subscription.data;
widgetContext.hiddenData = subscription.hiddenData;
widgetContext.timeWindow = subscription.timeWindow;
} else if (widget.type === types.widgetType.rpc.value) {
$scope.loadingData = false;
options = {
type: widget.type,
targetDeviceAliasIds: widget.config.targetDeviceAliasIds
}
options.callbacks = {
rpcStateChanged: function(subscription) {
$scope.rpcEnabled = subscription.rpcEnabled;
$scope.executingRpcRequest = subscription.executingRpcRequest;
},
onRpcSuccess: function(subscription) {
$scope.executingRpcRequest = subscription.executingRpcRequest;
},
onRpcFailed: function(subscription) {
$scope.executingRpcRequest = subscription.executingRpcRequest;
$scope.rpcErrorText = subscription.rpcErrorText;
$scope.rpcRejection = subscription.rpcRejection;
},
onRpcErrorCleared: function() {
$scope.rpcErrorText = null;
$scope.rpcRejection = null;
}
}
subscription = createSubscription(options);
} else if (widget.type === types.widgetType.static.value) {
$scope.loadingData = false; $scope.loadingData = false;
} }
if (subscription) {
widgetContext.defaultSubscription = subscription;
}
} }
function notifyDataLoading() {
if ($scope.loadingData === false) { function initialize() {
$scope.loadingData = true;
if (!vm.useCustomDatasources) {
createDefaultSubscription();
} else {
$scope.loadingData = false;
} }
$scope.$on('toggleDashboardEditMode', function (event, isEdit) {
onEditModeChanged(isEdit);
});
addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
$scope.$watch(function () {
return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;
}, function () {
//updateBounds();
$scope.$emit("widgetPositionChanged", widget);
});
$scope.$on('gridster-item-resized', function (event, item) {
if (!widgetContext.isMobile) {
widget.sizeX = item.sizeX;
widget.sizeY = item.sizeY;
}
});
$scope.$on('mobileModeChanged', function (event, newIsMobile) {
onMobileModeChanged(newIsMobile);
});
$scope.$on("$destroy", function () {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
onDestroy();
});
}
function handleWidgetException(e) {
$log.error(e);
$scope.widgetErrorData = utils.processWidgetException(e);
} }
function onInit() { function onInit() {
@ -166,42 +413,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} catch (e) { } catch (e) {
handleWidgetException(e); handleWidgetException(e);
} }
if (widgetContext.dataUpdatePending) { if (!vm.useCustomDatasources && widgetContext.defaultSubscription) {
widgetContext.dataUpdatePending = false; widgetContext.defaultSubscription.subscribe();
onDataUpdated();
} }
} }
} }
function updateTimewindow() {
widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000;
if (subscriptionTimewindow.realtimeWindowMs) {
widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff;
widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs;
} else if (subscriptionTimewindow.fixedWindow) {
widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
}
}
function onDataUpdated() {
if (widgetContext.inited) {
if (cafs['dataUpdate']) {
cafs['dataUpdate']();
cafs['dataUpdate'] = null;
}
cafs['dataUpdate'] = tbRaf(function() {
try {
widgetTypeInstance.onDataUpdated();
} catch (e) {
handleWidgetException(e);
}
});
} else {
widgetContext.dataUpdatePending = true;
}
}
function checkSize() { function checkSize() {
var width = widgetContext.$containerParent.width(); var width = widgetContext.$containerParent.width();
var height = widgetContext.$containerParent.height(); var height = widgetContext.$containerParent.height();
@ -289,11 +506,35 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} }
} }
function isNumeric(val) {
return (val - parseFloat( val ) + 1) >= 0;
}
function formatValue(value, dec, units) {
if (angular.isDefined(value) &&
value !== null && isNumeric(value)) {
var formatted = value;
if (angular.isDefined(dec)) {
formatted = formatted.toFixed(dec);
}
formatted = (formatted * 1).toString();
if (angular.isDefined(units) && units.length > 0) {
formatted += ' ' + units;
}
return formatted;
} else {
return '';
}
}
function onDestroy() { function onDestroy() {
unsubscribe(); for (var id in widgetContext.subscriptions) {
var subscription = widgetContext.subscriptions[id];
subscription.destroy();
}
widgetContext.subscriptions = [];
if (widgetContext.inited) { if (widgetContext.inited) {
widgetContext.inited = false; widgetContext.inited = false;
widgetContext.dataUpdatePending = false;
for (var cafId in cafs) { for (var cafId in cafs) {
if (cafs[cafId]) { if (cafs[cafId]) {
cafs[cafId](); cafs[cafId]();
@ -308,243 +549,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} }
} }
function onRestart() {
onDestroy();
onInit();
}
/* scope.legendData = {
keys: [],
data: []
key: {
label: '',
color: ''
dataIndex: 0
}
data: {
min: null,
max: null,
avg: null,
total: null
}
};*/
function initialize() {
$scope.caulculateLegendData = $scope.displayLegend &&
widget.type === types.widgetType.timeseries.value &&
($scope.legendConfig.showMin === true ||
$scope.legendConfig.showMax === true ||
$scope.legendConfig.showAvg === true ||
$scope.legendConfig.showTotal === true);
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
for (var i = 0; i < widgetContext.datasources.length; i++) {
var datasource = widgetContext.datasources[i];
for (var a = 0; a < datasource.dataKeys.length; a++) {
var dataKey = datasource.dataKeys[a];
dataKey.pattern = angular.copy(dataKey.label);
var datasourceData = {
datasource: datasource,
dataKey: dataKey,
data: []
};
widgetContext.data.push(datasourceData);
widgetContext.hiddenData.push({data: []});
if ($scope.displayLegend) {
var legendKey = {
dataKey: dataKey,
dataIndex: Number(i) + Number(a)
};
$scope.legendData.keys.push(legendKey);
var legendKeyData = {
min: null,
max: null,
avg: null,
total: null,
hidden: false
};
$scope.legendData.data.push(legendKeyData);
}
}
}
if ($scope.displayLegend) {
$scope.legendData.keys = $filter('orderBy')($scope.legendData.keys, '+label');
$scope.$on('legendDataHiddenChanged', function (event, index) {
event.stopPropagation();
var hidden = $scope.legendData.data[index].hidden;
if (hidden) {
widgetContext.hiddenData[index].data = widgetContext.data[index].data;
widgetContext.data[index].data = [];
} else {
widgetContext.data[index].data = widgetContext.hiddenData[index].data;
widgetContext.hiddenData[index].data = [];
}
onDataUpdated();
});
}
} else if (widget.type === types.widgetType.rpc.value) {
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
}
}
if (targetDeviceId) {
$scope.rpcEnabled = true;
} else {
$scope.rpcEnabled = $scope.widgetEditMode ? true : false;
}
}
$scope.$on('toggleDashboardEditMode', function (event, isEdit) {
onEditModeChanged(isEdit);
});
addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
$scope.$watch(function () {
return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;
}, function () {
//updateBounds();
$scope.$emit("widgetPositionChanged", widget);
});
$scope.$on('gridster-item-resized', function (event, item) {
if (!widgetContext.isMobile) {
widget.sizeX = item.sizeX;
widget.sizeY = item.sizeY;
}
});
$scope.$on('mobileModeChanged', function (event, newIsMobile) {
onMobileModeChanged(newIsMobile);
});
$scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) {
aliasesInfo = newAliasesInfo;
if (widget.type === types.widgetType.rpc.value) {
if (targetDeviceAliasId) {
var deviceId = null;
if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
}
if (!angular.equals(deviceId, targetDeviceId)) {
targetDeviceId = deviceId;
if (targetDeviceId) {
$scope.rpcEnabled = true;
} else {
$scope.rpcEnabled = $scope.widgetEditMode ? true : false;
}
onRestart();
}
}
} else {
checkSubscriptions();
}
});
$scope.$on("$destroy", function () {
removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
onDestroy();
});
if (widget.type === types.widgetType.timeseries.value) {
widgetContext.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
? widget.config.useDashboardTimewindow : true;
if (widgetContext.useDashboardTimewindow) {
$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
if (!angular.equals(dashboardTimewindow, newDashboardTimewindow)) {
dashboardTimewindow = newDashboardTimewindow;
unsubscribe();
subscribe();
}
});
} else {
$scope.$watch(function () {
return widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;
}, function (newTimewindow, prevTimewindow) {
if (!angular.equals(newTimewindow, prevTimewindow)) {
unsubscribe();
subscribe();
}
});
}
}
subscribe();
}
function sendCommand(oneWayElseTwoWay, method, params, timeout) {
if (!$scope.rpcEnabled) {
return $q.reject();
}
if ($scope.rpcRejection && $scope.rpcRejection.status !== 408) {
$scope.rpcRejection = null;
$scope.rpcErrorText = null;
}
var requestBody = {
method: method,
params: params
};
if (timeout && timeout > 0) {
requestBody.timeout = timeout;
}
var deferred = $q.defer();
$scope.executingRpcRequest = true;
if ($scope.widgetEditMode) {
$timeout(function() {
$scope.executingRpcRequest = false;
if (oneWayElseTwoWay) {
deferred.resolve();
} else {
deferred.resolve(requestBody);
}
}, 500);
} else {
$scope.executingPromises.push(deferred.promise);
var targetSendFunction = oneWayElseTwoWay ? deviceService.sendOneWayRpcCommand : deviceService.sendTwoWayRpcCommand;
targetSendFunction(targetDeviceId, requestBody).then(
function success(responseBody) {
$scope.rpcRejection = null;
$scope.rpcErrorText = null;
var index = $scope.executingPromises.indexOf(deferred.promise);
if (index >= 0) {
$scope.executingPromises.splice( index, 1 );
}
$scope.executingRpcRequest = $scope.executingPromises.length > 0;
deferred.resolve(responseBody);
},
function fail(rejection) {
var index = $scope.executingPromises.indexOf(deferred.promise);
if (index >= 0) {
$scope.executingPromises.splice( index, 1 );
}
$scope.executingRpcRequest = $scope.executingPromises.length > 0;
if (!$scope.executingRpcRequest || rejection.status === 408) {
$scope.rpcRejection = rejection;
if (rejection.status === 408) {
$scope.rpcErrorText = 'Device is offline.';
} else {
$scope.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
if (rejection.data && rejection.data.length > 0) {
$scope.rpcErrorText += '</br>';
$scope.rpcErrorText += rejection.data;
}
}
}
deferred.reject(rejection);
}
);
}
return deferred.promise;
}
//TODO: widgets visibility //TODO: widgets visibility
/*function updateVisibility(forceRedraw) { /*function updateVisibility(forceRedraw) {
if (visibleRect) { if (visibleRect) {
@ -583,285 +587,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
onRedraw(); onRedraw();
}*/ }*/
function onResetTimewindow() {
if (widgetContext.useDashboardTimewindow) {
dashboardTimewindowApi.onResetTimewindow();
} else {
if (originalTimewindow) {
widget.config.timewindow = angular.copy(originalTimewindow);
originalTimewindow = null;
}
}
}
function onUpdateTimewindow(startTimeMs, endTimeMs) {
if (widgetContext.useDashboardTimewindow) {
dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
} else {
if (!originalTimewindow) {
originalTimewindow = angular.copy(widget.config.timewindow);
}
widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs);
}
}
function dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
notifyDataLoaded();
var update = true;
var currentData;
if ($scope.displayLegend && $scope.legendData.data[datasourceIndex + dataKeyIndex].hidden) {
currentData = widgetContext.hiddenData[datasourceIndex + dataKeyIndex];
} else {
currentData = widgetContext.data[datasourceIndex + dataKeyIndex];
}
if (widget.type === types.widgetType.latest.value) {
var prevData = currentData.data;
if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {
var prevValue = prevData[0][1];
if (prevValue === sourceData.data[0][1]) {
update = false;
}
}
}
if (update) {
if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) {
updateTimewindow();
}
currentData.data = sourceData.data;
onDataUpdated();
if ($scope.caulculateLegendData) {
updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
}
}
if (apply) {
$scope.$digest();
}
}
function updateLegend(dataIndex, data, apply) {
var legendKeyData = $scope.legendData.data[dataIndex];
if ($scope.legendConfig.showMin) {
legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units);
}
if ($scope.legendConfig.showMax) {
legendKeyData.max = formatValue(calculateMax(data), widgetContext.decimals, widgetContext.units);
}
if ($scope.legendConfig.showAvg) {
legendKeyData.avg = formatValue(calculateAvg(data), widgetContext.decimals, widgetContext.units);
}
if ($scope.legendConfig.showTotal) {
legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units);
}
$scope.$broadcast('legendDataUpdated', apply !== false);
}
function isNumeric(val) {
return (val - parseFloat( val ) + 1) >= 0;
}
function formatValue(value, dec, units) {
if (angular.isDefined(value) &&
value !== null && isNumeric(value)) {
var formatted = value;
if (angular.isDefined(dec)) {
formatted = formatted.toFixed(dec);
}
formatted = (formatted * 1).toString();
if (angular.isDefined(units) && units.length > 0) {
formatted += ' ' + units;
}
return formatted;
} else {
return '';
}
}
function calculateMin(data) {
if (data.length > 0) {
var result = Number(data[0][1]);
for (var i=1;i<data.length;i++) {
result = Math.min(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateMax(data) {
if (data.length > 0) {
var result = Number(data[0][1]);
for (var i=1;i<data.length;i++) {
result = Math.max(result, Number(data[i][1]));
}
return result;
} else {
return null;
}
}
function calculateAvg(data) {
if (data.length > 0) {
return calculateTotal(data)/data.length;
} else {
return null;
}
}
function calculateTotal(data) {
if (data.length > 0) {
var result = 0;
for (var i = 0; i < data.length; i++) {
result += Number(data[i][1]);
}
return result;
} else {
return null;
}
}
function checkSubscriptions() {
if (widget.type !== types.widgetType.rpc.value) {
var subscriptionsChanged = false;
for (var i = 0; i < datasourceListeners.length; i++) {
var listener = datasourceListeners[i];
var deviceId = null;
var aliasName = null;
if (listener.datasource.type === types.datasourceType.device) {
if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
}
if (!angular.equals(deviceId, listener.deviceId) ||
!angular.equals(aliasName, listener.datasource.name)) {
subscriptionsChanged = true;
break;
}
}
}
if (subscriptionsChanged) {
unsubscribe();
subscribe();
}
}
}
function unsubscribe() {
if (widget.type !== types.widgetType.rpc.value) {
for (var i = 0; i < datasourceListeners.length; i++) {
var listener = datasourceListeners[i];
datasourceService.unsubscribeFromDatasource(listener);
}
datasourceListeners = [];
}
}
function updateRealtimeSubscription(_subscriptionTimewindow) {
if (_subscriptionTimewindow) {
subscriptionTimewindow = _subscriptionTimewindow;
} else {
subscriptionTimewindow =
timeService.createSubscriptionTimewindow(
widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow,
widgetContext.timeWindow.stDiff);
}
updateTimewindow();
return subscriptionTimewindow;
}
function hasTimewindow() {
if (widgetContext.useDashboardTimewindow) {
return angular.isDefined(dashboardTimewindow);
} else {
return angular.isDefined(widget.config.timewindow);
}
}
function updateDataKeyLabel(dataKey, deviceName, aliasName) {
var pattern = dataKey.pattern;
var label = dataKey.pattern;
var match = varsRegex.exec(pattern);
while (match !== null) {
var variable = match[0];
var variableName = match[1];
if (variableName === 'deviceName') {
label = label.split(variable).join(deviceName);
} else if (variableName === 'aliasName') {
label = label.split(variable).join(aliasName);
}
match = varsRegex.exec(pattern);
}
dataKey.label = label;
}
function subscribe() {
if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
notifyDataLoading();
if (widget.type === types.widgetType.timeseries.value &&
hasTimewindow()) {
updateRealtimeSubscription();
if (subscriptionTimewindow.fixedWindow) {
onDataUpdated();
}
}
var index = 0;
for (var i = 0; i < widgetContext.datasources.length; i++) {
var datasource = widgetContext.datasources[i];
if (angular.isFunction(datasource))
continue;
var deviceId = null;
if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
var deviceName = '';
var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
for (var d=0;d<devicesInfo.length;d++) {
if (devicesInfo[d].id === deviceId) {
deviceName = devicesInfo[d].name;
break;
}
}
for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
updateDataKeyLabel(datasource.dataKeys[dk], deviceName, aliasName);
}
}
} else {
datasource.name = types.datasourceType.function;
}
var listener = {
widget: widget,
subscriptionTimewindow: subscriptionTimewindow,
datasource: datasource,
deviceId: deviceId,
dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
},
updateRealtimeSubscription: function() {
this.subscriptionTimewindow = updateRealtimeSubscription();
return this.subscriptionTimewindow;
},
setRealtimeSubscription: function(subscriptionTimewindow) {
updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
},
datasourceIndex: index
};
for (var a = 0; a < datasource.dataKeys.length; a++) {
widgetContext.data[index + a].data = [];
}
index += datasource.dataKeys.length;
datasourceListeners.push(listener);
datasourceService.subscribeToDatasource(listener);
}
} else {
notifyDataLoaded();
}
}
} }
/* eslint-enable angular/angularelement */ /* eslint-enable angular/angularelement */

2
ui/src/app/customer/customer-card.tpl.html

@ -15,4 +15,4 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-uppercase">{{ item | contactShort }}</div> <div ng-show="item && (!item.additionalInfo || !item.additionalInfo.isPublic)" class="tb-uppercase">{{ item | contactShort }}</div>

6
ui/src/app/customer/customer-fieldset.tpl.html

@ -15,13 +15,13 @@
limitations under the License. limitations under the License.
--> -->
<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button> <md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
<md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button> <md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button>
<md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button> <md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button>
<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button> <md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
<md-content class="md-padding" layout="column"> <md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading || !isEdit"> <fieldset ng-show="!isPublic" ng-disabled="loading || !isEdit">
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>customer.title</label> <label translate>customer.title</label>
<input required name="title" ng-model="customer.title"> <input required name="title" ng-model="customer.title">

40
ui/src/app/customer/customer.controller.js

@ -30,14 +30,23 @@ export default function CustomerController(customerService, $state, $stateParams
}, },
name: function() { return $translate.instant('user.users') }, name: function() { return $translate.instant('user.users') },
details: function() { return $translate.instant('customer.manage-customer-users') }, details: function() { return $translate.instant('customer.manage-customer-users') },
icon: "account_circle" icon: "account_circle",
isEnabled: function(customer) {
return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
}
}, },
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
openCustomerDevices($event, item); openCustomerDevices($event, item);
}, },
name: function() { return $translate.instant('device.devices') }, name: function() { return $translate.instant('device.devices') },
details: function() { return $translate.instant('customer.manage-customer-devices') }, details: function(customer) {
if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) {
return $translate.instant('customer.manage-public-devices')
} else {
return $translate.instant('customer.manage-customer-devices')
}
},
icon: "devices_other" icon: "devices_other"
}, },
{ {
@ -45,7 +54,13 @@ export default function CustomerController(customerService, $state, $stateParams
openCustomerDashboards($event, item); openCustomerDashboards($event, item);
}, },
name: function() { return $translate.instant('dashboard.dashboards') }, name: function() { return $translate.instant('dashboard.dashboards') },
details: function() { return $translate.instant('customer.manage-customer-dashboards') }, details: function(customer) {
if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) {
return $translate.instant('customer.manage-public-dashboards')
} else {
return $translate.instant('customer.manage-customer-dashboards')
}
},
icon: "dashboard" icon: "dashboard"
}, },
{ {
@ -54,7 +69,10 @@ export default function CustomerController(customerService, $state, $stateParams
}, },
name: function() { return $translate.instant('action.delete') }, name: function() { return $translate.instant('action.delete') },
details: function() { return $translate.instant('customer.delete') }, details: function() { return $translate.instant('customer.delete') },
icon: "delete" icon: "delete",
isEnabled: function(customer) {
return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
}
} }
]; ];
@ -86,7 +104,19 @@ export default function CustomerController(customerService, $state, $stateParams
addItemText: function() { return $translate.instant('customer.add-customer-text') }, addItemText: function() { return $translate.instant('customer.add-customer-text') },
noItemsText: function() { return $translate.instant('customer.no-customers-text') }, noItemsText: function() { return $translate.instant('customer.no-customers-text') },
itemDetailsText: function() { return $translate.instant('customer.customer-details') } itemDetailsText: function(customer) {
if (customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic)) {
return $translate.instant('customer.customer-details')
} else {
return '';
}
},
isSelectionEnabled: function (customer) {
return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
},
isDetailsReadOnly: function (customer) {
return customer && customer.additionalInfo && customer.additionalInfo.isPublic;
}
}; };
if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {

14
ui/src/app/customer/customer.directive.js

@ -24,7 +24,21 @@ export default function CustomerDirective($compile, $templateCache) {
var linker = function (scope, element) { var linker = function (scope, element) {
var template = $templateCache.get(customerFieldsetTemplate); var template = $templateCache.get(customerFieldsetTemplate);
element.html(template); element.html(template);
scope.isPublic = false;
scope.$watch('customer', function(newVal) {
if (newVal) {
if (scope.customer.additionalInfo) {
scope.isPublic = scope.customer.additionalInfo.isPublic;
} else {
scope.isPublic = false;
}
}
});
$compile(element.contents())(scope); $compile(element.contents())(scope);
} }
return { return {
restrict: "E", restrict: "E",

6
ui/src/app/dashboard/dashboard-card.tpl.html

@ -15,6 +15,6 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-small" ng-if="item && <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
item.customerId.id != parentCtl.types.id.nullUid && <div class="tb-small" ng-show="vm.isPublic()">{{'dashboard.public' | translate}}</div>
parentCtl.dashboardsScope === 'tenant'" translate>dashboard.assignedToCustomer</div>

39
ui/src/app/dashboard/dashboard-fieldset.tpl.html

@ -15,25 +15,50 @@
limitations under the License. limitations under the License.
--> -->
<md-button ng-click="onExportDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
<md-button ng-click="onMakePublic({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer && !isPublic"
class="md-raised md-primary">{{ 'dashboard.make-public' | translate }}</md-button>
<md-button ng-click="onAssignToCustomer({event: $event})" <md-button ng-click="onAssignToCustomer({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer" ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer"
class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button> class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
<md-button ng-click="onUnassignFromCustomer({event: $event})" <md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer" ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer"
class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button> class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }}</md-button>
<md-button ng-click="onExportDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
<md-button ng-click="onDeleteDashboard({event: $event})" <md-button ng-click="onDeleteDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'" ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button> class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
<md-content class="md-padding" layout="column"> <md-content class="md-padding" layout="column">
<md-input-container class="md-block" <md-input-container class="md-block"
ng-show="isAssignedToCustomer && dashboardScope === 'tenant'"> ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'">
<label translate>dashboard.assignedToCustomer</label> <label translate>dashboard.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled> <input ng-model="assignedCustomer.title" disabled>
</md-input-container> </md-input-container>
<div layout="column" ng-show="!isEdit && isPublic && (dashboardScope === 'customer' || dashboardScope === 'tenant')">
<tb-social-share-panel style="padding-bottom: 10px;"
share-title="{{ 'dashboard.socialshare-title' | translate:{dashboardTitle: dashboard.title} }}"
share-text="{{ 'dashboard.socialshare-text' | translate:{dashboardTitle: dashboard.title} }}"
share-link="{{ publicLink }}"
share-hash-tags="thingsboard, iot">
</tb-social-share-panel>
<div layout="row">
<md-input-container class="md-block" flex>
<label translate>dashboard.public-link</label>
<input ng-model="publicLink" disabled>
</md-input-container>
<md-button class="md-icon-button" style="margin-top: 14px;"
ngclipboard
data-clipboard-text="{{ publicLink }}"
ngclipboard-success="onPublicLinkCopied(e)">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<md-tooltip md-direction="top">
{{ 'dashboard.copy-public-link' | translate }}
</md-tooltip>
</md-button>
</div>
</div>
<fieldset ng-disabled="loading || !isEdit"> <fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>dashboard.title</label> <label translate>dashboard.title</label>

12
ui/src/app/dashboard/dashboard-settings.controller.js

@ -31,6 +31,18 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
vm.gridSettings.showTitle = true; vm.gridSettings.showTitle = true;
} }
if (angular.isUndefined(vm.gridSettings.showDevicesSelect)) {
vm.gridSettings.showDevicesSelect = true;
}
if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) {
vm.gridSettings.showDashboardTimewindow = true;
}
if (angular.isUndefined(vm.gridSettings.showDashboardExport)) {
vm.gridSettings.showDashboardExport = true;
}
vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)'; vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)';
vm.gridSettings.columns = vm.gridSettings.columns || 24; vm.gridSettings.columns = vm.gridSettings.columns || 24;

11
ui/src/app/dashboard/dashboard-settings.tpl.html

@ -48,6 +48,17 @@
md-color-history="false" md-color-history="false"
></div> ></div>
</div> </div>
<div layout="row" layout-align="start center">
<md-checkbox flex aria-label="{{ 'dashboard.display-device-selection' | translate }}"
ng-model="vm.gridSettings.showDevicesSelect">{{ 'dashboard.display-device-selection' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
ng-model="vm.gridSettings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
ng-model="vm.gridSettings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
</md-checkbox>
</div>
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>dashboard.columns-count</label> <label translate>dashboard.columns-count</label>
<input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10" <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"

89
ui/src/app/dashboard/dashboard.controller.js

@ -48,6 +48,8 @@ export default function DashboardController(types, widgetService, userService,
vm.isToolbarOpened = false; vm.isToolbarOpened = false;
vm.thingsboardVersion = THINGSBOARD_VERSION; //eslint-disable-line
vm.currentDashboardId = $stateParams.dashboardId; vm.currentDashboardId = $stateParams.dashboardId;
if ($stateParams.customerId) { if ($stateParams.customerId) {
vm.currentCustomerId = $stateParams.customerId; vm.currentCustomerId = $stateParams.customerId;
@ -86,6 +88,7 @@ export default function DashboardController(types, widgetService, userService,
vm.exportDashboard = exportDashboard; vm.exportDashboard = exportDashboard;
vm.exportWidget = exportWidget; vm.exportWidget = exportWidget;
vm.importWidget = importWidget; vm.importWidget = importWidget;
vm.isPublicUser = isPublicUser;
vm.isTenantAdmin = isTenantAdmin; vm.isTenantAdmin = isTenantAdmin;
vm.isSystemAdmin = isSystemAdmin; vm.isSystemAdmin = isSystemAdmin;
vm.loadDashboard = loadDashboard; vm.loadDashboard = loadDashboard;
@ -104,6 +107,9 @@ export default function DashboardController(types, widgetService, userService,
vm.onRevertWidgetEdit = onRevertWidgetEdit; vm.onRevertWidgetEdit = onRevertWidgetEdit;
vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType; vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
vm.displayTitle = displayTitle; vm.displayTitle = displayTitle;
vm.displayExport = displayExport;
vm.displayDashboardTimewindow = displayDashboardTimewindow;
vm.displayDevicesSelect = displayDevicesSelect;
vm.widgetsBundle; vm.widgetsBundle;
@ -273,6 +279,10 @@ export default function DashboardController(types, widgetService, userService,
vm.dashboardInitComplete = true; vm.dashboardInitComplete = true;
} }
function isPublicUser() {
return vm.user.isPublic === true;
}
function isTenantAdmin() { function isTenantAdmin() {
return vm.user.authority === 'TENANT_ADMIN'; return vm.user.authority === 'TENANT_ADMIN';
} }
@ -560,6 +570,33 @@ export default function DashboardController(types, widgetService, userService,
} }
} }
function displayExport() {
if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardExport)) {
return vm.dashboard.configuration.gridSettings.showDashboardExport;
} else {
return true;
}
}
function displayDashboardTimewindow() {
if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardTimewindow)) {
return vm.dashboard.configuration.gridSettings.showDashboardTimewindow;
} else {
return true;
}
}
function displayDevicesSelect() {
if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
angular.isDefined(vm.dashboard.configuration.gridSettings.showDevicesSelect)) {
return vm.dashboard.configuration.gridSettings.showDevicesSelect;
} else {
return true;
}
}
function onRevertWidgetEdit(widgetForm) { function onRevertWidgetEdit(widgetForm) {
if (widgetForm.$dirty) { if (widgetForm.$dirty) {
widgetForm.$setPristine(); widgetForm.$setPristine();
@ -617,22 +654,8 @@ export default function DashboardController(types, widgetService, userService,
sizeY: widgetTypeInfo.sizeY, sizeY: widgetTypeInfo.sizeY,
config: config config: config
}; };
$mdDialog.show({
controller: 'AddWidgetController', function addWidget(widget) {
controllerAs: 'vm',
templateUrl: addWidgetTemplate,
locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event,
onComplete: function () {
var w = angular.element($window);
w.triggerHandler('resize');
}
}).then(function (result) {
var widget = result.widget;
vm.aliasesInfo = result.aliasesInfo;
var columns = 24; var columns = 24;
if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) { if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
columns = vm.dashboard.configuration.gridSettings.columns; columns = vm.dashboard.configuration.gridSettings.columns;
@ -643,9 +666,37 @@ export default function DashboardController(types, widgetService, userService,
widget.sizeY *= ratio; widget.sizeY *= ratio;
} }
vm.widgets.push(widget); vm.widgets.push(widget);
}, function (rejection) { }
vm.aliasesInfo = rejection.aliasesInfo;
}); if (widgetTypeInfo.useCustomDatasources) {
addWidget(newWidget);
} else {
$mdDialog.show({
controller: 'AddWidgetController',
controllerAs: 'vm',
templateUrl: addWidgetTemplate,
locals: {
dashboard: vm.dashboard,
aliasesInfo: vm.aliasesInfo,
widget: newWidget,
widgetInfo: widgetTypeInfo
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event,
onComplete: function () {
var w = angular.element($window);
w.triggerHandler('resize');
}
}).then(function (result) {
var widget = result.widget;
vm.aliasesInfo = result.aliasesInfo;
addWidget(widget);
}, function (rejection) {
vm.aliasesInfo = rejection.aliasesInfo;
});
}
} }
); );
} }

19
ui/src/app/dashboard/dashboard.directive.js

@ -20,30 +20,44 @@ import dashboardFieldsetTemplate from './dashboard-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function DashboardDirective($compile, $templateCache, types, customerService) { export default function DashboardDirective($compile, $templateCache, $translate, types, toast, customerService, dashboardService) {
var linker = function (scope, element) { var linker = function (scope, element) {
var template = $templateCache.get(dashboardFieldsetTemplate); var template = $templateCache.get(dashboardFieldsetTemplate);
element.html(template); element.html(template);
scope.isAssignedToCustomer = false; scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null; scope.assignedCustomer = null;
scope.publicLink = null;
scope.$watch('dashboard', function(newVal) { scope.$watch('dashboard', function(newVal) {
if (newVal) { if (newVal) {
if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) { if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true; scope.isAssignedToCustomer = true;
customerService.getCustomer(scope.dashboard.customerId.id).then( customerService.getShortCustomerInfo(scope.dashboard.customerId.id).then(
function success(customer) { function success(customer) {
scope.assignedCustomer = customer; scope.assignedCustomer = customer;
scope.isPublic = customer.isPublic;
if (scope.isPublic) {
scope.publicLink = dashboardService.getPublicDashboardLink(scope.dashboard);
} else {
scope.publicLink = null;
}
} }
); );
} else { } else {
scope.isAssignedToCustomer = false; scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.publicLink = null;
scope.assignedCustomer = null; scope.assignedCustomer = null;
} }
} }
}); });
scope.onPublicLinkCopied = function() {
toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element(element).parent().parent(), 'bottom left');
};
$compile(element.contents())(scope); $compile(element.contents())(scope);
} }
return { return {
@ -55,6 +69,7 @@ export default function DashboardDirective($compile, $templateCache, types, cust
dashboardScope: '=', dashboardScope: '=',
theForm: '=', theForm: '=',
onAssignToCustomer: '&', onAssignToCustomer: '&',
onMakePublic: '&',
onUnassignFromCustomer: '&', onUnassignFromCustomer: '&',
onExportDashboard: '&', onExportDashboard: '&',
onDeleteDashboard: '&' onDeleteDashboard: '&'

2
ui/src/app/dashboard/dashboard.routes.js

@ -62,7 +62,7 @@ export default function DashboardRoutes($stateProvider) {
pageTitle: 'customer.dashboards' pageTitle: 'customer.dashboards'
}, },
ncyBreadcrumb: { ncyBreadcrumb: {
label: '{"icon": "dashboard", "label": "customer.dashboards"}' label: '{"icon": "dashboard", "label": "{{ vm.customerDashboardsTitle }}", "translate": "false"}'
} }
}) })
.state('home.dashboards.dashboard', { .state('home.dashboards.dashboard', {

15
ui/src/app/dashboard/dashboard.tpl.html

@ -47,18 +47,23 @@
aria-label="{{ 'fullscreen.fullscreen' | translate }}" aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button"> class="md-icon-button">
</md-button> </md-button>
<tb-user-menu ng-show="forceFullscreen" display-user-info="true"> <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
</tb-user-menu> </tb-user-menu>
<md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button" <md-button ng-show="vm.isEdit || vm.displayExport()"
aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
ng-click="vm.exportDashboard($event)"> ng-click="vm.exportDashboard($event)">
<md-tooltip md-direction="bottom"> <md-tooltip md-direction="bottom">
{{ 'dashboard.export' | translate }} {{ 'dashboard.export' | translate }}
</md-tooltip> </md-tooltip>
<md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon> <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
</md-button> </md-button>
<tb-timewindow is-toolbar direction="left" tooltip-direction="bottom" aggregation ng-model="vm.dashboardConfiguration.timewindow"> <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
is-toolbar
direction="left"
tooltip-direction="bottom" aggregation
ng-model="vm.dashboardConfiguration.timewindow">
</tb-timewindow> </tb-timewindow>
<tb-aliases-device-select ng-show="!vm.isEdit" <tb-aliases-device-select ng-show="!vm.isEdit && vm.displayDevicesSelect()"
tooltip-direction="bottom" tooltip-direction="bottom"
ng-model="vm.aliasesInfo.deviceAliases" ng-model="vm.aliasesInfo.deviceAliases"
device-aliases-info="vm.aliasesInfo.deviceAliasesInfo"> device-aliases-info="vm.aliasesInfo.deviceAliasesInfo">
@ -304,6 +309,6 @@
</section> </section>
</section> </section>
<section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}"> <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
<span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard</a></span> <span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ vm.thingsboardVersion }}</a></span>
</section> </section>
</md-content> </md-content>

181
ui/src/app/dashboard/dashboards.controller.js

@ -19,11 +19,56 @@ import addDashboardTemplate from './add-dashboard.tpl.html';
import dashboardCard from './dashboard-card.tpl.html'; import dashboardCard from './dashboard-card.tpl.html';
import assignToCustomerTemplate from './assign-to-customer.tpl.html'; import assignToCustomerTemplate from './assign-to-customer.tpl.html';
import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html'; import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html';
import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function DashboardsController(userService, dashboardService, customerService, importExport, types, $scope, $controller, export function MakeDashboardPublicDialogController($mdDialog, $translate, toast, dashboardService, dashboard) {
var vm = this;
vm.dashboard = dashboard;
vm.publicLink = dashboardService.getPublicDashboardLink(dashboard);
vm.onPublicLinkCopied = onPublicLinkCopied;
vm.close = close;
function onPublicLinkCopied(){
toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element('#make-dialog-public-content'), 'bottom left');
}
function close() {
$mdDialog.hide();
}
}
/*@ngInject*/
export function DashboardCardController(types) {
var vm = this;
vm.types = types;
vm.isAssignedToCustomer = function() {
if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' &&
vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
return true;
}
return false;
}
vm.isPublic = function() {
if (vm.item && vm.item.assignedCustomer && vm.parentCtl.dashboardsScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
return true;
}
return false;
}
}
/*@ngInject*/
export function DashboardsController(userService, dashboardService, customerService, importExport, types,
$state, $stateParams, $mdDialog, $document, $q, $translate) { $state, $stateParams, $mdDialog, $document, $q, $translate) {
var customerId = $stateParams.customerId; var customerId = $stateParams.customerId;
@ -58,6 +103,7 @@ export default function DashboardsController(userService, dashboardService, cust
clickItemFunc: openDashboard, clickItemFunc: openDashboard,
getItemTitleFunc: getDashboardTitle, getItemTitleFunc: getDashboardTitle,
itemCardController: 'DashboardCardController',
itemCardTemplateUrl: dashboardCard, itemCardTemplateUrl: dashboardCard,
parentCtl: vm, parentCtl: vm,
@ -90,6 +136,7 @@ export default function DashboardsController(userService, dashboardService, cust
vm.dashboardsScope = $state.$current.data.dashboardsType; vm.dashboardsScope = $state.$current.data.dashboardsType;
vm.assignToCustomer = assignToCustomer; vm.assignToCustomer = assignToCustomer;
vm.makePublic = makePublic;
vm.unassignFromCustomer = unassignFromCustomer; vm.unassignFromCustomer = unassignFromCustomer;
vm.exportDashboard = exportDashboard; vm.exportDashboard = exportDashboard;
@ -107,6 +154,17 @@ export default function DashboardsController(userService, dashboardService, cust
customerId = user.customerId; customerId = user.customerId;
} }
if (customerId) {
vm.customerDashboardsTitle = $translate.instant('customer.dashboards');
customerService.getShortCustomerInfo(customerId).then(
function success(info) {
if (info.isPublic) {
vm.customerDashboardsTitle = $translate.instant('customer.public-dashboards');
}
}
);
}
if (vm.dashboardsScope === 'tenant') { if (vm.dashboardsScope === 'tenant') {
fetchDashboardsFunction = function (pageLink) { fetchDashboardsFunction = function (pageLink) {
return dashboardService.getTenantDashboards(pageLink); return dashboardService.getTenantDashboards(pageLink);
@ -126,8 +184,21 @@ export default function DashboardsController(userService, dashboardService, cust
name: function() { $translate.instant('action.export') }, name: function() { $translate.instant('action.export') },
details: function() { return $translate.instant('dashboard.export') }, details: function() { return $translate.instant('dashboard.export') },
icon: "file_download" icon: "file_download"
}, });
{
dashboardActionsList.push({
onAction: function ($event, item) {
makePublic($event, item);
},
name: function() { return $translate.instant('action.share') },
details: function() { return $translate.instant('dashboard.make-public') },
icon: "share",
isEnabled: function(dashboard) {
return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
}
});
dashboardActionsList.push({
onAction: function ($event, item) { onAction: function ($event, item) {
assignToCustomer($event, [ item.id.id ]); assignToCustomer($event, [ item.id.id ]);
}, },
@ -137,19 +208,29 @@ export default function DashboardsController(userService, dashboardService, cust
isEnabled: function(dashboard) { isEnabled: function(dashboard) {
return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
} }
}, });
{ dashboardActionsList.push({
onAction: function ($event, item) { onAction: function ($event, item) {
unassignFromCustomer($event, item); unassignFromCustomer($event, item, false);
}, },
name: function() { return $translate.instant('action.unassign') }, name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('dashboard.unassign-from-customer') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') },
icon: "assignment_return", icon: "assignment_return",
isEnabled: function(dashboard) { isEnabled: function(dashboard) {
return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid; return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && !dashboard.assignedCustomer.isPublic;
} }
} });
); dashboardActionsList.push({
onAction: function ($event, item) {
unassignFromCustomer($event, item, true);
},
name: function() { return $translate.instant('action.make-private') },
details: function() { return $translate.instant('dashboard.make-private') },
icon: "reply",
isEnabled: function(dashboard) {
return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && dashboard.assignedCustomer.isPublic;
}
});
dashboardActionsList.push( dashboardActionsList.push(
{ {
@ -233,11 +314,27 @@ export default function DashboardsController(userService, dashboardService, cust
dashboardActionsList.push( dashboardActionsList.push(
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
unassignFromCustomer($event, item); unassignFromCustomer($event, item, false);
}, },
name: function() { return $translate.instant('action.unassign') }, name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('dashboard.unassign-from-customer') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') },
icon: "assignment_return" icon: "assignment_return",
isEnabled: function(dashboard) {
return dashboard && !dashboard.assignedCustomer.isPublic;
}
}
);
dashboardActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item, true);
},
name: function() { return $translate.instant('action.make-private') },
details: function() { return $translate.instant('dashboard.make-private') },
icon: "reply",
isEnabled: function(dashboard) {
return dashboard && dashboard.assignedCustomer.isPublic;
}
} }
); );
@ -307,7 +404,28 @@ export default function DashboardsController(userService, dashboardService, cust
} }
function saveDashboard(dashboard) { function saveDashboard(dashboard) {
return dashboardService.saveDashboard(dashboard); var deferred = $q.defer();
dashboardService.saveDashboard(dashboard).then(
function success(savedDashboard) {
var dashboards = [ savedDashboard ];
customerService.applyAssignedCustomersInfo(dashboards).then(
function success(items) {
if (items && items.length == 1) {
deferred.resolve(items[0]);
} else {
deferred.reject();
}
},
function fail() {
deferred.reject();
}
);
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
} }
function assignToCustomer($event, dashboardIds) { function assignToCustomer($event, dashboardIds) {
@ -389,15 +507,27 @@ export default function DashboardsController(userService, dashboardService, cust
assignToCustomer($event, dashboardIds); assignToCustomer($event, dashboardIds);
} }
function unassignFromCustomer($event, dashboard) { function unassignFromCustomer($event, dashboard, isPublic) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
var title;
var content;
var label;
if (isPublic) {
title = $translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title});
content = $translate.instant('dashboard.make-private-dashboard-text');
label = $translate.instant('dashboard.make-private-dashboard');
} else {
title = $translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title});
content = $translate.instant('dashboard.unassign-dashboard-text');
label = $translate.instant('dashboard.unassign-dashboard');
}
var confirm = $mdDialog.confirm() var confirm = $mdDialog.confirm()
.targetEvent($event) .targetEvent($event)
.title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title})) .title(title)
.htmlContent($translate.instant('dashboard.unassign-dashboard-text')) .htmlContent(content)
.ariaLabel($translate.instant('dashboard.unassign-dashboard')) .ariaLabel(label)
.cancel($translate.instant('action.no')) .cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes')); .ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () { $mdDialog.show(confirm).then(function () {
@ -407,6 +537,25 @@ export default function DashboardsController(userService, dashboardService, cust
}); });
} }
function makePublic($event, dashboard) {
if ($event) {
$event.stopPropagation();
}
dashboardService.makeDashboardPublic(dashboard.id.id).then(function success(dashboard) {
$mdDialog.show({
controller: 'MakeDashboardPublicDialogController',
controllerAs: 'vm',
templateUrl: makeDashboardPublicDialogTemplate,
locals: {dashboard: dashboard},
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: $event
}).then(function () {
vm.grid.refreshList();
});
});
}
function exportDashboard($event, dashboard) { function exportDashboard($event, dashboard) {
$event.stopPropagation(); $event.stopPropagation();
importExport.exportDashboard(dashboard.id.id); importExport.exportDashboard(dashboard.id.id);

3
ui/src/app/dashboard/dashboards.tpl.html

@ -24,7 +24,8 @@
dashboard-scope="vm.dashboardsScope" dashboard-scope="vm.dashboardsScope"
the-form="vm.grid.detailsForm" the-form="vm.grid.detailsForm"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)" on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)" on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details> on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
</tb-grid> </tb-grid>

1
ui/src/app/dashboard/edit-widget.directive.js

@ -37,6 +37,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
scope.widgetConfig = scope.widget.config; scope.widgetConfig = scope.widget.config;
var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
scope.isDataEnabled = !widgetInfo.useCustomDatasources;
if (!settingsSchema || settingsSchema === '') { if (!settingsSchema || settingsSchema === '') {
scope.settingsSchema = {}; scope.settingsSchema = {};
} else { } else {

1
ui/src/app/dashboard/edit-widget.tpl.html

@ -18,6 +18,7 @@
<fieldset ng-disabled="loading"> <fieldset ng-disabled="loading">
<tb-widget-config widget-type="widget.type" <tb-widget-config widget-type="widget.type"
ng-model="widgetConfig" ng-model="widgetConfig"
is-data-enabled="isDataEnabled"
widget-settings-schema="settingsSchema" widget-settings-schema="settingsSchema"
datakey-settings-schema="dataKeySettingsSchema" datakey-settings-schema="dataKeySettingsSchema"
device-aliases="aliasesInfo.deviceAliases" device-aliases="aliasesInfo.deviceAliases"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save