Browse Source

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

TB-50: Public dashboards feature.
pull/122/head
Igor Kulikov 10 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.rest.RestLoginProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.*; import org.thingsboard.server.service.security.auth.jwt.*;
import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -56,6 +57,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
public static final String WEBJARS_ENTRY_POINT = "/webjars/**"; public static final String WEBJARS_ENTRY_POINT = "/webjars/**";
public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
@ -87,10 +89,18 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
return filter; return filter;
} }
@Bean
protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception {
RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
filter.setAuthenticationManager(this.authenticationManager);
return filter;
}
@Bean @Bean
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@ -146,6 +156,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
.antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.and() .and()
@ -156,6 +167,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.exceptionHandling().accessDeniedHandler(restAccessDeniedHandler) .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
.and() .and()
.addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);

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

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

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

@ -15,6 +15,9 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -43,14 +46,22 @@ public class CustomerController extends BaseController {
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @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 @ResponseBody
public String getCustomerTitleById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException { public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
checkParameter("customerId", strCustomerId); checkParameter("customerId", strCustomerId);
try { try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId)); CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId); 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) { } catch (Exception e) {
throw handleException(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.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -116,6 +117,21 @@ public class DashboardController extends BaseController {
} }
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST)
@ResponseBody
public Dashboard assignDashboardToPublicCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
checkParameter("dashboardId", strDashboardId);
try {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
@ResponseBody @ResponseBody

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

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

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

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

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

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.rest;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PublicLoginRequest {
private String publicId;
@JsonCreator
public PublicLoginRequest(@JsonProperty("publicId") String publicId) {
this.publicId = publicId;
}
public String getPublicId() {
return publicId;
}
}

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

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

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

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

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

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.model.UserPrincipal;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class);
private final AuthenticationSuccessHandler successHandler;
private final AuthenticationFailureHandler failureHandler;
private final ObjectMapper objectMapper;
public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
super(defaultProcessUrl);
this.successHandler = successHandler;
this.failureHandler = failureHandler;
this.objectMapper = mapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (!HttpMethod.POST.name().equals(request.getMethod())) {
if(logger.isDebugEnabled()) {
logger.debug("Authentication method not supported. Request method: " + request.getMethod());
}
throw new AuthMethodNotSupportedException("Authentication method not supported");
}
PublicLoginRequest loginRequest;
try {
loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class);
} catch (Exception e) {
throw new AuthenticationServiceException("Invalid public login request payload");
}
if (StringUtils.isBlank(loginRequest.getPublicId())) {
throw new AuthenticationServiceException("Public Id is not provided");
}
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "");
return this.getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
successHandler.onAuthenticationSuccess(request, response, authResult);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
}

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

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

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

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.model;
public class UserPrincipal {
private final Type type;
private final String value;
public UserPrincipal(Type type, String value) {
this.type = type;
this.value = value;
}
public Type getType() {
return type;
}
public String getValue() {
return value;
}
public enum Type {
USER_NAME,
PUBLIC_ID
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function Utils($mdColorPalette, $rootScope, $window, types) { function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
var predefinedFunctions = {}, var predefinedFunctions = {},
predefinedFunctionsList = [], predefinedFunctionsList = [],
@ -104,7 +104,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
parseException: parseException, parseException: parseException,
processWidgetException: processWidgetException, processWidgetException: processWidgetException,
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities filterSearchTextEntities: filterSearchTextEntities,
guid: guid,
createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
} }
return service; return service;
@ -276,4 +278,153 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
deferred.resolve(response); deferred.resolve(response);
} }
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
function genNextColor(datasources) {
var index = 0;
if (datasources) {
for (var i = 0; i < datasources.length; i++) {
var datasource = datasources[i];
index += datasource.dataKeys.length;
}
}
return getMaterialColor(index);
}
/*var defaultDataKey = {
name: 'f(x)',
type: types.dataKeyType.function,
label: 'Sin',
color: getMaterialColor(0),
funcBody: getPredefinedFunctionBody('Sin'),
settings: {},
_hash: Math.random()
};
var defaultDatasource = {
type: types.datasourceType.function,
name: types.datasourceType.function,
dataKeys: [angular.copy(defaultDataKey)]
};*/
function createKey(keyInfo, type, datasources) {
var dataKey = {
name: keyInfo.name,
type: type,
label: keyInfo.label || keyInfo.name,
color: genNextColor(datasources),
funcBody: keyInfo.funcBody,
settings: {},
_hash: Math.random()
}
return dataKey;
}
function createDatasourceKeys(keyInfos, type, datasource, datasources) {
for (var i=0;i<keyInfos.length;i++) {
var keyInfo = keyInfos[i];
var dataKey = createKey(keyInfo, type, datasources);
datasource.dataKeys.push(dataKey);
}
}
function createDatasourceFromSubscription(subscriptionInfo, datasources, device) {
var datasource;
if (subscriptionInfo.type === types.datasourceType.device) {
datasource = {
type: subscriptionInfo.type,
deviceName: device.name,
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 = { vm.dashboardTimewindowApi = {
onResetTimewindow: function() { onResetTimewindow: function() {
if (vm.originalDashboardTimewindow) { $timeout(function() {
$timeout(function() { if (vm.originalDashboardTimewindow) {
vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
vm.originalDashboardTimewindow = null; vm.originalDashboardTimewindow = null;
}, 0); }
} }, 0);
}, },
onUpdateTimewindow: function(startTimeMs, endTimeMs) { onUpdateTimewindow: function(startTimeMs, endTimeMs) {
if (!vm.originalDashboardTimewindow) { 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(); var deferred = $q.defer();
deviceService.getTenantDevices(pageLink).then(function success(result) { deviceService.getTenantDevices(pageLink, false).then(function success(result) {
deferred.resolve(result.data); deferred.resolve(result.data);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();

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" <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}"> ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
{{ action.details() }} {{ action.details( rowItem[n] ) }}
</md-tooltip> </md-tooltip>
<ng-md-icon icon="{{action.icon}}"></ng-md-icon> <ng-md-icon icon="{{action.icon}}"></ng-md-icon>
</md-button> </md-button>
@ -62,7 +62,7 @@
</div> </div>
<tb-details-sidenav <tb-details-sidenav
header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}" header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}"
header-subtitle="{{vm.itemDetailsText()}}" header-subtitle="{{vm.itemDetailsText(vm.operatingItem())}}"
is-read-only="vm.isDetailsReadOnly(vm.operatingItem())" is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
is-open="vm.detailsConfig.isDetailsOpen" is-open="vm.detailsConfig.isDetailsOpen"
is-edit="vm.detailsConfig.isDetailsEditMode" is-edit="vm.detailsConfig.isDetailsEditMode"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -15,5 +15,6 @@
limitations under the License. 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. limitations under the License.
--> -->
<md-button ng-click="onExportDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
<md-button ng-click="onMakePublic({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer && !isPublic"
class="md-raised md-primary">{{ 'dashboard.make-public' | translate }}</md-button>
<md-button ng-click="onAssignToCustomer({event: $event})" <md-button ng-click="onAssignToCustomer({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer" ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer"
class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button> class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
<md-button ng-click="onUnassignFromCustomer({event: $event})" <md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer" ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer"
class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button> class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }}</md-button>
<md-button ng-click="onExportDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
<md-button ng-click="onDeleteDashboard({event: $event})" <md-button ng-click="onDeleteDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'" ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button> class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
<md-content class="md-padding" layout="column"> <md-content class="md-padding" layout="column">
<md-input-container class="md-block" <md-input-container class="md-block"
ng-show="isAssignedToCustomer && dashboardScope === 'tenant'"> ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'">
<label translate>dashboard.assignedToCustomer</label> <label translate>dashboard.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled> <input ng-model="assignedCustomer.title" disabled>
</md-input-container> </md-input-container>
<div layout="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"> <fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>dashboard.title</label> <label translate>dashboard.title</label>

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

@ -86,6 +86,7 @@ export default function DashboardController(types, widgetService, userService,
vm.exportDashboard = exportDashboard; vm.exportDashboard = exportDashboard;
vm.exportWidget = exportWidget; vm.exportWidget = exportWidget;
vm.importWidget = importWidget; vm.importWidget = importWidget;
vm.isPublicUser = isPublicUser;
vm.isTenantAdmin = isTenantAdmin; vm.isTenantAdmin = isTenantAdmin;
vm.isSystemAdmin = isSystemAdmin; vm.isSystemAdmin = isSystemAdmin;
vm.loadDashboard = loadDashboard; vm.loadDashboard = loadDashboard;
@ -273,6 +274,10 @@ export default function DashboardController(types, widgetService, userService,
vm.dashboardInitComplete = true; vm.dashboardInitComplete = true;
} }
function isPublicUser() {
return vm.user.isPublic === true;
}
function isTenantAdmin() { function isTenantAdmin() {
return vm.user.authority === 'TENANT_ADMIN'; return vm.user.authority === 'TENANT_ADMIN';
} }
@ -617,22 +622,8 @@ export default function DashboardController(types, widgetService, userService,
sizeY: widgetTypeInfo.sizeY, sizeY: widgetTypeInfo.sizeY,
config: config config: config
}; };
$mdDialog.show({
controller: 'AddWidgetController', function addWidget(widget) {
controllerAs: 'vm',
templateUrl: addWidgetTemplate,
locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event,
onComplete: function () {
var w = angular.element($window);
w.triggerHandler('resize');
}
}).then(function (result) {
var widget = result.widget;
vm.aliasesInfo = result.aliasesInfo;
var columns = 24; var columns = 24;
if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) { if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
columns = vm.dashboard.configuration.gridSettings.columns; columns = vm.dashboard.configuration.gridSettings.columns;
@ -643,9 +634,37 @@ export default function DashboardController(types, widgetService, userService,
widget.sizeY *= ratio; widget.sizeY *= ratio;
} }
vm.widgets.push(widget); vm.widgets.push(widget);
}, function (rejection) { }
vm.aliasesInfo = rejection.aliasesInfo;
}); if (widgetTypeInfo.useCustomDatasources) {
addWidget(newWidget);
} else {
$mdDialog.show({
controller: 'AddWidgetController',
controllerAs: 'vm',
templateUrl: addWidgetTemplate,
locals: {
dashboard: vm.dashboard,
aliasesInfo: vm.aliasesInfo,
widget: newWidget,
widgetInfo: widgetTypeInfo
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event,
onComplete: function () {
var w = angular.element($window);
w.triggerHandler('resize');
}
}).then(function (result) {
var widget = result.widget;
vm.aliasesInfo = result.aliasesInfo;
addWidget(widget);
}, function (rejection) {
vm.aliasesInfo = rejection.aliasesInfo;
});
}
} }
); );
} }

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

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

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

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

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

@ -47,7 +47,7 @@
aria-label="{{ 'fullscreen.fullscreen' | translate }}" aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button"> class="md-icon-button">
</md-button> </md-button>
<tb-user-menu ng-show="forceFullscreen" display-user-info="true"> <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
</tb-user-menu> </tb-user-menu>
<md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button" <md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
ng-click="vm.exportDashboard($event)"> 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 dashboardCard from './dashboard-card.tpl.html';
import assignToCustomerTemplate from './assign-to-customer.tpl.html'; import assignToCustomerTemplate from './assign-to-customer.tpl.html';
import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html'; import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html';
import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export 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; var vm = this;
@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) {
vm.isAssignedToCustomer = function() { vm.isAssignedToCustomer = function() {
if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' && 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 true;
} }
return false; return false;
} }
$scope.$watch('vm.item', vm.isPublic = function() {
function() { if (vm.item && vm.item.assignedCustomer && vm.parentCtl.dashboardsScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
if (vm.isAssignedToCustomer()) { return true;
customerService.getCustomerTitle(vm.item.customerId.id).then(
function success(title) {
vm.customerTitle = title;
}
);
}
} }
); return false;
}
} }
/*@ngInject*/ /*@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) { $state, $stateParams, $mdDialog, $document, $q, $translate) {
var customerId = $stateParams.customerId; var customerId = $stateParams.customerId;
@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ
vm.dashboardsScope = $state.$current.data.dashboardsType; vm.dashboardsScope = $state.$current.data.dashboardsType;
vm.assignToCustomer = assignToCustomer; vm.assignToCustomer = assignToCustomer;
vm.makePublic = makePublic;
vm.unassignFromCustomer = unassignFromCustomer; vm.unassignFromCustomer = unassignFromCustomer;
vm.exportDashboard = exportDashboard; vm.exportDashboard = exportDashboard;
@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ
customerId = user.customerId; customerId = user.customerId;
} }
if (customerId) {
vm.customerDashboardsTitle = $translate.instant('customer.dashboards');
customerService.getShortCustomerInfo(customerId).then(
function success(info) {
if (info.isPublic) {
vm.customerDashboardsTitle = $translate.instant('customer.public-dashboards');
}
}
);
}
if (vm.dashboardsScope === 'tenant') { if (vm.dashboardsScope === 'tenant') {
fetchDashboardsFunction = function (pageLink) { fetchDashboardsFunction = function (pageLink) {
return dashboardService.getTenantDashboards(pageLink); return dashboardService.getTenantDashboards(pageLink);
@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ
name: function() { $translate.instant('action.export') }, name: function() { $translate.instant('action.export') },
details: function() { return $translate.instant('dashboard.export') }, details: function() { return $translate.instant('dashboard.export') },
icon: "file_download" icon: "file_download"
}, });
{
dashboardActionsList.push({
onAction: function ($event, item) {
makePublic($event, item);
},
name: function() { return $translate.instant('action.share') },
details: function() { return $translate.instant('dashboard.make-public') },
icon: "share",
isEnabled: function(dashboard) {
return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
}
});
dashboardActionsList.push({
onAction: function ($event, item) { onAction: function ($event, item) {
assignToCustomer($event, [ item.id.id ]); assignToCustomer($event, [ item.id.id ]);
}, },
@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ
isEnabled: function(dashboard) { isEnabled: function(dashboard) {
return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
} }
}, });
{ dashboardActionsList.push({
onAction: function ($event, item) { onAction: function ($event, item) {
unassignFromCustomer($event, item); unassignFromCustomer($event, item, false);
}, },
name: function() { return $translate.instant('action.unassign') }, name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('dashboard.unassign-from-customer') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') },
icon: "assignment_return", icon: "assignment_return",
isEnabled: function(dashboard) { isEnabled: function(dashboard) {
return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid; return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && !dashboard.assignedCustomer.isPublic;
} }
} });
); dashboardActionsList.push({
onAction: function ($event, item) {
unassignFromCustomer($event, item, true);
},
name: function() { return $translate.instant('action.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( dashboardActionsList.push(
{ {
@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ
dashboardActionsList.push( dashboardActionsList.push(
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
unassignFromCustomer($event, item); unassignFromCustomer($event, item, false);
}, },
name: function() { return $translate.instant('action.unassign') }, name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('dashboard.unassign-from-customer') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') },
icon: "assignment_return" icon: "assignment_return",
isEnabled: function(dashboard) {
return dashboard && !dashboard.assignedCustomer.isPublic;
}
}
);
dashboardActionsList.push(
{
onAction: function ($event, item) {
unassignFromCustomer($event, item, true);
},
name: function() { return $translate.instant('action.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); assignToCustomer($event, dashboardIds);
} }
function unassignFromCustomer($event, dashboard) { function unassignFromCustomer($event, dashboard, isPublic) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
var title;
var content;
var label;
if (isPublic) {
title = $translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title});
content = $translate.instant('dashboard.make-private-dashboard-text');
label = $translate.instant('dashboard.make-private-dashboard');
} else {
title = $translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title});
content = $translate.instant('dashboard.unassign-dashboard-text');
label = $translate.instant('dashboard.unassign-dashboard');
}
var confirm = $mdDialog.confirm() var confirm = $mdDialog.confirm()
.targetEvent($event) .targetEvent($event)
.title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title})) .title(title)
.htmlContent($translate.instant('dashboard.unassign-dashboard-text')) .htmlContent(content)
.ariaLabel($translate.instant('dashboard.unassign-dashboard')) .ariaLabel(label)
.cancel($translate.instant('action.no')) .cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes')); .ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () { $mdDialog.show(confirm).then(function () {
@ -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) { function exportDashboard($event, dashboard) {
$event.stopPropagation(); $event.stopPropagation();
importExport.exportDashboard(dashboard.id.id); importExport.exportDashboard(dashboard.id.id);

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

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

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

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

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

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

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

@ -35,7 +35,7 @@ import thingsboardItemBuffer from '../services/item-buffer.service';
import thingsboardImportExport from '../import-export'; import thingsboardImportExport from '../import-export';
import DashboardRoutes from './dashboard.routes'; import DashboardRoutes from './dashboard.routes';
import {DashboardsController, DashboardCardController} from './dashboards.controller'; import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
import DashboardController from './dashboard.controller'; import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.controller'; import DeviceAliasesController from './device-aliases.controller';
import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller'; import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
@ -69,6 +69,7 @@ export default angular.module('thingsboard.dashboard', [
.config(DashboardRoutes) .config(DashboardRoutes)
.controller('DashboardsController', DashboardsController) .controller('DashboardsController', DashboardsController)
.controller('DashboardCardController', DashboardCardController) .controller('DashboardCardController', DashboardCardController)
.controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController)
.controller('DashboardController', DashboardController) .controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController) .controller('DeviceAliasesController', DeviceAliasesController)
.controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController) .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 () { fetchMoreItems_: function () {
if (vm.devices.hasNext && !vm.devices.pending) { if (vm.devices.hasNext && !vm.devices.pending) {
vm.devices.pending = true; vm.devices.pending = true;
deviceService.getTenantDevices(vm.devices.nextPageLink).then( deviceService.getTenantDevices(vm.devices.nextPageLink, false).then(
function success(devices) { function success(devices) {
vm.devices.data = vm.devices.data.concat(devices.data); vm.devices.data = vm.devices.data.concat(devices.data);
vm.devices.nextPageLink = devices.nextPageLink; vm.devices.nextPageLink = devices.nextPageLink;

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

@ -15,4 +15,5 @@
limitations under the License. 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. 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})" <md-button ng-click="onAssignToCustomer({event: $event})"
ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer" ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer"
class="md-raised md-primary">{{ 'device.assign-to-customer' | translate }}</md-button> 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" 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})" <md-button ng-click="onManageCredentials({event: $event})"
ng-show="!isEdit" ng-show="!isEdit"
class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button> 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-content class="md-padding" layout="column">
<md-input-container class="md-block" <md-input-container class="md-block"
ng-show="isAssignedToCustomer && deviceScope === 'tenant'"> ng-show="!isEdit && isAssignedToCustomer && !isPublic && deviceScope === 'tenant'">
<label translate>device.assignedToCustomer</label> <label translate>device.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled> <input ng-model="assignedCustomer.title" disabled>
</md-input-container> </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"> <fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>device.name</label> <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 */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export function DeviceCardController($scope, types, customerService) { export function DeviceCardController(types) {
var vm = this; var vm = this;
@ -32,28 +32,23 @@ export function DeviceCardController($scope, types, customerService) {
vm.isAssignedToCustomer = function() { vm.isAssignedToCustomer = function() {
if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === 'tenant' && 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 true;
} }
return false; return false;
} }
$scope.$watch('vm.item', vm.isPublic = function() {
function() { if (vm.item && vm.item.assignedCustomer && vm.parentCtl.devicesScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
if (vm.isAssignedToCustomer()) { return true;
customerService.getCustomerTitle(vm.item.customerId.id).then(
function success(title) {
vm.customerTitle = title;
}
);
}
} }
); return false;
}
} }
/*@ngInject*/ /*@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; var customerId = $stateParams.customerId;
@ -107,6 +102,7 @@ export function DeviceController(userService, deviceService, customerService, $s
vm.devicesScope = $state.$current.data.devicesType; vm.devicesScope = $state.$current.data.devicesType;
vm.assignToCustomer = assignToCustomer; vm.assignToCustomer = assignToCustomer;
vm.makePublic = makePublic;
vm.unassignFromCustomer = unassignFromCustomer; vm.unassignFromCustomer = unassignFromCustomer;
vm.manageCredentials = manageCredentials; vm.manageCredentials = manageCredentials;
@ -123,10 +119,20 @@ export function DeviceController(userService, deviceService, customerService, $s
vm.devicesScope = 'customer_user'; vm.devicesScope = 'customer_user';
customerId = user.customerId; 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') { if (vm.devicesScope === 'tenant') {
fetchDevicesFunction = function (pageLink) { fetchDevicesFunction = function (pageLink) {
return deviceService.getTenantDevices(pageLink); return deviceService.getTenantDevices(pageLink, true);
}; };
deleteDeviceFunction = function (deviceId) { deleteDeviceFunction = function (deviceId) {
return deviceService.deleteDevice(deviceId); return deviceService.deleteDevice(deviceId);
@ -135,6 +141,18 @@ export function DeviceController(userService, deviceService, customerService, $s
return {"topIndex": vm.topIndex}; 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( deviceActionsList.push(
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
@ -152,17 +170,29 @@ export function DeviceController(userService, deviceService, customerService, $s
deviceActionsList.push( deviceActionsList.push(
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
unassignFromCustomer($event, item); unassignFromCustomer($event, item, false);
}, },
name: function() { return $translate.instant('action.unassign') }, name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('device.unassign-from-customer') }, details: function() { return $translate.instant('device.unassign-from-customer') },
icon: "assignment_return", icon: "assignment_return",
isEnabled: function(device) { 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( deviceActionsList.push(
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
@ -213,7 +243,7 @@ export function DeviceController(userService, deviceService, customerService, $s
} else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') { } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
fetchDevicesFunction = function (pageLink) { fetchDevicesFunction = function (pageLink) {
return deviceService.getCustomerDevices(customerId, pageLink); return deviceService.getCustomerDevices(customerId, pageLink, true);
}; };
deleteDeviceFunction = function (deviceId) { deleteDeviceFunction = function (deviceId) {
return deviceService.unassignDeviceFromCustomer(deviceId); return deviceService.unassignDeviceFromCustomer(deviceId);
@ -226,13 +256,30 @@ export function DeviceController(userService, deviceService, customerService, $s
deviceActionsList.push( deviceActionsList.push(
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
unassignFromCustomer($event, item); unassignFromCustomer($event, item, false);
}, },
name: function() { return $translate.instant('action.unassign') }, name: function() { return $translate.instant('action.unassign') },
details: function() { return $translate.instant('device.unassign-from-customer') }, 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( deviceActionsList.push(
{ {
onAction: function ($event, item) { onAction: function ($event, item) {
@ -365,7 +412,7 @@ export function DeviceController(userService, deviceService, customerService, $s
$event.stopPropagation(); $event.stopPropagation();
} }
var pageSize = 10; var pageSize = 10;
deviceService.getTenantDevices({limit: pageSize, textSearch: ''}).then( deviceService.getTenantDevices({limit: pageSize, textSearch: ''}, false).then(
function success(_devices) { function success(_devices) {
var devices = { var devices = {
pageSize: pageSize, pageSize: pageSize,
@ -404,15 +451,27 @@ export function DeviceController(userService, deviceService, customerService, $s
assignToCustomer($event, deviceIds); assignToCustomer($event, deviceIds);
} }
function unassignFromCustomer($event, device) { function unassignFromCustomer($event, device, isPublic) {
if ($event) { if ($event) {
$event.stopPropagation(); $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() var confirm = $mdDialog.confirm()
.targetEvent($event) .targetEvent($event)
.title($translate.instant('device.unassign-device-title', {deviceName: device.name})) .title(title)
.htmlContent($translate.instant('device.unassign-device-text')) .htmlContent(content)
.ariaLabel($translate.instant('device.unassign-device')) .ariaLabel(label)
.cancel($translate.instant('action.no')) .cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes')); .ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () { $mdDialog.show(confirm).then(function () {
@ -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) { function manageCredentials($event, device) {
if ($event) { if ($event) {
$event.stopPropagation(); $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); element.html(template);
scope.isAssignedToCustomer = false; scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null; scope.assignedCustomer = null;
scope.deviceCredentials = 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) { if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
scope.isAssignedToCustomer = true; scope.isAssignedToCustomer = true;
customerService.getCustomer(scope.device.customerId.id).then( customerService.getShortCustomerInfo(scope.device.customerId.id).then(
function success(customer) { function success(customer) {
scope.assignedCustomer = customer; scope.assignedCustomer = customer;
scope.isPublic = customer.isPublic;
} }
); );
} else { } else {
scope.isAssignedToCustomer = false; scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null; scope.assignedCustomer = null;
} }
} }
@ -72,6 +75,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
deviceScope: '=', deviceScope: '=',
theForm: '=', theForm: '=',
onAssignToCustomer: '&', onAssignToCustomer: '&',
onMakePublic: '&',
onUnassignFromCustomer: '&', onUnassignFromCustomer: '&',
onManageCredentials: '&', onManageCredentials: '&',
onDeleteDevice: '&' onDeleteDevice: '&'

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

@ -61,7 +61,7 @@ export default function DeviceRoutes($stateProvider) {
pageTitle: 'customer.devices' pageTitle: 'customer.devices'
}, },
ncyBreadcrumb: { 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" device-scope="vm.devicesScope"
the-form="vm.grid.detailsForm" the-form="vm.grid.detailsForm"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)" on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)" on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device> on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
</md-tab> </md-tab>

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

@ -43,6 +43,8 @@ export default angular.module('thingsboard.locale', [])
"search": "Search", "search": "Search",
"assign": "Assign", "assign": "Assign",
"unassign": "Unassign", "unassign": "Unassign",
"share": "Share",
"unshare": "Unshare",
"apply": "Apply", "apply": "Apply",
"apply-changes": "Apply changes", "apply-changes": "Apply changes",
"edit-mode": "Edit mode", "edit-mode": "Edit mode",
@ -154,11 +156,15 @@ export default angular.module('thingsboard.locale', [])
"dashboard": "Customer Dashboard", "dashboard": "Customer Dashboard",
"dashboards": "Customer Dashboards", "dashboards": "Customer Dashboards",
"devices": "Customer Devices", "devices": "Customer Devices",
"public-dashboards": "Public Dashboards",
"public-devices": "Public Devices",
"add": "Add Customer", "add": "Add Customer",
"delete": "Delete customer", "delete": "Delete customer",
"manage-customer-users": "Manage customer users", "manage-customer-users": "Manage customer users",
"manage-customer-devices": "Manage customer devices", "manage-customer-devices": "Manage customer devices",
"manage-customer-dashboards": "Manage customer dashboards", "manage-customer-dashboards": "Manage customer dashboards",
"manage-public-devices": "Manage public devices",
"manage-public-dashboards": "Manage public dashboards",
"add-customer-text": "Add new customer", "add-customer-text": "Add new customer",
"no-customers-text": "No customers found", "no-customers-text": "No customers found",
"customer-details": "Customer details", "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-text": "Please select the customer to assign the dashboard(s)",
"assign-to-customer": "Assign to customer", "assign-to-customer": "Assign to customer",
"unassign-from-customer": "Unassign from customer", "unassign-from-customer": "Unassign from customer",
"make-public": "Make dashboard public",
"make-private": "Make dashboard private",
"no-dashboards-text": "No dashboards found", "no-dashboards-text": "No dashboards found",
"no-widgets": "No widgets configured", "no-widgets": "No widgets configured",
"add-widget": "Add new widget", "add-widget": "Add new widget",
@ -219,6 +227,12 @@ export default angular.module('thingsboard.locale', [])
"unassign-dashboard": "Unassign dashboard", "unassign-dashboard": "Unassign dashboard",
"unassign-dashboards-title": "Are you sure you want to unassign { count, select, 1 {1 dashboard} other {# dashboards} }?", "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.", "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", "select-dashboard": "Select dashboard",
"no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.", "no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.",
"dashboard-required": "Dashboard is required.", "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/>" + "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.", "Please contact your administrator in order to resolve this issue.",
"select-devices": "Select devices", "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": { "datakey": {
"settings": "Settings", "settings": "Settings",
@ -323,6 +341,8 @@ export default angular.module('thingsboard.locale', [])
"assign-to-customer": "Assign to customer", "assign-to-customer": "Assign to customer",
"assign-device-to-customer": "Assign Device(s) 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", "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", "no-devices-text": "No devices found",
"assign-to-customer-text": "Please select the customer to assign the device(s)", "assign-to-customer-text": "Please select the customer to assign the device(s)",
"device-details": "Device details", "device-details": "Device details",
@ -337,6 +357,10 @@ export default angular.module('thingsboard.locale', [])
"unassign-devices": "Unassign devices", "unassign-devices": "Unassign devices",
"unassign-devices-action-title": "Unassign { count, select, 1 {1 device} other {# devices} } from customer", "unassign-devices-action-title": "Unassign { count, select, 1 {1 device} other {# devices} } from customer",
"assign-new-device": "Assign new device", "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", "view-credentials": "View credentials",
"delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?", "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.", "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", "assignedToCustomer": "Assigned to customer",
"unable-delete-device-alias-title": "Unable to delete device alias", "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}}", "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": { "dialog": {
"close": "Close 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'; this.chartType = chartType || 'line';
var settings = ctx.settings; 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'); ctx.tooltip = $('#flot-series-tooltip');
if (ctx.tooltip.length === 0) { if (ctx.tooltip.length === 0) {
ctx.tooltip = $("<div id='flot-series-tooltip' class='flot-mouse-value'></div>"); ctx.tooltip = $("<div id='flot-series-tooltip' class='flot-mouse-value'></div>");
@ -183,7 +150,6 @@ export default class TbFlot {
}; };
var options = { var options = {
colors: colors,
title: null, title: null,
subtitle: null, subtitle: null,
shadowSize: settings.shadowSize || 4, shadowSize: settings.shadowSize || 4,
@ -290,14 +256,10 @@ export default class TbFlot {
} }
options.series.bars ={ options.series.bars ={
show: true, show: true,
barWidth: ctx.timeWindow.interval * 0.6,
lineWidth: 0, lineWidth: 0,
fill: 0.9 fill: 0.9
} }
} }
options.xaxis.min = ctx.timeWindow.minTime;
options.xaxis.max = ctx.timeWindow.maxTime;
} else if (this.chartType === 'pie') { } else if (this.chartType === 'pie') {
options.series = { options.series = {
pie: { pie: {
@ -340,55 +302,112 @@ export default class TbFlot {
this.options = options; 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(); this.checkMouseEvents();
if (this.ctx.plot) {
this.ctx.plot.destroy();
}
if (this.chartType === 'pie' && this.ctx.animatedPie) { if (this.chartType === 'pie' && this.ctx.animatedPie) {
this.ctx.pieDataAnimationDuration = 250; this.ctx.pieDataAnimationDuration = 250;
this.ctx.pieData = angular.copy(this.ctx.data); this.pieData = angular.copy(this.subscription.data);
this.ctx.pieRenderedData = []; this.ctx.pieRenderedData = [];
this.ctx.pieTargetData = []; this.ctx.pieTargetData = [];
for (i = 0; i < this.ctx.data.length; i++) { for (i = 0; i < this.subscription.data.length; i++) {
this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0]) this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
? this.ctx.data[i].data[0][1] : 0; ? this.subscription.data[i].data[0][1] : 0;
} }
this.pieDataRendered(); 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 { } 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() { update() {
if (!this.isMouseInteraction && this.ctx.plot) { if (this.subscription) {
if (this.chartType === 'line' || this.chartType === 'bar') { if (!this.isMouseInteraction && this.ctx.plot) {
this.options.xaxis.min = this.ctx.timeWindow.minTime; if (this.chartType === 'line' || this.chartType === 'bar') {
this.options.xaxis.max = this.ctx.timeWindow.maxTime; this.options.xaxis.min = this.subscription.timeWindow.minTime;
this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime; this.options.xaxis.max = this.subscription.timeWindow.maxTime;
this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime; this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
if (this.chartType === 'bar') { this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
this.options.series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; if (this.chartType === 'bar') {
this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; 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.ctx.data); }
this.ctx.plot.setupGrid(); this.ctx.plot.setData(this.subscription.data);
this.ctx.plot.draw(); this.ctx.plot.setupGrid();
} else if (this.chartType === 'pie') {
if (this.ctx.animatedPie) {
this.nextPieDataAnimation(true);
} else {
this.ctx.plot.setData(this.ctx.data);
this.ctx.plot.draw(); 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() { resize() {
this.ctx.plot.resize(); if (this.ctx.plot) {
if (this.chartType !== 'pie') { this.ctx.plot.resize();
this.ctx.plot.setupGrid(); if (this.chartType !== 'pie') {
this.ctx.plot.setupGrid();
}
this.ctx.plot.draw();
} }
this.ctx.plot.draw();
} }
static get pieSettingsSchema() { static get pieSettingsSchema() {
@ -708,17 +727,19 @@ export default class TbFlot {
var enabled = !this.ctx.isMobile && !this.ctx.isEdit; var enabled = !this.ctx.isMobile && !this.ctx.isEdit;
if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) { if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) {
this.mouseEventsEnabled = enabled; this.mouseEventsEnabled = enabled;
if (enabled) { if (this.$element) {
this.enableMouseEvents(); if (enabled) {
} else { this.enableMouseEvents();
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);
} else { } 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() { enableMouseEvents() {
this.ctx.$container.css('pointer-events',''); this.$element.css('pointer-events','');
this.ctx.$container.addClass('mouse-events'); this.$element.addClass('mouse-events');
this.options.selection = { mode : 'x' }; this.options.selection = { mode : 'x' };
var tbFlot = this; var tbFlot = this;
@ -794,33 +815,33 @@ export default class TbFlot {
tbFlot.ctx.plot.unhighlight(); tbFlot.ctx.plot.unhighlight();
} }
}; };
this.ctx.$container.bind('plothover', this.flotHoverHandler); this.$element.bind('plothover', this.flotHoverHandler);
} }
if (!this.flotSelectHandler) { if (!this.flotSelectHandler) {
this.flotSelectHandler = function (event, ranges) { this.flotSelectHandler = function (event, ranges) {
tbFlot.ctx.plot.clearSelection(); 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) { if (!this.dblclickHandler) {
this.dblclickHandler = function () { 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) { if (!this.mousedownHandler) {
this.mousedownHandler = function () { this.mousedownHandler = function () {
tbFlot.isMouseInteraction = true; tbFlot.isMouseInteraction = true;
}; };
this.ctx.$container.bind('mousedown', this.mousedownHandler); this.$element.bind('mousedown', this.mousedownHandler);
} }
if (!this.mouseupHandler) { if (!this.mouseupHandler) {
this.mouseupHandler = function () { this.mouseupHandler = function () {
tbFlot.isMouseInteraction = false; tbFlot.isMouseInteraction = false;
}; };
this.ctx.$container.bind('mouseup', this.mouseupHandler); this.$element.bind('mouseup', this.mouseupHandler);
} }
if (!this.mouseleaveHandler) { if (!this.mouseleaveHandler) {
this.mouseleaveHandler = function () { this.mouseleaveHandler = function () {
@ -829,38 +850,38 @@ export default class TbFlot {
tbFlot.ctx.plot.unhighlight(); tbFlot.ctx.plot.unhighlight();
tbFlot.isMouseInteraction = false; tbFlot.isMouseInteraction = false;
}; };
this.ctx.$container.bind('mouseleave', this.mouseleaveHandler); this.$element.bind('mouseleave', this.mouseleaveHandler);
} }
} }
disableMouseEvents() { disableMouseEvents() {
this.ctx.$container.css('pointer-events','none'); this.$element.css('pointer-events','none');
this.ctx.$container.removeClass('mouse-events'); this.$element.removeClass('mouse-events');
this.options.selection = { mode : null }; this.options.selection = { mode : null };
if (this.flotHoverHandler) { if (this.flotHoverHandler) {
this.ctx.$container.unbind('plothover', this.flotHoverHandler); this.$element.unbind('plothover', this.flotHoverHandler);
this.flotHoverHandler = null; this.flotHoverHandler = null;
} }
if (this.flotSelectHandler) { if (this.flotSelectHandler) {
this.ctx.$container.unbind('plotselected', this.flotSelectHandler); this.$element.unbind('plotselected', this.flotSelectHandler);
this.flotSelectHandler = null; this.flotSelectHandler = null;
} }
if (this.dblclickHandler) { if (this.dblclickHandler) {
this.ctx.$container.unbind('dblclick', this.dblclickHandler); this.$element.unbind('dblclick', this.dblclickHandler);
this.dblclickHandler = null; this.dblclickHandler = null;
} }
if (this.mousedownHandler) { if (this.mousedownHandler) {
this.ctx.$container.unbind('mousedown', this.mousedownHandler); this.$element.unbind('mousedown', this.mousedownHandler);
this.mousedownHandler = null; this.mousedownHandler = null;
} }
if (this.mouseupHandler) { if (this.mouseupHandler) {
this.ctx.$container.unbind('mouseup', this.mouseupHandler); this.$element.unbind('mouseup', this.mouseupHandler);
this.mouseupHandler = null; this.mouseupHandler = null;
} }
if (this.mouseleaveHandler) { if (this.mouseleaveHandler) {
this.ctx.$container.unbind('mouseleave', this.mouseleaveHandler); this.$element.unbind('mouseleave', this.mouseleaveHandler);
this.mouseleaveHandler = null; this.mouseleaveHandler = null;
} }
} }
@ -952,10 +973,10 @@ export default class TbFlot {
for (var i = 0; i < this.ctx.pieTargetData.length; i++) { for (var i = 0; i < this.ctx.pieTargetData.length; i++) {
var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0; var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0;
this.ctx.pieRenderedData[i] = value; this.ctx.pieRenderedData[i] = value;
if (!this.ctx.pieData[i].data[0]) { if (!this.pieData[i].data[0]) {
this.ctx.pieData[i].data[0] = [0,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) { if (start) {
this.finishPieDataAnimation(); this.finishPieDataAnimation();
this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now(); this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now();
for (var i = 0; i < this.ctx.data.length; i++) { for (var i = 0; i < this.subscription.data.length; i++) {
this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0]) this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
? this.ctx.data[i].data[0][1] : 0; ? this.subscription.data[i].data[0][1] : 0;
} }
} }
if (this.ctx.pieAnimationCaf) { if (this.ctx.pieAnimationCaf) {
@ -992,12 +1013,12 @@ export default class TbFlot {
var prevValue = this.ctx.pieRenderedData[i]; var prevValue = this.ctx.pieRenderedData[i];
var targetValue = this.ctx.pieTargetData[i]; var targetValue = this.ctx.pieTargetData[i];
var value = prevValue + (targetValue - prevValue) * progress; var value = prevValue + (targetValue - prevValue) * progress;
if (!this.ctx.pieData[i].data[0]) { if (!this.pieData[i].data[0]) {
this.ctx.pieData[i].data[0] = [0,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.plot.draw();
this.ctx.pieAnimationLastTime = time; this.ctx.pieAnimationLastTime = time;
} }
@ -1007,7 +1028,7 @@ export default class TbFlot {
finishPieDataAnimation() { finishPieDataAnimation() {
this.pieDataRendered(); this.pieDataRendered();
this.ctx.plot.setData(this.ctx.pieData); this.ctx.plot.setData(this.pieData);
this.ctx.plot.draw(); 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-enable no-undef */
/* eslint-disable no-undef */ /* eslint-disable no-undef */
createMarker(location, settings) { createMarker(location, settings, onClickListener) {
var height = 34; var height = 34;
var pinColor = settings.color.substr(1); 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, 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); this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
if (onClickListener) {
marker.addListener('click', onClickListener);
}
return marker; return marker;
} }
removeMarker(marker) {
marker.setMap(null);
}
/* eslint-enable no-undef */ /* eslint-enable no-undef */
/* eslint-disable no-undef */ /* eslint-disable no-undef */
@ -266,6 +275,10 @@ export default class TbGoogleMap {
} }
/* eslint-enable no-undef */ /* eslint-enable no-undef */
removePolyline(polyline) {
polyline.setMap(null);
}
/* eslint-disable no-undef */ /* eslint-disable no-undef */
fitBounds(bounds) { fitBounds(bounds) {
if (this.dontFitMapBounds && this.defaultZoomLevel) { 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 TbGoogleMap from './google-map';
import TbOpenStreetMap from './openstreet-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 { export default class TbMapWidget {
constructor(mapProvider, drawRoutes, ctx) { constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
var tbMap = this; var tbMap = this;
this.ctx = ctx; this.ctx = ctx;
if (!$element) {
$element = ctx.$container;
}
this.drawRoutes = drawRoutes; this.drawRoutes = drawRoutes;
this.markers = []; this.markers = [];
@ -35,6 +84,9 @@ export default class TbMapWidget {
var settings = ctx.settings; var settings = ctx.settings;
this.callbacks = {};
this.callbacks.onLocationClick = function(){};
if (settings.defaultZoomLevel) { if (settings.defaultZoomLevel) {
if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) { if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {
this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel); this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel);
@ -43,52 +95,113 @@ export default class TbMapWidget {
this.dontFitMapBounds = settings.fitMapBounds === false; this.dontFitMapBounds = settings.fitMapBounds === false;
function procesTooltipPattern(pattern) { if (!useDynamicLocations) {
var match = tbMap.varsRegex.exec(pattern); this.subscription = this.ctx.defaultSubscription;
var replaceInfo = {}; this.configureLocationsFromSettings();
replaceInfo.variables = []; }
while (match !== null) {
var variableInfo = {}; var minZoomLevel = this.drawRoutes ? 18 : 15;
variableInfo.dataKeyIndex = -1;
var variable = match[0]; var initCallback = function() {
var label = match[1]; tbMap.update();
var valDec = 2; tbMap.resize();
var splitVals = label.split(':'); };
if (splitVals.length > 1) {
label = splitVals[0]; if (mapProvider === 'google-map') {
valDec = parseFloat(splitVals[1]); 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; if (location.polyline) {
variableInfo.valDec = valDec; tbMap.map.removePolyline(location.polyline);
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; this.locations = null;
for (var i=0;i<ctx.datasources.length;i++) { this.markers = [];
var datasource = ctx.datasources[i]; if (this.drawRoutes) {
for (var k = 0; k < datasource.dataKeys.length; k++) { this.polylines = [];
var dataKey = datasource.dataKeys[k]; }
if (dataKey.label === label) { }
variableInfo.dataKeyIndex = offset + k; }
break;
} 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); index++;
match = tbMap.varsRegex.exec(pattern);
} }
return replaceInfo;
} }
}
var configuredLocationsSettings = drawRoutes ? settings.routesSettings : settings.markersSettings; configureLocationsFromSettings() {
var configuredLocationsSettings = this.drawRoutes ? this.ctx.settings.routesSettings : this.ctx.settings.markersSettings;
if (!configuredLocationsSettings) { if (!configuredLocationsSettings) {
configuredLocationsSettings = []; configuredLocationsSettings = [];
} }
@ -99,7 +212,7 @@ export default class TbMapWidget {
lngKeyName: "lng", lngKeyName: "lng",
showLabel: true, showLabel: true,
label: "", label: "",
labelColor: ctx.widgetConfig.color || '#000000', labelColor: this.ctx.widgetConfig.color || '#000000',
color: "#FE7569", color: "#FE7569",
useColorFunction: false, useColorFunction: false,
colorFunction: null, colorFunction: null,
@ -112,7 +225,7 @@ export default class TbMapWidget {
tooltipPattern: "<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}" 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].strokeWeight = 2;
this.locationsSettings[i].strokeOpacity = 1.0; 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].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].showLabel = configuredLocationsSettings[i].showLabel !== false;
this.locationsSettings[i].label = configuredLocationsSettings[i].label || this.locationsSettings[i].label; 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; this.locationsSettings[i].useColorFunction = configuredLocationsSettings[i].useColorFunction === true;
if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) { if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) {
try { 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) { } catch (e) {
this.locationsSettings[i].colorFunction = null; this.locationsSettings[i].colorFunction = null;
} }
@ -141,7 +254,7 @@ export default class TbMapWidget {
this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true; this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true;
if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) { if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) {
try { 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) { } catch (e) {
this.locationsSettings[i].markerImageFunction = null; this.locationsSettings[i].markerImageFunction = null;
} }
@ -157,26 +270,12 @@ export default class TbMapWidget {
this.locationsSettings[i].markerImageSize = configuredLocationsSettings[i].markerImageSize || 34; 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].strokeWeight = configuredLocationsSettings[i].strokeWeight || this.locationsSettings[i].strokeWeight;
this.locationsSettings[i].strokeOpacity = angular.isDefined(configuredLocationsSettings[i].strokeOpacity) ? configuredLocationsSettings[i].strokeOpacity : this.locationsSettings[i].strokeOpacity; 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() { update() {
@ -231,22 +330,22 @@ export default class TbMapWidget {
return true; return true;
} }
function calculateLocationColor(settings, dataMap) { function calculateLocationColor(location, dataMap) {
if (settings.useColorFunction && settings.colorFunction) { if (location.settings.useColorFunction && location.settings.colorFunction) {
var color = '#FE7569'; var color = '#FE7569';
try { try {
color = settings.colorFunction(dataMap); color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
} catch (e) { } catch (e) {
color = '#FE7569'; color = '#FE7569';
} }
return tinycolor(color).toHexString(); return tinycolor(color).toHexString();
} else { } else {
return settings.color; return location.settings.color;
} }
} }
function updateLocationColor(location, dataMap) { 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.calculatedColor || location.settings.calculatedColor !== color) {
if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) { if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) {
tbMap.map.updateMarkerColor(location.marker, color); tbMap.map.updateMarkerColor(location.marker, color);
@ -258,11 +357,11 @@ export default class TbMapWidget {
} }
} }
function calculateLocationMarkerImage(settings, dataMap) { function calculateLocationMarkerImage(location, dataMap) {
if (settings.useMarkerImageFunction && settings.markerImageFunction) { if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) {
var image = null; var image = null;
try { try {
image = settings.markerImageFunction(dataMap, settings.markerImages); image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex);
} catch (e) { } catch (e) {
image = null; image = null;
} }
@ -273,7 +372,7 @@ export default class TbMapWidget {
} }
function updateLocationMarkerImage(location, dataMap) { 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))) { if (image != null && (!location.settings.calculatedImage || !angular.equals(location.settings.calculatedImage, image))) {
tbMap.map.updateMarkerImage(location.marker, location.settings, image.url, image.size); tbMap.map.updateMarkerImage(location.marker, location.settings, image.url, image.size);
location.settings.calculatedImage = image; location.settings.calculatedImage = image;
@ -306,7 +405,11 @@ export default class TbMapWidget {
if (latLngs.length > 0) { if (latLngs.length > 0) {
var markerLocation = latLngs[latLngs.length-1]; var markerLocation = latLngs[latLngs.length-1];
if (!location.marker) { 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 { } else {
tbMap.map.setMarkerPosition(location.marker, markerLocation); tbMap.map.setMarkerPosition(location.marker, markerLocation);
} }
@ -328,7 +431,9 @@ export default class TbMapWidget {
lng = lngData[lngData.length-1][1]; lng = lngData[lngData.length-1][1];
latLng = tbMap.map.createLatLng(lat, lng); latLng = tbMap.map.createLatLng(lat, lng);
if (!location.marker) { 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); tbMap.markers.push(location.marker);
locationChanged = true; locationChanged = true;
} else { } else {
@ -345,8 +450,12 @@ export default class TbMapWidget {
return locationChanged; return locationChanged;
} }
function toLabelValueMap(data) { function toLabelValueMap(data, datasources) {
var dataMap = {}; var dataMap = {};
var dsDataMap = [];
for (var d=0;d<datasources.length;d++) {
dsDataMap[d] = {};
}
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var dataKey = data[i].dataKey; var dataKey = data[i].dataKey;
var label = dataKey.label; var label = dataKey.label;
@ -356,30 +465,44 @@ export default class TbMapWidget {
val = keyData[keyData.length-1][1]; val = keyData[keyData.length-1][1];
} }
dataMap[label] = val; 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(); var bounds = tbMap.map.createBounds();
tbMap.locations = []; tbMap.locations = [];
var dataMap = toLabelValueMap(data); var dataMap = toLabelValueMap(data, datasources);
for (var l=0; l < tbMap.locationsSettings.length; l++) { for (var l=0; l < tbMap.locationsSettings.length; l++) {
var locationSettings = tbMap.locationsSettings[l]; var locationSettings = tbMap.locationsSettings[l];
var latIndex = -1; var latIndex = -1;
var lngIndex = -1; var lngIndex = -1;
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var dataKey = data[i].dataKey; 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; latIndex = i;
} else if (dataKey.label === locationSettings.lngKeyName) { } else if (nameToCheck === locationSettings.lngKeyName) {
lngIndex = i; lngIndex = i;
} }
} }
if (latIndex > -1 && lngIndex > -1) { if (latIndex > -1 && lngIndex > -1) {
var ds = data[latIndex].datasource;
var dsIndex = datasources.indexOf(ds);
var location = { var location = {
latIndex: latIndex, latIndex: latIndex,
lngIndex: lngIndex, lngIndex: lngIndex,
dsIndex: dsIndex,
settings: locationSettings settings: locationSettings
}; };
tbMap.locations.push(location); tbMap.locations.push(location);
@ -394,10 +517,10 @@ export default class TbMapWidget {
tbMap.map.fitBounds(bounds); tbMap.map.fitBounds(bounds);
} }
function updateLocations(data) { function updateLocations(data, datasources) {
var locationsChanged = false; var locationsChanged = false;
var bounds = tbMap.map.createBounds(); var bounds = tbMap.map.createBounds();
var dataMap = toLabelValueMap(data); var dataMap = toLabelValueMap(data, datasources);
for (var p = 0; p < tbMap.locations.length; p++) { for (var p = 0; p < tbMap.locations.length; p++) {
var location = tbMap.locations[p]; var location = tbMap.locations[p];
locationsChanged |= updateLocation(location, data, dataMap); locationsChanged |= updateLocation(location, data, dataMap);
@ -412,36 +535,36 @@ export default class TbMapWidget {
} }
} }
if (this.map && this.map.inited()) { if (this.map && this.map.inited() && this.subscription) {
if (this.ctx.data) { if (this.subscription.data) {
if (!this.locations) { if (!this.locations) {
loadLocations(this.ctx.data); loadLocations(this.subscription.data, this.subscription.datasources);
} else { } else {
updateLocations(this.ctx.data); updateLocations(this.subscription.data, this.subscription.datasources);
} }
} var tooltips = this.map.getTooltips();
var tooltips = this.map.getTooltips(); for (var t=0; t < tooltips.length; t++) {
for (var t=0; t < tooltips.length; t++) { var tooltip = tooltips[t];
var tooltip = tooltips[t]; var text = tooltip.pattern;
var text = tooltip.pattern; var replaceInfo = tooltip.replaceInfo;
var replaceInfo = tooltip.replaceInfo; for (var v = 0; v < replaceInfo.variables.length; v++) {
for (var v = 0; v < replaceInfo.variables.length; v++) { var variableInfo = replaceInfo.variables[v];
var variableInfo = replaceInfo.variables[v]; var txtVal = '';
var txtVal = ''; if (variableInfo.dataKeyIndex > -1 && this.subscription.data[variableInfo.dataKeyIndex]) {
if (variableInfo.dataKeyIndex > -1) { var varData = this.subscription.data[variableInfo.dataKeyIndex].data;
var varData = this.ctx.data[variableInfo.dataKeyIndex].data; if (varData.length > 0) {
if (varData.length > 0) { var val = varData[varData.length - 1][1];
var val = varData[varData.length - 1][1]; if (isNumber(val)) {
if (isNumber(val)) { txtVal = padValue(val, variableInfo.valDec, 0);
txtVal = padValue(val, variableInfo.valDec, 0); } else {
} else { txtVal = val;
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; testImage.src = image;
} }
createMarker(location, settings) { createMarker(location, settings, onClickListener) {
var height = 34; var height = 34;
var pinColor = settings.color.substr(1); var pinColor = settings.color.substr(1);
var icon = L.icon({ var icon = L.icon({
@ -111,9 +111,17 @@ export default class TbOpenStreetMap {
this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
if (onClickListener) {
marker.on('click', onClickListener);
}
return marker; return marker;
} }
removeMarker(marker) {
this.map.removeLayer(marker);
}
createTooltip(marker, pattern, replaceInfo) { createTooltip(marker, pattern, replaceInfo) {
var popup = L.popup(); var popup = L.popup();
popup.setContent(''); popup.setContent('');
@ -145,6 +153,10 @@ export default class TbOpenStreetMap {
return polyline; return polyline;
} }
removePolyline(polyline) {
this.map.removeLayer(polyline);
}
fitBounds(bounds) { fitBounds(bounds) {
if (bounds.isValid()) { if (bounds.isValid()) {
if (this.dontFitMapBounds && this.defaultZoomLevel) { 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 * Flow
***********************/ ***********************/

Loading…
Cancel
Save