Browse Source

Merge pull request #116 from thingsboard/feature/TB-50

TB-50: Public dashboards feature.
pull/122/head
Igor Kulikov 9 years ago
committed by GitHub
parent
commit
7020e60aed
  1. 14
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  2. 7
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  3. 17
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  4. 16
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  5. 16
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  6. 69
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
  7. 34
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java
  8. 64
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  9. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
  10. 96
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
  11. 12
      application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
  12. 42
      application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java
  13. 18
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  14. 11
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
  15. 16
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java
  16. 2
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  17. 53
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  18. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  19. 7
      dao/src/main/resources/schema.cql
  20. 2
      dao/src/main/resources/system-data.cql
  21. 81
      ui/src/app/api/customer.service.js
  22. 75
      ui/src/app/api/dashboard.service.js
  23. 4
      ui/src/app/api/datasource.service.js
  24. 104
      ui/src/app/api/device.service.js
  25. 14
      ui/src/app/api/login.service.js
  26. 647
      ui/src/app/api/subscription.js
  27. 166
      ui/src/app/api/user.service.js
  28. 16
      ui/src/app/api/widget.service.js
  29. 50
      ui/src/app/app.run.js
  30. 155
      ui/src/app/common/utils.service.js
  31. 8
      ui/src/app/components/dashboard.directive.js
  32. 2
      ui/src/app/components/device-filter.directive.js
  33. 4
      ui/src/app/components/grid.tpl.html
  34. 7
      ui/src/app/components/legend.directive.js
  35. 18
      ui/src/app/components/widget-config.directive.js
  36. 4
      ui/src/app/components/widget-config.tpl.html
  37. 902
      ui/src/app/components/widget.controller.js
  38. 2
      ui/src/app/customer/customer-card.tpl.html
  39. 6
      ui/src/app/customer/customer-fieldset.tpl.html
  40. 40
      ui/src/app/customer/customer.controller.js
  41. 14
      ui/src/app/customer/customer.directive.js
  42. 3
      ui/src/app/dashboard/dashboard-card.tpl.html
  43. 31
      ui/src/app/dashboard/dashboard-fieldset.tpl.html
  44. 57
      ui/src/app/dashboard/dashboard.controller.js
  45. 19
      ui/src/app/dashboard/dashboard.directive.js
  46. 2
      ui/src/app/dashboard/dashboard.routes.js
  47. 2
      ui/src/app/dashboard/dashboard.tpl.html
  48. 153
      ui/src/app/dashboard/dashboards.controller.js
  49. 3
      ui/src/app/dashboard/dashboards.tpl.html
  50. 1
      ui/src/app/dashboard/edit-widget.directive.js
  51. 1
      ui/src/app/dashboard/edit-widget.tpl.html
  52. 3
      ui/src/app/dashboard/index.js
  53. 56
      ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html
  54. 2
      ui/src/app/device/add-devices-to-customer.controller.js
  55. 3
      ui/src/app/device/device-card.tpl.html
  56. 13
      ui/src/app/device/device-fieldset.tpl.html
  57. 125
      ui/src/app/device/device.controller.js
  58. 6
      ui/src/app/device/device.directive.js
  59. 2
      ui/src/app/device/device.routes.js
  60. 3
      ui/src/app/device/devices.tpl.html
  61. 30
      ui/src/app/locale/locale.constant.js
  62. 231
      ui/src/app/widget/lib/flot-widget.js
  63. 15
      ui/src/app/widget/lib/google-map.js
  64. 325
      ui/src/app/widget/lib/map-widget.js
  65. 14
      ui/src/app/widget/lib/openstreet-map.js
  66. 325
      ui/src/app/widget/lib/timeseries-table-widget.js
  67. 29
      ui/src/app/widget/lib/timeseries-table-widget.scss
  68. 43
      ui/src/app/widget/lib/timeseries-table-widget.tpl.html
  69. 26
      ui/src/scss/main.scss

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

@ -39,6 +39,7 @@ import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvi
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 +57,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 +89,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);
@ -146,6 +156,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,6 +167,7 @@ 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);

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();

17
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.*;
@ -43,14 +46,22 @@ public class CustomerController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/title", method = RequestMethod.GET, produces = "application/text")
@RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET)
@ResponseBody
public String getCustomerTitleById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId);
return customer.getTitle();
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);
}

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;
}

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

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

@ -18,12 +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,
getCustomerTitle: getCustomerTitle,
getShortCustomerInfo: getShortCustomerInfo,
applyAssignedCustomersInfo: applyAssignedCustomersInfo,
applyAssignedCustomerInfo: applyAssignedCustomerInfo,
deleteCustomer: deleteCustomer,
saveCustomer: saveCustomer
}
@ -61,9 +63,9 @@ function CustomerService($http, $q) {
return deferred.promise;
}
function getCustomerTitle(customerId) {
function getShortCustomerInfo(customerId) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/title';
var url = '/api/customer/' + customerId + '/shortInfo';
$http.get(url, null).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
@ -72,6 +74,77 @@ function CustomerService($http, $q) {
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) {

104
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, config) {
function getTenantDevices(pageLink, applyCustomersInfo, config) {
var deferred = $q.defer();
var url = '/api/tenant/devices?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
@ -64,14 +66,26 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
url += '&textOffset=' + pageLink.textOffset;
}
$http.get(url, config).then(function success(response) {
deferred.resolve(response.data);
if (applyCustomersInfo) {
customerService.applyAssignedCustomersInfo(response.data.data).then(
function success(data) {
response.data.data = data;
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.resolve(response.data);
}
}, function fail() {
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;
}

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;

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

@ -0,0 +1,647 @@
/*
* 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 {
registration = this.ctx.$scope.$watch(function () {
return subscription.timeWindowConfig;
}, function (newTimewindow, prevTimewindow) {
if (!angular.equals(newTimewindow, prevTimewindow)) {
subscription.unsubscribe();
subscription.subscribe();
}
});
this.registrations.push(registration);
}
}
registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
subscription.checkSubscriptions();
});
this.registrations.push(registration);
}
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(this, apply);
} catch (e) {
subscription.callbacks.onDataUpdateError(this, 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.timeWindowConfig = angular.copy(this.originalTimewindow);
this.originalTimewindow = null;
this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
}
}
}
onUpdateTimewindow(startTimeMs, endTimeMs) {
if (this.useDashboardTimewindow) {
this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
} else {
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);
}
}
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;
}
}

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);

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;

155
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,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
parseException: parseException,
processWidgetException: processWidgetException,
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities
filterSearchTextEntities: filterSearchTextEntities,
guid: guid,
createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
}
return service;
@ -276,4 +278,153 @@ 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,
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;
}
}

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

@ -182,12 +182,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
vm.dashboardTimewindowApi = {
onResetTimewindow: function() {
if (vm.originalDashboardTimewindow) {
$timeout(function() {
$timeout(function() {
if (vm.originalDashboardTimewindow) {
vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
vm.originalDashboardTimewindow = null;
}, 0);
}
}
}, 0);
},
onUpdateTimewindow: function(startTimeMs, endTimeMs) {
if (!vm.originalDashboardTimewindow) {

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();

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

@ -49,7 +49,7 @@
<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);

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 }}

902
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,244 +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) {
var dataIndex = 0;
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: dataIndex++
};
$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) {
@ -584,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",

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

@ -15,5 +15,6 @@
limitations under the License.
-->
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</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>

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

@ -15,25 +15,42 @@
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="row" ng-show="!isEdit && isPublic && (dashboardScope === 'customer' || dashboardScope === 'tenant')">
<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>
<fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
<label translate>dashboard.title</label>

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

@ -86,6 +86,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;
@ -273,6 +274,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';
}
@ -617,22 +622,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 +634,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', {

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

@ -47,7 +47,7 @@
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"
ng-click="vm.exportDashboard($event)">

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

@ -19,11 +19,33 @@ 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 function DashboardCardController($scope, types, customerService) {
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;
@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) {
vm.isAssignedToCustomer = function() {
if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' &&
vm.item.customerId.id != vm.types.id.nullUid) {
vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
return true;
}
return false;
}
$scope.$watch('vm.item',
function() {
if (vm.isAssignedToCustomer()) {
customerService.getCustomerTitle(vm.item.customerId.id).then(
function success(title) {
vm.customerTitle = title;
}
);
}
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, $scope, $controller,
export function DashboardsController(userService, dashboardService, customerService, importExport, types,
$state, $stateParams, $mdDialog, $document, $q, $translate) {
var customerId = $stateParams.customerId;
@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ
vm.dashboardsScope = $state.$current.data.dashboardsType;
vm.assignToCustomer = assignToCustomer;
vm.makePublic = makePublic;
vm.unassignFromCustomer = unassignFromCustomer;
vm.exportDashboard = exportDashboard;
@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ
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);
@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ
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 ]);
},
@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ
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.unshare') },
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(
{
@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ
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.unshare') },
details: function() { return $translate.instant('dashboard.make-private') },
icon: "reply",
isEnabled: function(dashboard) {
return dashboard && dashboard.assignedCustomer.isPublic;
}
}
);
@ -418,15 +486,27 @@ export function DashboardsController(userService, dashboardService, customerServ
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 () {
@ -436,6 +516,25 @@ export function DashboardsController(userService, dashboardService, customerServ
});
}
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"

3
ui/src/app/dashboard/index.js

@ -35,7 +35,7 @@ import thingsboardItemBuffer from '../services/item-buffer.service';
import thingsboardImportExport from '../import-export';
import DashboardRoutes from './dashboard.routes';
import {DashboardsController, DashboardCardController} from './dashboards.controller';
import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.controller';
import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
@ -69,6 +69,7 @@ export default angular.module('thingsboard.dashboard', [
.config(DashboardRoutes)
.controller('DashboardsController', DashboardsController)
.controller('DashboardCardController', DashboardCardController)
.controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController)
.controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController)
.controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)

56
ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html

@ -0,0 +1,56 @@
<!--
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.
-->
<md-dialog aria-label="{{ 'dashboard.make-public' | translate }}" style="min-width: 400px;">
<form>
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate="dashboard.public-dashboard-title"></h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.close()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content>
<div id="make-dialog-public-content" class="md-dialog-content">
<md-content class="md-padding" layout="column">
<span translate="dashboard.public-dashboard-text" translate-values="{dashboardTitle: vm.dashboard.title, publicLink: vm.publicLink}"></span>
<div layout="row" layout-align="start center">
<pre class="tb-highlight" flex><code>{{ vm.publicLink }}</code></pre>
<md-button class="md-icon-button"
ngclipboard
data-clipboard-text="{{ vm.publicLink }}"
ngclipboard-success="vm.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 class="tb-notice" translate>dashboard.public-dashboard-notice</div>
</md-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-click="vm.close()">{{ 'action.ok' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

2
ui/src/app/device/add-devices-to-customer.controller.js

@ -52,7 +52,7 @@ export default function AddDevicesToCustomerController(deviceService, $mdDialog,
fetchMoreItems_: function () {
if (vm.devices.hasNext && !vm.devices.pending) {
vm.devices.pending = true;
deviceService.getTenantDevices(vm.devices.nextPageLink).then(
deviceService.getTenantDevices(vm.devices.nextPageLink, false).then(
function success(devices) {
vm.devices.data = vm.devices.data.concat(devices.data);
vm.devices.nextPageLink = devices.nextPageLink;

3
ui/src/app/device/device-card.tpl.html

@ -15,4 +15,5 @@
limitations under the License.
-->
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</div>
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>

13
ui/src/app/device/device-fieldset.tpl.html

@ -15,12 +15,15 @@
limitations under the License.
-->
<md-button ng-click="onMakePublic({event: $event})"
ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer && !isPublic"
class="md-raised md-primary">{{ 'device.make-public' | translate }}</md-button>
<md-button ng-click="onAssignToCustomer({event: $event})"
ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer"
class="md-raised md-primary">{{ 'device.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 && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer"
class="md-raised md-primary">{{ 'device.unassign-from-customer' | translate }}</md-button>
class="md-raised md-primary">{{ isPublic ? 'device.make-private' : 'device.unassign-from-customer' | translate }}</md-button>
<md-button ng-click="onManageCredentials({event: $event})"
ng-show="!isEdit"
class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button>
@ -47,10 +50,14 @@
<md-content class="md-padding" layout="column">
<md-input-container class="md-block"
ng-show="isAssignedToCustomer && deviceScope === 'tenant'">
ng-show="!isEdit && isAssignedToCustomer && !isPublic && deviceScope === 'tenant'">
<label translate>device.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled>
</md-input-container>
<div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
ng-show="!isEdit && isPublic && (deviceScope === 'customer' || deviceScope === 'tenant')">
{{ 'device.device-public' | translate }}
</div>
<fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
<label translate>device.name</label>

125
ui/src/app/device/device.controller.js

@ -24,7 +24,7 @@ import deviceCredentialsTemplate from './device-credentials.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export function DeviceCardController($scope, types, customerService) {
export function DeviceCardController(types) {
var vm = this;
@ -32,28 +32,23 @@ export function DeviceCardController($scope, types, customerService) {
vm.isAssignedToCustomer = function() {
if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === 'tenant' &&
vm.item.customerId.id != vm.types.id.nullUid) {
vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
return true;
}
return false;
}
$scope.$watch('vm.item',
function() {
if (vm.isAssignedToCustomer()) {
customerService.getCustomerTitle(vm.item.customerId.id).then(
function success(title) {
vm.customerTitle = title;
}
);
}
vm.isPublic = function() {
if (vm.item && vm.item.assignedCustomer && vm.parentCtl.devicesScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
return true;
}
);
return false;
}
}
/*@ngInject*/
export function DeviceController(userService, deviceService, customerService, $scope, $controller, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
var customerId = $stateParams.customerId;
@ -107,6 +102,7 @@ export function DeviceController(userService, deviceService, customerService, $s
vm.devicesScope = $state.$current.data.devicesType;
vm.assignToCustomer = assignToCustomer;
vm.makePublic = makePublic;
vm.unassignFromCustomer = unassignFromCustomer;
vm.manageCredentials = manageCredentials;
@ -123,10 +119,20 @@ export function DeviceController(userService, deviceService, customerService, $s
vm.devicesScope = 'customer_user';
customerId = user.customerId;
}
if (customerId) {
vm.customerDevicesTitle = $translate.instant('customer.devices');
customerService.getShortCustomerInfo(customerId).then(
function success(info) {
if (info.isPublic) {
vm.customerDevicesTitle = $translate.instant('customer.public-devices');
}
}
);
}
if (vm.devicesScope === 'tenant') {
fetchDevicesFunction = function (pageLink) {
return deviceService.getTenantDevices(pageLink);
return deviceService.getTenantDevices(pageLink, true);
};
deleteDeviceFunction = function (deviceId) {
return deviceService.deleteDevice(deviceId);
@ -135,6 +141,18 @@ export function DeviceController(userService, deviceService, customerService, $s
return {"topIndex": vm.topIndex};
};
deviceActionsList.push({
onAction: function ($event, item) {
makePublic($event, item);
},
name: function() { return $translate.instant('action.share') },
details: function() { return $translate.instant('device.make-public') },
icon: "share",
isEnabled: function(device) {
return device && (!device.customerId || device.customerId.id === types.id.nullUid);
}
});
deviceActionsList.push(
{
onAction: function ($event, item) {
@ -152,17 +170,29 @@ export function DeviceController(userService, deviceService, customerService, $s
deviceActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item);
unassignFromCustomer($event, item, false);
},
name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('device.unassign-from-customer') },
icon: "assignment_return",
isEnabled: function(device) {
return device && device.customerId && device.customerId.id !== types.id.nullUid;
return device && device.customerId && device.customerId.id !== types.id.nullUid && !device.assignedCustomer.isPublic;
}
}
);
deviceActionsList.push({
onAction: function ($event, item) {
unassignFromCustomer($event, item, true);
},
name: function() { return $translate.instant('action.unshare') },
details: function() { return $translate.instant('device.make-private') },
icon: "reply",
isEnabled: function(device) {
return device && device.customerId && device.customerId.id !== types.id.nullUid && device.assignedCustomer.isPublic;
}
});
deviceActionsList.push(
{
onAction: function ($event, item) {
@ -213,7 +243,7 @@ export function DeviceController(userService, deviceService, customerService, $s
} else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
fetchDevicesFunction = function (pageLink) {
return deviceService.getCustomerDevices(customerId, pageLink);
return deviceService.getCustomerDevices(customerId, pageLink, true);
};
deleteDeviceFunction = function (deviceId) {
return deviceService.unassignDeviceFromCustomer(deviceId);
@ -226,13 +256,30 @@ export function DeviceController(userService, deviceService, customerService, $s
deviceActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item);
unassignFromCustomer($event, item, false);
},
name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('device.unassign-from-customer') },
icon: "assignment_return"
icon: "assignment_return",
isEnabled: function(device) {
return device && !device.assignedCustomer.isPublic;
}
}
);
deviceActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item, true);
},
name: function() { return $translate.instant('action.unshare') },
details: function() { return $translate.instant('device.make-private') },
icon: "reply",
isEnabled: function(device) {
return device && device.assignedCustomer.isPublic;
}
}
);
deviceActionsList.push(
{
onAction: function ($event, item) {
@ -365,7 +412,7 @@ export function DeviceController(userService, deviceService, customerService, $s
$event.stopPropagation();
}
var pageSize = 10;
deviceService.getTenantDevices({limit: pageSize, textSearch: ''}).then(
deviceService.getTenantDevices({limit: pageSize, textSearch: ''}, false).then(
function success(_devices) {
var devices = {
pageSize: pageSize,
@ -404,15 +451,27 @@ export function DeviceController(userService, deviceService, customerService, $s
assignToCustomer($event, deviceIds);
}
function unassignFromCustomer($event, device) {
function unassignFromCustomer($event, device, isPublic) {
if ($event) {
$event.stopPropagation();
}
var title;
var content;
var label;
if (isPublic) {
title = $translate.instant('device.make-private-device-title', {deviceName: device.name});
content = $translate.instant('device.make-private-device-text');
label = $translate.instant('device.make-private');
} else {
title = $translate.instant('device.unassign-device-title', {deviceName: device.name});
content = $translate.instant('device.unassign-device-text');
label = $translate.instant('device.unassign-device');
}
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title($translate.instant('device.unassign-device-title', {deviceName: device.name}))
.htmlContent($translate.instant('device.unassign-device-text'))
.ariaLabel($translate.instant('device.unassign-device'))
.title(title)
.htmlContent(content)
.ariaLabel(label)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
@ -441,6 +500,24 @@ export function DeviceController(userService, deviceService, customerService, $s
});
}
function makePublic($event, device) {
if ($event) {
$event.stopPropagation();
}
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title($translate.instant('device.make-public-device-title', {deviceName: device.name}))
.htmlContent($translate.instant('device.make-public-device-text'))
.ariaLabel($translate.instant('device.make-public'))
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
deviceService.makeDevicePublic(device.id.id).then(function success() {
vm.grid.refreshList();
});
});
}
function manageCredentials($event, device) {
if ($event) {
$event.stopPropagation();

6
ui/src/app/device/device.directive.js

@ -26,6 +26,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
element.html(template);
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null;
scope.deviceCredentials = null;
@ -41,13 +42,15 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
}
if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true;
customerService.getCustomer(scope.device.customerId.id).then(
customerService.getShortCustomerInfo(scope.device.customerId.id).then(
function success(customer) {
scope.assignedCustomer = customer;
scope.isPublic = customer.isPublic;
}
);
} else {
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null;
}
}
@ -72,6 +75,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
deviceScope: '=',
theForm: '=',
onAssignToCustomer: '&',
onMakePublic: '&',
onUnassignFromCustomer: '&',
onManageCredentials: '&',
onDeleteDevice: '&'

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

@ -61,7 +61,7 @@ export default function DeviceRoutes($stateProvider) {
pageTitle: 'customer.devices'
},
ncyBreadcrumb: {
label: '{"icon": "devices_other", "label": "customer.devices"}'
label: '{"icon": "devices_other", "label": "{{ vm.customerDevicesTitle }}", "translate": "false"}'
}
});

3
ui/src/app/device/devices.tpl.html

@ -27,7 +27,8 @@
device-scope="vm.devicesScope"
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-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
</md-tab>

30
ui/src/app/locale/locale.constant.js

@ -43,6 +43,8 @@ export default angular.module('thingsboard.locale', [])
"search": "Search",
"assign": "Assign",
"unassign": "Unassign",
"share": "Share",
"unshare": "Unshare",
"apply": "Apply",
"apply-changes": "Apply changes",
"edit-mode": "Edit mode",
@ -154,11 +156,15 @@ export default angular.module('thingsboard.locale', [])
"dashboard": "Customer Dashboard",
"dashboards": "Customer Dashboards",
"devices": "Customer Devices",
"public-dashboards": "Public Dashboards",
"public-devices": "Public Devices",
"add": "Add Customer",
"delete": "Delete customer",
"manage-customer-users": "Manage customer users",
"manage-customer-devices": "Manage customer devices",
"manage-customer-dashboards": "Manage customer dashboards",
"manage-public-devices": "Manage public devices",
"manage-public-dashboards": "Manage public dashboards",
"add-customer-text": "Add new customer",
"no-customers-text": "No customers found",
"customer-details": "Customer details",
@ -191,6 +197,8 @@ export default angular.module('thingsboard.locale', [])
"assign-to-customer-text": "Please select the customer to assign the dashboard(s)",
"assign-to-customer": "Assign to customer",
"unassign-from-customer": "Unassign from customer",
"make-public": "Make dashboard public",
"make-private": "Make dashboard private",
"no-dashboards-text": "No dashboards found",
"no-widgets": "No widgets configured",
"add-widget": "Add new widget",
@ -219,6 +227,12 @@ export default angular.module('thingsboard.locale', [])
"unassign-dashboard": "Unassign dashboard",
"unassign-dashboards-title": "Are you sure you want to unassign { count, select, 1 {1 dashboard} other {# dashboards} }?",
"unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.",
"public-dashboard-title": "Dashboard is now public",
"public-dashboard-text": "Your dashboard <b>{{dashboardTitle}}</b> is now public and accessible via next public <a href='{{publicLink}}' target='_blank'>link</a>:",
"public-dashboard-notice": "<b>Note:</b> Do not forget to make related devices public in order to access their data.",
"make-private-dashboard-title": "Are you sure you want to make the dashboard '{{dashboardTitle}}' private?",
"make-private-dashboard-text": "After the confirmation the dashboard will be made private and won't be accessible by others.",
"make-private-dashboard": "Make dashboard private",
"select-dashboard": "Select dashboard",
"no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.",
"dashboard-required": "Dashboard is required.",
@ -267,7 +281,11 @@ export default angular.module('thingsboard.locale', [])
"invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.<br/>" +
"Please contact your administrator in order to resolve this issue.",
"select-devices": "Select devices",
"assignedToCustomer": "Assigned to customer"
"assignedToCustomer": "Assigned to customer",
"public": "Public",
"public-link": "Public link",
"copy-public-link": "Copy public link",
"public-link-copied-message": "Dashboard public link has been copied to clipboard"
},
"datakey": {
"settings": "Settings",
@ -323,6 +341,8 @@ export default angular.module('thingsboard.locale', [])
"assign-to-customer": "Assign to customer",
"assign-device-to-customer": "Assign Device(s) To Customer",
"assign-device-to-customer-text": "Please select the devices to assign to the customer",
"make-public": "Make device public",
"make-private": "Make device private",
"no-devices-text": "No devices found",
"assign-to-customer-text": "Please select the customer to assign the device(s)",
"device-details": "Device details",
@ -337,6 +357,10 @@ export default angular.module('thingsboard.locale', [])
"unassign-devices": "Unassign devices",
"unassign-devices-action-title": "Unassign { count, select, 1 {1 device} other {# devices} } from customer",
"assign-new-device": "Assign new device",
"make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?",
"make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.",
"make-private-device-title": "Are you sure you want to make the device '{{deviceName}}' private?",
"make-private-device-text": "After the confirmation the device and all its data will be made private and won't be accessible by others.",
"view-credentials": "View credentials",
"delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?",
"delete-device-text": "Be careful, after the confirmation the device and all related data will become unrecoverable.",
@ -369,7 +393,9 @@ export default angular.module('thingsboard.locale', [])
"assignedToCustomer": "Assigned to customer",
"unable-delete-device-alias-title": "Unable to delete device alias",
"unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"is-gateway": "Is gateway"
"is-gateway": "Is gateway",
"public": "Public",
"device-public": "Device is public"
},
"dialog": {
"close": "Close dialog"

231
ui/src/app/widget/lib/flot-widget.js

@ -34,39 +34,6 @@ export default class TbFlot {
this.chartType = chartType || 'line';
var settings = ctx.settings;
var colors = [];
for (var i = 0; i < ctx.data.length; i++) {
var series = ctx.data[i];
colors.push(series.dataKey.color);
var keySettings = series.dataKey.settings;
series.lines = {
fill: keySettings.fillLines === true,
show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
};
series.points = {
show: false,
radius: 8
};
if (keySettings.showPoints === true) {
series.points.show = true;
series.points.lineWidth = 5;
series.points.radius = 3;
}
if (this.chartType === 'line' && settings.smoothLines && !series.points.show) {
series.curvedLines = {
apply: true
}
}
var lineColor = tinycolor(series.dataKey.color);
lineColor.setAlpha(.75);
series.highlightColor = lineColor.toRgbString();
}
ctx.tooltip = $('#flot-series-tooltip');
if (ctx.tooltip.length === 0) {
ctx.tooltip = $("<div id='flot-series-tooltip' class='flot-mouse-value'></div>");
@ -183,7 +150,6 @@ export default class TbFlot {
};
var options = {
colors: colors,
title: null,
subtitle: null,
shadowSize: settings.shadowSize || 4,
@ -290,14 +256,10 @@ export default class TbFlot {
}
options.series.bars ={
show: true,
barWidth: ctx.timeWindow.interval * 0.6,
lineWidth: 0,
fill: 0.9
}
}
options.xaxis.min = ctx.timeWindow.minTime;
options.xaxis.max = ctx.timeWindow.maxTime;
} else if (this.chartType === 'pie') {
options.series = {
pie: {
@ -340,55 +302,112 @@ export default class TbFlot {
this.options = options;
if (this.ctx.defaultSubscription) {
this.init(this.ctx.$container, this.ctx.defaultSubscription);
}
}
init($element, subscription) {
this.subscription = subscription;
this.$element = $element;
var colors = [];
for (var i = 0; i < this.subscription.data.length; i++) {
var series = this.subscription.data[i];
colors.push(series.dataKey.color);
var keySettings = series.dataKey.settings;
series.lines = {
fill: keySettings.fillLines === true,
show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
};
series.points = {
show: false,
radius: 8
};
if (keySettings.showPoints === true) {
series.points.show = true;
series.points.lineWidth = 5;
series.points.radius = 3;
}
if (this.chartType === 'line' && this.ctx.settings.smoothLines && !series.points.show) {
series.curvedLines = {
apply: true
}
}
var lineColor = tinycolor(series.dataKey.color);
lineColor.setAlpha(.75);
series.highlightColor = lineColor.toRgbString();
}
this.options.colors = colors;
if (this.chartType === 'line' || this.chartType === 'bar') {
if (this.chartType === 'bar') {
this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
}
this.options.xaxis.min = this.subscription.timeWindow.minTime;
this.options.xaxis.max = this.subscription.timeWindow.maxTime;
}
this.checkMouseEvents();
if (this.ctx.plot) {
this.ctx.plot.destroy();
}
if (this.chartType === 'pie' && this.ctx.animatedPie) {
this.ctx.pieDataAnimationDuration = 250;
this.ctx.pieData = angular.copy(this.ctx.data);
this.pieData = angular.copy(this.subscription.data);
this.ctx.pieRenderedData = [];
this.ctx.pieTargetData = [];
for (i = 0; i < this.ctx.data.length; i++) {
this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])
? this.ctx.data[i].data[0][1] : 0;
for (i = 0; i < this.subscription.data.length; i++) {
this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
? this.subscription.data[i].data[0][1] : 0;
}
this.pieDataRendered();
this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options);
this.ctx.plot = $.plot(this.$element, this.pieData, this.options);
} else {
this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options);
}
}
update() {
if (!this.isMouseInteraction && this.ctx.plot) {
if (this.chartType === 'line' || this.chartType === 'bar') {
this.options.xaxis.min = this.ctx.timeWindow.minTime;
this.options.xaxis.max = this.ctx.timeWindow.maxTime;
this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime;
this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime;
if (this.chartType === 'bar') {
this.options.series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
}
this.ctx.plot.setData(this.ctx.data);
this.ctx.plot.setupGrid();
this.ctx.plot.draw();
} else if (this.chartType === 'pie') {
if (this.ctx.animatedPie) {
this.nextPieDataAnimation(true);
} else {
this.ctx.plot.setData(this.ctx.data);
if (this.subscription) {
if (!this.isMouseInteraction && this.ctx.plot) {
if (this.chartType === 'line' || this.chartType === 'bar') {
this.options.xaxis.min = this.subscription.timeWindow.minTime;
this.options.xaxis.max = this.subscription.timeWindow.maxTime;
this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
if (this.chartType === 'bar') {
this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
this.ctx.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
}
this.ctx.plot.setData(this.subscription.data);
this.ctx.plot.setupGrid();
this.ctx.plot.draw();
} else if (this.chartType === 'pie') {
if (this.ctx.animatedPie) {
this.nextPieDataAnimation(true);
} else {
this.ctx.plot.setData(this.subscription.data);
this.ctx.plot.draw();
}
}
}
}
}
resize() {
this.ctx.plot.resize();
if (this.chartType !== 'pie') {
this.ctx.plot.setupGrid();
if (this.ctx.plot) {
this.ctx.plot.resize();
if (this.chartType !== 'pie') {
this.ctx.plot.setupGrid();
}
this.ctx.plot.draw();
}
this.ctx.plot.draw();
}
static get pieSettingsSchema() {
@ -708,17 +727,19 @@ export default class TbFlot {
var enabled = !this.ctx.isMobile && !this.ctx.isEdit;
if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) {
this.mouseEventsEnabled = enabled;
if (enabled) {
this.enableMouseEvents();
} else {
this.disableMouseEvents();
}
if (this.ctx.plot) {
this.ctx.plot.destroy();
if (this.chartType === 'pie' && this.ctx.animatedPie) {
this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options);
if (this.$element) {
if (enabled) {
this.enableMouseEvents();
} else {
this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
this.disableMouseEvents();
}
if (this.ctx.plot) {
this.ctx.plot.destroy();
if (this.chartType === 'pie' && this.ctx.animatedPie) {
this.ctx.plot = $.plot(this.$element, this.pieData, this.options);
} else {
this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options);
}
}
}
}
@ -732,8 +753,8 @@ export default class TbFlot {
}
enableMouseEvents() {
this.ctx.$container.css('pointer-events','');
this.ctx.$container.addClass('mouse-events');
this.$element.css('pointer-events','');
this.$element.addClass('mouse-events');
this.options.selection = { mode : 'x' };
var tbFlot = this;
@ -794,33 +815,33 @@ export default class TbFlot {
tbFlot.ctx.plot.unhighlight();
}
};
this.ctx.$container.bind('plothover', this.flotHoverHandler);
this.$element.bind('plothover', this.flotHoverHandler);
}
if (!this.flotSelectHandler) {
this.flotSelectHandler = function (event, ranges) {
tbFlot.ctx.plot.clearSelection();
tbFlot.ctx.timewindowFunctions.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to);
tbFlot.subscription.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to);
};
this.ctx.$container.bind('plotselected', this.flotSelectHandler);
this.$element.bind('plotselected', this.flotSelectHandler);
}
if (!this.dblclickHandler) {
this.dblclickHandler = function () {
tbFlot.ctx.timewindowFunctions.onResetTimewindow();
tbFlot.subscription.onResetTimewindow();
};
this.ctx.$container.bind('dblclick', this.dblclickHandler);
this.$element.bind('dblclick', this.dblclickHandler);
}
if (!this.mousedownHandler) {
this.mousedownHandler = function () {
tbFlot.isMouseInteraction = true;
};
this.ctx.$container.bind('mousedown', this.mousedownHandler);
this.$element.bind('mousedown', this.mousedownHandler);
}
if (!this.mouseupHandler) {
this.mouseupHandler = function () {
tbFlot.isMouseInteraction = false;
};
this.ctx.$container.bind('mouseup', this.mouseupHandler);
this.$element.bind('mouseup', this.mouseupHandler);
}
if (!this.mouseleaveHandler) {
this.mouseleaveHandler = function () {
@ -829,38 +850,38 @@ export default class TbFlot {
tbFlot.ctx.plot.unhighlight();
tbFlot.isMouseInteraction = false;
};
this.ctx.$container.bind('mouseleave', this.mouseleaveHandler);
this.$element.bind('mouseleave', this.mouseleaveHandler);
}
}
disableMouseEvents() {
this.ctx.$container.css('pointer-events','none');
this.ctx.$container.removeClass('mouse-events');
this.$element.css('pointer-events','none');
this.$element.removeClass('mouse-events');
this.options.selection = { mode : null };
if (this.flotHoverHandler) {
this.ctx.$container.unbind('plothover', this.flotHoverHandler);
this.$element.unbind('plothover', this.flotHoverHandler);
this.flotHoverHandler = null;
}
if (this.flotSelectHandler) {
this.ctx.$container.unbind('plotselected', this.flotSelectHandler);
this.$element.unbind('plotselected', this.flotSelectHandler);
this.flotSelectHandler = null;
}
if (this.dblclickHandler) {
this.ctx.$container.unbind('dblclick', this.dblclickHandler);
this.$element.unbind('dblclick', this.dblclickHandler);
this.dblclickHandler = null;
}
if (this.mousedownHandler) {
this.ctx.$container.unbind('mousedown', this.mousedownHandler);
this.$element.unbind('mousedown', this.mousedownHandler);
this.mousedownHandler = null;
}
if (this.mouseupHandler) {
this.ctx.$container.unbind('mouseup', this.mouseupHandler);
this.$element.unbind('mouseup', this.mouseupHandler);
this.mouseupHandler = null;
}
if (this.mouseleaveHandler) {
this.ctx.$container.unbind('mouseleave', this.mouseleaveHandler);
this.$element.unbind('mouseleave', this.mouseleaveHandler);
this.mouseleaveHandler = null;
}
}
@ -952,10 +973,10 @@ export default class TbFlot {
for (var i = 0; i < this.ctx.pieTargetData.length; i++) {
var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0;
this.ctx.pieRenderedData[i] = value;
if (!this.ctx.pieData[i].data[0]) {
this.ctx.pieData[i].data[0] = [0,0];
if (!this.pieData[i].data[0]) {
this.pieData[i].data[0] = [0,0];
}
this.ctx.pieData[i].data[0][1] = value;
this.pieData[i].data[0][1] = value;
}
}
@ -963,9 +984,9 @@ export default class TbFlot {
if (start) {
this.finishPieDataAnimation();
this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now();
for (var i = 0; i < this.ctx.data.length; i++) {
this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])
? this.ctx.data[i].data[0][1] : 0;
for (var i = 0; i < this.subscription.data.length; i++) {
this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
? this.subscription.data[i].data[0][1] : 0;
}
}
if (this.ctx.pieAnimationCaf) {
@ -992,12 +1013,12 @@ export default class TbFlot {
var prevValue = this.ctx.pieRenderedData[i];
var targetValue = this.ctx.pieTargetData[i];
var value = prevValue + (targetValue - prevValue) * progress;
if (!this.ctx.pieData[i].data[0]) {
this.ctx.pieData[i].data[0] = [0,0];
if (!this.pieData[i].data[0]) {
this.pieData[i].data[0] = [0,0];
}
this.ctx.pieData[i].data[0][1] = value;
this.pieData[i].data[0][1] = value;
}
this.ctx.plot.setData(this.ctx.pieData);
this.ctx.plot.setData(this.pieData);
this.ctx.plot.draw();
this.ctx.pieAnimationLastTime = time;
}
@ -1007,7 +1028,7 @@ export default class TbFlot {
finishPieDataAnimation() {
this.pieDataRendered();
this.ctx.plot.setData(this.ctx.pieData);
this.ctx.plot.setData(this.pieData);
this.ctx.plot.draw();
}
}

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

@ -182,7 +182,7 @@ export default class TbGoogleMap {
/* eslint-enable no-undef */
/* eslint-disable no-undef */
createMarker(location, settings) {
createMarker(location, settings, onClickListener) {
var height = 34;
var pinColor = settings.color.substr(1);
var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + pinColor,
@ -219,8 +219,17 @@ export default class TbGoogleMap {
this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
if (onClickListener) {
marker.addListener('click', onClickListener);
}
return marker;
}
removeMarker(marker) {
marker.setMap(null);
}
/* eslint-enable no-undef */
/* eslint-disable no-undef */
@ -266,6 +275,10 @@ export default class TbGoogleMap {
}
/* eslint-enable no-undef */
removePolyline(polyline) {
polyline.setMap(null);
}
/* eslint-disable no-undef */
fitBounds(bounds) {
if (this.dontFitMapBounds && this.defaultZoomLevel) {

325
ui/src/app/widget/lib/map-widget.js

@ -19,11 +19,60 @@ import tinycolor from 'tinycolor2';
import TbGoogleMap from './google-map';
import TbOpenStreetMap from './openstreet-map';
function procesTooltipPattern(tbMap, pattern, datasources) {
var match = tbMap.varsRegex.exec(pattern);
var replaceInfo = {};
replaceInfo.variables = [];
while (match !== null) {
var variableInfo = {};
variableInfo.dataKeyIndex = -1;
var variable = match[0];
var label = match[1];
var valDec = 2;
var splitVals = label.split(':');
if (splitVals.length > 1) {
label = splitVals[0];
valDec = parseFloat(splitVals[1]);
}
variableInfo.variable = variable;
variableInfo.valDec = valDec;
if (label.startsWith('#')) {
var keyIndexStr = label.substring(1);
var n = Math.floor(Number(keyIndexStr));
if (String(n) === keyIndexStr && n >= 0) {
variableInfo.dataKeyIndex = n;
}
}
if (variableInfo.dataKeyIndex === -1) {
var offset = 0;
for (var i=0;i<datasources.length;i++) {
var datasource = datasources[i];
for (var k = 0; k < datasource.dataKeys.length; k++) {
var dataKey = datasource.dataKeys[k];
if (dataKey.label === label) {
variableInfo.dataKeyIndex = offset + k;
break;
}
}
offset += datasource.dataKeys.length;
}
}
replaceInfo.variables.push(variableInfo);
match = tbMap.varsRegex.exec(pattern);
}
return replaceInfo;
}
export default class TbMapWidget {
constructor(mapProvider, drawRoutes, ctx) {
constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
var tbMap = this;
this.ctx = ctx;
if (!$element) {
$element = ctx.$container;
}
this.drawRoutes = drawRoutes;
this.markers = [];
@ -35,6 +84,9 @@ export default class TbMapWidget {
var settings = ctx.settings;
this.callbacks = {};
this.callbacks.onLocationClick = function(){};
if (settings.defaultZoomLevel) {
if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {
this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel);
@ -43,52 +95,113 @@ export default class TbMapWidget {
this.dontFitMapBounds = settings.fitMapBounds === false;
function procesTooltipPattern(pattern) {
var match = tbMap.varsRegex.exec(pattern);
var replaceInfo = {};
replaceInfo.variables = [];
while (match !== null) {
var variableInfo = {};
variableInfo.dataKeyIndex = -1;
var variable = match[0];
var label = match[1];
var valDec = 2;
var splitVals = label.split(':');
if (splitVals.length > 1) {
label = splitVals[0];
valDec = parseFloat(splitVals[1]);
if (!useDynamicLocations) {
this.subscription = this.ctx.defaultSubscription;
this.configureLocationsFromSettings();
}
var minZoomLevel = this.drawRoutes ? 18 : 15;
var initCallback = function() {
tbMap.update();
tbMap.resize();
};
if (mapProvider === 'google-map') {
this.map = new TbGoogleMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
} else if (mapProvider === 'openstreet-map') {
this.map = new TbOpenStreetMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel);
}
}
setCallbacks(callbacks) {
Object.assign(this.callbacks, callbacks);
}
clearLocations() {
if (this.locations) {
var tbMap = this;
this.locations.forEach(function(location) {
if (location.marker) {
tbMap.map.removeMarker(location.marker);
}
variableInfo.variable = variable;
variableInfo.valDec = valDec;
if (label.startsWith('#')) {
var keyIndexStr = label.substring(1);
var n = Math.floor(Number(keyIndexStr));
if (String(n) === keyIndexStr && n >= 0) {
variableInfo.dataKeyIndex = n;
}
if (location.polyline) {
tbMap.map.removePolyline(location.polyline);
}
if (variableInfo.dataKeyIndex === -1) {
var offset = 0;
for (var i=0;i<ctx.datasources.length;i++) {
var datasource = ctx.datasources[i];
for (var k = 0; k < datasource.dataKeys.length; k++) {
var dataKey = datasource.dataKeys[k];
if (dataKey.label === label) {
variableInfo.dataKeyIndex = offset + k;
break;
}
});
this.locations = null;
this.markers = [];
if (this.drawRoutes) {
this.polylines = [];
}
}
}
configureLocationsFromSubscription(subscription, subscriptionLocationSettings) {
this.subscription = subscription;
this.clearLocations();
this.locationsSettings = [];
var latKeyName = subscriptionLocationSettings.latKeyName;
var lngKeyName = subscriptionLocationSettings.lngKeyName;
var index = 0;
for (var i=0;i<subscription.datasources.length;i++) {
var datasource = subscription.datasources[i];
var dataKeys = datasource.dataKeys;
var latKeyIndex = -1;
var lngKeyIndex = -1;
var localLatKeyName = latKeyName;
var localLngKeyName = lngKeyName;
for (var k=0;k<dataKeys.length;k++) {
var dataKey = dataKeys[k];
if (dataKey.name === latKeyName) {
latKeyIndex = index;
localLatKeyName = localLatKeyName + index;
dataKey.locationAttrName = localLatKeyName;
} else if (dataKey.name === lngKeyName) {
lngKeyIndex = index;
localLngKeyName = localLngKeyName + index;
dataKey.locationAttrName = localLngKeyName;
}
if (latKeyIndex > -1 && lngKeyIndex > -1) {
var locationsSettings = {
latKeyName: localLatKeyName,
lngKeyName: localLngKeyName,
showLabel: subscriptionLocationSettings.showLabel !== false,
label: datasource.name,
labelColor: this.ctx.widgetConfig.color || '#000000',
color: "#FE7569",
useColorFunction: false,
colorFunction: null,
markerImage: null,
markerImageSize: 34,
useMarkerImage: false,
useMarkerImageFunction: false,
markerImageFunction: null,
markerImages: [],
tooltipPattern: "<b>Latitude:</b> ${#"+latKeyIndex+":7}<br/><b>Longitude:</b> ${#"+lngKeyIndex+":7}"
};
locationsSettings.tooltipReplaceInfo = procesTooltipPattern(this, locationsSettings.tooltipPattern, this.subscription.datasources);
locationsSettings.useColorFunction = subscriptionLocationSettings.useColorFunction === true;
if (angular.isDefined(subscriptionLocationSettings.colorFunction) && subscriptionLocationSettings.colorFunction.length > 0) {
try {
locationsSettings.colorFunction = new Function('data, dsData, dsIndex', subscriptionLocationSettings.colorFunction);
} catch (e) {
locationsSettings.colorFunction = null;
}
offset += datasource.dataKeys.length;
}
this.locationsSettings.push(locationsSettings);
}
replaceInfo.variables.push(variableInfo);
match = tbMap.varsRegex.exec(pattern);
index++;
}
return replaceInfo;
}
}
var configuredLocationsSettings = drawRoutes ? settings.routesSettings : settings.markersSettings;
configureLocationsFromSettings() {
var configuredLocationsSettings = this.drawRoutes ? this.ctx.settings.routesSettings : this.ctx.settings.markersSettings;
if (!configuredLocationsSettings) {
configuredLocationsSettings = [];
}
@ -99,7 +212,7 @@ export default class TbMapWidget {
lngKeyName: "lng",
showLabel: true,
label: "",
labelColor: ctx.widgetConfig.color || '#000000',
labelColor: this.ctx.widgetConfig.color || '#000000',
color: "#FE7569",
useColorFunction: false,
colorFunction: null,
@ -112,7 +225,7 @@ export default class TbMapWidget {
tooltipPattern: "<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}"
};
if (drawRoutes) {
if (this.drawRoutes) {
this.locationsSettings[i].strokeWeight = 2;
this.locationsSettings[i].strokeOpacity = 1.0;
}
@ -123,7 +236,7 @@ export default class TbMapWidget {
this.locationsSettings[i].tooltipPattern = configuredLocationsSettings[i].tooltipPattern || "<b>Latitude:</b> ${"+this.locationsSettings[i].latKeyName+":7}<br/><b>Longitude:</b> ${"+this.locationsSettings[i].lngKeyName+":7}";
this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this.locationsSettings[i].tooltipPattern);
this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this, this.locationsSettings[i].tooltipPattern, this.subscription.datasources);
this.locationsSettings[i].showLabel = configuredLocationsSettings[i].showLabel !== false;
this.locationsSettings[i].label = configuredLocationsSettings[i].label || this.locationsSettings[i].label;
@ -132,7 +245,7 @@ export default class TbMapWidget {
this.locationsSettings[i].useColorFunction = configuredLocationsSettings[i].useColorFunction === true;
if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) {
try {
this.locationsSettings[i].colorFunction = new Function('data', configuredLocationsSettings[i].colorFunction);
this.locationsSettings[i].colorFunction = new Function('data, dsData, dsIndex', configuredLocationsSettings[i].colorFunction);
} catch (e) {
this.locationsSettings[i].colorFunction = null;
}
@ -141,7 +254,7 @@ export default class TbMapWidget {
this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true;
if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) {
try {
this.locationsSettings[i].markerImageFunction = new Function('data, images', configuredLocationsSettings[i].markerImageFunction);
this.locationsSettings[i].markerImageFunction = new Function('data, images, dsData, dsIndex', configuredLocationsSettings[i].markerImageFunction);
} catch (e) {
this.locationsSettings[i].markerImageFunction = null;
}
@ -157,26 +270,12 @@ export default class TbMapWidget {
this.locationsSettings[i].markerImageSize = configuredLocationsSettings[i].markerImageSize || 34;
}
if (drawRoutes) {
if (this.drawRoutes) {
this.locationsSettings[i].strokeWeight = configuredLocationsSettings[i].strokeWeight || this.locationsSettings[i].strokeWeight;
this.locationsSettings[i].strokeOpacity = angular.isDefined(configuredLocationsSettings[i].strokeOpacity) ? configuredLocationsSettings[i].strokeOpacity : this.locationsSettings[i].strokeOpacity;
}
}
}
var minZoomLevel = this.drawRoutes ? 18 : 15;
var initCallback = function() {
tbMap.update();
tbMap.resize();
};
if (mapProvider === 'google-map') {
this.map = new TbGoogleMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
} else if (mapProvider === 'openstreet-map') {
this.map = new TbOpenStreetMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel);
}
}
update() {
@ -231,22 +330,22 @@ export default class TbMapWidget {
return true;
}
function calculateLocationColor(settings, dataMap) {
if (settings.useColorFunction && settings.colorFunction) {
function calculateLocationColor(location, dataMap) {
if (location.settings.useColorFunction && location.settings.colorFunction) {
var color = '#FE7569';
try {
color = settings.colorFunction(dataMap);
color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
} catch (e) {
color = '#FE7569';
}
return tinycolor(color).toHexString();
} else {
return settings.color;
return location.settings.color;
}
}
function updateLocationColor(location, dataMap) {
var color = calculateLocationColor(location.settings, dataMap);
var color = calculateLocationColor(location, dataMap);
if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) {
if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) {
tbMap.map.updateMarkerColor(location.marker, color);
@ -258,11 +357,11 @@ export default class TbMapWidget {
}
}
function calculateLocationMarkerImage(settings, dataMap) {
if (settings.useMarkerImageFunction && settings.markerImageFunction) {
function calculateLocationMarkerImage(location, dataMap) {
if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) {
var image = null;
try {
image = settings.markerImageFunction(dataMap, settings.markerImages);
image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex);
} catch (e) {
image = null;
}
@ -273,7 +372,7 @@ export default class TbMapWidget {
}
function updateLocationMarkerImage(location, dataMap) {
var image = calculateLocationMarkerImage(location.settings, dataMap);
var image = calculateLocationMarkerImage(location, dataMap);
if (image != null && (!location.settings.calculatedImage || !angular.equals(location.settings.calculatedImage, image))) {
tbMap.map.updateMarkerImage(location.marker, location.settings, image.url, image.size);
location.settings.calculatedImage = image;
@ -306,7 +405,11 @@ export default class TbMapWidget {
if (latLngs.length > 0) {
var markerLocation = latLngs[latLngs.length-1];
if (!location.marker) {
location.marker = tbMap.map.createMarker(markerLocation, location.settings);
location.marker = tbMap.map.createMarker(markerLocation, location.settings,
function() {
tbMap.callbacks.onLocationClick(location);
}
);
} else {
tbMap.map.setMarkerPosition(location.marker, markerLocation);
}
@ -328,7 +431,9 @@ export default class TbMapWidget {
lng = lngData[lngData.length-1][1];
latLng = tbMap.map.createLatLng(lat, lng);
if (!location.marker) {
location.marker = tbMap.map.createMarker(latLng, location.settings);
location.marker = tbMap.map.createMarker(latLng, location.settings, function() {
tbMap.callbacks.onLocationClick(location);
});
tbMap.markers.push(location.marker);
locationChanged = true;
} else {
@ -345,8 +450,12 @@ export default class TbMapWidget {
return locationChanged;
}
function toLabelValueMap(data) {
function toLabelValueMap(data, datasources) {
var dataMap = {};
var dsDataMap = [];
for (var d=0;d<datasources.length;d++) {
dsDataMap[d] = {};
}
for (var i = 0; i < data.length; i++) {
var dataKey = data[i].dataKey;
var label = dataKey.label;
@ -356,30 +465,44 @@ export default class TbMapWidget {
val = keyData[keyData.length-1][1];
}
dataMap[label] = val;
var dsIndex = datasources.indexOf(data[i].datasource);
dsDataMap[dsIndex][label] = val;
}
return dataMap;
return {
dataMap: dataMap,
dsDataMap: dsDataMap
};
}
function loadLocations(data) {
function loadLocations(data, datasources) {
var bounds = tbMap.map.createBounds();
tbMap.locations = [];
var dataMap = toLabelValueMap(data);
var dataMap = toLabelValueMap(data, datasources);
for (var l=0; l < tbMap.locationsSettings.length; l++) {
var locationSettings = tbMap.locationsSettings[l];
var latIndex = -1;
var lngIndex = -1;
for (var i = 0; i < data.length; i++) {
var dataKey = data[i].dataKey;
if (dataKey.label === locationSettings.latKeyName) {
var nameToCheck;
if (dataKey.locationAttrName) {
nameToCheck = dataKey.locationAttrName;
} else {
nameToCheck = dataKey.label;
}
if (nameToCheck === locationSettings.latKeyName) {
latIndex = i;
} else if (dataKey.label === locationSettings.lngKeyName) {
} else if (nameToCheck === locationSettings.lngKeyName) {
lngIndex = i;
}
}
if (latIndex > -1 && lngIndex > -1) {
var ds = data[latIndex].datasource;
var dsIndex = datasources.indexOf(ds);
var location = {
latIndex: latIndex,
lngIndex: lngIndex,
dsIndex: dsIndex,
settings: locationSettings
};
tbMap.locations.push(location);
@ -394,10 +517,10 @@ export default class TbMapWidget {
tbMap.map.fitBounds(bounds);
}
function updateLocations(data) {
function updateLocations(data, datasources) {
var locationsChanged = false;
var bounds = tbMap.map.createBounds();
var dataMap = toLabelValueMap(data);
var dataMap = toLabelValueMap(data, datasources);
for (var p = 0; p < tbMap.locations.length; p++) {
var location = tbMap.locations[p];
locationsChanged |= updateLocation(location, data, dataMap);
@ -412,36 +535,36 @@ export default class TbMapWidget {
}
}
if (this.map && this.map.inited()) {
if (this.ctx.data) {
if (this.map && this.map.inited() && this.subscription) {
if (this.subscription.data) {
if (!this.locations) {
loadLocations(this.ctx.data);
loadLocations(this.subscription.data, this.subscription.datasources);
} else {
updateLocations(this.ctx.data);
updateLocations(this.subscription.data, this.subscription.datasources);
}
}
var tooltips = this.map.getTooltips();
for (var t=0; t < tooltips.length; t++) {
var tooltip = tooltips[t];
var text = tooltip.pattern;
var replaceInfo = tooltip.replaceInfo;
for (var v = 0; v < replaceInfo.variables.length; v++) {
var variableInfo = replaceInfo.variables[v];
var txtVal = '';
if (variableInfo.dataKeyIndex > -1) {
var varData = this.ctx.data[variableInfo.dataKeyIndex].data;
if (varData.length > 0) {
var val = varData[varData.length - 1][1];
if (isNumber(val)) {
txtVal = padValue(val, variableInfo.valDec, 0);
} else {
txtVal = val;
var tooltips = this.map.getTooltips();
for (var t=0; t < tooltips.length; t++) {
var tooltip = tooltips[t];
var text = tooltip.pattern;
var replaceInfo = tooltip.replaceInfo;
for (var v = 0; v < replaceInfo.variables.length; v++) {
var variableInfo = replaceInfo.variables[v];
var txtVal = '';
if (variableInfo.dataKeyIndex > -1 && this.subscription.data[variableInfo.dataKeyIndex]) {
var varData = this.subscription.data[variableInfo.dataKeyIndex].data;
if (varData.length > 0) {
var val = varData[varData.length - 1][1];
if (isNumber(val)) {
txtVal = padValue(val, variableInfo.valDec, 0);
} else {
txtVal = val;
}
}
}
text = text.split(variableInfo.variable).join(txtVal);
}
text = text.split(variableInfo.variable).join(txtVal);
tooltip.popup.setContent(text);
}
tooltip.popup.setContent(text);
}
}
}

14
ui/src/app/widget/lib/openstreet-map.js

@ -85,7 +85,7 @@ export default class TbOpenStreetMap {
testImage.src = image;
}
createMarker(location, settings) {
createMarker(location, settings, onClickListener) {
var height = 34;
var pinColor = settings.color.substr(1);
var icon = L.icon({
@ -111,9 +111,17 @@ export default class TbOpenStreetMap {
this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
if (onClickListener) {
marker.on('click', onClickListener);
}
return marker;
}
removeMarker(marker) {
this.map.removeLayer(marker);
}
createTooltip(marker, pattern, replaceInfo) {
var popup = L.popup();
popup.setContent('');
@ -145,6 +153,10 @@ export default class TbOpenStreetMap {
return polyline;
}
removePolyline(polyline) {
this.map.removeLayer(polyline);
}
fitBounds(bounds) {
if (bounds.isValid()) {
if (this.dontFitMapBounds && this.defaultZoomLevel) {

325
ui/src/app/widget/lib/timeseries-table-widget.js

@ -0,0 +1,325 @@
/*
* 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.
*/
import './timeseries-table-widget.scss';
/* eslint-disable import/no-unresolved, import/default */
import timeseriesTableWidgetTemplate from './timeseries-table-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import tinycolor from 'tinycolor2';
import cssjs from '../../../vendor/css.js/css';
export default angular.module('thingsboard.widgets.timeseriesTableWidget', [])
.directive('tbTimeseriesTableWidget', TimeseriesTableWidget)
.name;
/*@ngInject*/
function TimeseriesTableWidget() {
return {
restrict: "E",
scope: true,
bindToController: {
tableId: '=',
config: '=',
datasources: '=',
data: '='
},
controller: TimeseriesTableWidgetController,
controllerAs: 'vm',
templateUrl: timeseriesTableWidgetTemplate
};
}
/*@ngInject*/
function TimeseriesTableWidgetController($element, $scope, $filter) {
var vm = this;
vm.sources = [];
vm.sourceIndex = 0;
$scope.$watch('vm.config', function() {
if (vm.config) {
vm.settings = vm.config.settings;
vm.widgetConfig = vm.config.widgetConfig;
initialize();
}
});
function initialize() {
vm.showTimestamp = vm.settings.showTimestamp !== false;
var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
var defaultColor = tinycolor(origColor);
var mdDark = defaultColor.setAlpha(0.87).toRgbString();
var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
//var mdDarkIcon = mdDarkSecondary;
var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
var cssString = 'table.md-table th.md-column {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'table.md-table th.md-column md-icon.md-sort-icon {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'table.md-table td.md-cell {\n'+
'color: ' + mdDark + ';\n'+
'border-top: 1px '+mdDarkDivider+' solid;\n'+
'}\n'+
'table.md-table td.md-cell.md-placeholder {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.md-table-pagination {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'border-top: 1px '+mdDarkDivider+' solid;\n'+
'}\n'+
'.md-table-pagination .buttons md-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}';
var cssParser = new cssjs();
cssParser.testMode = false;
var namespace = 'ts-table-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$element.addClass(namespace);
function hashCode(str) {
var hash = 0;
var i, char;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}
}
$scope.$watch('vm.datasources', function() {
updateDatasources();
});
$scope.$on('timeseries-table-data-updated', function(event, tableId) {
if (vm.tableId == tableId) {
dataUpdated();
}
});
function dataUpdated() {
for (var s=0; s < vm.sources.length; s++) {
var source = vm.sources[s];
source.rawData = vm.data.slice(source.keyStartIndex, source.keyEndIndex);
}
updateSourceData(vm.sources[vm.sourceIndex]);
$scope.$digest();
}
vm.onPaginate = function(source) {
updatePage(source);
}
vm.onReorder = function(source) {
reorder(source);
updatePage(source);
}
vm.cellStyle = function(source, index, value) {
var style = {};
if (index > 0) {
var styleInfo = source.ts.stylesInfo[index-1];
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
style = styleInfo.cellStyleFunction(value);
} catch (e) {
style = {};
}
}
}
return style;
}
vm.cellContent = function(source, index, row, value) {
if (index === 0) {
return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss');
} else {
var strContent = '';
if (angular.isDefined(value)) {
strContent = ''+value;
}
var content = strContent;
var contentInfo = source.ts.contentsInfo[index-1];
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
var rowData = source.ts.rowDataTemplate;
rowData['Timestamp'] = row[0];
for (var h=0; h < source.ts.header.length; h++) {
var headerInfo = source.ts.header[h];
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
}
content = contentInfo.cellContentFunction(value, rowData, $filter);
} catch (e) {
content = strContent;
}
}
return content;
}
}
$scope.$watch('vm.sourceIndex', function(newIndex, oldIndex) {
if (newIndex != oldIndex) {
updateSourceData(vm.sources[vm.sourceIndex]);
}
});
function updateDatasources() {
vm.sources = [];
vm.sourceIndex = 0;
var keyOffset = 0;
if (vm.datasources) {
for (var ds = 0; ds < vm.datasources.length; ds++) {
var source = {};
var datasource = vm.datasources[ds];
source.keyStartIndex = keyOffset;
keyOffset += datasource.dataKeys.length;
source.keyEndIndex = keyOffset;
source.datasource = datasource;
source.data = [];
source.rawData = [];
source.query = {
limit: 5,
page: 1,
order: '-0'
}
source.ts = {
header: [],
count: 0,
data: [],
stylesInfo: [],
contentsInfo: [],
rowDataTemplate: {}
}
source.ts.rowDataTemplate['Timestamp'] = null;
for (var a = 0; a < datasource.dataKeys.length; a++ ) {
var dataKey = datasource.dataKeys[a];
var keySettings = dataKey.settings;
source.ts.header.push({
index: a+1,
dataKey: dataKey
});
source.ts.rowDataTemplate[dataKey.label] = null;
var cellStyleFunction = null;
var useCellStyleFunction = false;
if (keySettings.useCellStyleFunction === true) {
if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
try {
cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
useCellStyleFunction = true;
} catch (e) {
cellStyleFunction = null;
useCellStyleFunction = false;
}
}
}
source.ts.stylesInfo.push({
useCellStyleFunction: useCellStyleFunction,
cellStyleFunction: cellStyleFunction
});
var cellContentFunction = null;
var useCellContentFunction = false;
if (keySettings.useCellContentFunction === true) {
if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
try {
cellContentFunction = new Function('value, rowData, filter', keySettings.cellContentFunction);
useCellContentFunction = true;
} catch (e) {
cellContentFunction = null;
useCellContentFunction = false;
}
}
}
source.ts.contentsInfo.push({
useCellContentFunction: useCellContentFunction,
cellContentFunction: cellContentFunction
});
}
vm.sources.push(source);
}
}
}
function updatePage(source) {
var startIndex = source.query.limit * (source.query.page - 1);
source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);
}
function reorder(source) {
source.data = $filter('orderBy')(source.data, source.query.order);
}
function convertData(data) {
var rowsMap = {};
for (var d = 0; d < data.length; d++) {
var columnData = data[d].data;
for (var i = 0; i < columnData.length; i++) {
var cellData = columnData[i];
var timestamp = cellData[0];
var row = rowsMap[timestamp];
if (!row) {
row = [];
row[0] = timestamp;
for (var c = 0; c < data.length; c++) {
row[c+1] = undefined;
}
rowsMap[timestamp] = row;
}
row[d+1] = cellData[1];
}
}
var rows = [];
for (var t in rowsMap) {
rows.push(rowsMap[t]);
}
return rows;
}
function updateSourceData(source) {
source.data = convertData(source.rawData);
source.ts.count = source.data.length;
reorder(source);
updatePage(source);
}
}

29
ui/src/app/widget/lib/timeseries-table-widget.scss

@ -0,0 +1,29 @@
/**
* 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.
*/
tb-timeseries-table-widget {
table.md-table thead.md-head>tr.md-row {
height: 40px;
}
table.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {
height: 38px;
}
.md-table-pagination>* {
height: 46px;
}
}

43
ui/src/app/widget/lib/timeseries-table-widget.tpl.html

@ -0,0 +1,43 @@
<!--
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.
-->
<md-tabs md-selected="vm.sourceIndex" ng-class="{'tb-headless': vm.sources.length === 1}"
id="tabs" md-border-bottom flex class="tb-absolute-fill">
<md-tab ng-repeat="source in vm.sources" label="{{ source.datasource.name }}">
<md-table-container>
<table md-table>
<thead md-head md-order="source.query.order" md-on-reorder="vm.onReorder(source)">
<tr md-row>
<th ng-show="vm.showTimestamp" md-column md-order-by="0"><span>Timestamp</span></th>
<th md-column md-order-by="{{ h.index }}" ng-repeat="h in source.ts.header"><span>{{ h.dataKey.label }}</span></th>
</tr>
</thead>
<tbody md-body>
<tr md-row ng-repeat="row in source.ts.data">
<td ng-show="$index > 0 || ($index === 0 && vm.showTimestamp)" md-cell ng-repeat="d in row track by $index" ng-style="vm.cellStyle(source, $index, d)" ng-bind-html="vm.cellContent(source, $index, row, d)">
</td>
</tr>
</tbody>
</table>
</md-table-container>
<md-table-pagination md-limit="source.query.limit" md-limit-options="[5, 10, 15]"
md-page="source.query.page" md-total="{{source.ts.count}}"
md-on-paginate="vm.onPaginate(source)" md-page-select>
</md-table-pagination>
</md-tab>
</md-tabs>

26
ui/src/scss/main.scss

@ -236,6 +236,32 @@ div {
}
}
pre.tb-highlight {
background-color: #f7f7f7;
display: block;
margin: 20px 0;
padding: 15px;
overflow-x: auto;
code {
padding: 0;
color: #303030;
font-family: monospace;
display: inline-block;
box-sizing: border-box;
vertical-align: bottom;
font-size: 16px;
font-weight: bold;
}
}
.tb-notice {
background-color: #f7f7f7;
padding: 15px;
border: 1px solid #ccc;
font-size: 16px;
}
/***********************
* Flow
***********************/

Loading…
Cancel
Save