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/**
*.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).
## Sample Dashboards
## IoT use cases
[**Smart energy monitoring**](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github)
[![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**](https://thingsboard.io/smart-energy/)
[![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)
[![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://thingsboard.io/smart-farming/)
[![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)
[![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://thingsboard.io/fleet-tracking/)
[![Fleet tracking](https://cloud.githubusercontent.com/assets/8308069/24497169/3a1a61e0-1543-11e7-8d55-3c8a13f35634.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/)
## Getting Started

3
application/.gitignore

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

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<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.ToPluginRpcResponseDeviceMsg;
import java.util.HashMap;
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.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
@ -205,25 +199,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
refreshAttributes(msg);
Set<AttributeKey> keys = msg.getDeletedKeys();
if (attributeSubscriptions.size() > 0) {
ToDeviceMsg notification = null;
if (msg.isDeleted()) {
List<AttributeKey> sharedKeys = keys.stream()
List<AttributeKey> sharedKeys = msg.getDeletedKeys().stream()
.filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
.collect(Collectors.toList());
notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys));
} else {
List<AttributeKvEntry> attributes = keys.stream()
.filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
.map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
if (attributes.size() > 0) {
notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
} else {
logger.debug("[{}] No public server side attributes changed!", deviceId);
if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) {
List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues());
if (attributes.size() > 0) {
notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
} else {
logger.debug("[{}] No public server side attributes changed!", deviceId);
}
}
}
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 org.springframework.beans.factory.annotation.Autowired;
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.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.UsernamePasswordAuthenticationFilter;
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.service.security.auth.rest.RestAuthenticationProvider;
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.extractor.TokenExtractor;
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
import java.util.ArrayList;
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 DEVICE_API_ENTRY_POINT = "/api/v1/**";
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[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
@ -87,10 +94,18 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
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
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
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);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@ -135,6 +150,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().disable().frameOptions().disable()
.and()
.cors()
.and()
.csrf().disable()
.exceptionHandling()
@ -146,6 +163,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
.antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
.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(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.and()
@ -156,8 +174,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
.and()
.addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), 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.security.auth.jwt.RefreshTokenRepository;
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.JwtTokenFactory;
@ -167,7 +168,8 @@ public class AuthController extends BaseController {
String encodedPassword = passwordEncoder.encode(password);
UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
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 loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
@ -201,7 +203,8 @@ public class AuthController extends BaseController {
userCredentials.setResetToken(null);
userCredentials = userService.saveUserCredentials(userCredentials);
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 loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();

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

@ -15,6 +15,9 @@
*/
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.security.access.prepost.PreAuthorize;
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')")
@RequestMapping(value = "/customer", method = RequestMethod.POST)
@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.security.access.prepost.PreAuthorize;
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.DashboardInfo;
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')")
@RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
@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.security.access.prepost.PreAuthorize;
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.id.CustomerId;
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')")
@RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET)
@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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
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.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.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
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.RawAccessJwtToken;
import java.util.UUID;
@Component
public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private final JwtTokenFactory tokenFactory;
private final UserService userService;
private final CustomerService customerService;
@Autowired
public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) {
public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) {
this.userService = userService;
this.customerService = customerService;
this.tokenFactory = tokenFactory;
}
@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
Assert.notNull(authentication, "No authentication data provided");
RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
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) {
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");
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

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.stereotype.Component;
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.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.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.util.UUID;
@Component
public class RestAuthenticationProvider implements AuthenticationProvider {
private final BCryptPasswordEncoder encoder;
private final UserService userService;
private final CustomerService customerService;
@Autowired
public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) {
public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) {
this.userService = userService;
this.customerService = customerService;
this.encoder = encoder;
}
@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided");
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
Object principal = authentication.getPrincipal();
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);
if (user == null) {
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");
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());
}

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.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;
@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
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);
}

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 boolean enabled;
private UserPrincipal userPrincipal;
public SecurityUser() {
super();
@ -39,9 +40,10 @@ public class SecurityUser extends User {
super(id);
}
public SecurityUser(User user, boolean enabled) {
public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) {
super(user);
this.enabled = enabled;
this.userPrincipal = userPrincipal;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
@ -57,8 +59,16 @@ public class SecurityUser extends User {
return enabled;
}
public UserPrincipal getUserPrincipal() {
return userPrincipal;
}
public void setEnabled(boolean 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.config.JwtSettings;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.util.Arrays;
import java.util.List;
@ -43,6 +44,7 @@ public class JwtTokenFactory {
private static final String FIRST_NAME = "firstName";
private static final String LAST_NAME = "lastName";
private static final String ENABLED = "enabled";
private static final String IS_PUBLIC = "isPublic";
private static final String TENANT_ID = "tenantId";
private static final String CUSTOMER_ID = "customerId";
@ -63,12 +65,15 @@ public class JwtTokenFactory {
if (securityUser.getAuthority() == null)
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(USER_ID, securityUser.getId().getId().toString());
claims.put(FIRST_NAME, securityUser.getFirstName());
claims.put(LAST_NAME, securityUser.getLastName());
claims.put(ENABLED, securityUser.isEnabled());
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
if (securityUser.getTenantId() != null) {
claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
}
@ -104,6 +109,9 @@ public class JwtTokenFactory {
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
securityUser.setLastName(claims.get(LAST_NAME, String.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);
if (tenantId != null) {
securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
@ -123,9 +131,11 @@ public class JwtTokenFactory {
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(USER_ID, securityUser.getId().getId().toString());
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
String token = Jwts.builder()
.setClaims(claims)
@ -150,8 +160,10 @@ public class JwtTokenFactory {
if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
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.setEmail(subject);
securityUser.setUserPrincipal(principal);
return securityUser;
}

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

@ -188,3 +188,25 @@ cache:
updates:
# Enable/disable updates checking.
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
IF %PROCESSOR_ARCHITECTURE%==AMD64 GOTO CHECK_JAVA_64
IF %PROCESSOR_ARCHITECTURE%==x86 GOTO CHECK_JAVA_32
@ECHO Detecting Java version installed.
:CHECK_JAVA_64
@ECHO Detecting if it is 64 bit machine
@ -36,7 +33,6 @@ if defined ValueName (
)
IF NOT "%JRE_PATH2%" == "" GOTO JAVA_INSTALLED
IF "%JRE_PATH2%" == "" GOTO JAVA_NOT_INSTALLED
:CHECK_JAVA_32
@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++) {
Customer customer = new Customer();
customer.setTenantId(tenantId);
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title1+suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
customer.setTitle(title);
@ -299,7 +299,7 @@ public class CustomerControllerTest extends AbstractControllerTest {
for (int i=0;i<175;i++) {
Customer customer = new Customer();
customer.setTenantId(tenantId);
String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
String title = title2+suffix;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
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<>();
for (int i=0;i<134;i++) {
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;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
tenant.setTitle(title);
@ -159,7 +159,7 @@ public class TenantControllerTest extends AbstractControllerTest {
List<Tenant> tenantsTitle2 = new ArrayList<>();
for (int i=0;i<127;i++) {
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;
title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
tenant.setTitle(title);

2
common/data/pom.xml

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

2
common/message/pom.xml

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

2
common/pom.xml

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

2
common/transport/pom.xml

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

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<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;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.model.CustomerEntity;
import org.thingsboard.server.dao.model.DeviceEntity;
/**
* The Interface CustomerDao.
@ -44,5 +46,14 @@ public interface CustomerDao extends Dao<CustomerEntity> {
* @return the list of customer objects
*/
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;
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.List;
import java.util.Optional;
import java.util.UUID;
import com.datastax.driver.core.querybuilder.Select;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer;
@ -60,4 +67,13 @@ public class CustomerDaoImpl extends AbstractSearchTextDao<CustomerEntity> imple
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 void deleteCustomer(CustomerId customerId);
public Customer findOrCreatePublicCustomer(TenantId tenantId);
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.getData;
import java.io.IOException;
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 org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.Customer;
@ -46,6 +50,8 @@ import org.thingsboard.server.dao.service.Validator;
@Slf4j
public class CustomerServiceImpl implements CustomerService {
private static final String PUBLIC_CUSTOMER_TITLE = "Public";
@Autowired
private CustomerDao customerDao;
@ -80,7 +86,7 @@ public class CustomerServiceImpl implements CustomerService {
@Override
public void deleteCustomer(CustomerId customerId) {
log.trace("Executing deleteCustomer [{}]", customerId);
Validator.validateId(customerId, "Incorrect tenantId " + customerId);
Validator.validateId(customerId, "Incorrect customerId " + customerId);
Customer customer = findCustomerById(customerId);
if (customer == null) {
throw new IncorrectParameterException("Unable to delete non-existent customer.");
@ -91,6 +97,27 @@ public class CustomerServiceImpl implements CustomerService {
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
public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) {
log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService {
private DataValidator<Customer> customerValidator =
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
protected void validateDataImpl(Customer customer) {
if (StringUtils.isEmpty(customer.getTitle())) {
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())) {
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_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.

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

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

2
extensions-api/pom.xml

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

2
extensions-core/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<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;
import com.sun.javafx.collections.MappingChange;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DataConstants;
@ -292,7 +291,6 @@ public class SubscriptionManager {
} else {
log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress);
subscriptionIterator.remove();
//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) {
Map<String, List<TsData>> result = new LinkedHashMap<>();
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));
}

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.core.action.rpc.RpcPluginAction;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
/**
@ -51,7 +51,7 @@ public class TimePlugin extends AbstractPlugin<TimePluginConfiguration> implemen
String reply;
if (!StringUtils.isEmpty(format)) {
reply = "\"" + formatter.format(LocalDateTime.now()) + "\"";
reply = "\"" + formatter.format(ZonedDateTime.now()) + "\"";
} else {
reply = Long.toString(System.currentTimeMillis());
}

2
extensions/extension-kafka/pom.xml

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

2
extensions/extension-rabbitmq/pom.xml

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

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

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<artifactId>extensions</artifactId>
</parent>
<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)));
}
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();
}

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;
import lombok.Data;
import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties;
import java.util.List;
@Data
public class RestApiCallPluginConfiguration {
@ -27,4 +30,6 @@ public class RestApiCallPluginConfiguration {
private String userName;
private String password;
private List<KeyValuePluginProperties> headers;
}

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

@ -30,6 +30,24 @@
"password": {
"title": "Password",
"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": [
@ -62,6 +80,7 @@
{
"key": "password",
"type": "password"
}
},
"headers"
]
}

2
extensions/pom.xml

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

2
pom.xml

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

38
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
@ -54,40 +54,4 @@
</dependency>
</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>

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.RestTemplate;
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.security.DeviceCredentials;
@ -76,6 +77,12 @@ public class RestClient implements ClientHttpRequestInterceptor {
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) {
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>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

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

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.2.2</version>
<version>1.2.3-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<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) {
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);
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> {
private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
private final SessionMsgProcessor processor;
private final DeviceService deviceService;
private final DeviceAuthService authService;
@ -63,7 +65,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
sslHandler = sslHandlerProvider.getSslHandler();
pipeline.addLast(sslHandler);
}
pipeline.addLast("decoder", new MqttDecoder());
pipeline.addLast("decoder", new MqttDecoder(MAX_PAYLOAD_SIZE));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
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();
String deviceName = checkDeviceConnected(jsonObj.get("device").getAsString());
Integer requestId = jsonObj.get("id").getAsInt();
String data = jsonObj.get("data").getAsString();
String data = jsonObj.get("data").toString();
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));

2
transport/pom.xml

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

4
ui/package.json

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

2
ui/pom.xml

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

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

@ -18,11 +18,14 @@ export default angular.module('thingsboard.api.customer', [])
.name;
/*@ngInject*/
function CustomerService($http, $q) {
function CustomerService($http, $q, types) {
var service = {
getCustomers: getCustomers,
getCustomer: getCustomer,
getShortCustomerInfo: getShortCustomerInfo,
applyAssignedCustomersInfo: applyAssignedCustomersInfo,
applyAssignedCustomerInfo: applyAssignedCustomerInfo,
deleteCustomer: deleteCustomer,
saveCustomer: saveCustomer
}
@ -60,6 +63,88 @@ function CustomerService($http, $q) {
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) {
var deferred = $q.defer();
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;
/*@ngInject*/
function DashboardService($http, $q) {
function DashboardService($http, $q, $location, customerService) {
var service = {
assignDashboardToCustomer: assignDashboardToCustomer,
@ -27,7 +27,9 @@ function DashboardService($http, $q) {
getTenantDashboards: getTenantDashboards,
deleteDashboard: deleteDashboard,
saveDashboard: saveDashboard,
unassignDashboardFromCustomer: unassignDashboardFromCustomer
unassignDashboardFromCustomer: unassignDashboardFromCustomer,
makeDashboardPublic: makeDashboardPublic,
getPublicDashboardLink: getPublicDashboardLink
}
return service;
@ -45,7 +47,15 @@ function DashboardService($http, $q) {
url += '&textOffset=' + pageLink.textOffset;
}
$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() {
deferred.reject();
});
@ -65,7 +75,15 @@ function DashboardService($http, $q) {
url += '&textOffset=' + pageLink.textOffset;
}
$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() {
deferred.reject();
});
@ -92,8 +110,8 @@ function DashboardService($http, $q) {
var url = '/api/dashboard/' + dashboardId;
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -103,8 +121,8 @@ function DashboardService($http, $q) {
var url = '/api/dashboard';
$http.post(url, dashboard).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -114,8 +132,8 @@ function DashboardService($http, $q) {
var url = '/api/dashboard/' + dashboardId;
$http.delete(url).then(function success() {
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -123,10 +141,10 @@ function DashboardService($http, $q) {
function assignDashboardToCustomer(customerId, dashboardId) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId;
$http.post(url, null).then(function success() {
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
$http.post(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -134,12 +152,33 @@ function DashboardService($http, $q) {
function unassignDashboardFromCustomer(dashboardId) {
var deferred = $q.defer();
var url = '/api/customer/dashboard/' + dashboardId;
$http.delete(url).then(function success() {
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
$http.delete(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
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;
}
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 = {
datasourceType: datasource.type,
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);
}
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;
/*@ngInject*/
function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
function DeviceService($http, $q, $filter, userService, customerService, telemetryWebsocketService, types) {
var deviceAttributesSubscriptionMap = {};
@ -33,6 +33,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
getDevices: getDevices,
processDeviceAliases: processDeviceAliases,
checkDeviceAlias: checkDeviceAlias,
fetchAliasDeviceByNameFilter: fetchAliasDeviceByNameFilter,
getDeviceCredentials: getDeviceCredentials,
getDeviceKeys: getDeviceKeys,
getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@ -40,6 +41,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
saveDevice: saveDevice,
saveDeviceCredentials: saveDeviceCredentials,
unassignDeviceFromCustomer: unassignDeviceFromCustomer,
makeDevicePublic: makeDevicePublic,
getDeviceAttributes: getDeviceAttributes,
subscribeForDeviceAttributes: subscribeForDeviceAttributes,
unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes,
@ -51,7 +53,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
return service;
function getTenantDevices(pageLink) {
function getTenantDevices(pageLink, applyCustomersInfo, config) {
var deferred = $q.defer();
var url = '/api/tenant/devices?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
@ -63,15 +65,27 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
$http.get(url, config).then(function success(response) {
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() {
deferred.reject();
});
return deferred.promise;
}
function getCustomerDevices(customerId, pageLink) {
function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
@ -83,18 +97,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
$http.get(url, config).then(function success(response) {
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() {
deferred.reject();
});
return deferred.promise;
}
function getDevice(deviceId, ignoreErrors) {
function getDevice(deviceId, ignoreErrors, config) {
var deferred = $q.defer();
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);
}, function fail(response) {
deferred.reject(response.data);
@ -102,7 +133,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
return deferred.promise;
}
function getDevices(deviceIds) {
function getDevices(deviceIds, config) {
var deferred = $q.defer();
var ids = '';
for (var i=0;i<deviceIds.length;i++) {
@ -112,7 +143,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
ids += deviceIds[i];
}
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;
devices.sort(function (device1, device2) {
var id1 = device1.id.id;
@ -128,16 +159,16 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
return deferred.promise;
}
function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
function fetchAliasDeviceByNameFilter(deviceNameFilter, limit, applyCustomersInfo, config) {
var deferred = $q.defer();
var user = userService.getCurrentUser();
var promise;
var pageLink = {limit: limit, textSearch: deviceNameFilter};
if (user.authority === 'CUSTOMER_USER') {
var customerId = user.customerId;
promise = getCustomerDevices(customerId, pageLink);
promise = getCustomerDevices(customerId, pageLink, applyCustomersInfo, config);
} else {
promise = getTenantDevices(pageLink);
promise = getTenantDevices(pageLink, applyCustomersInfo, config);
}
promise.then(
function success(result) {
@ -194,7 +225,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var deviceFilter = deviceAlias.deviceFilter;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
fetchAliasDeviceByNameFilter(deviceNameFilter, 100, false).then(
function(devices) {
if (devices && devices != null) {
var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
@ -276,7 +307,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var promise;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1, false);
} else {
var deviceList = deviceFilter.deviceList;
promise = getDevices(deviceList);
@ -301,8 +332,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device';
$http.post(url, device).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -312,8 +343,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/' + deviceId;
$http.delete(url).then(function success() {
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -323,8 +354,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/' + deviceId + '/credentials';
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -334,8 +365,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/credentials';
$http.post(url, deviceCredentials).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -343,10 +374,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
function assignDeviceToCustomer(customerId, deviceId) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/device/' + deviceId;
$http.post(url, null).then(function success() {
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
$http.post(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
@ -354,10 +385,21 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
function unassignDeviceFromCustomer(deviceId) {
var deferred = $q.defer();
var url = '/api/customer/device/' + deviceId;
$http.delete(url).then(function success() {
deferred.resolve();
}, function fail(response) {
deferred.reject(response.data);
$http.delete(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
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;
}
@ -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 subscriptionId = deviceId + attributeScope;
var das = deviceAttributesSubscriptionMap[subscriptionId];
@ -445,7 +487,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
}
} else {
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);
}, function fail() {
deferred.reject();

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

@ -25,6 +25,7 @@ function LoginService($http, $q) {
changePassword: changePassword,
hasUser: hasUser,
login: login,
publicLogin: publicLogin,
resetPassword: resetPassword,
sendResetPasswordLink: sendResetPasswordLink,
}
@ -49,6 +50,19 @@ function LoginService($http, $q) {
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) {
var deferred = $q.defer();
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,
reconnectTimer;
var port = location.port;
if (location.protocol === "https:") {
if (!port) {
port = "443";
}
telemetryUri = "wss:";
} else {
if (!port) {
port = "80";
}
telemetryUri = "ws:";
}
telemetryUri += "//" + location.hostname + ":" + location.port;
telemetryUri += "//" + location.hostname + ":" + port;
telemetryUri += "/api/ws/plugins/telemetry";
var service = {
subscribe: subscribe,
unsubscribe: unsubscribe

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

@ -22,9 +22,10 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
.name;
/*@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,
currentUserDetails = null,
lastPublicDashboardId = null,
allowedDashboardIds = [],
userLoaded = false;
@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
var service = {
deleteUser: deleteUser,
getAuthority: getAuthority,
isPublic: isPublic,
getPublicId: getPublicId,
parsePublicId: parsePublicId,
isAuthenticated: isAuthenticated,
getCurrentUser: getCurrentUser,
getCustomerUsers: getCustomerUsers,
@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
updateAuthorizationHeader: updateAuthorizationHeader,
gotoDefaultPlace: gotoDefaultPlace,
forceDefaultPlace: forceDefaultPlace,
logout: logout
updateLastPublicDashboardId: updateLastPublicDashboardId,
logout: logout,
reloadUser: reloadUser
}
loadUser(true).then(function success() {
notifyUserLoaded();
}, function fail() {
notifyUserLoaded();
});
reloadUser();
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 tokenData = jwtHelper.decodeToken(token);
var issuedAt = tokenData.iat;
@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
valid = true;
}
}
if (!valid) {
if (!valid && notify) {
$rootScope.$broadcast('unauthenticated');
}
}
@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
currentUser = null;
currentUserDetails = null;
lastPublicDashboardId = null;
allowedDashboardIds = [];
if (!jwtToken) {
clearTokenData();
@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
$rootScope.$broadcast('unauthenticated', doLogout);
}
} else {
updateAndValidateToken(jwtToken, 'jwt_token');
updateAndValidateToken(refreshToken, 'refresh_token');
updateAndValidateToken(jwtToken, 'jwt_token', true);
updateAndValidateToken(refreshToken, 'refresh_token', true);
if (notify) {
loadUser(false).then(function success() {
$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() {
return userLoaded;
}
function loadUser(doTokenRefresh) {
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() {
var jwtToken = store.get('jwt_token');
currentUser = jwtHelper.decodeToken(jwtToken);
@ -228,29 +285,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
} else if (currentUser) {
currentUser.authority = "ANONYMOUS";
}
if (currentUser.userId) {
if (currentUser.isPublic) {
$rootScope.forceFullscreen = true;
fetchAllowedDashboardIds();
} else if (currentUser.userId) {
getUser(currentUser.userId).then(
function success(user) {
currentUserDetails = user;
$rootScope.forceFullscreen = false;
if (currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardFullscreen) {
$rootScope.forceFullscreen = currentUserDetails.additionalInfo.defaultDashboardFullscreen === true;
if (userForceFullscreen()) {
$rootScope.forceFullscreen = true;
}
if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
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();
}
);
fetchAllowedDashboardIds();
} else {
deferred.resolve();
}
@ -265,6 +312,23 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
}, function fail() {
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 {
deferred.resolve();
}
@ -373,17 +437,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) {
if (currentUser.authority === 'CUSTOMER_USER') {
if (currentUserDetails &&
currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardId) {
if ($rootScope.forceFullscreen) {
if (to.name === 'home.profile') {
return false;
} else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) {
if (to.name === 'home.profile') {
if (userHasProfile()) {
return false;
} else {
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()) {
var place = 'home.links';
if (currentUser.authority === 'CUSTOMER_USER') {
if (currentUserDetails &&
currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardId) {
if (userHasDefaultDashboard()) {
place = 'home.dashboards.dashboard';
params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
} else if (isPublic()) {
place = 'home.dashboards.dashboard';
params = {dashboardId: lastPublicDashboardId};
}
} else if (currentUser.authority === 'SYS_ADMIN') {
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 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 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 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)
.name;
@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
' }\n\n' +
' self.useCustomDatasources = function() {\n\n' +
' }\n\n' +
' self.onResize = function() {\n\n' +
' }\n\n' +
@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
}
if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) {
result.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
} else {
result.useCustomDatasources = false;
}
return result;
} catch (e) {
utils.processWidgetException(e);
@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if (widgetType.dataKeySettingsSchema) {
widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
}
widgetInfo.useCustomDatasources = widgetType.useCustomDatasources;
putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
deferred.resolve(widgetInfo);

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

@ -16,6 +16,7 @@
import injectTapEventPlugin from 'react-tap-event-plugin';
import UrlHandler from './url.handler';
import addLocaleKorean from './locale/locale.constant-ko';
import addLocaleChinese from './locale/locale.constant-zh';
/* eslint-disable import/no-unresolved, import/default */
@ -50,11 +51,15 @@ export default function AppConfig($provide,
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
addLocaleKorean(locales);
addLocaleChinese(locales);
var $window = angular.injector(['ng']).get('$window');
var lang = $window.navigator.language || $window.navigator.userLanguage;
if (lang === 'ko') {
$translateProvider.useSanitizeValueStrategy(null);
$translateProvider.preferredLanguage('ko_KR');
} else if (lang === 'zh') {
$translateProvider.useSanitizeValueStrategy(null);
$translateProvider.preferredLanguage('zh_CN');
}
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 ngMdIcons from 'angular-material-icons';
import ngCookies from 'angular-cookies';
import angularSocialshare from 'angular-socialshare';
import 'angular-translate';
import 'angular-translate-loader-static-files';
import 'angular-translate-storage-local';
@ -82,6 +83,7 @@ angular.module('thingsboard', [
ngMaterial,
ngMdIcons,
ngCookies,
angularSocialshare,
'pascalprecht.translate',
'mdColorPicker',
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) {
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.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)) {
evt.preventDefault();
gotoDefaultPlace(params);
@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
}
}
} else {
if (to.module === 'private') {
if (publicId && publicId.length > 0) {
evt.preventDefault();
reloadUserFromPublicId();
} else if (to.module === 'private') {
evt.preventDefault();
if (to.url === '/home' || to.url === '/') {
$state.go('login', params);
@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
}
} else {
evt.preventDefault();
if ($rootScope.userLoadedHandle) {
$rootScope.userLoadedHandle();
}
$rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
$rootScope.userLoadedHandle();
$state.go(to.name, params);
});
waitForUserLoaded();
}
})
$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)) {
$translate(to.data.pageTitle).then(function (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;
/*@ngInject*/
function Utils($mdColorPalette, $rootScope, $window, types) {
function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
var predefinedFunctions = {},
predefinedFunctionsList = [],
@ -104,7 +104,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
parseException: parseException,
processWidgetException: processWidgetException,
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities
filterSearchTextEntities: filterSearchTextEntities,
guid: guid,
createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo,
isLocalUrl: isLocalUrl
}
return service;
@ -276,4 +279,165 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
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 highlightedWidget = null;
var selectedWidget = null;
var mouseDownWidget = -1;
var widgetMouseMoved = false;
var gridsterParent = $('#gridster-parent', $element);
var gridsterElement = angular.element($('#gridster-child', gridsterParent));
@ -152,9 +150,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
vm.resetHighlight = resetHighlight;
vm.onWidgetFullscreenChanged = onWidgetFullscreenChanged;
vm.widgetMouseDown = widgetMouseDown;
vm.widgetMouseMove = widgetMouseMove;
vm.widgetMouseUp = widgetMouseUp;
vm.widgetClicked = widgetClicked;
vm.widgetSizeX = widgetSizeX;
vm.widgetSizeY = widgetSizeY;
@ -184,23 +182,23 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
vm.dashboardTimewindowApi = {
onResetTimewindow: function() {
if (vm.originalDashboardTimewindow) {
vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
vm.originalDashboardTimewindow = null;
}
$timeout(function() {
if (vm.originalDashboardTimewindow) {
vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
vm.originalDashboardTimewindow = null;
}
}, 0);
},
onUpdateTimewindow: function(startTimeMs, endTimeMs) {
if (!vm.originalDashboardTimewindow) {
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
/*gridsterParent.scroll(function () {
updateVisibleRect();
@ -350,7 +348,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function loadDashboard() {
resetWidgetClick();
$timeout(function () {
if (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];
}
function resetWidgetClick () {
mouseDownWidget = -1;
widgetMouseMoved = false;
}
function onWidgetFullscreenChanged(expanded, widget) {
vm.isWidgetExpanded = expanded;
$scope.$broadcast('onWidgetFullscreenChanged', vm.isWidgetExpanded, widget);
}
function widgetMouseDown ($event, widget) {
mouseDownWidget = widget;
widgetMouseMoved = false;
if (vm.onWidgetMouseDown) {
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) {
if ($event) {
$event.stopPropagation();
@ -518,7 +490,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function editWidget ($event, widget) {
resetWidgetClick();
if ($event) {
$event.stopPropagation();
}
@ -528,7 +499,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function exportWidget ($event, widget) {
resetWidgetClick();
if ($event) {
$event.stopPropagation();
}
@ -538,7 +508,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function removeWidget($event, widget) {
resetWidgetClick();
if ($event) {
$event.stopPropagation();
}
@ -548,27 +517,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
}
function highlightWidget(widget, delay) {
highlightedMode = true;
highlightedWidget = widget;
if (vm.gridster) {
var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)];
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);
}
if (!highlightedMode || highlightedWidget != widget) {
highlightedMode = true;
highlightedWidget = widget;
scrollToWidget(widget, delay);
}
}
function selectWidget(widget, delay) {
selectedWidget = widget;
if (selectedWidget != widget) {
selectedWidget = widget;
scrollToWidget(widget, delay);
}
}
function scrollToWidget(widget, delay) {
if (vm.gridster) {
var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)];
if (item) {

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

@ -37,9 +37,7 @@
'tb-not-highlighted': vm.isNotHighlighted(widget),
'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}"
tb-mousedown="vm.widgetMouseDown($event, widget)"
tb-mousemove="vm.widgetMouseMove($event, widget)"
tb-mouseup="vm.widgetMouseUp($event, widget)"
ng-click=""
ng-click="vm.widgetClicked($event, widget)"
tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)"
ng-style="{cursor: 'pointer',
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>
<tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</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"
ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
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();
deviceService.getTenantDevices(pageLink).then(function success(result) {
deviceService.getTenantDevices(pageLink, false).then(function success(result) {
deferred.resolve(result.data);
}, function fail() {
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])
.directive('tbGrid', Grid)
.controller('ItemCardController', ItemCardController)
.directive('tbGridCardContent', GridCardContent)
.filter('range', RangeFilter)
.name;
@ -44,14 +45,52 @@ function RangeFilter() {
}
/*@ngInject*/
function GridCardContent($compile) {
function ItemCardController() {
var vm = this; //eslint-disable-line
}
/*@ngInject*/
function GridCardContent($compile, $controller) {
var linker = function(scope, element) {
var controllerInstance = null;
scope.$watch('itemTemplate',
function(value) {
element.html(value);
$compile(element.contents())(scope);
function() {
initContent();
}
);
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 {
@ -61,6 +100,7 @@ function GridCardContent($compile) {
parentCtl: "=parentCtl",
gridCtl: "=gridCtl",
itemTemplate: "=itemTemplate",
itemController: "=itemController",
item: "=item"
}
};
@ -171,26 +211,31 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
vm.items.pending = true;
promise.then(
function success(items) {
vm.items.data = vm.items.data.concat(items.data);
var startIndex = vm.items.data.length - items.data.length;
var endIndex = vm.items.data.length;
for (var i = startIndex; i < endIndex; i++) {
var item = vm.items.data[i];
item.index = i;
var row = Math.floor(i / vm.columns);
var itemRow = vm.items.rowData[row];
if (!itemRow) {
itemRow = [];
vm.items.rowData.push(itemRow);
if (vm.items.reloadPending) {
vm.items.pending = false;
reload();
} else {
vm.items.data = vm.items.data.concat(items.data);
var startIndex = vm.items.data.length - items.data.length;
var endIndex = vm.items.data.length;
for (var i = startIndex; i < endIndex; i++) {
var item = vm.items.data[i];
item.index = i;
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;
if (vm.items.hasNext) {
vm.items.nextPageLink.limit = pageSize;
vm.items.nextPageLink = items.nextPageLink;
vm.items.hasNext = items.hasNext;
if (vm.items.hasNext) {
vm.items.nextPageLink.limit = pageSize;
}
vm.items.pending = false;
}
vm.items.pending = false;
},
function fail() {
vm.items.hasNext = false;
@ -291,6 +336,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
} else if (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;
@ -380,26 +430,35 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
}
$scope.$on('searchTextUpdated', function () {
vm.items = {
data: [],
rowData: [],
nextPageLink: {
limit: pageSize,
textSearch: $scope.searchConfig.searchText
},
selections: {},
selectedCount: 0,
hasNext: true,
pending: false
};
vm.detailsConfig.isDetailsOpen = false;
vm.itemRows.getItemAtIndex(pageSize);
reload();
});
vm.onGridInited(vm);
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() {
$state.go($state.current, vm.refreshParamsFunc(), {reload: true});
}

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

@ -43,13 +43,13 @@
</md-card-title>
</section>
<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-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"
ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
<md-tooltip md-direction="top">
{{ action.details() }}
{{ action.details( rowItem[n] ) }}
</md-tooltip>
<ng-md-icon icon="{{action.icon}}"></ng-md-icon>
</md-button>
@ -62,7 +62,7 @@
</div>
<tb-details-sidenav
header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}"
header-subtitle="{{vm.itemDetailsText()}}"
header-subtitle="{{vm.itemDetailsText(vm.operatingItem())}}"
is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
is-open="vm.detailsConfig.isDetailsOpen"
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.legendConfig.position === types.position.top.value;
scope.$on('legendDataUpdated', function (event, apply) {
if (apply) {
scope.$digest();
}
});
scope.toggleHideData = function(index) {
scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
scope.$emit('legendDataHiddenChanged', index);
}
$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;
}
if (angular.isUndefined(scope.isDataEnabled)) {
scope.isDataEnabled = true;
}
scope.currentSettingsSchema = {};
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) ?
ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
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) {
scope.datasources.splice(0, scope.datasources.length);
} else {
@ -119,7 +124,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
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) {
var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
if (scope.deviceAliases[aliasId]) {
@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
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;
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;
ngModelCtrl.$setValidity('datasources', valid);
}
@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.$watch('datasources', function () {
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;
if (value.datasources) {
value.datasources.splice(0, value.datasources.length);
@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}, true);
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;
if (scope.targetDeviceAlias.value) {
value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
@ -359,6 +364,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
require: "^ngModel",
scope: {
forceExpandDatasources: '=?',
isDataEnabled: '=?',
widgetType: '=',
widgetSettingsSchema: '=',
datakeySettingsSchema: '=',

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

@ -31,7 +31,7 @@
</section>
</div>
<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-header>
{{ 'widget-config.datasources' | translate }}
@ -96,7 +96,7 @@
</v-pane>
</v-accordion>
<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-header>
{{ 'widget-config.target-device' | translate }}

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

@ -15,6 +15,7 @@
*/
import $ from 'jquery';
import 'javascript-detect-element-resize/detect-element-resize';
import Subscription from '../api/subscription';
/* eslint-disable angular/angularelement */
@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
$scope.rpcErrorText = null;
$scope.rpcEnabled = false;
$scope.executingRpcRequest = false;
$scope.executingPromises = [];
var gridsterItemInited = false;
var datasourceListeners = [];
var targetDeviceAliasId = null;
var targetDeviceId = null;
var originalTimewindow = null;
var subscriptionTimewindow = null;
var cafs = {};
var varsRegex = /\$\{([^\}]*)\}/g;
/*
* data = array of datasourceData
* datasourceData = {
@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
* dataKey, { name, config }
* data = array of [time, value]
* }
*
*
*/
var widgetContext = {
@ -71,22 +62,70 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
settings: widget.config.settings,
units: widget.config.units || '',
decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
datasources: angular.copy(widget.config.datasources),
data: [],
hiddenData: [],
timeWindow: {
stDiff: stDiff
},
subscriptions: {},
defaultSubscription: null,
timewindowFunctions: {
onUpdateTimewindow: onUpdateTimewindow,
onResetTimewindow: onResetTimewindow
onUpdateTimewindow: function(startTimeMs, endTimeMs) {
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: {
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) {
return sendCommand(false, method, params, timeout);
if (widgetContext.defaultSubscription) {
return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout);
}
return null;
}
},
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;
vm.useCustomDatasources = false;
try {
widgetTypeInstance = new widgetType(widgetContext);
} catch (e) {
@ -119,19 +178,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
if (!widgetTypeInstance.onDestroy) {
widgetTypeInstance.onDestroy = function() {};
}
//var bounds = {top: 0, left: 0, bottom: 0, right: 0};
//TODO: widgets visibility
/*var visible = false;*/
$scope.clearRpcError = function() {
$scope.rpcRejection = null;
$scope.rpcErrorText = null;
if (widgetTypeInstance.useCustomDatasources) {
vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
}
vm.gridsterItemInitialized = gridsterItemInitialized;
//TODO: widgets visibility
//var bounds = {top: 0, left: 0, bottom: 0, right: 0};
/*var visible = false;*/
/*vm.visibleRectChanged = visibleRectChanged;
function visibleRectChanged(newVisibleRect) {
@ -139,23 +193,216 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
updateVisibility();
}*/
$scope.clearRpcError = function() {
if (widgetContext.defaultSubscription) {
widgetContext.defaultSubscription.clearRpcError();
}
}
vm.gridsterItemInitialized = gridsterItemInitialized;
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() {
if ($scope.loadingData === true) {
function defaultComponentsOptions(options) {
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;
}
if (subscription) {
widgetContext.defaultSubscription = subscription;
}
}
function notifyDataLoading() {
if ($scope.loadingData === false) {
$scope.loadingData = true;
function initialize() {
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() {
@ -166,42 +413,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} catch (e) {
handleWidgetException(e);
}
if (widgetContext.dataUpdatePending) {
widgetContext.dataUpdatePending = false;
onDataUpdated();
if (!vm.useCustomDatasources && widgetContext.defaultSubscription) {
widgetContext.defaultSubscription.subscribe();
}
}
}
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() {
var width = widgetContext.$containerParent.width();
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() {
unsubscribe();
for (var id in widgetContext.subscriptions) {
var subscription = widgetContext.subscriptions[id];
subscription.destroy();
}
widgetContext.subscriptions = [];
if (widgetContext.inited) {
widgetContext.inited = false;
widgetContext.dataUpdatePending = false;
for (var cafId in cafs) {
if (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
/*function updateVisibility(forceRedraw) {
if (visibleRect) {
@ -583,285 +587,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
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 */

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

@ -15,4 +15,4 @@
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.
-->
<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="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">
<fieldset ng-disabled="loading || !isEdit">
<fieldset ng-show="!isPublic" ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
<label translate>customer.title</label>
<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') },
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) {
openCustomerDevices($event, item);
},
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"
},
{
@ -45,7 +54,13 @@ export default function CustomerController(customerService, $state, $stateParams
openCustomerDashboards($event, item);
},
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"
},
{
@ -54,7 +69,10 @@ export default function CustomerController(customerService, $state, $stateParams
},
name: function() { return $translate.instant('action.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') },
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) {

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

@ -24,7 +24,21 @@ export default function CustomerDirective($compile, $templateCache) {
var linker = function (scope, element) {
var template = $templateCache.get(customerFieldsetTemplate);
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);
}
return {
restrict: "E",

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

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

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

@ -15,25 +15,50 @@
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})"
ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer"
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"
class="md-raised md-primary">{{ '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>
class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }}</md-button>
<md-button ng-click="onDeleteDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
<md-content class="md-padding" layout="column">
<md-input-container class="md-block"
ng-show="isAssignedToCustomer && dashboardScope === 'tenant'">
ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'">
<label translate>dashboard.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled>
</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">
<md-input-container class="md-block">
<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;
}
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.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)';
vm.gridSettings.columns = vm.gridSettings.columns || 24;

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

@ -48,6 +48,17 @@
md-color-history="false"
></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">
<label translate>dashboard.columns-count</label>
<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.thingsboardVersion = THINGSBOARD_VERSION; //eslint-disable-line
vm.currentDashboardId = $stateParams.dashboardId;
if ($stateParams.customerId) {
vm.currentCustomerId = $stateParams.customerId;
@ -86,6 +88,7 @@ export default function DashboardController(types, widgetService, userService,
vm.exportDashboard = exportDashboard;
vm.exportWidget = exportWidget;
vm.importWidget = importWidget;
vm.isPublicUser = isPublicUser;
vm.isTenantAdmin = isTenantAdmin;
vm.isSystemAdmin = isSystemAdmin;
vm.loadDashboard = loadDashboard;
@ -104,6 +107,9 @@ export default function DashboardController(types, widgetService, userService,
vm.onRevertWidgetEdit = onRevertWidgetEdit;
vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
vm.displayTitle = displayTitle;
vm.displayExport = displayExport;
vm.displayDashboardTimewindow = displayDashboardTimewindow;
vm.displayDevicesSelect = displayDevicesSelect;
vm.widgetsBundle;
@ -273,6 +279,10 @@ export default function DashboardController(types, widgetService, userService,
vm.dashboardInitComplete = true;
}
function isPublicUser() {
return vm.user.isPublic === true;
}
function isTenantAdmin() {
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) {
if (widgetForm.$dirty) {
widgetForm.$setPristine();
@ -617,22 +654,8 @@ export default function DashboardController(types, widgetService, userService,
sizeY: widgetTypeInfo.sizeY,
config: config
};
$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;
function addWidget(widget) {
var columns = 24;
if (vm.dashboard.configuration.gridSettings && 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;
}
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 */
/*@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 template = $templateCache.get(dashboardFieldsetTemplate);
element.html(template);
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null;
scope.publicLink = null;
scope.$watch('dashboard', function(newVal) {
if (newVal) {
if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getCustomer(scope.dashboard.customerId.id).then(
customerService.getShortCustomerInfo(scope.dashboard.customerId.id).then(
function success(customer) {
scope.assignedCustomer = customer;
scope.isPublic = customer.isPublic;
if (scope.isPublic) {
scope.publicLink = dashboardService.getPublicDashboardLink(scope.dashboard);
} else {
scope.publicLink = null;
}
}
);
} else {
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.publicLink = 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);
}
return {
@ -55,6 +69,7 @@ export default function DashboardDirective($compile, $templateCache, types, cust
dashboardScope: '=',
theForm: '=',
onAssignToCustomer: '&',
onMakePublic: '&',
onUnassignFromCustomer: '&',
onExportDashboard: '&',
onDeleteDashboard: '&'

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

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

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

@ -47,18 +47,23 @@
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-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>
<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)">
<md-tooltip md-direction="bottom">
{{ 'dashboard.export' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
</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-aliases-device-select ng-show="!vm.isEdit"
<tb-aliases-device-select ng-show="!vm.isEdit && vm.displayDevicesSelect()"
tooltip-direction="bottom"
ng-model="vm.aliasesInfo.deviceAliases"
device-aliases-info="vm.aliasesInfo.deviceAliasesInfo">
@ -304,6 +309,6 @@
</section>
</section>
<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>
</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 assignToCustomerTemplate from './assign-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 */
/*@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) {
var customerId = $stateParams.customerId;
@ -58,6 +103,7 @@ export default function DashboardsController(userService, dashboardService, cust
clickItemFunc: openDashboard,
getItemTitleFunc: getDashboardTitle,
itemCardController: 'DashboardCardController',
itemCardTemplateUrl: dashboardCard,
parentCtl: vm,
@ -90,6 +136,7 @@ export default function DashboardsController(userService, dashboardService, cust
vm.dashboardsScope = $state.$current.data.dashboardsType;
vm.assignToCustomer = assignToCustomer;
vm.makePublic = makePublic;
vm.unassignFromCustomer = unassignFromCustomer;
vm.exportDashboard = exportDashboard;
@ -107,6 +154,17 @@ export default function DashboardsController(userService, dashboardService, cust
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') {
fetchDashboardsFunction = function (pageLink) {
return dashboardService.getTenantDashboards(pageLink);
@ -126,8 +184,21 @@ export default function DashboardsController(userService, dashboardService, cust
name: function() { $translate.instant('action.export') },
details: function() { return $translate.instant('dashboard.export') },
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) {
assignToCustomer($event, [ item.id.id ]);
},
@ -137,19 +208,29 @@ export default function DashboardsController(userService, dashboardService, cust
isEnabled: function(dashboard) {
return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
}
},
{
});
dashboardActionsList.push({
onAction: function ($event, item) {
unassignFromCustomer($event, item);
unassignFromCustomer($event, item, false);
},
name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('dashboard.unassign-from-customer') },
icon: "assignment_return",
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(
{
@ -233,11 +314,27 @@ export default function DashboardsController(userService, dashboardService, cust
dashboardActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item);
unassignFromCustomer($event, item, false);
},
name: function() { return $translate.instant('action.unassign') },
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) {
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) {
@ -389,15 +507,27 @@ export default function DashboardsController(userService, dashboardService, cust
assignToCustomer($event, dashboardIds);
}
function unassignFromCustomer($event, dashboard) {
function unassignFromCustomer($event, dashboard, isPublic) {
if ($event) {
$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()
.targetEvent($event)
.title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}))
.htmlContent($translate.instant('dashboard.unassign-dashboard-text'))
.ariaLabel($translate.instant('dashboard.unassign-dashboard'))
.title(title)
.htmlContent(content)
.ariaLabel(label)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$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) {
$event.stopPropagation();
importExport.exportDashboard(dashboard.id.id);

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

@ -24,7 +24,8 @@
dashboard-scope="vm.dashboardsScope"
the-form="vm.grid.detailsForm"
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-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
</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;
var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
scope.isDataEnabled = !widgetInfo.useCustomDatasources;
if (!settingsSchema || settingsSchema === '') {
scope.settingsSchema = {};
} else {

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

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

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

Loading…
Cancel
Save