Commit 876be70c79c0ee6c50081e167600ac0e6c508ae7

Authored by Igor Kulikov
1 parent ea32b2d2

TB-50: Public dashboards feature.

Showing 60 changed files with 2462 additions and 844 deletions

Too many changes to show.

To preserve performance only 60 of 69 files are displayed.

@@ -39,6 +39,7 @@ import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvi @@ -39,6 +39,7 @@ import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvi
39 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; 39 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
40 import org.thingsboard.server.service.security.auth.jwt.*; 40 import org.thingsboard.server.service.security.auth.jwt.*;
41 import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; 41 import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
  42 +import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
42 43
43 import java.util.ArrayList; 44 import java.util.ArrayList;
44 import java.util.Arrays; 45 import java.util.Arrays;
@@ -56,6 +57,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -56,6 +57,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
56 public static final String WEBJARS_ENTRY_POINT = "/webjars/**"; 57 public static final String WEBJARS_ENTRY_POINT = "/webjars/**";
57 public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; 58 public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
58 public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; 59 public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
  60 + public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
59 public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; 61 public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
60 public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; 62 public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};
61 public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; 63 public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
@@ -88,9 +90,17 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -88,9 +90,17 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
88 } 90 }
89 91
90 @Bean 92 @Bean
  93 + protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception {
  94 + RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
  95 + filter.setAuthenticationManager(this.authenticationManager);
  96 + return filter;
  97 + }
  98 +
  99 + @Bean
91 protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { 100 protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
92 List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); 101 List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
93 - 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)); 102 + pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
  103 + PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
94 SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); 104 SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
95 JwtTokenAuthenticationProcessingFilter filter 105 JwtTokenAuthenticationProcessingFilter filter
96 = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); 106 = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@@ -146,6 +156,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -146,6 +156,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
146 .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars 156 .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
147 .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API 157 .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
148 .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point 158 .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
  159 + .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
149 .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point 160 .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
150 .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points 161 .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
151 .and() 162 .and()
@@ -156,6 +167,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -156,6 +167,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
156 .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler) 167 .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
157 .and() 168 .and()
158 .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) 169 .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
  170 + .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
159 .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) 171 .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
160 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) 172 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
161 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); 173 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
@@ -36,6 +36,7 @@ import org.thingsboard.server.exception.ThingsboardException; @@ -36,6 +36,7 @@ import org.thingsboard.server.exception.ThingsboardException;
36 import org.thingsboard.server.service.mail.MailService; 36 import org.thingsboard.server.service.mail.MailService;
37 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; 37 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
38 import org.thingsboard.server.service.security.model.SecurityUser; 38 import org.thingsboard.server.service.security.model.SecurityUser;
  39 +import org.thingsboard.server.service.security.model.UserPrincipal;
39 import org.thingsboard.server.service.security.model.token.JwtToken; 40 import org.thingsboard.server.service.security.model.token.JwtToken;
40 import org.thingsboard.server.service.security.model.token.JwtTokenFactory; 41 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
41 42
@@ -167,7 +168,8 @@ public class AuthController extends BaseController { @@ -167,7 +168,8 @@ public class AuthController extends BaseController {
167 String encodedPassword = passwordEncoder.encode(password); 168 String encodedPassword = passwordEncoder.encode(password);
168 UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword); 169 UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
169 User user = userService.findUserById(credentials.getUserId()); 170 User user = userService.findUserById(credentials.getUserId());
170 - SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled()); 171 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
  172 + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
171 String baseUrl = constructBaseUrl(request); 173 String baseUrl = constructBaseUrl(request);
172 String loginUrl = String.format("%s/login", baseUrl); 174 String loginUrl = String.format("%s/login", baseUrl);
173 String email = user.getEmail(); 175 String email = user.getEmail();
@@ -201,7 +203,8 @@ public class AuthController extends BaseController { @@ -201,7 +203,8 @@ public class AuthController extends BaseController {
201 userCredentials.setResetToken(null); 203 userCredentials.setResetToken(null);
202 userCredentials = userService.saveUserCredentials(userCredentials); 204 userCredentials = userService.saveUserCredentials(userCredentials);
203 User user = userService.findUserById(userCredentials.getUserId()); 205 User user = userService.findUserById(userCredentials.getUserId());
204 - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); 206 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
  207 + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
205 String baseUrl = constructBaseUrl(request); 208 String baseUrl = constructBaseUrl(request);
206 String loginUrl = String.format("%s/login", baseUrl); 209 String loginUrl = String.format("%s/login", baseUrl);
207 String email = user.getEmail(); 210 String email = user.getEmail();
@@ -15,6 +15,9 @@ @@ -15,6 +15,9 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 import org.springframework.http.HttpStatus; 21 import org.springframework.http.HttpStatus;
19 import org.springframework.security.access.prepost.PreAuthorize; 22 import org.springframework.security.access.prepost.PreAuthorize;
20 import org.springframework.web.bind.annotation.*; 23 import org.springframework.web.bind.annotation.*;
@@ -43,14 +46,22 @@ public class CustomerController extends BaseController { @@ -43,14 +46,22 @@ public class CustomerController extends BaseController {
43 } 46 }
44 47
45 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 48 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
46 - @RequestMapping(value = "/customer/{customerId}/title", method = RequestMethod.GET, produces = "application/text") 49 + @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET)
47 @ResponseBody 50 @ResponseBody
48 - public String getCustomerTitleById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException { 51 + public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
49 checkParameter("customerId", strCustomerId); 52 checkParameter("customerId", strCustomerId);
50 try { 53 try {
51 CustomerId customerId = new CustomerId(toUUID(strCustomerId)); 54 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
52 Customer customer = checkCustomerId(customerId); 55 Customer customer = checkCustomerId(customerId);
53 - return customer.getTitle(); 56 + ObjectMapper objectMapper = new ObjectMapper();
  57 + ObjectNode infoObject = objectMapper.createObjectNode();
  58 + infoObject.put("title", customer.getTitle());
  59 + boolean isPublic = false;
  60 + if (customer.getAdditionalInfo() != null && customer.getAdditionalInfo().has("isPublic")) {
  61 + isPublic = customer.getAdditionalInfo().get("isPublic").asBoolean();
  62 + }
  63 + infoObject.put("isPublic", isPublic);
  64 + return infoObject;
54 } catch (Exception e) { 65 } catch (Exception e) {
55 throw handleException(e); 66 throw handleException(e);
56 } 67 }
@@ -18,6 +18,7 @@ package org.thingsboard.server.controller; @@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
18 import org.springframework.http.HttpStatus; 18 import org.springframework.http.HttpStatus;
19 import org.springframework.security.access.prepost.PreAuthorize; 19 import org.springframework.security.access.prepost.PreAuthorize;
20 import org.springframework.web.bind.annotation.*; 20 import org.springframework.web.bind.annotation.*;
  21 +import org.thingsboard.server.common.data.Customer;
21 import org.thingsboard.server.common.data.Dashboard; 22 import org.thingsboard.server.common.data.Dashboard;
22 import org.thingsboard.server.common.data.DashboardInfo; 23 import org.thingsboard.server.common.data.DashboardInfo;
23 import org.thingsboard.server.common.data.id.CustomerId; 24 import org.thingsboard.server.common.data.id.CustomerId;
@@ -117,6 +118,21 @@ public class DashboardController extends BaseController { @@ -117,6 +118,21 @@ public class DashboardController extends BaseController {
117 } 118 }
118 119
119 @PreAuthorize("hasAuthority('TENANT_ADMIN')") 120 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  121 + @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST)
  122 + @ResponseBody
  123 + public Dashboard assignDashboardToPublicCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
  124 + checkParameter("dashboardId", strDashboardId);
  125 + try {
  126 + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
  127 + Dashboard dashboard = checkDashboardId(dashboardId);
  128 + Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
  129 + return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
  130 + } catch (Exception e) {
  131 + throw handleException(e);
  132 + }
  133 + }
  134 +
  135 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
120 @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) 136 @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
121 @ResponseBody 137 @ResponseBody
122 public TextPageData<DashboardInfo> getTenantDashboards( 138 public TextPageData<DashboardInfo> getTenantDashboards(
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
19 import org.springframework.http.HttpStatus; 19 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 20 import org.springframework.security.access.prepost.PreAuthorize;
21 import org.springframework.web.bind.annotation.*; 21 import org.springframework.web.bind.annotation.*;
  22 +import org.thingsboard.server.common.data.Customer;
22 import org.thingsboard.server.common.data.Device; 23 import org.thingsboard.server.common.data.Device;
23 import org.thingsboard.server.common.data.id.CustomerId; 24 import org.thingsboard.server.common.data.id.CustomerId;
24 import org.thingsboard.server.common.data.id.DeviceId; 25 import org.thingsboard.server.common.data.id.DeviceId;
@@ -117,6 +118,21 @@ public class DeviceController extends BaseController { @@ -117,6 +118,21 @@ public class DeviceController extends BaseController {
117 } 118 }
118 } 119 }
119 120
  121 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  122 + @RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST)
  123 + @ResponseBody
  124 + public Device assignDeviceToPublicCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
  125 + checkParameter("deviceId", strDeviceId);
  126 + try {
  127 + DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
  128 + Device device = checkDeviceId(deviceId);
  129 + Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId());
  130 + return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
  131 + } catch (Exception e) {
  132 + throw handleException(e);
  133 + }
  134 + }
  135 +
120 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 136 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
121 @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET) 137 @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET)
122 @ResponseBody 138 @ResponseBody
@@ -16,32 +16,40 @@ @@ -16,32 +16,40 @@
16 package org.thingsboard.server.service.security.auth.jwt; 16 package org.thingsboard.server.service.security.auth.jwt;
17 17
18 import org.springframework.beans.factory.annotation.Autowired; 18 import org.springframework.beans.factory.annotation.Autowired;
19 -import org.springframework.security.authentication.AuthenticationProvider;  
20 -import org.springframework.security.authentication.DisabledException;  
21 -import org.springframework.security.authentication.InsufficientAuthenticationException;  
22 -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 19 +import org.springframework.security.authentication.*;
23 import org.springframework.security.core.Authentication; 20 import org.springframework.security.core.Authentication;
24 import org.springframework.security.core.AuthenticationException; 21 import org.springframework.security.core.AuthenticationException;
25 import org.springframework.security.core.userdetails.UsernameNotFoundException; 22 import org.springframework.security.core.userdetails.UsernameNotFoundException;
26 import org.springframework.stereotype.Component; 23 import org.springframework.stereotype.Component;
27 import org.springframework.util.Assert; 24 import org.springframework.util.Assert;
  25 +import org.thingsboard.server.common.data.Customer;
28 import org.thingsboard.server.common.data.User; 26 import org.thingsboard.server.common.data.User;
  27 +import org.thingsboard.server.common.data.id.CustomerId;
  28 +import org.thingsboard.server.common.data.id.UUIDBased;
  29 +import org.thingsboard.server.common.data.id.UserId;
  30 +import org.thingsboard.server.common.data.security.Authority;
29 import org.thingsboard.server.common.data.security.UserCredentials; 31 import org.thingsboard.server.common.data.security.UserCredentials;
  32 +import org.thingsboard.server.dao.customer.CustomerService;
30 import org.thingsboard.server.dao.user.UserService; 33 import org.thingsboard.server.dao.user.UserService;
31 import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; 34 import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
32 import org.thingsboard.server.service.security.model.SecurityUser; 35 import org.thingsboard.server.service.security.model.SecurityUser;
  36 +import org.thingsboard.server.service.security.model.UserPrincipal;
33 import org.thingsboard.server.service.security.model.token.JwtTokenFactory; 37 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
34 import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; 38 import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
35 39
  40 +import java.util.UUID;
  41 +
36 @Component 42 @Component
37 public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { 43 public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
38 44
39 private final JwtTokenFactory tokenFactory; 45 private final JwtTokenFactory tokenFactory;
40 private final UserService userService; 46 private final UserService userService;
  47 + private final CustomerService customerService;
41 48
42 @Autowired 49 @Autowired
43 - public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) { 50 + public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) {
44 this.userService = userService; 51 this.userService = userService;
  52 + this.customerService = customerService;
45 this.tokenFactory = tokenFactory; 53 this.tokenFactory = tokenFactory;
46 } 54 }
47 55
@@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide @@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
50 Assert.notNull(authentication, "No authentication data provided"); 58 Assert.notNull(authentication, "No authentication data provided");
51 RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); 59 RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
52 SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); 60 SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken);
  61 + UserPrincipal principal = unsafeUser.getUserPrincipal();
  62 + SecurityUser securityUser;
  63 + if (principal.getType() == UserPrincipal.Type.USER_NAME) {
  64 + securityUser = authenticateByUserId(unsafeUser.getId());
  65 + } else {
  66 + securityUser = authenticateByPublicId(principal.getValue());
  67 + }
  68 + return new RefreshAuthenticationToken(securityUser);
  69 + }
53 70
54 - User user = userService.findUserById(unsafeUser.getId()); 71 + private SecurityUser authenticateByUserId(UserId userId) {
  72 + User user = userService.findUserById(userId);
55 if (user == null) { 73 if (user == null) {
56 throw new UsernameNotFoundException("User not found by refresh token"); 74 throw new UsernameNotFoundException("User not found by refresh token");
57 } 75 }
@@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide @@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
67 85
68 if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); 86 if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
69 87
70 - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); 88 + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
71 89
72 - return new RefreshAuthenticationToken(securityUser); 90 + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
  91 +
  92 + return securityUser;
  93 + }
  94 +
  95 + private SecurityUser authenticateByPublicId(String publicId) {
  96 + CustomerId customerId;
  97 + try {
  98 + customerId = new CustomerId(UUID.fromString(publicId));
  99 + } catch (Exception e) {
  100 + throw new BadCredentialsException("Refresh token is not valid");
  101 + }
  102 + Customer publicCustomer = customerService.findCustomerById(customerId);
  103 + if (publicCustomer == null) {
  104 + throw new UsernameNotFoundException("Public entity not found by refresh token");
  105 + }
  106 + boolean isPublic = false;
  107 + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) {
  108 + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean();
  109 + }
  110 + if (!isPublic) {
  111 + throw new BadCredentialsException("Refresh token is not valid");
  112 + }
  113 + User user = new User(new UserId(UUIDBased.EMPTY));
  114 + user.setTenantId(publicCustomer.getTenantId());
  115 + user.setCustomerId(publicCustomer.getId());
  116 + user.setEmail(publicId);
  117 + user.setAuthority(Authority.CUSTOMER_USER);
  118 + user.setFirstName("Public");
  119 + user.setLastName("Public");
  120 +
  121 + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId);
  122 +
  123 + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
  124 +
  125 + return securityUser;
73 } 126 }
74 127
75 @Override 128 @Override
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.rest;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonCreator;
  19 +import com.fasterxml.jackson.annotation.JsonProperty;
  20 +
  21 +public class PublicLoginRequest {
  22 +
  23 + private String publicId;
  24 +
  25 + @JsonCreator
  26 + public PublicLoginRequest(@JsonProperty("publicId") String publicId) {
  27 + this.publicId = publicId;
  28 + }
  29 +
  30 + public String getPublicId() {
  31 + return publicId;
  32 + }
  33 +
  34 +}
@@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
23 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 23 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
24 import org.springframework.stereotype.Component; 24 import org.springframework.stereotype.Component;
25 import org.springframework.util.Assert; 25 import org.springframework.util.Assert;
  26 +import org.thingsboard.server.common.data.Customer;
26 import org.thingsboard.server.common.data.User; 27 import org.thingsboard.server.common.data.User;
  28 +import org.thingsboard.server.common.data.id.CustomerId;
  29 +import org.thingsboard.server.common.data.id.UUIDBased;
  30 +import org.thingsboard.server.common.data.id.UserId;
  31 +import org.thingsboard.server.common.data.security.Authority;
27 import org.thingsboard.server.common.data.security.UserCredentials; 32 import org.thingsboard.server.common.data.security.UserCredentials;
  33 +import org.thingsboard.server.dao.customer.CustomerService;
28 import org.thingsboard.server.dao.user.UserService; 34 import org.thingsboard.server.dao.user.UserService;
29 import org.thingsboard.server.service.security.model.SecurityUser; 35 import org.thingsboard.server.service.security.model.SecurityUser;
  36 +import org.thingsboard.server.service.security.model.UserPrincipal;
  37 +
  38 +import java.util.UUID;
30 39
31 @Component 40 @Component
32 public class RestAuthenticationProvider implements AuthenticationProvider { 41 public class RestAuthenticationProvider implements AuthenticationProvider {
33 42
34 private final BCryptPasswordEncoder encoder; 43 private final BCryptPasswordEncoder encoder;
35 private final UserService userService; 44 private final UserService userService;
  45 + private final CustomerService customerService;
36 46
37 @Autowired 47 @Autowired
38 - public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) { 48 + public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) {
39 this.userService = userService; 49 this.userService = userService;
  50 + this.customerService = customerService;
40 this.encoder = encoder; 51 this.encoder = encoder;
41 } 52 }
42 53
@@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider { @@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
44 public Authentication authenticate(Authentication authentication) throws AuthenticationException { 55 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
45 Assert.notNull(authentication, "No authentication data provided"); 56 Assert.notNull(authentication, "No authentication data provided");
46 57
47 - String username = (String) authentication.getPrincipal();  
48 - String password = (String) authentication.getCredentials(); 58 + Object principal = authentication.getPrincipal();
  59 + if (!(principal instanceof UserPrincipal)) {
  60 + throw new BadCredentialsException("Authentication Failed. Bad user principal.");
  61 + }
49 62
  63 + UserPrincipal userPrincipal = (UserPrincipal) principal;
  64 + if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
  65 + String username = userPrincipal.getValue();
  66 + String password = (String) authentication.getCredentials();
  67 + return authenticateByUsernameAndPassword(userPrincipal, username, password);
  68 + } else {
  69 + String publicId = userPrincipal.getValue();
  70 + return authenticateByPublicId(userPrincipal, publicId);
  71 + }
  72 + }
  73 +
  74 + private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) {
50 User user = userService.findUserByEmail(username); 75 User user = userService.findUserByEmail(username);
51 if (user == null) { 76 if (user == null) {
52 throw new UsernameNotFoundException("User not found: " + username); 77 throw new UsernameNotFoundException("User not found: " + username);
@@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider { @@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
67 92
68 if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); 93 if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
69 94
70 - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); 95 + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
  96 +
  97 + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
  98 + }
  99 +
  100 + private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
  101 + CustomerId customerId;
  102 + try {
  103 + customerId = new CustomerId(UUID.fromString(publicId));
  104 + } catch (Exception e) {
  105 + throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
  106 + }
  107 + Customer publicCustomer = customerService.findCustomerById(customerId);
  108 + if (publicCustomer == null) {
  109 + throw new UsernameNotFoundException("Public entity not found: " + publicId);
  110 + }
  111 + boolean isPublic = false;
  112 + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) {
  113 + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean();
  114 + }
  115 + if (!isPublic) {
  116 + throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
  117 + }
  118 + User user = new User(new UserId(UUIDBased.EMPTY));
  119 + user.setTenantId(publicCustomer.getTenantId());
  120 + user.setCustomerId(publicCustomer.getId());
  121 + user.setEmail(publicId);
  122 + user.setAuthority(Authority.CUSTOMER_USER);
  123 + user.setFirstName("Public");
  124 + user.setLastName("Public");
  125 +
  126 + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
71 127
72 return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); 128 return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
73 } 129 }
@@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro @@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
29 import org.springframework.security.web.authentication.AuthenticationFailureHandler; 29 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
30 import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 30 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
31 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; 31 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
32 33
33 import javax.servlet.FilterChain; 34 import javax.servlet.FilterChain;
34 import javax.servlet.ServletException; 35 import javax.servlet.ServletException;
@@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF @@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
73 throw new AuthenticationServiceException("Username or Password not provided"); 74 throw new AuthenticationServiceException("Username or Password not provided");
74 } 75 }
75 76
76 - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); 77 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername());
  78 +
  79 + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword());
77 80
78 return this.getAuthenticationManager().authenticate(token); 81 return this.getAuthenticationManager().authenticate(token);
79 } 82 }
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.rest;
  17 +
  18 +import com.fasterxml.jackson.databind.ObjectMapper;
  19 +import org.apache.commons.lang3.StringUtils;
  20 +import org.slf4j.Logger;
  21 +import org.slf4j.LoggerFactory;
  22 +import org.springframework.http.HttpMethod;
  23 +import org.springframework.security.authentication.AuthenticationServiceException;
  24 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  25 +import org.springframework.security.core.Authentication;
  26 +import org.springframework.security.core.AuthenticationException;
  27 +import org.springframework.security.core.context.SecurityContextHolder;
  28 +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
  29 +import org.springframework.security.web.authentication.AuthenticationFailureHandler;
  30 +import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
  31 +import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
  33 +
  34 +import javax.servlet.FilterChain;
  35 +import javax.servlet.ServletException;
  36 +import javax.servlet.http.HttpServletRequest;
  37 +import javax.servlet.http.HttpServletResponse;
  38 +import java.io.IOException;
  39 +
  40 +public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
  41 + private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class);
  42 +
  43 + private final AuthenticationSuccessHandler successHandler;
  44 + private final AuthenticationFailureHandler failureHandler;
  45 +
  46 + private final ObjectMapper objectMapper;
  47 +
  48 + public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
  49 + AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
  50 + super(defaultProcessUrl);
  51 + this.successHandler = successHandler;
  52 + this.failureHandler = failureHandler;
  53 + this.objectMapper = mapper;
  54 + }
  55 +
  56 + @Override
  57 + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
  58 + throws AuthenticationException, IOException, ServletException {
  59 + if (!HttpMethod.POST.name().equals(request.getMethod())) {
  60 + if(logger.isDebugEnabled()) {
  61 + logger.debug("Authentication method not supported. Request method: " + request.getMethod());
  62 + }
  63 + throw new AuthMethodNotSupportedException("Authentication method not supported");
  64 + }
  65 +
  66 + PublicLoginRequest loginRequest;
  67 + try {
  68 + loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class);
  69 + } catch (Exception e) {
  70 + throw new AuthenticationServiceException("Invalid public login request payload");
  71 + }
  72 +
  73 + if (StringUtils.isBlank(loginRequest.getPublicId())) {
  74 + throw new AuthenticationServiceException("Public Id is not provided");
  75 + }
  76 +
  77 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId());
  78 +
  79 + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "");
  80 +
  81 + return this.getAuthenticationManager().authenticate(token);
  82 + }
  83 +
  84 + @Override
  85 + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
  86 + Authentication authResult) throws IOException, ServletException {
  87 + successHandler.onAuthenticationSuccess(request, response, authResult);
  88 + }
  89 +
  90 + @Override
  91 + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
  92 + AuthenticationException failed) throws IOException, ServletException {
  93 + SecurityContextHolder.clearContext();
  94 + failureHandler.onAuthenticationFailure(request, response, failed);
  95 + }
  96 +}
@@ -30,6 +30,7 @@ public class SecurityUser extends User { @@ -30,6 +30,7 @@ public class SecurityUser extends User {
30 30
31 private Collection<GrantedAuthority> authorities; 31 private Collection<GrantedAuthority> authorities;
32 private boolean enabled; 32 private boolean enabled;
  33 + private UserPrincipal userPrincipal;
33 34
34 public SecurityUser() { 35 public SecurityUser() {
35 super(); 36 super();
@@ -39,9 +40,10 @@ public class SecurityUser extends User { @@ -39,9 +40,10 @@ public class SecurityUser extends User {
39 super(id); 40 super(id);
40 } 41 }
41 42
42 - public SecurityUser(User user, boolean enabled) { 43 + public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) {
43 super(user); 44 super(user);
44 this.enabled = enabled; 45 this.enabled = enabled;
  46 + this.userPrincipal = userPrincipal;
45 } 47 }
46 48
47 public Collection<? extends GrantedAuthority> getAuthorities() { 49 public Collection<? extends GrantedAuthority> getAuthorities() {
@@ -57,8 +59,16 @@ public class SecurityUser extends User { @@ -57,8 +59,16 @@ public class SecurityUser extends User {
57 return enabled; 59 return enabled;
58 } 60 }
59 61
  62 + public UserPrincipal getUserPrincipal() {
  63 + return userPrincipal;
  64 + }
  65 +
60 public void setEnabled(boolean enabled) { 66 public void setEnabled(boolean enabled) {
61 this.enabled = enabled; 67 this.enabled = enabled;
62 } 68 }
63 69
  70 + public void setUserPrincipal(UserPrincipal userPrincipal) {
  71 + this.userPrincipal = userPrincipal;
  72 + }
  73 +
64 } 74 }
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package org.thingsboard.server.service.security.model;
  18 +
  19 +public class UserPrincipal {
  20 +
  21 + private final Type type;
  22 + private final String value;
  23 +
  24 + public UserPrincipal(Type type, String value) {
  25 + this.type = type;
  26 + this.value = value;
  27 + }
  28 +
  29 + public Type getType() {
  30 + return type;
  31 + }
  32 +
  33 + public String getValue() {
  34 + return value;
  35 + }
  36 +
  37 + public enum Type {
  38 + USER_NAME,
  39 + PUBLIC_ID
  40 + }
  41 +
  42 +}
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId; @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId;
29 import org.thingsboard.server.common.data.security.Authority; 29 import org.thingsboard.server.common.data.security.Authority;
30 import org.thingsboard.server.config.JwtSettings; 30 import org.thingsboard.server.config.JwtSettings;
31 import org.thingsboard.server.service.security.model.SecurityUser; 31 import org.thingsboard.server.service.security.model.SecurityUser;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
32 33
33 import java.util.Arrays; 34 import java.util.Arrays;
34 import java.util.List; 35 import java.util.List;
@@ -43,6 +44,7 @@ public class JwtTokenFactory { @@ -43,6 +44,7 @@ public class JwtTokenFactory {
43 private static final String FIRST_NAME = "firstName"; 44 private static final String FIRST_NAME = "firstName";
44 private static final String LAST_NAME = "lastName"; 45 private static final String LAST_NAME = "lastName";
45 private static final String ENABLED = "enabled"; 46 private static final String ENABLED = "enabled";
  47 + private static final String IS_PUBLIC = "isPublic";
46 private static final String TENANT_ID = "tenantId"; 48 private static final String TENANT_ID = "tenantId";
47 private static final String CUSTOMER_ID = "customerId"; 49 private static final String CUSTOMER_ID = "customerId";
48 50
@@ -63,12 +65,15 @@ public class JwtTokenFactory { @@ -63,12 +65,15 @@ public class JwtTokenFactory {
63 if (securityUser.getAuthority() == null) 65 if (securityUser.getAuthority() == null)
64 throw new IllegalArgumentException("User doesn't have any privileges"); 66 throw new IllegalArgumentException("User doesn't have any privileges");
65 67
66 - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); 68 + UserPrincipal principal = securityUser.getUserPrincipal();
  69 + String subject = principal.getValue();
  70 + Claims claims = Jwts.claims().setSubject(subject);
67 claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList())); 71 claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
68 claims.put(USER_ID, securityUser.getId().getId().toString()); 72 claims.put(USER_ID, securityUser.getId().getId().toString());
69 claims.put(FIRST_NAME, securityUser.getFirstName()); 73 claims.put(FIRST_NAME, securityUser.getFirstName());
70 claims.put(LAST_NAME, securityUser.getLastName()); 74 claims.put(LAST_NAME, securityUser.getLastName());
71 claims.put(ENABLED, securityUser.isEnabled()); 75 claims.put(ENABLED, securityUser.isEnabled());
  76 + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
72 if (securityUser.getTenantId() != null) { 77 if (securityUser.getTenantId() != null) {
73 claims.put(TENANT_ID, securityUser.getTenantId().getId().toString()); 78 claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
74 } 79 }
@@ -104,6 +109,9 @@ public class JwtTokenFactory { @@ -104,6 +109,9 @@ public class JwtTokenFactory {
104 securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); 109 securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
105 securityUser.setLastName(claims.get(LAST_NAME, String.class)); 110 securityUser.setLastName(claims.get(LAST_NAME, String.class));
106 securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); 111 securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
  112 + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
  113 + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
  114 + securityUser.setUserPrincipal(principal);
107 String tenantId = claims.get(TENANT_ID, String.class); 115 String tenantId = claims.get(TENANT_ID, String.class);
108 if (tenantId != null) { 116 if (tenantId != null) {
109 securityUser.setTenantId(new TenantId(UUID.fromString(tenantId))); 117 securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
@@ -123,9 +131,11 @@ public class JwtTokenFactory { @@ -123,9 +131,11 @@ public class JwtTokenFactory {
123 131
124 DateTime currentTime = new DateTime(); 132 DateTime currentTime = new DateTime();
125 133
126 - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); 134 + UserPrincipal principal = securityUser.getUserPrincipal();
  135 + Claims claims = Jwts.claims().setSubject(principal.getValue());
127 claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name())); 136 claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name()));
128 claims.put(USER_ID, securityUser.getId().getId().toString()); 137 claims.put(USER_ID, securityUser.getId().getId().toString());
  138 + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
129 139
130 String token = Jwts.builder() 140 String token = Jwts.builder()
131 .setClaims(claims) 141 .setClaims(claims)
@@ -150,8 +160,10 @@ public class JwtTokenFactory { @@ -150,8 +160,10 @@ public class JwtTokenFactory {
150 if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) { 160 if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
151 throw new IllegalArgumentException("Invalid Refresh Token scope"); 161 throw new IllegalArgumentException("Invalid Refresh Token scope");
152 } 162 }
  163 + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
  164 + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
153 SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); 165 SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
154 - securityUser.setEmail(subject); 166 + securityUser.setUserPrincipal(principal);
155 return securityUser; 167 return securityUser;
156 } 168 }
157 169
@@ -16,12 +16,14 @@ @@ -16,12 +16,14 @@
16 package org.thingsboard.server.dao.customer; 16 package org.thingsboard.server.dao.customer;
17 17
18 import java.util.List; 18 import java.util.List;
  19 +import java.util.Optional;
19 import java.util.UUID; 20 import java.util.UUID;
20 21
21 import org.thingsboard.server.common.data.Customer; 22 import org.thingsboard.server.common.data.Customer;
22 import org.thingsboard.server.common.data.page.TextPageLink; 23 import org.thingsboard.server.common.data.page.TextPageLink;
23 import org.thingsboard.server.dao.Dao; 24 import org.thingsboard.server.dao.Dao;
24 import org.thingsboard.server.dao.model.CustomerEntity; 25 import org.thingsboard.server.dao.model.CustomerEntity;
  26 +import org.thingsboard.server.dao.model.DeviceEntity;
25 27
26 /** 28 /**
27 * The Interface CustomerDao. 29 * The Interface CustomerDao.
@@ -44,5 +46,14 @@ public interface CustomerDao extends Dao<CustomerEntity> { @@ -44,5 +46,14 @@ public interface CustomerDao extends Dao<CustomerEntity> {
44 * @return the list of customer objects 46 * @return the list of customer objects
45 */ 47 */
46 List<CustomerEntity> findCustomersByTenantId(UUID tenantId, TextPageLink pageLink); 48 List<CustomerEntity> findCustomersByTenantId(UUID tenantId, TextPageLink pageLink);
  49 +
  50 + /**
  51 + * Find customers by tenantId and customer title.
  52 + *
  53 + * @param tenantId the tenantId
  54 + * @param title the customer title
  55 + * @return the optional customer object
  56 + */
  57 + Optional<CustomerEntity> findCustomersByTenantIdAndTitle(UUID tenantId, String title);
47 58
48 } 59 }
@@ -16,11 +16,18 @@ @@ -16,11 +16,18 @@
16 package org.thingsboard.server.dao.customer; 16 package org.thingsboard.server.dao.customer;
17 17
18 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; 18 import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
  19 +import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
  20 +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME;
  21 +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
  22 +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
  23 +
19 24
20 import java.util.Arrays; 25 import java.util.Arrays;
21 import java.util.List; 26 import java.util.List;
  27 +import java.util.Optional;
22 import java.util.UUID; 28 import java.util.UUID;
23 29
  30 +import com.datastax.driver.core.querybuilder.Select;
24 import lombok.extern.slf4j.Slf4j; 31 import lombok.extern.slf4j.Slf4j;
25 import org.springframework.stereotype.Component; 32 import org.springframework.stereotype.Component;
26 import org.thingsboard.server.common.data.Customer; 33 import org.thingsboard.server.common.data.Customer;
@@ -60,4 +67,13 @@ public class CustomerDaoImpl extends AbstractSearchTextDao<CustomerEntity> imple @@ -60,4 +67,13 @@ public class CustomerDaoImpl extends AbstractSearchTextDao<CustomerEntity> imple
60 return customerEntities; 67 return customerEntities;
61 } 68 }
62 69
  70 + @Override
  71 + public Optional<CustomerEntity> findCustomersByTenantIdAndTitle(UUID tenantId, String title) {
  72 + Select select = select().from(CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME);
  73 + Select.Where query = select.where();
  74 + query.and(eq(CUSTOMER_TENANT_ID_PROPERTY, tenantId));
  75 + query.and(eq(CUSTOMER_TITLE_PROPERTY, title));
  76 + return Optional.ofNullable(findOneByStatement(query));
  77 + }
  78 +
63 } 79 }
@@ -28,6 +28,8 @@ public interface CustomerService { @@ -28,6 +28,8 @@ public interface CustomerService {
28 public Customer saveCustomer(Customer customer); 28 public Customer saveCustomer(Customer customer);
29 29
30 public void deleteCustomer(CustomerId customerId); 30 public void deleteCustomer(CustomerId customerId);
  31 +
  32 + public Customer findOrCreatePublicCustomer(TenantId tenantId);
31 33
32 public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink); 34 public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink);
33 35
@@ -18,8 +18,12 @@ package org.thingsboard.server.dao.customer; @@ -18,8 +18,12 @@ package org.thingsboard.server.dao.customer;
18 import static org.thingsboard.server.dao.DaoUtil.convertDataList; 18 import static org.thingsboard.server.dao.DaoUtil.convertDataList;
19 import static org.thingsboard.server.dao.DaoUtil.getData; 19 import static org.thingsboard.server.dao.DaoUtil.getData;
20 20
  21 +import java.io.IOException;
21 import java.util.List; 22 import java.util.List;
  23 +import java.util.Optional;
22 24
  25 +import com.fasterxml.jackson.databind.JsonNode;
  26 +import com.fasterxml.jackson.databind.ObjectMapper;
23 import lombok.extern.slf4j.Slf4j; 27 import lombok.extern.slf4j.Slf4j;
24 import org.apache.commons.lang3.StringUtils; 28 import org.apache.commons.lang3.StringUtils;
25 import org.thingsboard.server.common.data.Customer; 29 import org.thingsboard.server.common.data.Customer;
@@ -46,6 +50,8 @@ import org.thingsboard.server.dao.service.Validator; @@ -46,6 +50,8 @@ import org.thingsboard.server.dao.service.Validator;
46 @Slf4j 50 @Slf4j
47 public class CustomerServiceImpl implements CustomerService { 51 public class CustomerServiceImpl implements CustomerService {
48 52
  53 + private static final String PUBLIC_CUSTOMER_TITLE = "Public";
  54 +
49 @Autowired 55 @Autowired
50 private CustomerDao customerDao; 56 private CustomerDao customerDao;
51 57
@@ -80,7 +86,7 @@ public class CustomerServiceImpl implements CustomerService { @@ -80,7 +86,7 @@ public class CustomerServiceImpl implements CustomerService {
80 @Override 86 @Override
81 public void deleteCustomer(CustomerId customerId) { 87 public void deleteCustomer(CustomerId customerId) {
82 log.trace("Executing deleteCustomer [{}]", customerId); 88 log.trace("Executing deleteCustomer [{}]", customerId);
83 - Validator.validateId(customerId, "Incorrect tenantId " + customerId); 89 + Validator.validateId(customerId, "Incorrect customerId " + customerId);
84 Customer customer = findCustomerById(customerId); 90 Customer customer = findCustomerById(customerId);
85 if (customer == null) { 91 if (customer == null) {
86 throw new IncorrectParameterException("Unable to delete non-existent customer."); 92 throw new IncorrectParameterException("Unable to delete non-existent customer.");
@@ -92,6 +98,27 @@ public class CustomerServiceImpl implements CustomerService { @@ -92,6 +98,27 @@ public class CustomerServiceImpl implements CustomerService {
92 } 98 }
93 99
94 @Override 100 @Override
  101 + public Customer findOrCreatePublicCustomer(TenantId tenantId) {
  102 + log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
  103 + Validator.validateId(tenantId, "Incorrect customerId " + tenantId);
  104 + Optional<CustomerEntity> publicCustomerEntity = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE);
  105 + if (publicCustomerEntity.isPresent()) {
  106 + return getData(publicCustomerEntity.get());
  107 + } else {
  108 + Customer publicCustomer = new Customer();
  109 + publicCustomer.setTenantId(tenantId);
  110 + publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
  111 + try {
  112 + publicCustomer.setAdditionalInfo(new ObjectMapper().readValue("{ \"isPublic\": true }", JsonNode.class));
  113 + } catch (IOException e) {
  114 + throw new IncorrectParameterException("Unable to create public customer.", e);
  115 + }
  116 + CustomerEntity customerEntity = customerDao.save(publicCustomer);
  117 + return getData(customerEntity);
  118 + }
  119 + }
  120 +
  121 + @Override
95 public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) { 122 public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) {
96 log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); 123 log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
97 Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); 124 Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
@@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService { @@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService {
110 137
111 private DataValidator<Customer> customerValidator = 138 private DataValidator<Customer> customerValidator =
112 new DataValidator<Customer>() { 139 new DataValidator<Customer>() {
  140 +
  141 + @Override
  142 + protected void validateCreate(Customer customer) {
  143 + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
  144 + c -> {
  145 + throw new DataValidationException("Customer with such title already exists!");
  146 + }
  147 + );
  148 + }
  149 +
  150 + @Override
  151 + protected void validateUpdate(Customer customer) {
  152 + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
  153 + c -> {
  154 + if (!c.getId().equals(customer.getUuidId())) {
  155 + throw new DataValidationException("Customer with such title already exists!");
  156 + }
  157 + }
  158 + );
  159 + }
  160 +
113 @Override 161 @Override
114 protected void validateDataImpl(Customer customer) { 162 protected void validateDataImpl(Customer customer) {
115 if (StringUtils.isEmpty(customer.getTitle())) { 163 if (StringUtils.isEmpty(customer.getTitle())) {
116 throw new DataValidationException("Customer title should be specified!"); 164 throw new DataValidationException("Customer title should be specified!");
117 } 165 }
  166 + if (customer.getTitle().equals(PUBLIC_CUSTOMER_TITLE)) {
  167 + throw new DataValidationException("'Public' title for customer is system reserved!");
  168 + }
118 if (!StringUtils.isEmpty(customer.getEmail())) { 169 if (!StringUtils.isEmpty(customer.getEmail())) {
119 validateEmail(customer.getEmail()); 170 validateEmail(customer.getEmail());
120 } 171 }
@@ -111,6 +111,7 @@ public class ModelConstants { @@ -111,6 +111,7 @@ public class ModelConstants {
111 public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; 111 public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
112 112
113 public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text"; 113 public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text";
  114 + public static final String CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME = "customer_by_tenant_and_title";
114 115
115 /** 116 /**
116 * Cassandra device constants. 117 * Cassandra device constants.
@@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer ( @@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer (
137 PRIMARY KEY (id, tenant_id) 137 PRIMARY KEY (id, tenant_id)
138 ); 138 );
139 139
  140 +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_title AS
  141 + SELECT *
  142 + from thingsboard.customer
  143 + WHERE tenant_id IS NOT NULL AND title IS NOT NULL AND id IS NOT NULL
  144 + PRIMARY KEY ( tenant_id, title, id )
  145 + WITH CLUSTERING ORDER BY ( title ASC, id DESC );
  146 +
140 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS 147 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS
141 SELECT * 148 SELECT *
142 from thingsboard.customer 149 from thingsboard.customer
@@ -80,7 +80,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel', @@ -80,7 +80,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel',
80 80
81 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) 81 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
82 VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table', 82 VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table',
83 -'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<md-tabs md-selected=\"sourceIndex\" ng-class=\"{''tb-headless'': sources.length === 1}\"\n id=\"tabs\" md-border-bottom flex class=\"tb-absolute-fill\">\n <md-tab ng-repeat=\"source in sources\" label=\"{{ source.datasource.name }}\">\n <md-table-container>\n <table md-table>\n <thead md-head md-order=\"source.query.order\" md-on-reorder=\"onReorder(source)\">\n <tr md-row>\n <th ng-show=\"showTimestamp\" md-column md-order-by=\"0\"><span>Timestamp</span></th>\n <th md-column md-order-by=\"{{ h.index }}\" ng-repeat=\"h in source.ts.header\"><span>{{ h.dataKey.label }}</span></th>\n </tr>\n </thead>\n <tbody md-body>\n <tr md-row ng-repeat=\"row in source.ts.data\">\n <td ng-show=\"$index > 0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n </td>\n </tr> \n </tbody> \n </table>\n </md-table-container>\n <md-table-pagination md-limit=\"source.query.limit\" md-limit-options=\"[5, 10, 15]\"\n md-page=\"source.query.page\" md-total=\"{{source.ts.count}}\"\n md-on-paginate=\"onPaginate(source)\" md-page-select>\n </md-table-pagination>\n </md-tab>\n</md-tabs>","templateCss":"table.md-table thead.md-head>tr.md-row {\n height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n height: 38px;\n}\n\n.md-table-pagination>* {\n height: 46px;\n}\n","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n \n self.ctx.filter = scope.$injector.get(\"$filter\");\n\n scope.sources = [];\n scope.sourceIndex = 0;\n scope.showTimestamp = self.ctx.settings.showTimestamp !== false;\n var origColor = self.ctx.widgetConfig.color || ''rgba(0, 0, 0, 0.87)'';\n var defaultColor = tinycolor(origColor);\n var mdDark = defaultColor.setAlpha(0.87).toRgbString();\n var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();\n var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();\n var mdDarkIcon = mdDarkSecondary;\n var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();\n \n var cssString = ''table.md-table th.md-column {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column md-icon.md-sort-icon {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell.md-placeholder {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''.md-table-pagination .buttons md-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination md-select:not([disabled]):focus .md-select-value {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}'';\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = ''ts-table-'' + hashCode(cssString);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, cssString);\n self.ctx.$container.addClass(namespace);\n \n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n var keyOffset = 0;\n for (var ds = 0; ds < self.ctx.datasources.length; ds++) {\n var source = {};\n var datasource = self.ctx.datasources[ds];\n source.keyStartIndex = keyOffset;\n keyOffset += datasource.dataKeys.length;\n source.keyEndIndex = keyOffset;\n source.datasource = datasource;\n source.data = [];\n source.rawData = [];\n source.query = {\n limit: 5,\n page: 1,\n order: ''-0''\n }\n source.ts = {\n header: [],\n count: 0,\n data: [],\n stylesInfo: [],\n contentsInfo: [],\n rowDataTemplate: {}\n }\n source.ts.rowDataTemplate[''Timestamp''] = null;\n for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n var dataKey = datasource.dataKeys[a];\n var keySettings = dataKey.settings;\n source.ts.header.push({\n index: a+1,\n dataKey: dataKey\n });\n source.ts.rowDataTemplate[dataKey.label] = null;\n\n var cellStyleFunction = null;\n var useCellStyleFunction = false;\n \n if (keySettings.useCellStyleFunction === true) {\n if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n try {\n cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n useCellStyleFunction = true;\n } catch (e) {\n cellStyleFunction = null;\n useCellStyleFunction = false;\n }\n }\n }\n\n source.ts.stylesInfo.push({\n useCellStyleFunction: useCellStyleFunction,\n cellStyleFunction: cellStyleFunction\n });\n \n var cellContentFunction = null;\n var useCellContentFunction = false;\n \n if (keySettings.useCellContentFunction === true) {\n if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n try {\n cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n useCellContentFunction = true;\n } catch (e) {\n cellContentFunction = null;\n useCellContentFunction = false;\n }\n }\n }\n \n source.ts.contentsInfo.push({\n useCellContentFunction: useCellContentFunction,\n cellContentFunction: cellContentFunction\n });\n \n }\n scope.sources.push(source);\n }\n\n scope.onPaginate = function(source) {\n updatePage(source);\n }\n \n scope.onReorder = function(source) {\n reorder(source);\n updatePage(source);\n }\n \n scope.cellStyle = function(source, index, value) {\n var style = {};\n if (index > 0) {\n var styleInfo = source.ts.stylesInfo[index-1];\n if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n try {\n style = styleInfo.cellStyleFunction(value);\n } catch (e) {\n style = {};\n }\n }\n }\n return style;\n }\n\n scope.cellContent = function(source, index, row, value) {\n if (index === 0) {\n return self.ctx.filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n } else {\n var strContent = '''';\n if (angular.isDefined(value)) {\n strContent = ''''+value;\n }\n var content = strContent;\n var contentInfo = source.ts.contentsInfo[index-1];\n if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n try {\n var rowData = source.ts.rowDataTemplate;\n rowData[''Timestamp''] = row[0];\n for (var h=0; h < source.ts.header.length; h++) {\n var headerInfo = source.ts.header[h];\n rowData[headerInfo.dataKey.name] = row[headerInfo.index];\n }\n content = contentInfo.cellContentFunction(value, rowData, self.ctx.filter);\n } catch (e) {\n content = strContent;\n }\n } \n return content;\n }\n }\n \n scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n if (newIndex != oldIndex) {\n updateSourceData(scope.sources[scope.sourceIndex]);\n } \n });\n}\n\nself.onDataUpdated = function() {\n var scope = self.ctx.$scope;\n for (var s=0; s < scope.sources.length; s++) {\n var source = scope.sources[s];\n source.rawData = self.ctx.data.slice(source.keyStartIndex, source.keyEndIndex);\n }\n updateSourceData(scope.sources[scope.sourceIndex]);\n scope.$digest();\n}\n\nself.onDestroy = function() {\n}\n\nfunction updatePage(source) {\n var startIndex = source.query.limit * (source.query.page - 1);\n source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n source.data = self.ctx.filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n var rowsMap = {};\n for (var d = 0; d < data.length; d++) {\n var columnData = data[d].data;\n for (var i = 0; i < columnData.length; i++) {\n var cellData = columnData[i];\n var timestamp = cellData[0];\n var row = rowsMap[timestamp];\n if (!row) {\n row = [];\n row[0] = timestamp;\n for (var c = 0; c < data.length; c++) {\n row[c+1] = undefined;\n }\n rowsMap[timestamp] = row;\n }\n row[d+1] = cellData[1];\n }\n }\n var rows = [];\n for (var t in rowsMap) {\n rows.push(rowsMap[t]);\n }\n return rows;\n}\n\nfunction updateSourceData(source) {\n source.data = convertData(source.rawData);\n source.ts.count = source.data.length;\n reorder(source);\n updatePage(source);\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}', 83 +'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<tb-timeseries-table-widget \n config=\"config\"\n table-id=\"tableId\"\n datasources=\"datasources\"\n data=\"data\">\n</tb-timeseries-table-widget>","templateCss":"","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get(''utils'').guid();\n\n scope.config = {\n settings: self.ctx.settings,\n widgetConfig: self.ctx.widgetConfig\n }\n\n scope.datasources = self.ctx.datasources;\n scope.data = self.ctx.data;\n scope.tableId = \"table-\"+id;\n \n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.data = self.ctx.data;\n self.ctx.$scope.$broadcast(''timeseries-table-data-updated'', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}',
84 'Timeseries table' ); 84 'Timeseries table' );
85 85
86 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) 86 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
@@ -18,12 +18,14 @@ export default angular.module('thingsboard.api.customer', []) @@ -18,12 +18,14 @@ export default angular.module('thingsboard.api.customer', [])
18 .name; 18 .name;
19 19
20 /*@ngInject*/ 20 /*@ngInject*/
21 -function CustomerService($http, $q) { 21 +function CustomerService($http, $q, types) {
22 22
23 var service = { 23 var service = {
24 getCustomers: getCustomers, 24 getCustomers: getCustomers,
25 getCustomer: getCustomer, 25 getCustomer: getCustomer,
26 - getCustomerTitle: getCustomerTitle, 26 + getShortCustomerInfo: getShortCustomerInfo,
  27 + applyAssignedCustomersInfo: applyAssignedCustomersInfo,
  28 + applyAssignedCustomerInfo: applyAssignedCustomerInfo,
27 deleteCustomer: deleteCustomer, 29 deleteCustomer: deleteCustomer,
28 saveCustomer: saveCustomer 30 saveCustomer: saveCustomer
29 } 31 }
@@ -61,9 +63,9 @@ function CustomerService($http, $q) { @@ -61,9 +63,9 @@ function CustomerService($http, $q) {
61 return deferred.promise; 63 return deferred.promise;
62 } 64 }
63 65
64 - function getCustomerTitle(customerId) { 66 + function getShortCustomerInfo(customerId) {
65 var deferred = $q.defer(); 67 var deferred = $q.defer();
66 - var url = '/api/customer/' + customerId + '/title'; 68 + var url = '/api/customer/' + customerId + '/shortInfo';
67 $http.get(url, null).then(function success(response) { 69 $http.get(url, null).then(function success(response) {
68 deferred.resolve(response.data); 70 deferred.resolve(response.data);
69 }, function fail(response) { 71 }, function fail(response) {
@@ -72,6 +74,77 @@ function CustomerService($http, $q) { @@ -72,6 +74,77 @@ function CustomerService($http, $q) {
72 return deferred.promise; 74 return deferred.promise;
73 } 75 }
74 76
  77 + function applyAssignedCustomersInfo(items) {
  78 + var deferred = $q.defer();
  79 + var assignedCustomersMap = {};
  80 + function loadNextCustomerInfoOrComplete(i) {
  81 + i++;
  82 + if (i < items.length) {
  83 + loadNextCustomerInfo(i);
  84 + } else {
  85 + deferred.resolve(items);
  86 + }
  87 + }
  88 +
  89 + function loadNextCustomerInfo(i) {
  90 + var item = items[i];
  91 + item.assignedCustomer = {};
  92 + if (item.customerId && item.customerId.id != types.id.nullUid) {
  93 + item.assignedCustomer.id = item.customerId.id;
  94 + var assignedCustomer = assignedCustomersMap[item.customerId.id];
  95 + if (assignedCustomer){
  96 + item.assignedCustomer = assignedCustomer;
  97 + loadNextCustomerInfoOrComplete(i);
  98 + } else {
  99 + getShortCustomerInfo(item.customerId.id).then(
  100 + function success(info) {
  101 + assignedCustomer = {
  102 + id: item.customerId.id,
  103 + title: info.title,
  104 + isPublic: info.isPublic
  105 + };
  106 + assignedCustomersMap[assignedCustomer.id] = assignedCustomer;
  107 + item.assignedCustomer = assignedCustomer;
  108 + loadNextCustomerInfoOrComplete(i);
  109 + },
  110 + function fail() {
  111 + loadNextCustomerInfoOrComplete(i);
  112 + }
  113 + );
  114 + }
  115 + } else {
  116 + loadNextCustomerInfoOrComplete(i);
  117 + }
  118 + }
  119 + if (items.length > 0) {
  120 + loadNextCustomerInfo(0);
  121 + } else {
  122 + deferred.resolve(items);
  123 + }
  124 + return deferred.promise;
  125 + }
  126 +
  127 + function applyAssignedCustomerInfo(items, customerId) {
  128 + var deferred = $q.defer();
  129 + getShortCustomerInfo(customerId).then(
  130 + function success(info) {
  131 + var assignedCustomer = {
  132 + id: customerId,
  133 + title: info.title,
  134 + isPublic: info.isPublic
  135 + }
  136 + items.forEach(function(item) {
  137 + item.assignedCustomer = assignedCustomer;
  138 + });
  139 + deferred.resolve(items);
  140 + },
  141 + function fail() {
  142 + deferred.reject();
  143 + }
  144 + );
  145 + return deferred.promise;
  146 + }
  147 +
75 function saveCustomer(customer) { 148 function saveCustomer(customer) {
76 var deferred = $q.defer(); 149 var deferred = $q.defer();
77 var url = '/api/customer'; 150 var url = '/api/customer';
@@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', []) @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', [])
17 .factory('dashboardService', DashboardService).name; 17 .factory('dashboardService', DashboardService).name;
18 18
19 /*@ngInject*/ 19 /*@ngInject*/
20 -function DashboardService($http, $q) { 20 +function DashboardService($http, $q, $location, customerService) {
21 21
22 var service = { 22 var service = {
23 assignDashboardToCustomer: assignDashboardToCustomer, 23 assignDashboardToCustomer: assignDashboardToCustomer,
@@ -27,7 +27,9 @@ function DashboardService($http, $q) { @@ -27,7 +27,9 @@ function DashboardService($http, $q) {
27 getTenantDashboards: getTenantDashboards, 27 getTenantDashboards: getTenantDashboards,
28 deleteDashboard: deleteDashboard, 28 deleteDashboard: deleteDashboard,
29 saveDashboard: saveDashboard, 29 saveDashboard: saveDashboard,
30 - unassignDashboardFromCustomer: unassignDashboardFromCustomer 30 + unassignDashboardFromCustomer: unassignDashboardFromCustomer,
  31 + makeDashboardPublic: makeDashboardPublic,
  32 + getPublicDashboardLink: getPublicDashboardLink
31 } 33 }
32 34
33 return service; 35 return service;
@@ -45,7 +47,15 @@ function DashboardService($http, $q) { @@ -45,7 +47,15 @@ function DashboardService($http, $q) {
45 url += '&textOffset=' + pageLink.textOffset; 47 url += '&textOffset=' + pageLink.textOffset;
46 } 48 }
47 $http.get(url, null).then(function success(response) { 49 $http.get(url, null).then(function success(response) {
48 - deferred.resolve(response.data); 50 + customerService.applyAssignedCustomersInfo(response.data.data).then(
  51 + function success(data) {
  52 + response.data.data = data;
  53 + deferred.resolve(response.data);
  54 + },
  55 + function fail() {
  56 + deferred.reject();
  57 + }
  58 + );
49 }, function fail() { 59 }, function fail() {
50 deferred.reject(); 60 deferred.reject();
51 }); 61 });
@@ -65,7 +75,15 @@ function DashboardService($http, $q) { @@ -65,7 +75,15 @@ function DashboardService($http, $q) {
65 url += '&textOffset=' + pageLink.textOffset; 75 url += '&textOffset=' + pageLink.textOffset;
66 } 76 }
67 $http.get(url, null).then(function success(response) { 77 $http.get(url, null).then(function success(response) {
68 - deferred.resolve(response.data); 78 + customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
  79 + function success(data) {
  80 + response.data.data = data;
  81 + deferred.resolve(response.data);
  82 + },
  83 + function fail() {
  84 + deferred.reject();
  85 + }
  86 + );
69 }, function fail() { 87 }, function fail() {
70 deferred.reject(); 88 deferred.reject();
71 }); 89 });
@@ -92,8 +110,8 @@ function DashboardService($http, $q) { @@ -92,8 +110,8 @@ function DashboardService($http, $q) {
92 var url = '/api/dashboard/' + dashboardId; 110 var url = '/api/dashboard/' + dashboardId;
93 $http.get(url, null).then(function success(response) { 111 $http.get(url, null).then(function success(response) {
94 deferred.resolve(response.data); 112 deferred.resolve(response.data);
95 - }, function fail(response) {  
96 - deferred.reject(response.data); 113 + }, function fail() {
  114 + deferred.reject();
97 }); 115 });
98 return deferred.promise; 116 return deferred.promise;
99 } 117 }
@@ -103,8 +121,8 @@ function DashboardService($http, $q) { @@ -103,8 +121,8 @@ function DashboardService($http, $q) {
103 var url = '/api/dashboard'; 121 var url = '/api/dashboard';
104 $http.post(url, dashboard).then(function success(response) { 122 $http.post(url, dashboard).then(function success(response) {
105 deferred.resolve(response.data); 123 deferred.resolve(response.data);
106 - }, function fail(response) {  
107 - deferred.reject(response.data); 124 + }, function fail() {
  125 + deferred.reject();
108 }); 126 });
109 return deferred.promise; 127 return deferred.promise;
110 } 128 }
@@ -114,8 +132,8 @@ function DashboardService($http, $q) { @@ -114,8 +132,8 @@ function DashboardService($http, $q) {
114 var url = '/api/dashboard/' + dashboardId; 132 var url = '/api/dashboard/' + dashboardId;
115 $http.delete(url).then(function success() { 133 $http.delete(url).then(function success() {
116 deferred.resolve(); 134 deferred.resolve();
117 - }, function fail(response) {  
118 - deferred.reject(response.data); 135 + }, function fail() {
  136 + deferred.reject();
119 }); 137 });
120 return deferred.promise; 138 return deferred.promise;
121 } 139 }
@@ -123,10 +141,10 @@ function DashboardService($http, $q) { @@ -123,10 +141,10 @@ function DashboardService($http, $q) {
123 function assignDashboardToCustomer(customerId, dashboardId) { 141 function assignDashboardToCustomer(customerId, dashboardId) {
124 var deferred = $q.defer(); 142 var deferred = $q.defer();
125 var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; 143 var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId;
126 - $http.post(url, null).then(function success() {  
127 - deferred.resolve();  
128 - }, function fail(response) {  
129 - deferred.reject(response.data); 144 + $http.post(url, null).then(function success(response) {
  145 + deferred.resolve(response.data);
  146 + }, function fail() {
  147 + deferred.reject();
130 }); 148 });
131 return deferred.promise; 149 return deferred.promise;
132 } 150 }
@@ -134,12 +152,33 @@ function DashboardService($http, $q) { @@ -134,12 +152,33 @@ function DashboardService($http, $q) {
134 function unassignDashboardFromCustomer(dashboardId) { 152 function unassignDashboardFromCustomer(dashboardId) {
135 var deferred = $q.defer(); 153 var deferred = $q.defer();
136 var url = '/api/customer/dashboard/' + dashboardId; 154 var url = '/api/customer/dashboard/' + dashboardId;
137 - $http.delete(url).then(function success() {  
138 - deferred.resolve();  
139 - }, function fail(response) {  
140 - deferred.reject(response.data); 155 + $http.delete(url).then(function success(response) {
  156 + deferred.resolve(response.data);
  157 + }, function fail() {
  158 + deferred.reject();
  159 + });
  160 + return deferred.promise;
  161 + }
  162 +
  163 + function makeDashboardPublic(dashboardId) {
  164 + var deferred = $q.defer();
  165 + var url = '/api/customer/public/dashboard/' + dashboardId;
  166 + $http.post(url, null).then(function success(response) {
  167 + deferred.resolve(response.data);
  168 + }, function fail() {
  169 + deferred.reject();
141 }); 170 });
142 return deferred.promise; 171 return deferred.promise;
143 } 172 }
144 173
  174 + function getPublicDashboardLink(dashboard) {
  175 + var url = $location.protocol() + '://' + $location.host();
  176 + var port = $location.port();
  177 + if (port != 80 && port != 443) {
  178 + url += ":" + port;
  179 + }
  180 + url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.customerId.id;
  181 + return url;
  182 + }
  183 +
145 } 184 }
@@ -58,10 +58,10 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t @@ -58,10 +58,10 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t
58 var datasourceSubscription = { 58 var datasourceSubscription = {
59 datasourceType: datasource.type, 59 datasourceType: datasource.type,
60 dataKeys: subscriptionDataKeys, 60 dataKeys: subscriptionDataKeys,
61 - type: listener.widget.type 61 + type: listener.subscriptionType
62 }; 62 };
63 63
64 - if (listener.widget.type === types.widgetType.timeseries.value) { 64 + if (listener.subscriptionType === types.widgetType.timeseries.value) {
65 datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow); 65 datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow);
66 } 66 }
67 if (datasourceSubscription.datasourceType === types.datasourceType.device) { 67 if (datasourceSubscription.datasourceType === types.datasourceType.device) {
@@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes]) @@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
20 .name; 20 .name;
21 21
22 /*@ngInject*/ 22 /*@ngInject*/
23 -function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) { 23 +function DeviceService($http, $q, $filter, userService, customerService, telemetryWebsocketService, types) {
24 24
25 25
26 var deviceAttributesSubscriptionMap = {}; 26 var deviceAttributesSubscriptionMap = {};
@@ -33,6 +33,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -33,6 +33,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
33 getDevices: getDevices, 33 getDevices: getDevices,
34 processDeviceAliases: processDeviceAliases, 34 processDeviceAliases: processDeviceAliases,
35 checkDeviceAlias: checkDeviceAlias, 35 checkDeviceAlias: checkDeviceAlias,
  36 + fetchAliasDeviceByNameFilter: fetchAliasDeviceByNameFilter,
36 getDeviceCredentials: getDeviceCredentials, 37 getDeviceCredentials: getDeviceCredentials,
37 getDeviceKeys: getDeviceKeys, 38 getDeviceKeys: getDeviceKeys,
38 getDeviceTimeseriesValues: getDeviceTimeseriesValues, 39 getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@@ -40,6 +41,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -40,6 +41,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
40 saveDevice: saveDevice, 41 saveDevice: saveDevice,
41 saveDeviceCredentials: saveDeviceCredentials, 42 saveDeviceCredentials: saveDeviceCredentials,
42 unassignDeviceFromCustomer: unassignDeviceFromCustomer, 43 unassignDeviceFromCustomer: unassignDeviceFromCustomer,
  44 + makeDevicePublic: makeDevicePublic,
43 getDeviceAttributes: getDeviceAttributes, 45 getDeviceAttributes: getDeviceAttributes,
44 subscribeForDeviceAttributes: subscribeForDeviceAttributes, 46 subscribeForDeviceAttributes: subscribeForDeviceAttributes,
45 unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes, 47 unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes,
@@ -51,7 +53,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -51,7 +53,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
51 53
52 return service; 54 return service;
53 55
54 - function getTenantDevices(pageLink, config) { 56 + function getTenantDevices(pageLink, applyCustomersInfo, config) {
55 var deferred = $q.defer(); 57 var deferred = $q.defer();
56 var url = '/api/tenant/devices?limit=' + pageLink.limit; 58 var url = '/api/tenant/devices?limit=' + pageLink.limit;
57 if (angular.isDefined(pageLink.textSearch)) { 59 if (angular.isDefined(pageLink.textSearch)) {
@@ -64,14 +66,26 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -64,14 +66,26 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
64 url += '&textOffset=' + pageLink.textOffset; 66 url += '&textOffset=' + pageLink.textOffset;
65 } 67 }
66 $http.get(url, config).then(function success(response) { 68 $http.get(url, config).then(function success(response) {
67 - deferred.resolve(response.data); 69 + if (applyCustomersInfo) {
  70 + customerService.applyAssignedCustomersInfo(response.data.data).then(
  71 + function success(data) {
  72 + response.data.data = data;
  73 + deferred.resolve(response.data);
  74 + },
  75 + function fail() {
  76 + deferred.reject();
  77 + }
  78 + );
  79 + } else {
  80 + deferred.resolve(response.data);
  81 + }
68 }, function fail() { 82 }, function fail() {
69 deferred.reject(); 83 deferred.reject();
70 }); 84 });
71 return deferred.promise; 85 return deferred.promise;
72 } 86 }
73 87
74 - function getCustomerDevices(customerId, pageLink) { 88 + function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
75 var deferred = $q.defer(); 89 var deferred = $q.defer();
76 var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit; 90 var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
77 if (angular.isDefined(pageLink.textSearch)) { 91 if (angular.isDefined(pageLink.textSearch)) {
@@ -83,18 +97,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -83,18 +97,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
83 if (angular.isDefined(pageLink.textOffset)) { 97 if (angular.isDefined(pageLink.textOffset)) {
84 url += '&textOffset=' + pageLink.textOffset; 98 url += '&textOffset=' + pageLink.textOffset;
85 } 99 }
86 - $http.get(url, null).then(function success(response) {  
87 - deferred.resolve(response.data); 100 + $http.get(url, config).then(function success(response) {
  101 + if (applyCustomersInfo) {
  102 + customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
  103 + function success(data) {
  104 + response.data.data = data;
  105 + deferred.resolve(response.data);
  106 + },
  107 + function fail() {
  108 + deferred.reject();
  109 + }
  110 + );
  111 + } else {
  112 + deferred.resolve(response.data);
  113 + }
88 }, function fail() { 114 }, function fail() {
89 deferred.reject(); 115 deferred.reject();
90 }); 116 });
  117 +
91 return deferred.promise; 118 return deferred.promise;
92 } 119 }
93 120
94 - function getDevice(deviceId, ignoreErrors) { 121 + function getDevice(deviceId, ignoreErrors, config) {
95 var deferred = $q.defer(); 122 var deferred = $q.defer();
96 var url = '/api/device/' + deviceId; 123 var url = '/api/device/' + deviceId;
97 - $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) { 124 + if (!config) {
  125 + config = {};
  126 + }
  127 + config = Object.assign(config, { ignoreErrors: ignoreErrors });
  128 + $http.get(url, config).then(function success(response) {
98 deferred.resolve(response.data); 129 deferred.resolve(response.data);
99 }, function fail(response) { 130 }, function fail(response) {
100 deferred.reject(response.data); 131 deferred.reject(response.data);
@@ -102,7 +133,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -102,7 +133,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
102 return deferred.promise; 133 return deferred.promise;
103 } 134 }
104 135
105 - function getDevices(deviceIds) { 136 + function getDevices(deviceIds, config) {
106 var deferred = $q.defer(); 137 var deferred = $q.defer();
107 var ids = ''; 138 var ids = '';
108 for (var i=0;i<deviceIds.length;i++) { 139 for (var i=0;i<deviceIds.length;i++) {
@@ -112,7 +143,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -112,7 +143,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
112 ids += deviceIds[i]; 143 ids += deviceIds[i];
113 } 144 }
114 var url = '/api/devices?deviceIds=' + ids; 145 var url = '/api/devices?deviceIds=' + ids;
115 - $http.get(url, null).then(function success(response) { 146 + $http.get(url, config).then(function success(response) {
116 var devices = response.data; 147 var devices = response.data;
117 devices.sort(function (device1, device2) { 148 devices.sort(function (device1, device2) {
118 var id1 = device1.id.id; 149 var id1 = device1.id.id;
@@ -128,16 +159,16 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -128,16 +159,16 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
128 return deferred.promise; 159 return deferred.promise;
129 } 160 }
130 161
131 - function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) { 162 + function fetchAliasDeviceByNameFilter(deviceNameFilter, limit, applyCustomersInfo, config) {
132 var deferred = $q.defer(); 163 var deferred = $q.defer();
133 var user = userService.getCurrentUser(); 164 var user = userService.getCurrentUser();
134 var promise; 165 var promise;
135 var pageLink = {limit: limit, textSearch: deviceNameFilter}; 166 var pageLink = {limit: limit, textSearch: deviceNameFilter};
136 if (user.authority === 'CUSTOMER_USER') { 167 if (user.authority === 'CUSTOMER_USER') {
137 var customerId = user.customerId; 168 var customerId = user.customerId;
138 - promise = getCustomerDevices(customerId, pageLink); 169 + promise = getCustomerDevices(customerId, pageLink, applyCustomersInfo, config);
139 } else { 170 } else {
140 - promise = getTenantDevices(pageLink); 171 + promise = getTenantDevices(pageLink, applyCustomersInfo, config);
141 } 172 }
142 promise.then( 173 promise.then(
143 function success(result) { 174 function success(result) {
@@ -194,7 +225,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -194,7 +225,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
194 var deviceFilter = deviceAlias.deviceFilter; 225 var deviceFilter = deviceAlias.deviceFilter;
195 if (deviceFilter.useFilter) { 226 if (deviceFilter.useFilter) {
196 var deviceNameFilter = deviceFilter.deviceNameFilter; 227 var deviceNameFilter = deviceFilter.deviceNameFilter;
197 - fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then( 228 + fetchAliasDeviceByNameFilter(deviceNameFilter, 100, false).then(
198 function(devices) { 229 function(devices) {
199 if (devices && devices != null) { 230 if (devices && devices != null) {
200 var resolvedAlias = {alias: alias, deviceId: devices[0].id.id}; 231 var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
@@ -276,7 +307,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -276,7 +307,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
276 var promise; 307 var promise;
277 if (deviceFilter.useFilter) { 308 if (deviceFilter.useFilter) {
278 var deviceNameFilter = deviceFilter.deviceNameFilter; 309 var deviceNameFilter = deviceFilter.deviceNameFilter;
279 - promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1); 310 + promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1, false);
280 } else { 311 } else {
281 var deviceList = deviceFilter.deviceList; 312 var deviceList = deviceFilter.deviceList;
282 promise = getDevices(deviceList); 313 promise = getDevices(deviceList);
@@ -301,8 +332,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -301,8 +332,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
301 var url = '/api/device'; 332 var url = '/api/device';
302 $http.post(url, device).then(function success(response) { 333 $http.post(url, device).then(function success(response) {
303 deferred.resolve(response.data); 334 deferred.resolve(response.data);
304 - }, function fail(response) {  
305 - deferred.reject(response.data); 335 + }, function fail() {
  336 + deferred.reject();
306 }); 337 });
307 return deferred.promise; 338 return deferred.promise;
308 } 339 }
@@ -312,8 +343,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -312,8 +343,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
312 var url = '/api/device/' + deviceId; 343 var url = '/api/device/' + deviceId;
313 $http.delete(url).then(function success() { 344 $http.delete(url).then(function success() {
314 deferred.resolve(); 345 deferred.resolve();
315 - }, function fail(response) {  
316 - deferred.reject(response.data); 346 + }, function fail() {
  347 + deferred.reject();
317 }); 348 });
318 return deferred.promise; 349 return deferred.promise;
319 } 350 }
@@ -323,8 +354,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -323,8 +354,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
323 var url = '/api/device/' + deviceId + '/credentials'; 354 var url = '/api/device/' + deviceId + '/credentials';
324 $http.get(url, null).then(function success(response) { 355 $http.get(url, null).then(function success(response) {
325 deferred.resolve(response.data); 356 deferred.resolve(response.data);
326 - }, function fail(response) {  
327 - deferred.reject(response.data); 357 + }, function fail() {
  358 + deferred.reject();
328 }); 359 });
329 return deferred.promise; 360 return deferred.promise;
330 } 361 }
@@ -334,8 +365,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -334,8 +365,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
334 var url = '/api/device/credentials'; 365 var url = '/api/device/credentials';
335 $http.post(url, deviceCredentials).then(function success(response) { 366 $http.post(url, deviceCredentials).then(function success(response) {
336 deferred.resolve(response.data); 367 deferred.resolve(response.data);
337 - }, function fail(response) {  
338 - deferred.reject(response.data); 368 + }, function fail() {
  369 + deferred.reject();
339 }); 370 });
340 return deferred.promise; 371 return deferred.promise;
341 } 372 }
@@ -343,10 +374,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -343,10 +374,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
343 function assignDeviceToCustomer(customerId, deviceId) { 374 function assignDeviceToCustomer(customerId, deviceId) {
344 var deferred = $q.defer(); 375 var deferred = $q.defer();
345 var url = '/api/customer/' + customerId + '/device/' + deviceId; 376 var url = '/api/customer/' + customerId + '/device/' + deviceId;
346 - $http.post(url, null).then(function success() {  
347 - deferred.resolve();  
348 - }, function fail(response) {  
349 - deferred.reject(response.data); 377 + $http.post(url, null).then(function success(response) {
  378 + deferred.resolve(response.data);
  379 + }, function fail() {
  380 + deferred.reject();
350 }); 381 });
351 return deferred.promise; 382 return deferred.promise;
352 } 383 }
@@ -354,10 +385,21 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic @@ -354,10 +385,21 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
354 function unassignDeviceFromCustomer(deviceId) { 385 function unassignDeviceFromCustomer(deviceId) {
355 var deferred = $q.defer(); 386 var deferred = $q.defer();
356 var url = '/api/customer/device/' + deviceId; 387 var url = '/api/customer/device/' + deviceId;
357 - $http.delete(url).then(function success() {  
358 - deferred.resolve();  
359 - }, function fail(response) {  
360 - deferred.reject(response.data); 388 + $http.delete(url).then(function success(response) {
  389 + deferred.resolve(response.data);
  390 + }, function fail() {
  391 + deferred.reject();
  392 + });
  393 + return deferred.promise;
  394 + }
  395 +
  396 + function makeDevicePublic(deviceId) {
  397 + var deferred = $q.defer();
  398 + var url = '/api/customer/public/device/' + deviceId;
  399 + $http.post(url, null).then(function success(response) {
  400 + deferred.resolve(response.data);
  401 + }, function fail() {
  402 + deferred.reject();
361 }); 403 });
362 return deferred.promise; 404 return deferred.promise;
363 } 405 }
@@ -25,6 +25,7 @@ function LoginService($http, $q) { @@ -25,6 +25,7 @@ function LoginService($http, $q) {
25 changePassword: changePassword, 25 changePassword: changePassword,
26 hasUser: hasUser, 26 hasUser: hasUser,
27 login: login, 27 login: login,
  28 + publicLogin: publicLogin,
28 resetPassword: resetPassword, 29 resetPassword: resetPassword,
29 sendResetPasswordLink: sendResetPasswordLink, 30 sendResetPasswordLink: sendResetPasswordLink,
30 } 31 }
@@ -49,6 +50,19 @@ function LoginService($http, $q) { @@ -49,6 +50,19 @@ function LoginService($http, $q) {
49 return deferred.promise; 50 return deferred.promise;
50 } 51 }
51 52
  53 + function publicLogin(publicId) {
  54 + var deferred = $q.defer();
  55 + var pubilcLoginRequest = {
  56 + publicId: publicId
  57 + };
  58 + $http.post('/api/auth/login/public', pubilcLoginRequest).then(function success(response) {
  59 + deferred.resolve(response);
  60 + }, function fail(response) {
  61 + deferred.reject(response);
  62 + });
  63 + return deferred.promise;
  64 + }
  65 +
52 function sendResetPasswordLink(email) { 66 function sendResetPasswordLink(email) {
53 var deferred = $q.defer(); 67 var deferred = $q.defer();
54 var url = '/api/noauth/resetPasswordByEmail?email=' + email; 68 var url = '/api/noauth/resetPasswordByEmail?email=' + email;
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/*
  18 + options = {
  19 + type,
  20 + targetDeviceAliasIds, // RPC
  21 + targetDeviceIds, // RPC
  22 + datasources,
  23 + timeWindowConfig,
  24 + useDashboardTimewindow,
  25 + legendConfig,
  26 + decimals,
  27 + units,
  28 + callbacks
  29 + }
  30 + */
  31 +
  32 +export default class Subscription {
  33 + constructor(subscriptionContext, options) {
  34 +
  35 + this.ctx = subscriptionContext;
  36 + this.type = options.type;
  37 + this.callbacks = options.callbacks;
  38 + this.id = this.ctx.utils.guid();
  39 + this.cafs = {};
  40 + this.registrations = [];
  41 +
  42 + if (this.type === this.ctx.types.widgetType.rpc.value) {
  43 + this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || function(){};
  44 + this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || function(){};
  45 + this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || function(){};
  46 + this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || function(){};
  47 +
  48 + this.targetDeviceAliasIds = options.targetDeviceAliasIds;
  49 + this.targetDeviceIds = options.targetDeviceIds;
  50 +
  51 + this.targetDeviceAliasId = null;
  52 + this.targetDeviceId = null;
  53 +
  54 + this.rpcRejection = null;
  55 + this.rpcErrorText = null;
  56 + this.rpcEnabled = false;
  57 + this.executingRpcRequest = false;
  58 + this.executingPromises = [];
  59 + this.initRpc();
  60 + } else {
  61 + this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
  62 + this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
  63 + this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
  64 + this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){};
  65 + this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
  66 +
  67 + this.datasources = options.datasources;
  68 + this.datasourceListeners = [];
  69 + this.data = [];
  70 + this.hiddenData = [];
  71 + this.originalTimewindow = null;
  72 + this.timeWindow = {
  73 + stDiff: this.ctx.stDiff
  74 + }
  75 + this.useDashboardTimewindow = options.useDashboardTimewindow;
  76 +
  77 + if (this.useDashboardTimewindow) {
  78 + this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
  79 + } else {
  80 + this.timeWindowConfig = angular.copy(options.timeWindowConfig);
  81 + }
  82 +
  83 + this.subscriptionTimewindow = null;
  84 +
  85 + this.units = options.units || '';
  86 + this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2;
  87 +
  88 + this.loadingData = false;
  89 +
  90 + if (options.legendConfig) {
  91 + this.legendConfig = options.legendConfig;
  92 + this.legendData = {
  93 + keys: [],
  94 + data: []
  95 + };
  96 + this.displayLegend = true;
  97 + } else {
  98 + this.displayLegend = false;
  99 + }
  100 + this.caulculateLegendData = this.displayLegend &&
  101 + this.type === this.ctx.types.widgetType.timeseries.value &&
  102 + (this.legendConfig.showMin === true ||
  103 + this.legendConfig.showMax === true ||
  104 + this.legendConfig.showAvg === true ||
  105 + this.legendConfig.showTotal === true);
  106 + this.initDataSubscription();
  107 + }
  108 + }
  109 +
  110 + initDataSubscription() {
  111 + var dataIndex = 0;
  112 + for (var i = 0; i < this.datasources.length; i++) {
  113 + var datasource = this.datasources[i];
  114 + for (var a = 0; a < datasource.dataKeys.length; a++) {
  115 + var dataKey = datasource.dataKeys[a];
  116 + dataKey.pattern = angular.copy(dataKey.label);
  117 + var datasourceData = {
  118 + datasource: datasource,
  119 + dataKey: dataKey,
  120 + data: []
  121 + };
  122 + this.data.push(datasourceData);
  123 + this.hiddenData.push({data: []});
  124 + if (this.displayLegend) {
  125 + var legendKey = {
  126 + dataKey: dataKey,
  127 + dataIndex: dataIndex++
  128 + };
  129 + this.legendData.keys.push(legendKey);
  130 + var legendKeyData = {
  131 + min: null,
  132 + max: null,
  133 + avg: null,
  134 + total: null,
  135 + hidden: false
  136 + };
  137 + this.legendData.data.push(legendKeyData);
  138 + }
  139 + }
  140 + }
  141 +
  142 + var subscription = this;
  143 + var registration;
  144 +
  145 + if (this.displayLegend) {
  146 + this.legendData.keys = this.ctx.$filter('orderBy')(this.legendData.keys, '+label');
  147 + registration = this.ctx.$scope.$watch(
  148 + function() {
  149 + return subscription.legendData.data;
  150 + },
  151 + function (newValue, oldValue) {
  152 + for(var i = 0; i < newValue.length; i++) {
  153 + if(newValue[i].hidden != oldValue[i].hidden) {
  154 + subscription.updateDataVisibility(i);
  155 + }
  156 + }
  157 + }, true);
  158 + this.registrations.push(registration);
  159 + }
  160 +
  161 + if (this.type === this.ctx.types.widgetType.timeseries.value) {
  162 + if (this.useDashboardTimewindow) {
  163 + registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
  164 + if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
  165 + subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
  166 + subscription.unsubscribe();
  167 + subscription.subscribe();
  168 + }
  169 + });
  170 + this.registrations.push(registration);
  171 + } else {
  172 + registration = this.ctx.$scope.$watch(function () {
  173 + return subscription.timeWindowConfig;
  174 + }, function (newTimewindow, prevTimewindow) {
  175 + if (!angular.equals(newTimewindow, prevTimewindow)) {
  176 + subscription.unsubscribe();
  177 + subscription.subscribe();
  178 + }
  179 + });
  180 + this.registrations.push(registration);
  181 + }
  182 + }
  183 +
  184 + registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
  185 + subscription.checkSubscriptions();
  186 + });
  187 +
  188 + this.registrations.push(registration);
  189 + }
  190 +
  191 + initRpc() {
  192 +
  193 + if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
  194 + this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
  195 + if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) {
  196 + this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId;
  197 + }
  198 + var subscription = this;
  199 + var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
  200 + var deviceId = null;
  201 + if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) {
  202 + deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId;
  203 + }
  204 + if (!angular.equals(deviceId, subscription.targetDeviceId)) {
  205 + subscription.targetDeviceId = deviceId;
  206 + if (subscription.targetDeviceId) {
  207 + subscription.rpcEnabled = true;
  208 + } else {
  209 + subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false;
  210 + }
  211 + subscription.callbacks.rpcStateChanged(subscription);
  212 + }
  213 + });
  214 + this.registrations.push(registration);
  215 + } else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
  216 + this.targetDeviceId = this.targetDeviceIds[0];
  217 + }
  218 +
  219 + if (this.targetDeviceId) {
  220 + this.rpcEnabled = true;
  221 + } else {
  222 + this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
  223 + }
  224 + this.callbacks.rpcStateChanged(this);
  225 + }
  226 +
  227 + clearRpcError() {
  228 + this.rpcRejection = null;
  229 + this.rpcErrorText = null;
  230 + this.callbacks.onRpcErrorCleared(this);
  231 + }
  232 +
  233 + sendOneWayCommand(method, params, timeout) {
  234 + return this.sendCommand(true, method, params, timeout);
  235 + }
  236 +
  237 + sendTwoWayCommand(method, params, timeout) {
  238 + return this.sendCommand(false, method, params, timeout);
  239 + }
  240 +
  241 + sendCommand(oneWayElseTwoWay, method, params, timeout) {
  242 + if (!this.rpcEnabled) {
  243 + return this.ctx.$q.reject();
  244 + }
  245 +
  246 + if (this.rpcRejection && this.rpcRejection.status !== 408) {
  247 + this.rpcRejection = null;
  248 + this.rpcErrorText = null;
  249 + this.callbacks.onRpcErrorCleared(this);
  250 + }
  251 +
  252 + var subscription = this;
  253 +
  254 + var requestBody = {
  255 + method: method,
  256 + params: params
  257 + };
  258 +
  259 + if (timeout && timeout > 0) {
  260 + requestBody.timeout = timeout;
  261 + }
  262 +
  263 + var deferred = this.ctx.$q.defer();
  264 + this.executingRpcRequest = true;
  265 + this.callbacks.rpcStateChanged(this);
  266 + if (this.ctx.$scope.widgetEditMode) {
  267 + this.ctx.$timeout(function() {
  268 + subscription.executingRpcRequest = false;
  269 + subscription.callbacks.rpcStateChanged(subscription);
  270 + if (oneWayElseTwoWay) {
  271 + deferred.resolve();
  272 + } else {
  273 + deferred.resolve(requestBody);
  274 + }
  275 + }, 500);
  276 + } else {
  277 + this.executingPromises.push(deferred.promise);
  278 + var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand;
  279 + targetSendFunction(this.targetDeviceId, requestBody).then(
  280 + function success(responseBody) {
  281 + subscription.rpcRejection = null;
  282 + subscription.rpcErrorText = null;
  283 + var index = subscription.executingPromises.indexOf(deferred.promise);
  284 + if (index >= 0) {
  285 + subscription.executingPromises.splice( index, 1 );
  286 + }
  287 + subscription.executingRpcRequest = subscription.executingPromises.length > 0;
  288 + subscription.callbacks.onRpcSuccess(subscription);
  289 + deferred.resolve(responseBody);
  290 + },
  291 + function fail(rejection) {
  292 + var index = subscription.executingPromises.indexOf(deferred.promise);
  293 + if (index >= 0) {
  294 + subscription.executingPromises.splice( index, 1 );
  295 + }
  296 + subscription.executingRpcRequest = subscription.executingPromises.length > 0;
  297 + subscription.callbacks.rpcStateChanged(subscription);
  298 + if (!subscription.executingRpcRequest || rejection.status === 408) {
  299 + subscription.rpcRejection = rejection;
  300 + if (rejection.status === 408) {
  301 + subscription.rpcErrorText = 'Device is offline.';
  302 + } else {
  303 + subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
  304 + if (rejection.data && rejection.data.length > 0) {
  305 + subscription.rpcErrorText += '</br>';
  306 + subscription.rpcErrorText += rejection.data;
  307 + }
  308 + }
  309 + subscription.callbacks.onRpcFailed(subscription);
  310 + }
  311 + deferred.reject(rejection);
  312 + }
  313 + );
  314 + }
  315 + return deferred.promise;
  316 + }
  317 +
  318 + updateDataVisibility(index) {
  319 + var hidden = this.legendData.data[index].hidden;
  320 + if (hidden) {
  321 + this.hiddenData[index].data = this.data[index].data;
  322 + this.data[index].data = [];
  323 + } else {
  324 + this.data[index].data = this.hiddenData[index].data;
  325 + this.hiddenData[index].data = [];
  326 + }
  327 + this.onDataUpdated();
  328 + }
  329 +
  330 + onDataUpdated(apply) {
  331 + if (this.cafs['dataUpdated']) {
  332 + this.cafs['dataUpdated']();
  333 + this.cafs['dataUpdated'] = null;
  334 + }
  335 + var subscription = this;
  336 + this.cafs['dataUpdated'] = this.ctx.tbRaf(function() {
  337 + try {
  338 + subscription.callbacks.onDataUpdated(this, apply);
  339 + } catch (e) {
  340 + subscription.callbacks.onDataUpdateError(this, e);
  341 + }
  342 + });
  343 + if (apply) {
  344 + this.ctx.$scope.$digest();
  345 + }
  346 + }
  347 +
  348 + updateTimewindowConfig(newTimewindow) {
  349 + this.timeWindowConfig = newTimewindow;
  350 + }
  351 +
  352 + onResetTimewindow() {
  353 + if (this.useDashboardTimewindow) {
  354 + this.ctx.dashboardTimewindowApi.onResetTimewindow();
  355 + } else {
  356 + if (this.originalTimewindow) {
  357 + this.timeWindowConfig = angular.copy(this.originalTimewindow);
  358 + this.originalTimewindow = null;
  359 + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
  360 + }
  361 + }
  362 + }
  363 +
  364 + onUpdateTimewindow(startTimeMs, endTimeMs) {
  365 + if (this.useDashboardTimewindow) {
  366 + this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
  367 + } else {
  368 + if (!this.originalTimewindow) {
  369 + this.originalTimewindow = angular.copy(this.timeWindowConfig);
  370 + }
  371 + this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs);
  372 + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
  373 + }
  374 + }
  375 +
  376 + notifyDataLoading() {
  377 + this.loadingData = true;
  378 + this.callbacks.dataLoading(this);
  379 + }
  380 +
  381 + notifyDataLoaded() {
  382 + this.loadingData = false;
  383 + this.callbacks.dataLoading(this);
  384 + }
  385 +
  386 + updateTimewindow() {
  387 + this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
  388 + if (this.subscriptionTimewindow.realtimeWindowMs) {
  389 + this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff;
  390 + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
  391 + } else if (this.subscriptionTimewindow.fixedWindow) {
  392 + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
  393 + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs;
  394 + }
  395 + }
  396 +
  397 + updateRealtimeSubscription(subscriptionTimewindow) {
  398 + if (subscriptionTimewindow) {
  399 + this.subscriptionTimewindow = subscriptionTimewindow;
  400 + } else {
  401 + this.subscriptionTimewindow =
  402 + this.ctx.timeService.createSubscriptionTimewindow(
  403 + this.timeWindowConfig,
  404 + this.timeWindow.stDiff);
  405 + }
  406 + this.updateTimewindow();
  407 + return this.subscriptionTimewindow;
  408 + }
  409 +
  410 + dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
  411 + this.notifyDataLoaded();
  412 + var update = true;
  413 + var currentData;
  414 + if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) {
  415 + currentData = this.hiddenData[datasourceIndex + dataKeyIndex];
  416 + } else {
  417 + currentData = this.data[datasourceIndex + dataKeyIndex];
  418 + }
  419 + if (this.type === this.ctx.types.widgetType.latest.value) {
  420 + var prevData = currentData.data;
  421 + if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {
  422 + var prevValue = prevData[0][1];
  423 + if (prevValue === sourceData.data[0][1]) {
  424 + update = false;
  425 + }
  426 + }
  427 + }
  428 + if (update) {
  429 + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
  430 + this.updateTimewindow();
  431 + }
  432 + currentData.data = sourceData.data;
  433 + if (this.caulculateLegendData) {
  434 + this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
  435 + }
  436 + this.onDataUpdated(apply);
  437 + }
  438 + }
  439 +
  440 + updateLegend(dataIndex, data, apply) {
  441 + var legendKeyData = this.legendData.data[dataIndex];
  442 + if (this.legendConfig.showMin) {
  443 + legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units);
  444 + }
  445 + if (this.legendConfig.showMax) {
  446 + legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units);
  447 + }
  448 + if (this.legendConfig.showAvg) {
  449 + legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units);
  450 + }
  451 + if (this.legendConfig.showTotal) {
  452 + legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units);
  453 + }
  454 + this.callbacks.legendDataUpdated(this, apply !== false);
  455 + }
  456 +
  457 + subscribe() {
  458 + if (this.type === this.ctx.types.widgetType.rpc.value) {
  459 + return;
  460 + }
  461 + this.notifyDataLoading();
  462 + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
  463 + this.updateRealtimeSubscription();
  464 + if (this.subscriptionTimewindow.fixedWindow) {
  465 + this.onDataUpdated();
  466 + }
  467 + }
  468 + var index = 0;
  469 + for (var i = 0; i < this.datasources.length; i++) {
  470 + var datasource = this.datasources[i];
  471 + if (angular.isFunction(datasource))
  472 + continue;
  473 + var deviceId = null;
  474 + if (datasource.type === this.ctx.types.datasourceType.device) {
  475 + var aliasName = null;
  476 + var deviceName = null;
  477 + if (datasource.deviceId) {
  478 + deviceId = datasource.deviceId;
  479 + datasource.name = datasource.deviceName;
  480 + aliasName = datasource.deviceName;
  481 + deviceName = datasource.deviceName;
  482 + } else if (datasource.deviceAliasId && this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
  483 + deviceId = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
  484 + datasource.name = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
  485 + aliasName = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
  486 + deviceName = '';
  487 + var devicesInfo = this.ctx.aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
  488 + for (var d = 0; d < devicesInfo.length; d++) {
  489 + if (devicesInfo[d].id === deviceId) {
  490 + deviceName = devicesInfo[d].name;
  491 + break;
  492 + }
  493 + }
  494 + }
  495 + } else {
  496 + datasource.name = datasource.name || this.ctx.types.datasourceType.function;
  497 + }
  498 + for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
  499 + updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, deviceName, aliasName);
  500 + }
  501 +
  502 + var subscription = this;
  503 +
  504 + var listener = {
  505 + subscriptionType: this.type,
  506 + subscriptionTimewindow: this.subscriptionTimewindow,
  507 + datasource: datasource,
  508 + deviceId: deviceId,
  509 + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
  510 + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
  511 + },
  512 + updateRealtimeSubscription: function () {
  513 + this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
  514 + return this.subscriptionTimewindow;
  515 + },
  516 + setRealtimeSubscription: function (subscriptionTimewindow) {
  517 + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
  518 + },
  519 + datasourceIndex: index
  520 + };
  521 +
  522 + for (var a = 0; a < datasource.dataKeys.length; a++) {
  523 + this.data[index + a].data = [];
  524 + }
  525 +
  526 + index += datasource.dataKeys.length;
  527 +
  528 + this.datasourceListeners.push(listener);
  529 + this.ctx.datasourceService.subscribeToDatasource(listener);
  530 + }
  531 + }
  532 +
  533 + unsubscribe() {
  534 + if (this.type !== this.ctx.types.widgetType.rpc.value) {
  535 + for (var i = 0; i < this.datasourceListeners.length; i++) {
  536 + var listener = this.datasourceListeners[i];
  537 + this.ctx.datasourceService.unsubscribeFromDatasource(listener);
  538 + }
  539 + this.datasourceListeners = [];
  540 + }
  541 + }
  542 +
  543 + checkSubscriptions() {
  544 + var subscriptionsChanged = false;
  545 + for (var i = 0; i < this.datasourceListeners.length; i++) {
  546 + var listener = this.datasourceListeners[i];
  547 + var deviceId = null;
  548 + var aliasName = null;
  549 + if (listener.datasource.type === this.ctx.types.datasourceType.device) {
  550 + if (listener.datasource.deviceAliasId &&
  551 + this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
  552 + deviceId = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
  553 + aliasName = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
  554 + }
  555 + if (!angular.equals(deviceId, listener.deviceId) ||
  556 + !angular.equals(aliasName, listener.datasource.name)) {
  557 + subscriptionsChanged = true;
  558 + break;
  559 + }
  560 + }
  561 + }
  562 + if (subscriptionsChanged) {
  563 + this.unsubscribe();
  564 + this.subscribe();
  565 + }
  566 + }
  567 +
  568 + destroy() {
  569 + this.unsubscribe();
  570 + for (var cafId in this.cafs) {
  571 + if (this.cafs[cafId]) {
  572 + this.cafs[cafId]();
  573 + this.cafs[cafId] = null;
  574 + }
  575 + }
  576 + this.registrations.forEach(function (registration) {
  577 + registration();
  578 + });
  579 + this.registrations = [];
  580 + }
  581 +
  582 +}
  583 +
  584 +const varsRegex = /\$\{([^\}]*)\}/g;
  585 +
  586 +function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) {
  587 + var pattern = dataKey.pattern;
  588 + var label = dataKey.pattern;
  589 + var match = varsRegex.exec(pattern);
  590 + while (match !== null) {
  591 + var variable = match[0];
  592 + var variableName = match[1];
  593 + if (variableName === 'dsName') {
  594 + label = label.split(variable).join(dsName);
  595 + } else if (variableName === 'deviceName') {
  596 + label = label.split(variable).join(deviceName);
  597 + } else if (variableName === 'aliasName') {
  598 + label = label.split(variable).join(aliasName);
  599 + }
  600 + match = varsRegex.exec(pattern);
  601 + }
  602 + dataKey.label = label;
  603 +}
  604 +
  605 +function calculateMin(data) {
  606 + if (data.length > 0) {
  607 + var result = Number(data[0][1]);
  608 + for (var i=1;i<data.length;i++) {
  609 + result = Math.min(result, Number(data[i][1]));
  610 + }
  611 + return result;
  612 + } else {
  613 + return null;
  614 + }
  615 +}
  616 +
  617 +function calculateMax(data) {
  618 + if (data.length > 0) {
  619 + var result = Number(data[0][1]);
  620 + for (var i=1;i<data.length;i++) {
  621 + result = Math.max(result, Number(data[i][1]));
  622 + }
  623 + return result;
  624 + } else {
  625 + return null;
  626 + }
  627 +}
  628 +
  629 +function calculateAvg(data) {
  630 + if (data.length > 0) {
  631 + return calculateTotal(data)/data.length;
  632 + } else {
  633 + return null;
  634 + }
  635 +}
  636 +
  637 +function calculateTotal(data) {
  638 + if (data.length > 0) {
  639 + var result = 0;
  640 + for (var i = 0; i < data.length; i++) {
  641 + result += Number(data[i][1]);
  642 + }
  643 + return result;
  644 + } else {
  645 + return null;
  646 + }
  647 +}
@@ -22,9 +22,10 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin, @@ -22,9 +22,10 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
22 .name; 22 .name;
23 23
24 /*@ngInject*/ 24 /*@ngInject*/
25 -function UserService($http, $q, $rootScope, adminService, dashboardService, toast, store, jwtHelper, $translate, $state) { 25 +function UserService($http, $q, $rootScope, adminService, dashboardService, loginService, toast, store, jwtHelper, $translate, $state, $location) {
26 var currentUser = null, 26 var currentUser = null,
27 currentUserDetails = null, 27 currentUserDetails = null,
  28 + lastPublicDashboardId = null,
28 allowedDashboardIds = [], 29 allowedDashboardIds = [],
29 userLoaded = false; 30 userLoaded = false;
30 31
@@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
33 var service = { 34 var service = {
34 deleteUser: deleteUser, 35 deleteUser: deleteUser,
35 getAuthority: getAuthority, 36 getAuthority: getAuthority,
  37 + isPublic: isPublic,
  38 + getPublicId: getPublicId,
  39 + parsePublicId: parsePublicId,
36 isAuthenticated: isAuthenticated, 40 isAuthenticated: isAuthenticated,
37 getCurrentUser: getCurrentUser, 41 getCurrentUser: getCurrentUser,
38 getCustomerUsers: getCustomerUsers, 42 getCustomerUsers: getCustomerUsers,
@@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
51 updateAuthorizationHeader: updateAuthorizationHeader, 55 updateAuthorizationHeader: updateAuthorizationHeader,
52 gotoDefaultPlace: gotoDefaultPlace, 56 gotoDefaultPlace: gotoDefaultPlace,
53 forceDefaultPlace: forceDefaultPlace, 57 forceDefaultPlace: forceDefaultPlace,
54 - logout: logout 58 + updateLastPublicDashboardId: updateLastPublicDashboardId,
  59 + logout: logout,
  60 + reloadUser: reloadUser
55 } 61 }
56 62
57 - loadUser(true).then(function success() {  
58 - notifyUserLoaded();  
59 - }, function fail() {  
60 - notifyUserLoaded();  
61 - }); 63 + reloadUser();
62 64
63 return service; 65 return service;
64 66
65 - function updateAndValidateToken(token, prefix) { 67 + function reloadUser() {
  68 + userLoaded = false;
  69 + loadUser(true).then(function success() {
  70 + notifyUserLoaded();
  71 + }, function fail() {
  72 + notifyUserLoaded();
  73 + });
  74 + }
  75 +
  76 + function updateAndValidateToken(token, prefix, notify) {
66 var valid = false; 77 var valid = false;
67 var tokenData = jwtHelper.decodeToken(token); 78 var tokenData = jwtHelper.decodeToken(token);
68 var issuedAt = tokenData.iat; 79 var issuedAt = tokenData.iat;
@@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
76 valid = true; 87 valid = true;
77 } 88 }
78 } 89 }
79 - if (!valid) { 90 + if (!valid && notify) {
80 $rootScope.$broadcast('unauthenticated'); 91 $rootScope.$broadcast('unauthenticated');
81 } 92 }
82 } 93 }
@@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
91 function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) { 102 function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
92 currentUser = null; 103 currentUser = null;
93 currentUserDetails = null; 104 currentUserDetails = null;
  105 + lastPublicDashboardId = null;
94 allowedDashboardIds = []; 106 allowedDashboardIds = [];
95 if (!jwtToken) { 107 if (!jwtToken) {
96 clearTokenData(); 108 clearTokenData();
@@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
98 $rootScope.$broadcast('unauthenticated', doLogout); 110 $rootScope.$broadcast('unauthenticated', doLogout);
99 } 111 }
100 } else { 112 } else {
101 - updateAndValidateToken(jwtToken, 'jwt_token');  
102 - updateAndValidateToken(refreshToken, 'refresh_token'); 113 + updateAndValidateToken(jwtToken, 'jwt_token', true);
  114 + updateAndValidateToken(refreshToken, 'refresh_token', true);
103 if (notify) { 115 if (notify) {
104 loadUser(false).then(function success() { 116 loadUser(false).then(function success() {
105 $rootScope.$broadcast('authenticated'); 117 $rootScope.$broadcast('authenticated');
@@ -213,13 +225,58 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -213,13 +225,58 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
213 } 225 }
214 } 226 }
215 227
  228 + function isPublic() {
  229 + if (currentUser) {
  230 + return currentUser.isPublic;
  231 + } else {
  232 + return false;
  233 + }
  234 + }
  235 +
  236 + function getPublicId() {
  237 + if (isPublic()) {
  238 + return currentUser.sub;
  239 + } else {
  240 + return null;
  241 + }
  242 + }
  243 +
  244 + function parsePublicId() {
  245 + var token = getJwtToken();
  246 + if (token) {
  247 + var tokenData = jwtHelper.decodeToken(token);
  248 + if (tokenData && tokenData.isPublic) {
  249 + return tokenData.sub;
  250 + }
  251 + }
  252 + return null;
  253 + }
  254 +
216 function isUserLoaded() { 255 function isUserLoaded() {
217 return userLoaded; 256 return userLoaded;
218 } 257 }
219 258
220 function loadUser(doTokenRefresh) { 259 function loadUser(doTokenRefresh) {
  260 +
221 var deferred = $q.defer(); 261 var deferred = $q.defer();
222 - if (!currentUser) { 262 +
  263 + function fetchAllowedDashboardIds() {
  264 + var pageLink = {limit: 100};
  265 + dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
  266 + function success(result) {
  267 + var dashboards = result.data;
  268 + for (var d=0;d<dashboards.length;d++) {
  269 + allowedDashboardIds.push(dashboards[d].id.id);
  270 + }
  271 + deferred.resolve();
  272 + },
  273 + function fail() {
  274 + deferred.reject();
  275 + }
  276 + );
  277 + }
  278 +
  279 + function procceedJwtTokenValidate() {
223 validateJwtToken(doTokenRefresh).then(function success() { 280 validateJwtToken(doTokenRefresh).then(function success() {
224 var jwtToken = store.get('jwt_token'); 281 var jwtToken = store.get('jwt_token');
225 currentUser = jwtHelper.decodeToken(jwtToken); 282 currentUser = jwtHelper.decodeToken(jwtToken);
@@ -228,29 +285,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -228,29 +285,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
228 } else if (currentUser) { 285 } else if (currentUser) {
229 currentUser.authority = "ANONYMOUS"; 286 currentUser.authority = "ANONYMOUS";
230 } 287 }
231 - if (currentUser.userId) { 288 + if (currentUser.isPublic) {
  289 + $rootScope.forceFullscreen = true;
  290 + fetchAllowedDashboardIds();
  291 + } else if (currentUser.userId) {
232 getUser(currentUser.userId).then( 292 getUser(currentUser.userId).then(
233 function success(user) { 293 function success(user) {
234 currentUserDetails = user; 294 currentUserDetails = user;
235 $rootScope.forceFullscreen = false; 295 $rootScope.forceFullscreen = false;
236 - if (currentUserDetails.additionalInfo &&  
237 - currentUserDetails.additionalInfo.defaultDashboardFullscreen) {  
238 - $rootScope.forceFullscreen = currentUserDetails.additionalInfo.defaultDashboardFullscreen === true; 296 + if (userForceFullscreen()) {
  297 + $rootScope.forceFullscreen = true;
239 } 298 }
240 if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') { 299 if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
241 - var pageLink = {limit: 100};  
242 - dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(  
243 - function success(result) {  
244 - var dashboards = result.data;  
245 - for (var d=0;d<dashboards.length;d++) {  
246 - allowedDashboardIds.push(dashboards[d].id.id);  
247 - }  
248 - deferred.resolve();  
249 - },  
250 - function fail() {  
251 - deferred.reject();  
252 - }  
253 - ); 300 + fetchAllowedDashboardIds();
254 } else { 301 } else {
255 deferred.resolve(); 302 deferred.resolve();
256 } 303 }
@@ -265,6 +312,23 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -265,6 +312,23 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
265 }, function fail() { 312 }, function fail() {
266 deferred.reject(); 313 deferred.reject();
267 }); 314 });
  315 + }
  316 +
  317 + if (!currentUser) {
  318 + var locationSearch = $location.search();
  319 + if (locationSearch.publicId) {
  320 + loginService.publicLogin(locationSearch.publicId).then(function success(response) {
  321 + var token = response.data.token;
  322 + var refreshToken = response.data.refreshToken;
  323 + updateAndValidateToken(token, 'jwt_token', false);
  324 + updateAndValidateToken(refreshToken, 'refresh_token', false);
  325 + procceedJwtTokenValidate();
  326 + }, function fail() {
  327 + deferred.reject();
  328 + });
  329 + } else {
  330 + procceedJwtTokenValidate();
  331 + }
268 } else { 332 } else {
269 deferred.resolve(); 333 deferred.resolve();
270 } 334 }
@@ -373,17 +437,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -373,17 +437,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
373 function forceDefaultPlace(to, params) { 437 function forceDefaultPlace(to, params) {
374 if (currentUser && isAuthenticated()) { 438 if (currentUser && isAuthenticated()) {
375 if (currentUser.authority === 'CUSTOMER_USER') { 439 if (currentUser.authority === 'CUSTOMER_USER') {
376 - if (currentUserDetails &&  
377 - currentUserDetails.additionalInfo &&  
378 - currentUserDetails.additionalInfo.defaultDashboardId) {  
379 - if ($rootScope.forceFullscreen) {  
380 - if (to.name === 'home.profile') {  
381 - return false;  
382 - } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) { 440 + if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) {
  441 + if (to.name === 'home.profile') {
  442 + if (userHasProfile()) {
383 return false; 443 return false;
384 } else { 444 } else {
385 return true; 445 return true;
386 } 446 }
  447 + } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
  448 + return false;
  449 + } else {
  450 + return true;
387 } 451 }
388 } 452 }
389 } 453 }
@@ -395,11 +459,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -395,11 +459,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
395 if (currentUser && isAuthenticated()) { 459 if (currentUser && isAuthenticated()) {
396 var place = 'home.links'; 460 var place = 'home.links';
397 if (currentUser.authority === 'CUSTOMER_USER') { 461 if (currentUser.authority === 'CUSTOMER_USER') {
398 - if (currentUserDetails &&  
399 - currentUserDetails.additionalInfo &&  
400 - currentUserDetails.additionalInfo.defaultDashboardId) { 462 + if (userHasDefaultDashboard()) {
401 place = 'home.dashboards.dashboard'; 463 place = 'home.dashboards.dashboard';
402 params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId}; 464 params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
  465 + } else if (isPublic()) {
  466 + place = 'home.dashboards.dashboard';
  467 + params = {dashboardId: lastPublicDashboardId};
403 } 468 }
404 } else if (currentUser.authority === 'SYS_ADMIN') { 469 } else if (currentUser.authority === 'SYS_ADMIN') {
405 adminService.checkUpdates().then( 470 adminService.checkUpdates().then(
@@ -416,4 +481,27 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas @@ -416,4 +481,27 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
416 } 481 }
417 } 482 }
418 483
  484 + function userHasDefaultDashboard() {
  485 + return currentUserDetails &&
  486 + currentUserDetails.additionalInfo &&
  487 + currentUserDetails.additionalInfo.defaultDashboardId;
  488 + }
  489 +
  490 + function userForceFullscreen() {
  491 + return (currentUser && currentUser.isPublic) ||
  492 + (currentUserDetails.additionalInfo &&
  493 + currentUserDetails.additionalInfo.defaultDashboardFullscreen &&
  494 + currentUserDetails.additionalInfo.defaultDashboardFullscreen === true);
  495 + }
  496 +
  497 + function userHasProfile() {
  498 + return currentUser && !currentUser.isPublic;
  499 + }
  500 +
  501 + function updateLastPublicDashboardId(dashboardId) {
  502 + if (isPublic()) {
  503 + lastPublicDashboardId = dashboardId;
  504 + }
  505 + }
  506 +
419 } 507 }
@@ -17,7 +17,8 @@ import $ from 'jquery'; @@ -17,7 +17,8 @@ import $ from 'jquery';
17 import moment from 'moment'; 17 import moment from 'moment';
18 import tinycolor from 'tinycolor2'; 18 import tinycolor from 'tinycolor2';
19 19
20 -import thinsboardLedLight from '../components/led-light.directive'; 20 +import thingsboardLedLight from '../components/led-light.directive';
  21 +import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
21 22
22 import TbFlot from '../widget/lib/flot-widget'; 23 import TbFlot from '../widget/lib/flot-widget';
23 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; 24 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
@@ -31,7 +32,8 @@ import cssjs from '../../vendor/css.js/css'; @@ -31,7 +32,8 @@ import cssjs from '../../vendor/css.js/css';
31 import thingsboardTypes from '../common/types.constant'; 32 import thingsboardTypes from '../common/types.constant';
32 import thingsboardUtils from '../common/utils.service'; 33 import thingsboardUtils from '../common/utils.service';
33 34
34 -export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thinsboardLedLight, thingsboardTypes, thingsboardUtils]) 35 +export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
  36 + thingsboardTypes, thingsboardUtils])
35 .factory('widgetService', WidgetService) 37 .factory('widgetService', WidgetService)
36 .name; 38 .name;
37 39
@@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ @@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
539 541
540 ' }\n\n' + 542 ' }\n\n' +
541 543
  544 + ' self.useCustomDatasources = function() {\n\n' +
  545 +
  546 + ' }\n\n' +
  547 +
542 ' self.onResize = function() {\n\n' + 548 ' self.onResize = function() {\n\n' +
543 549
544 ' }\n\n' + 550 ' }\n\n' +
@@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ @@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
579 if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { 585 if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
580 result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); 586 result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
581 } 587 }
  588 + if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) {
  589 + result.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
  590 + } else {
  591 + result.useCustomDatasources = false;
  592 + }
582 return result; 593 return result;
583 } catch (e) { 594 } catch (e) {
584 utils.processWidgetException(e); 595 utils.processWidgetException(e);
@@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ @@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
617 if (widgetType.dataKeySettingsSchema) { 628 if (widgetType.dataKeySettingsSchema) {
618 widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; 629 widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
619 } 630 }
  631 + widgetInfo.useCustomDatasources = widgetType.useCustomDatasources;
620 putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); 632 putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
621 putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); 633 putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
622 deferred.resolve(widgetInfo); 634 deferred.resolve(widgetInfo);
@@ -55,8 +55,39 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, @@ -55,8 +55,39 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
55 }); 55 });
56 56
57 $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { 57 $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
  58 +
  59 + function waitForUserLoaded() {
  60 + if ($rootScope.userLoadedHandle) {
  61 + $rootScope.userLoadedHandle();
  62 + }
  63 + $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
  64 + $rootScope.userLoadedHandle();
  65 + $state.go(to.name, params);
  66 + });
  67 + }
  68 +
  69 + function reloadUserFromPublicId() {
  70 + userService.setUserFromJwtToken(null, null, false);
  71 + waitForUserLoaded();
  72 + userService.reloadUser();
  73 + }
  74 +
  75 + var locationSearch = $location.search();
  76 + var publicId = locationSearch.publicId;
  77 +
58 if (userService.isUserLoaded() === true) { 78 if (userService.isUserLoaded() === true) {
59 if (userService.isAuthenticated()) { 79 if (userService.isAuthenticated()) {
  80 + if (userService.isPublic()) {
  81 + if (userService.parsePublicId() !== publicId) {
  82 + evt.preventDefault();
  83 + if (publicId && publicId.length > 0) {
  84 + reloadUserFromPublicId();
  85 + } else {
  86 + userService.logout();
  87 + }
  88 + return;
  89 + }
  90 + }
60 if (userService.forceDefaultPlace(to, params)) { 91 if (userService.forceDefaultPlace(to, params)) {
61 evt.preventDefault(); 92 evt.preventDefault();
62 gotoDefaultPlace(params); 93 gotoDefaultPlace(params);
@@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, @@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
75 } 106 }
76 } 107 }
77 } else { 108 } else {
78 - if (to.module === 'private') { 109 + if (publicId && publicId.length > 0) {
  110 + evt.preventDefault();
  111 + reloadUserFromPublicId();
  112 + } else if (to.module === 'private') {
79 evt.preventDefault(); 113 evt.preventDefault();
80 if (to.url === '/home' || to.url === '/') { 114 if (to.url === '/home' || to.url === '/') {
81 $state.go('login', params); 115 $state.go('login', params);
@@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, @@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
86 } 120 }
87 } else { 121 } else {
88 evt.preventDefault(); 122 evt.preventDefault();
89 - if ($rootScope.userLoadedHandle) {  
90 - $rootScope.userLoadedHandle();  
91 - }  
92 - $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {  
93 - $rootScope.userLoadedHandle();  
94 - $state.go(to.name, params);  
95 - }); 123 + waitForUserLoaded();
96 } 124 }
97 }) 125 })
98 126
99 $rootScope.pageTitle = 'Thingsboard'; 127 $rootScope.pageTitle = 'Thingsboard';
100 128
101 - $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to) { 129 + $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) {
  130 + if (userService.isPublic() && to.name === 'home.dashboards.dashboard') {
  131 + $location.search('publicId', userService.getPublicId());
  132 + userService.updateLastPublicDashboardId(params.dashboardId);
  133 + }
102 if (angular.isDefined(to.data.pageTitle)) { 134 if (angular.isDefined(to.data.pageTitle)) {
103 $translate(to.data.pageTitle).then(function (translation) { 135 $translate(to.data.pageTitle).then(function (translation) {
104 $rootScope.pageTitle = 'Thingsboard | ' + translation; 136 $rootScope.pageTitle = 'Thingsboard | ' + translation;
@@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) @@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
22 .name; 22 .name;
23 23
24 /*@ngInject*/ 24 /*@ngInject*/
25 -function Utils($mdColorPalette, $rootScope, $window, types) { 25 +function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
26 26
27 var predefinedFunctions = {}, 27 var predefinedFunctions = {},
28 predefinedFunctionsList = [], 28 predefinedFunctionsList = [],
@@ -104,7 +104,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) { @@ -104,7 +104,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
104 parseException: parseException, 104 parseException: parseException,
105 processWidgetException: processWidgetException, 105 processWidgetException: processWidgetException,
106 isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, 106 isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
107 - filterSearchTextEntities: filterSearchTextEntities 107 + filterSearchTextEntities: filterSearchTextEntities,
  108 + guid: guid,
  109 + createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
108 } 110 }
109 111
110 return service; 112 return service;
@@ -276,4 +278,153 @@ function Utils($mdColorPalette, $rootScope, $window, types) { @@ -276,4 +278,153 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
276 deferred.resolve(response); 278 deferred.resolve(response);
277 } 279 }
278 280
  281 + function guid() {
  282 + function s4() {
  283 + return Math.floor((1 + Math.random()) * 0x10000)
  284 + .toString(16)
  285 + .substring(1);
  286 + }
  287 + return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
  288 + s4() + '-' + s4() + s4() + s4();
  289 + }
  290 +
  291 + function genNextColor(datasources) {
  292 + var index = 0;
  293 + if (datasources) {
  294 + for (var i = 0; i < datasources.length; i++) {
  295 + var datasource = datasources[i];
  296 + index += datasource.dataKeys.length;
  297 + }
  298 + }
  299 + return getMaterialColor(index);
  300 + }
  301 +
  302 + /*var defaultDataKey = {
  303 + name: 'f(x)',
  304 + type: types.dataKeyType.function,
  305 + label: 'Sin',
  306 + color: getMaterialColor(0),
  307 + funcBody: getPredefinedFunctionBody('Sin'),
  308 + settings: {},
  309 + _hash: Math.random()
  310 + };
  311 +
  312 + var defaultDatasource = {
  313 + type: types.datasourceType.function,
  314 + name: types.datasourceType.function,
  315 + dataKeys: [angular.copy(defaultDataKey)]
  316 + };*/
  317 +
  318 + function createKey(keyInfo, type, datasources) {
  319 + var dataKey = {
  320 + name: keyInfo.name,
  321 + type: type,
  322 + label: keyInfo.label || keyInfo.name,
  323 + color: genNextColor(datasources),
  324 + funcBody: keyInfo.funcBody,
  325 + settings: {},
  326 + _hash: Math.random()
  327 + }
  328 + return dataKey;
  329 + }
  330 +
  331 + function createDatasourceKeys(keyInfos, type, datasource, datasources) {
  332 + for (var i=0;i<keyInfos.length;i++) {
  333 + var keyInfo = keyInfos[i];
  334 + var dataKey = createKey(keyInfo, type, datasources);
  335 + datasource.dataKeys.push(dataKey);
  336 + }
  337 + }
  338 +
  339 + function createDatasourceFromSubscription(subscriptionInfo, datasources, device) {
  340 + var datasource;
  341 + if (subscriptionInfo.type === types.datasourceType.device) {
  342 + datasource = {
  343 + type: subscriptionInfo.type,
  344 + deviceName: device.name,
  345 + deviceId: device.id.id,
  346 + dataKeys: []
  347 + }
  348 + } else if (subscriptionInfo.type === types.datasourceType.function) {
  349 + datasource = {
  350 + type: subscriptionInfo.type,
  351 + name: subscriptionInfo.name || types.datasourceType.function,
  352 + dataKeys: []
  353 + }
  354 + }
  355 + datasources.push(datasource);
  356 + if (subscriptionInfo.timeseries) {
  357 + createDatasourceKeys(subscriptionInfo.timeseries, types.dataKeyType.timeseries, datasource, datasources);
  358 + }
  359 + if (subscriptionInfo.attributes) {
  360 + createDatasourceKeys(subscriptionInfo.attributes, types.dataKeyType.attribute, datasource, datasources);
  361 + }
  362 + if (subscriptionInfo.functions) {
  363 + createDatasourceKeys(subscriptionInfo.functions, types.dataKeyType.function, datasource, datasources);
  364 + }
  365 + }
  366 +
  367 + function processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred) {
  368 + if (index < subscriptionsInfo.length) {
  369 + var subscriptionInfo = subscriptionsInfo[index];
  370 + if (subscriptionInfo.type === types.datasourceType.device) {
  371 + if (subscriptionInfo.deviceId) {
  372 + deviceService.getDevice(subscriptionInfo.deviceId, true, {ignoreLoading: true}).then(
  373 + function success(device) {
  374 + createDatasourceFromSubscription(subscriptionInfo, datasources, device);
  375 + index++;
  376 + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
  377 + },
  378 + function fail() {
  379 + index++;
  380 + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
  381 + }
  382 + );
  383 + } else if (subscriptionInfo.deviceName || subscriptionInfo.deviceNamePrefix
  384 + || subscriptionInfo.deviceIds) {
  385 + var promise;
  386 + if (subscriptionInfo.deviceName) {
  387 + promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceName, 1, false, {ignoreLoading: true});
  388 + } else if (subscriptionInfo.deviceNamePrefix) {
  389 + promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceNamePrefix, 100, false, {ignoreLoading: true});
  390 + } else if (subscriptionInfo.deviceIds) {
  391 + promise = deviceService.getDevices(subscriptionInfo.deviceIds, {ignoreLoading: true});
  392 + }
  393 + promise.then(
  394 + function success(devices) {
  395 + if (devices && devices.length > 0) {
  396 + for (var i = 0; i < devices.length; i++) {
  397 + var device = devices[i];
  398 + createDatasourceFromSubscription(subscriptionInfo, datasources, device);
  399 + }
  400 + }
  401 + index++;
  402 + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
  403 + },
  404 + function fail() {
  405 + index++;
  406 + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
  407 + }
  408 + )
  409 + } else {
  410 + index++;
  411 + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
  412 + }
  413 + } else if (subscriptionInfo.type === types.datasourceType.function) {
  414 + createDatasourceFromSubscription(subscriptionInfo, datasources);
  415 + index++;
  416 + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
  417 + }
  418 + } else {
  419 + deferred.resolve(datasources);
  420 + }
  421 + }
  422 +
  423 + function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) {
  424 + var deferred = $q.defer();
  425 + var datasources = [];
  426 + processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred);
  427 + return deferred.promise;
  428 + }
  429 +
279 } 430 }
@@ -182,12 +182,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t @@ -182,12 +182,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
182 182
183 vm.dashboardTimewindowApi = { 183 vm.dashboardTimewindowApi = {
184 onResetTimewindow: function() { 184 onResetTimewindow: function() {
185 - if (vm.originalDashboardTimewindow) {  
186 - $timeout(function() { 185 + $timeout(function() {
  186 + if (vm.originalDashboardTimewindow) {
187 vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); 187 vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
188 vm.originalDashboardTimewindow = null; 188 vm.originalDashboardTimewindow = null;
189 - }, 0);  
190 - } 189 + }
  190 + }, 0);
191 }, 191 },
192 onUpdateTimewindow: function(startTimeMs, endTimeMs) { 192 onUpdateTimewindow: function(startTimeMs, endTimeMs) {
193 if (!vm.originalDashboardTimewindow) { 193 if (!vm.originalDashboardTimewindow) {
@@ -41,7 +41,7 @@ function DeviceFilter($compile, $templateCache, $q, deviceService) { @@ -41,7 +41,7 @@ function DeviceFilter($compile, $templateCache, $q, deviceService) {
41 41
42 var deferred = $q.defer(); 42 var deferred = $q.defer();
43 43
44 - deviceService.getTenantDevices(pageLink).then(function success(result) { 44 + deviceService.getTenantDevices(pageLink, false).then(function success(result) {
45 deferred.resolve(result.data); 45 deferred.resolve(result.data);
46 }, function fail() { 46 }, function fail() {
47 deferred.reject(); 47 deferred.reject();
@@ -49,7 +49,7 @@ @@ -49,7 +49,7 @@
49 <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList" 49 <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
50 ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}"> 50 ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
51 <md-tooltip md-direction="top"> 51 <md-tooltip md-direction="top">
52 - {{ action.details() }} 52 + {{ action.details( rowItem[n] ) }}
53 </md-tooltip> 53 </md-tooltip>
54 <ng-md-icon icon="{{action.icon}}"></ng-md-icon> 54 <ng-md-icon icon="{{action.icon}}"></ng-md-icon>
55 </md-button> 55 </md-button>
@@ -62,7 +62,7 @@ @@ -62,7 +62,7 @@
62 </div> 62 </div>
63 <tb-details-sidenav 63 <tb-details-sidenav
64 header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}" 64 header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}"
65 - header-subtitle="{{vm.itemDetailsText()}}" 65 + header-subtitle="{{vm.itemDetailsText(vm.operatingItem())}}"
66 is-read-only="vm.isDetailsReadOnly(vm.operatingItem())" 66 is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
67 is-open="vm.detailsConfig.isDetailsOpen" 67 is-open="vm.detailsConfig.isDetailsOpen"
68 is-edit="vm.detailsConfig.isDetailsEditMode" 68 is-edit="vm.detailsConfig.isDetailsEditMode"
@@ -44,15 +44,8 @@ function Legend($compile, $templateCache, types) { @@ -44,15 +44,8 @@ function Legend($compile, $templateCache, types) {
44 scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value || 44 scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
45 scope.legendConfig.position === types.position.top.value; 45 scope.legendConfig.position === types.position.top.value;
46 46
47 - scope.$on('legendDataUpdated', function (event, apply) {  
48 - if (apply) {  
49 - scope.$digest();  
50 - }  
51 - });  
52 -  
53 scope.toggleHideData = function(index) { 47 scope.toggleHideData = function(index) {
54 scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden; 48 scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
55 - scope.$emit('legendDataHiddenChanged', index);  
56 } 49 }
57 50
58 $compile(element.contents())(scope); 51 $compile(element.contents())(scope);
@@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
76 scope.forceExpandDatasources = false; 76 scope.forceExpandDatasources = false;
77 } 77 }
78 78
  79 + if (angular.isUndefined(scope.isDataEnabled)) {
  80 + scope.isDataEnabled = true;
  81 + }
  82 +
79 scope.currentSettingsSchema = {}; 83 scope.currentSettingsSchema = {};
80 scope.currentSettings = angular.copy(scope.emptySettingsSchema); 84 scope.currentSettings = angular.copy(scope.emptySettingsSchema);
81 85
@@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
108 scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ? 112 scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ?
109 ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value; 113 ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
110 scope.legendConfig = ngModelCtrl.$viewValue.legendConfig; 114 scope.legendConfig = ngModelCtrl.$viewValue.legendConfig;
111 - if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) { 115 + if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
  116 + && scope.isDataEnabled) {
112 if (scope.datasources) { 117 if (scope.datasources) {
113 scope.datasources.splice(0, scope.datasources.length); 118 scope.datasources.splice(0, scope.datasources.length);
114 } else { 119 } else {
@@ -119,7 +124,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -119,7 +124,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
119 scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]}); 124 scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
120 } 125 }
121 } 126 }
122 - } else if (scope.widgetType === types.widgetType.rpc.value) { 127 + } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
123 if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) { 128 if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
124 var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0]; 129 var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
125 if (scope.deviceAliases[aliasId]) { 130 if (scope.deviceAliases[aliasId]) {
@@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
159 if (ngModelCtrl.$viewValue) { 164 if (ngModelCtrl.$viewValue) {
160 var value = ngModelCtrl.$viewValue; 165 var value = ngModelCtrl.$viewValue;
161 var valid; 166 var valid;
162 - if (scope.widgetType === types.widgetType.rpc.value) { 167 + if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
163 valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0; 168 valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
164 ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); 169 ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
165 - } else if (scope.widgetType !== types.widgetType.static.value) { 170 + } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
166 valid = value && value.datasources && value.datasources.length > 0; 171 valid = value && value.datasources && value.datasources.length > 0;
167 ngModelCtrl.$setValidity('datasources', valid); 172 ngModelCtrl.$setValidity('datasources', valid);
168 } 173 }
@@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
228 233
229 scope.$watch('datasources', function () { 234 scope.$watch('datasources', function () {
230 if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value 235 if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value
231 - && scope.widgetType !== types.widgetType.static.value) { 236 + && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
232 var value = ngModelCtrl.$viewValue; 237 var value = ngModelCtrl.$viewValue;
233 if (value.datasources) { 238 if (value.datasources) {
234 value.datasources.splice(0, value.datasources.length); 239 value.datasources.splice(0, value.datasources.length);
@@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
246 }, true); 251 }, true);
247 252
248 scope.$watch('targetDeviceAlias.value', function () { 253 scope.$watch('targetDeviceAlias.value', function () {
249 - if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value) { 254 + if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
250 var value = ngModelCtrl.$viewValue; 255 var value = ngModelCtrl.$viewValue;
251 if (scope.targetDeviceAlias.value) { 256 if (scope.targetDeviceAlias.value) {
252 value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; 257 value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
@@ -359,6 +364,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti @@ -359,6 +364,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
359 require: "^ngModel", 364 require: "^ngModel",
360 scope: { 365 scope: {
361 forceExpandDatasources: '=?', 366 forceExpandDatasources: '=?',
  367 + isDataEnabled: '=?',
362 widgetType: '=', 368 widgetType: '=',
363 widgetSettingsSchema: '=', 369 widgetSettingsSchema: '=',
364 datakeySettingsSchema: '=', 370 datakeySettingsSchema: '=',
@@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
31 </section> 31 </section>
32 </div> 32 </div>
33 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" 33 <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
34 - ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value"> 34 + ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled">
35 <v-pane id="datasources-pane" expanded="true"> 35 <v-pane id="datasources-pane" expanded="true">
36 <v-pane-header> 36 <v-pane-header>
37 {{ 'widget-config.datasources' | translate }} 37 {{ 'widget-config.datasources' | translate }}
@@ -96,7 +96,7 @@ @@ -96,7 +96,7 @@
96 </v-pane> 96 </v-pane>
97 </v-accordion> 97 </v-accordion>
98 <v-accordion id="target-devices-accordion" control="targetDevicesAccordion" class="vAccordion--default" 98 <v-accordion id="target-devices-accordion" control="targetDevicesAccordion" class="vAccordion--default"
99 - ng-show="widgetType === types.widgetType.rpc.value"> 99 + ng-show="widgetType === types.widgetType.rpc.value && isDataEnabled">
100 <v-pane id="target-devices-pane" expanded="true"> 100 <v-pane id="target-devices-pane" expanded="true">
101 <v-pane-header> 101 <v-pane-header>
102 {{ 'widget-config.target-device' | translate }} 102 {{ 'widget-config.target-device' | translate }}
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 import $ from 'jquery'; 16 import $ from 'jquery';
17 import 'javascript-detect-element-resize/detect-element-resize'; 17 import 'javascript-detect-element-resize/detect-element-resize';
  18 +import Subscription from '../api/subscription';
18 19
19 /* eslint-disable angular/angularelement */ 20 /* eslint-disable angular/angularelement */
20 21
@@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
34 $scope.rpcErrorText = null; 35 $scope.rpcErrorText = null;
35 $scope.rpcEnabled = false; 36 $scope.rpcEnabled = false;
36 $scope.executingRpcRequest = false; 37 $scope.executingRpcRequest = false;
37 - $scope.executingPromises = [];  
38 38
39 var gridsterItemInited = false; 39 var gridsterItemInited = false;
40 40
41 - var datasourceListeners = [];  
42 - var targetDeviceAliasId = null;  
43 - var targetDeviceId = null;  
44 - var originalTimewindow = null;  
45 - var subscriptionTimewindow = null;  
46 var cafs = {}; 41 var cafs = {};
47 42
48 - var varsRegex = /\$\{([^\}]*)\}/g;  
49 -  
50 /* 43 /*
51 * data = array of datasourceData 44 * data = array of datasourceData
52 * datasourceData = { 45 * datasourceData = {
@@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
54 * dataKey, { name, config } 47 * dataKey, { name, config }
55 * data = array of [time, value] 48 * data = array of [time, value]
56 * } 49 * }
57 - *  
58 - *  
59 */ 50 */
60 51
61 var widgetContext = { 52 var widgetContext = {
@@ -71,22 +62,70 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -71,22 +62,70 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
71 settings: widget.config.settings, 62 settings: widget.config.settings,
72 units: widget.config.units || '', 63 units: widget.config.units || '',
73 decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2, 64 decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
74 - datasources: angular.copy(widget.config.datasources),  
75 - data: [],  
76 - hiddenData: [],  
77 - timeWindow: {  
78 - stDiff: stDiff  
79 - }, 65 + subscriptions: {},
  66 + defaultSubscription: null,
80 timewindowFunctions: { 67 timewindowFunctions: {
81 - onUpdateTimewindow: onUpdateTimewindow,  
82 - onResetTimewindow: onResetTimewindow 68 + onUpdateTimewindow: function(startTimeMs, endTimeMs) {
  69 + if (widgetContext.defaultSubscription) {
  70 + widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs);
  71 + }
  72 + },
  73 + onResetTimewindow: function() {
  74 + if (widgetContext.defaultSubscription) {
  75 + widgetContext.defaultSubscription.onResetTimewindow();
  76 + }
  77 + }
  78 + },
  79 + subscriptionApi: {
  80 + createSubscription: function(options, subscribe) {
  81 + return createSubscription(options, subscribe);
  82 + },
  83 +
  84 +
  85 + // type: "timeseries" or "latest" or "rpc"
  86 + /* devicesSubscriptionInfo = [
  87 + {
  88 + deviceId: ""
  89 + deviceName: ""
  90 + timeseries: [{ name: "", label: "" }, ..]
  91 + attributes: [{ name: "", label: "" }, ..]
  92 + }
  93 + ..
  94 + ]*/
  95 +
  96 + // options = {
  97 + // timeWindowConfig,
  98 + // useDashboardTimewindow,
  99 + // legendConfig,
  100 + // decimals,
  101 + // units,
  102 + // callbacks [ onDataUpdated(subscription, apply) ]
  103 + // }
  104 + //
  105 +
  106 + createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
  107 + return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe);
  108 + },
  109 + removeSubscription: function(id) {
  110 + var subscription = widgetContext.subscriptions[id];
  111 + if (subscription) {
  112 + subscription.destroy();
  113 + delete widgetContext.subscriptions[id];
  114 + }
  115 + }
83 }, 116 },
84 controlApi: { 117 controlApi: {
85 sendOneWayCommand: function(method, params, timeout) { 118 sendOneWayCommand: function(method, params, timeout) {
86 - return sendCommand(true, method, params, timeout); 119 + if (widgetContext.defaultSubscription) {
  120 + return widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout);
  121 + }
  122 + return null;
87 }, 123 },
88 sendTwoWayCommand: function(method, params, timeout) { 124 sendTwoWayCommand: function(method, params, timeout) {
89 - return sendCommand(false, method, params, timeout); 125 + if (widgetContext.defaultSubscription) {
  126 + return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout);
  127 + }
  128 + return null;
90 } 129 }
91 }, 130 },
92 utils: { 131 utils: {
@@ -94,7 +133,27 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -94,7 +133,27 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
94 } 133 }
95 }; 134 };
96 135
  136 + var subscriptionContext = {
  137 + $scope: $scope,
  138 + $q: $q,
  139 + $filter: $filter,
  140 + $timeout: $timeout,
  141 + tbRaf: tbRaf,
  142 + timeService: timeService,
  143 + deviceService: deviceService,
  144 + datasourceService: datasourceService,
  145 + utils: utils,
  146 + widgetUtils: widgetContext.utils,
  147 + dashboardTimewindowApi: dashboardTimewindowApi,
  148 + types: types,
  149 + stDiff: stDiff,
  150 + aliasesInfo: aliasesInfo
  151 + };
  152 +
97 var widgetTypeInstance; 153 var widgetTypeInstance;
  154 +
  155 + vm.useCustomDatasources = false;
  156 +
98 try { 157 try {
99 widgetTypeInstance = new widgetType(widgetContext); 158 widgetTypeInstance = new widgetType(widgetContext);
100 } catch (e) { 159 } catch (e) {
@@ -119,19 +178,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -119,19 +178,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
119 if (!widgetTypeInstance.onDestroy) { 178 if (!widgetTypeInstance.onDestroy) {
120 widgetTypeInstance.onDestroy = function() {}; 179 widgetTypeInstance.onDestroy = function() {};
121 } 180 }
122 -  
123 - //var bounds = {top: 0, left: 0, bottom: 0, right: 0};  
124 - //TODO: widgets visibility  
125 - /*var visible = false;*/  
126 -  
127 - $scope.clearRpcError = function() {  
128 - $scope.rpcRejection = null;  
129 - $scope.rpcErrorText = null; 181 + if (widgetTypeInstance.useCustomDatasources) {
  182 + vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
130 } 183 }
131 184
132 - vm.gridsterItemInitialized = gridsterItemInitialized;  
133 -  
134 //TODO: widgets visibility 185 //TODO: widgets visibility
  186 +
  187 + //var bounds = {top: 0, left: 0, bottom: 0, right: 0};
  188 + /*var visible = false;*/
135 /*vm.visibleRectChanged = visibleRectChanged; 189 /*vm.visibleRectChanged = visibleRectChanged;
136 190
137 function visibleRectChanged(newVisibleRect) { 191 function visibleRectChanged(newVisibleRect) {
@@ -139,23 +193,216 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -139,23 +193,216 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
139 updateVisibility(); 193 updateVisibility();
140 }*/ 194 }*/
141 195
  196 + $scope.clearRpcError = function() {
  197 + if (widgetContext.defaultSubscription) {
  198 + widgetContext.defaultSubscription.clearRpcError();
  199 + }
  200 + }
  201 +
  202 + vm.gridsterItemInitialized = gridsterItemInitialized;
  203 +
142 initialize(); 204 initialize();
143 205
144 - function handleWidgetException(e) {  
145 - $log.error(e);  
146 - $scope.widgetErrorData = utils.processWidgetException(e); 206 +
  207 + /*
  208 + options = {
  209 + type,
  210 + targetDeviceAliasIds, // RPC
  211 + targetDeviceIds, // RPC
  212 + datasources,
  213 + timeWindowConfig,
  214 + useDashboardTimewindow,
  215 + legendConfig,
  216 + decimals,
  217 + units,
  218 + callbacks
  219 + }
  220 + */
  221 +
  222 + function createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
  223 + var deferred = $q.defer();
  224 + options.type = type;
  225 +
  226 + if (useDefaultComponents) {
  227 + defaultComponentsOptions(options);
  228 + } else {
  229 + if (!options.timeWindowConfig) {
  230 + options.useDashboardTimewindow = true;
  231 + }
  232 + }
  233 +
  234 + utils.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then(
  235 + function (datasources) {
  236 + options.datasources = datasources;
  237 + var subscription = createSubscription(options, subscribe);
  238 + if (useDefaultComponents) {
  239 + defaultSubscriptionOptions(subscription, options);
  240 + }
  241 + deferred.resolve(subscription);
  242 + }
  243 + );
  244 + return deferred.promise;
  245 + }
  246 +
  247 + function createSubscription(options, subscribe) {
  248 + options.dashboardTimewindow = dashboardTimewindow;
  249 + var subscription =
  250 + new Subscription(subscriptionContext, options);
  251 + widgetContext.subscriptions[subscription.id] = subscription;
  252 + if (subscribe) {
  253 + subscription.subscribe();
  254 + }
  255 + return subscription;
147 } 256 }
148 257
149 - function notifyDataLoaded() {  
150 - if ($scope.loadingData === true) { 258 + function defaultComponentsOptions(options) {
  259 + options.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
  260 + ? widget.config.useDashboardTimewindow : true;
  261 +
  262 + options.timeWindowConfig = options.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;
  263 + options.legendConfig = null;
  264 +
  265 + if ($scope.displayLegend) {
  266 + options.legendConfig = $scope.legendConfig;
  267 + }
  268 + options.decimals = widgetContext.decimals;
  269 + options.units = widgetContext.units;
  270 +
  271 + options.callbacks = {
  272 + onDataUpdated: function() {
  273 + widgetTypeInstance.onDataUpdated();
  274 + },
  275 + onDataUpdateError: function(subscription, e) {
  276 + handleWidgetException(e);
  277 + },
  278 + dataLoading: function(subscription) {
  279 + if ($scope.loadingData !== subscription.loadingData) {
  280 + $scope.loadingData = subscription.loadingData;
  281 + }
  282 + },
  283 + legendDataUpdated: function(subscription, apply) {
  284 + if (apply) {
  285 + $scope.$digest();
  286 + }
  287 + },
  288 + timeWindowUpdated: function(subscription, timeWindowConfig) {
  289 + widget.config.timewindow = timeWindowConfig;
  290 + $scope.$apply();
  291 + }
  292 + }
  293 + }
  294 +
  295 + function defaultSubscriptionOptions(subscription, options) {
  296 + if (!options.useDashboardTimewindow) {
  297 + $scope.$watch(function () {
  298 + return widget.config.timewindow;
  299 + }, function (newTimewindow, prevTimewindow) {
  300 + if (!angular.equals(newTimewindow, prevTimewindow)) {
  301 + subscription.updateTimewindowConfig(widget.config.timewindow);
  302 + }
  303 + });
  304 + }
  305 + if ($scope.displayLegend) {
  306 + $scope.legendData = subscription.legendData;
  307 + }
  308 + }
  309 +
  310 + function createDefaultSubscription() {
  311 + var subscription;
  312 + var options;
  313 + if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
  314 + options = {
  315 + type: widget.type,
  316 + datasources: angular.copy(widget.config.datasources)
  317 + };
  318 + defaultComponentsOptions(options);
  319 +
  320 + subscription = createSubscription(options);
  321 +
  322 + defaultSubscriptionOptions(subscription, options);
  323 +
  324 + // backward compatibility
  325 +
  326 + widgetContext.datasources = subscription.datasources;
  327 + widgetContext.data = subscription.data;
  328 + widgetContext.hiddenData = subscription.hiddenData;
  329 + widgetContext.timeWindow = subscription.timeWindow;
  330 +
  331 + } else if (widget.type === types.widgetType.rpc.value) {
  332 + $scope.loadingData = false;
  333 + options = {
  334 + type: widget.type,
  335 + targetDeviceAliasIds: widget.config.targetDeviceAliasIds
  336 + }
  337 + options.callbacks = {
  338 + rpcStateChanged: function(subscription) {
  339 + $scope.rpcEnabled = subscription.rpcEnabled;
  340 + $scope.executingRpcRequest = subscription.executingRpcRequest;
  341 + },
  342 + onRpcSuccess: function(subscription) {
  343 + $scope.executingRpcRequest = subscription.executingRpcRequest;
  344 + },
  345 + onRpcFailed: function(subscription) {
  346 + $scope.executingRpcRequest = subscription.executingRpcRequest;
  347 + $scope.rpcErrorText = subscription.rpcErrorText;
  348 + $scope.rpcRejection = subscription.rpcRejection;
  349 + },
  350 + onRpcErrorCleared: function() {
  351 + $scope.rpcErrorText = null;
  352 + $scope.rpcRejection = null;
  353 + }
  354 + }
  355 + subscription = createSubscription(options);
  356 + } else if (widget.type === types.widgetType.static.value) {
151 $scope.loadingData = false; 357 $scope.loadingData = false;
152 } 358 }
  359 + if (subscription) {
  360 + widgetContext.defaultSubscription = subscription;
  361 + }
153 } 362 }
154 363
155 - function notifyDataLoading() {  
156 - if ($scope.loadingData === false) {  
157 - $scope.loadingData = true; 364 +
  365 + function initialize() {
  366 +
  367 + if (!vm.useCustomDatasources) {
  368 + createDefaultSubscription();
  369 + } else {
  370 + $scope.loadingData = false;
158 } 371 }
  372 +
  373 + $scope.$on('toggleDashboardEditMode', function (event, isEdit) {
  374 + onEditModeChanged(isEdit);
  375 + });
  376 +
  377 + addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
  378 +
  379 + $scope.$watch(function () {
  380 + return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;
  381 + }, function () {
  382 + //updateBounds();
  383 + $scope.$emit("widgetPositionChanged", widget);
  384 + });
  385 +
  386 + $scope.$on('gridster-item-resized', function (event, item) {
  387 + if (!widgetContext.isMobile) {
  388 + widget.sizeX = item.sizeX;
  389 + widget.sizeY = item.sizeY;
  390 + }
  391 + });
  392 +
  393 + $scope.$on('mobileModeChanged', function (event, newIsMobile) {
  394 + onMobileModeChanged(newIsMobile);
  395 + });
  396 +
  397 + $scope.$on("$destroy", function () {
  398 + removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
  399 + onDestroy();
  400 + });
  401 + }
  402 +
  403 + function handleWidgetException(e) {
  404 + $log.error(e);
  405 + $scope.widgetErrorData = utils.processWidgetException(e);
159 } 406 }
160 407
161 function onInit() { 408 function onInit() {
@@ -166,42 +413,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -166,42 +413,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
166 } catch (e) { 413 } catch (e) {
167 handleWidgetException(e); 414 handleWidgetException(e);
168 } 415 }
169 - if (widgetContext.dataUpdatePending) {  
170 - widgetContext.dataUpdatePending = false;  
171 - onDataUpdated(); 416 + if (!vm.useCustomDatasources && widgetContext.defaultSubscription) {
  417 + widgetContext.defaultSubscription.subscribe();
172 } 418 }
173 } 419 }
174 } 420 }
175 421
176 - function updateTimewindow() {  
177 - widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000;  
178 - if (subscriptionTimewindow.realtimeWindowMs) {  
179 - widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff;  
180 - widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs;  
181 - } else if (subscriptionTimewindow.fixedWindow) {  
182 - widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;  
183 - widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;  
184 - }  
185 - }  
186 -  
187 - function onDataUpdated() {  
188 - if (widgetContext.inited) {  
189 - if (cafs['dataUpdate']) {  
190 - cafs['dataUpdate']();  
191 - cafs['dataUpdate'] = null;  
192 - }  
193 - cafs['dataUpdate'] = tbRaf(function() {  
194 - try {  
195 - widgetTypeInstance.onDataUpdated();  
196 - } catch (e) {  
197 - handleWidgetException(e);  
198 - }  
199 - });  
200 - } else {  
201 - widgetContext.dataUpdatePending = true;  
202 - }  
203 - }  
204 -  
205 function checkSize() { 422 function checkSize() {
206 var width = widgetContext.$containerParent.width(); 423 var width = widgetContext.$containerParent.width();
207 var height = widgetContext.$containerParent.height(); 424 var height = widgetContext.$containerParent.height();
@@ -289,11 +506,35 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -289,11 +506,35 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
289 } 506 }
290 } 507 }
291 508
  509 + function isNumeric(val) {
  510 + return (val - parseFloat( val ) + 1) >= 0;
  511 + }
  512 +
  513 + function formatValue(value, dec, units) {
  514 + if (angular.isDefined(value) &&
  515 + value !== null && isNumeric(value)) {
  516 + var formatted = value;
  517 + if (angular.isDefined(dec)) {
  518 + formatted = formatted.toFixed(dec);
  519 + }
  520 + formatted = (formatted * 1).toString();
  521 + if (angular.isDefined(units) && units.length > 0) {
  522 + formatted += ' ' + units;
  523 + }
  524 + return formatted;
  525 + } else {
  526 + return '';
  527 + }
  528 + }
  529 +
292 function onDestroy() { 530 function onDestroy() {
293 - unsubscribe(); 531 + for (var id in widgetContext.subscriptions) {
  532 + var subscription = widgetContext.subscriptions[id];
  533 + subscription.destroy();
  534 + }
  535 + widgetContext.subscriptions = [];
294 if (widgetContext.inited) { 536 if (widgetContext.inited) {
295 widgetContext.inited = false; 537 widgetContext.inited = false;
296 - widgetContext.dataUpdatePending = false;  
297 for (var cafId in cafs) { 538 for (var cafId in cafs) {
298 if (cafs[cafId]) { 539 if (cafs[cafId]) {
299 cafs[cafId](); 540 cafs[cafId]();
@@ -308,244 +549,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -308,244 +549,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
308 } 549 }
309 } 550 }
310 551
311 - function onRestart() {  
312 - onDestroy();  
313 - onInit();  
314 - }  
315 -  
316 -/* scope.legendData = {  
317 - keys: [],  
318 - data: []  
319 -  
320 - key: {  
321 - label: '',  
322 - color: ''  
323 - dataIndex: 0  
324 - }  
325 - data: {  
326 - min: null,  
327 - max: null,  
328 - avg: null,  
329 - total: null  
330 - }  
331 - };*/  
332 -  
333 -  
334 - function initialize() {  
335 -  
336 - $scope.caulculateLegendData = $scope.displayLegend &&  
337 - widget.type === types.widgetType.timeseries.value &&  
338 - ($scope.legendConfig.showMin === true ||  
339 - $scope.legendConfig.showMax === true ||  
340 - $scope.legendConfig.showAvg === true ||  
341 - $scope.legendConfig.showTotal === true);  
342 -  
343 - if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {  
344 - var dataIndex = 0;  
345 - for (var i = 0; i < widgetContext.datasources.length; i++) {  
346 - var datasource = widgetContext.datasources[i];  
347 - for (var a = 0; a < datasource.dataKeys.length; a++) {  
348 - var dataKey = datasource.dataKeys[a];  
349 - dataKey.pattern = angular.copy(dataKey.label);  
350 - var datasourceData = {  
351 - datasource: datasource,  
352 - dataKey: dataKey,  
353 - data: []  
354 - };  
355 - widgetContext.data.push(datasourceData);  
356 - widgetContext.hiddenData.push({data: []});  
357 - if ($scope.displayLegend) {  
358 - var legendKey = {  
359 - dataKey: dataKey,  
360 - dataIndex: dataIndex++  
361 - };  
362 - $scope.legendData.keys.push(legendKey);  
363 - var legendKeyData = {  
364 - min: null,  
365 - max: null,  
366 - avg: null,  
367 - total: null,  
368 - hidden: false  
369 - };  
370 - $scope.legendData.data.push(legendKeyData);  
371 - }  
372 - }  
373 - }  
374 - if ($scope.displayLegend) {  
375 - $scope.legendData.keys = $filter('orderBy')($scope.legendData.keys, '+label');  
376 - $scope.$on('legendDataHiddenChanged', function (event, index) {  
377 - event.stopPropagation();  
378 - var hidden = $scope.legendData.data[index].hidden;  
379 - if (hidden) {  
380 - widgetContext.hiddenData[index].data = widgetContext.data[index].data;  
381 - widgetContext.data[index].data = [];  
382 - } else {  
383 - widgetContext.data[index].data = widgetContext.hiddenData[index].data;  
384 - widgetContext.hiddenData[index].data = [];  
385 - }  
386 - onDataUpdated();  
387 - });  
388 - }  
389 - } else if (widget.type === types.widgetType.rpc.value) {  
390 - if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {  
391 - targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];  
392 - if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {  
393 - targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;  
394 - }  
395 - }  
396 - if (targetDeviceId) {  
397 - $scope.rpcEnabled = true;  
398 - } else {  
399 - $scope.rpcEnabled = $scope.widgetEditMode ? true : false;  
400 - }  
401 - }  
402 -  
403 - $scope.$on('toggleDashboardEditMode', function (event, isEdit) {  
404 - onEditModeChanged(isEdit);  
405 - });  
406 -  
407 - addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef  
408 -  
409 - $scope.$watch(function () {  
410 - return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;  
411 - }, function () {  
412 - //updateBounds();  
413 - $scope.$emit("widgetPositionChanged", widget);  
414 - });  
415 -  
416 - $scope.$on('gridster-item-resized', function (event, item) {  
417 - if (!widgetContext.isMobile) {  
418 - widget.sizeX = item.sizeX;  
419 - widget.sizeY = item.sizeY;  
420 - }  
421 - });  
422 -  
423 - $scope.$on('mobileModeChanged', function (event, newIsMobile) {  
424 - onMobileModeChanged(newIsMobile);  
425 - });  
426 -  
427 - $scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) {  
428 - aliasesInfo = newAliasesInfo;  
429 - if (widget.type === types.widgetType.rpc.value) {  
430 - if (targetDeviceAliasId) {  
431 - var deviceId = null;  
432 - if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {  
433 - deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;  
434 - }  
435 - if (!angular.equals(deviceId, targetDeviceId)) {  
436 - targetDeviceId = deviceId;  
437 - if (targetDeviceId) {  
438 - $scope.rpcEnabled = true;  
439 - } else {  
440 - $scope.rpcEnabled = $scope.widgetEditMode ? true : false;  
441 - }  
442 - onRestart();  
443 - }  
444 - }  
445 - } else {  
446 - checkSubscriptions();  
447 - }  
448 - });  
449 -  
450 - $scope.$on("$destroy", function () {  
451 - removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef  
452 - onDestroy();  
453 - });  
454 -  
455 - if (widget.type === types.widgetType.timeseries.value) {  
456 - widgetContext.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)  
457 - ? widget.config.useDashboardTimewindow : true;  
458 - if (widgetContext.useDashboardTimewindow) {  
459 - $scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {  
460 - if (!angular.equals(dashboardTimewindow, newDashboardTimewindow)) {  
461 - dashboardTimewindow = newDashboardTimewindow;  
462 - unsubscribe();  
463 - subscribe();  
464 - }  
465 - });  
466 - } else {  
467 - $scope.$watch(function () {  
468 - return widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;  
469 - }, function (newTimewindow, prevTimewindow) {  
470 - if (!angular.equals(newTimewindow, prevTimewindow)) {  
471 - unsubscribe();  
472 - subscribe();  
473 - }  
474 - });  
475 - }  
476 - }  
477 - subscribe();  
478 - }  
479 -  
480 - function sendCommand(oneWayElseTwoWay, method, params, timeout) {  
481 - if (!$scope.rpcEnabled) {  
482 - return $q.reject();  
483 - }  
484 -  
485 - if ($scope.rpcRejection && $scope.rpcRejection.status !== 408) {  
486 - $scope.rpcRejection = null;  
487 - $scope.rpcErrorText = null;  
488 - }  
489 -  
490 - var requestBody = {  
491 - method: method,  
492 - params: params  
493 - };  
494 -  
495 - if (timeout && timeout > 0) {  
496 - requestBody.timeout = timeout;  
497 - }  
498 -  
499 - var deferred = $q.defer();  
500 - $scope.executingRpcRequest = true;  
501 - if ($scope.widgetEditMode) {  
502 - $timeout(function() {  
503 - $scope.executingRpcRequest = false;  
504 - if (oneWayElseTwoWay) {  
505 - deferred.resolve();  
506 - } else {  
507 - deferred.resolve(requestBody);  
508 - }  
509 - }, 500);  
510 - } else {  
511 - $scope.executingPromises.push(deferred.promise);  
512 - var targetSendFunction = oneWayElseTwoWay ? deviceService.sendOneWayRpcCommand : deviceService.sendTwoWayRpcCommand;  
513 - targetSendFunction(targetDeviceId, requestBody).then(  
514 - function success(responseBody) {  
515 - $scope.rpcRejection = null;  
516 - $scope.rpcErrorText = null;  
517 - var index = $scope.executingPromises.indexOf(deferred.promise);  
518 - if (index >= 0) {  
519 - $scope.executingPromises.splice( index, 1 );  
520 - }  
521 - $scope.executingRpcRequest = $scope.executingPromises.length > 0;  
522 - deferred.resolve(responseBody);  
523 - },  
524 - function fail(rejection) {  
525 - var index = $scope.executingPromises.indexOf(deferred.promise);  
526 - if (index >= 0) {  
527 - $scope.executingPromises.splice( index, 1 );  
528 - }  
529 - $scope.executingRpcRequest = $scope.executingPromises.length > 0;  
530 - if (!$scope.executingRpcRequest || rejection.status === 408) {  
531 - $scope.rpcRejection = rejection;  
532 - if (rejection.status === 408) {  
533 - $scope.rpcErrorText = 'Device is offline.';  
534 - } else {  
535 - $scope.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;  
536 - if (rejection.data && rejection.data.length > 0) {  
537 - $scope.rpcErrorText += '</br>';  
538 - $scope.rpcErrorText += rejection.data;  
539 - }  
540 - }  
541 - }  
542 - deferred.reject(rejection);  
543 - }  
544 - );  
545 - }  
546 - return deferred.promise;  
547 - }  
548 -  
549 //TODO: widgets visibility 552 //TODO: widgets visibility
550 /*function updateVisibility(forceRedraw) { 553 /*function updateVisibility(forceRedraw) {
551 if (visibleRect) { 554 if (visibleRect) {
@@ -584,285 +587,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -584,285 +587,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
584 onRedraw(); 587 onRedraw();
585 }*/ 588 }*/
586 589
587 - function onResetTimewindow() {  
588 - if (widgetContext.useDashboardTimewindow) {  
589 - dashboardTimewindowApi.onResetTimewindow();  
590 - } else {  
591 - if (originalTimewindow) {  
592 - widget.config.timewindow = angular.copy(originalTimewindow);  
593 - originalTimewindow = null;  
594 - }  
595 - }  
596 - }  
597 -  
598 - function onUpdateTimewindow(startTimeMs, endTimeMs) {  
599 - if (widgetContext.useDashboardTimewindow) {  
600 - dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);  
601 - } else {  
602 - if (!originalTimewindow) {  
603 - originalTimewindow = angular.copy(widget.config.timewindow);  
604 - }  
605 - widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs);  
606 - }  
607 - }  
608 -  
609 - function dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {  
610 - notifyDataLoaded();  
611 - var update = true;  
612 - var currentData;  
613 - if ($scope.displayLegend && $scope.legendData.data[datasourceIndex + dataKeyIndex].hidden) {  
614 - currentData = widgetContext.hiddenData[datasourceIndex + dataKeyIndex];  
615 - } else {  
616 - currentData = widgetContext.data[datasourceIndex + dataKeyIndex];  
617 - }  
618 - if (widget.type === types.widgetType.latest.value) {  
619 - var prevData = currentData.data;  
620 - if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {  
621 - var prevValue = prevData[0][1];  
622 - if (prevValue === sourceData.data[0][1]) {  
623 - update = false;  
624 - }  
625 - }  
626 - }  
627 - if (update) {  
628 - if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) {  
629 - updateTimewindow();  
630 - }  
631 - currentData.data = sourceData.data;  
632 - onDataUpdated();  
633 - if ($scope.caulculateLegendData) {  
634 - updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);  
635 - }  
636 - }  
637 - if (apply) {  
638 - $scope.$digest();  
639 - }  
640 - }  
641 -  
642 - function updateLegend(dataIndex, data, apply) {  
643 - var legendKeyData = $scope.legendData.data[dataIndex];  
644 - if ($scope.legendConfig.showMin) {  
645 - legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units);  
646 - }  
647 - if ($scope.legendConfig.showMax) {  
648 - legendKeyData.max = formatValue(calculateMax(data), widgetContext.decimals, widgetContext.units);  
649 - }  
650 - if ($scope.legendConfig.showAvg) {  
651 - legendKeyData.avg = formatValue(calculateAvg(data), widgetContext.decimals, widgetContext.units);  
652 - }  
653 - if ($scope.legendConfig.showTotal) {  
654 - legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units);  
655 - }  
656 - $scope.$broadcast('legendDataUpdated', apply !== false);  
657 - }  
658 -  
659 - function isNumeric(val) {  
660 - return (val - parseFloat( val ) + 1) >= 0;  
661 - }  
662 -  
663 - function formatValue(value, dec, units) {  
664 - if (angular.isDefined(value) &&  
665 - value !== null && isNumeric(value)) {  
666 - var formatted = value;  
667 - if (angular.isDefined(dec)) {  
668 - formatted = formatted.toFixed(dec);  
669 - }  
670 - formatted = (formatted * 1).toString();  
671 - if (angular.isDefined(units) && units.length > 0) {  
672 - formatted += ' ' + units;  
673 - }  
674 - return formatted;  
675 - } else {  
676 - return '';  
677 - }  
678 - }  
679 -  
680 - function calculateMin(data) {  
681 - if (data.length > 0) {  
682 - var result = Number(data[0][1]);  
683 - for (var i=1;i<data.length;i++) {  
684 - result = Math.min(result, Number(data[i][1]));  
685 - }  
686 - return result;  
687 - } else {  
688 - return null;  
689 - }  
690 - }  
691 -  
692 - function calculateMax(data) {  
693 - if (data.length > 0) {  
694 - var result = Number(data[0][1]);  
695 - for (var i=1;i<data.length;i++) {  
696 - result = Math.max(result, Number(data[i][1]));  
697 - }  
698 - return result;  
699 - } else {  
700 - return null;  
701 - }  
702 - }  
703 -  
704 - function calculateAvg(data) {  
705 - if (data.length > 0) {  
706 - return calculateTotal(data)/data.length;  
707 - } else {  
708 - return null;  
709 - }  
710 - }  
711 -  
712 - function calculateTotal(data) {  
713 - if (data.length > 0) {  
714 - var result = 0;  
715 - for (var i = 0; i < data.length; i++) {  
716 - result += Number(data[i][1]);  
717 - }  
718 - return result;  
719 - } else {  
720 - return null;  
721 - }  
722 - }  
723 -  
724 - function checkSubscriptions() {  
725 - if (widget.type !== types.widgetType.rpc.value) {  
726 - var subscriptionsChanged = false;  
727 - for (var i = 0; i < datasourceListeners.length; i++) {  
728 - var listener = datasourceListeners[i];  
729 - var deviceId = null;  
730 - var aliasName = null;  
731 - if (listener.datasource.type === types.datasourceType.device) {  
732 - if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {  
733 - deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;  
734 - aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;  
735 - }  
736 - if (!angular.equals(deviceId, listener.deviceId) ||  
737 - !angular.equals(aliasName, listener.datasource.name)) {  
738 - subscriptionsChanged = true;  
739 - break;  
740 - }  
741 - }  
742 - }  
743 - if (subscriptionsChanged) {  
744 - unsubscribe();  
745 - subscribe();  
746 - }  
747 - }  
748 - }  
749 -  
750 - function unsubscribe() {  
751 - if (widget.type !== types.widgetType.rpc.value) {  
752 - for (var i = 0; i < datasourceListeners.length; i++) {  
753 - var listener = datasourceListeners[i];  
754 - datasourceService.unsubscribeFromDatasource(listener);  
755 - }  
756 - datasourceListeners = [];  
757 - }  
758 - }  
759 -  
760 - function updateRealtimeSubscription(_subscriptionTimewindow) {  
761 - if (_subscriptionTimewindow) {  
762 - subscriptionTimewindow = _subscriptionTimewindow;  
763 - } else {  
764 - subscriptionTimewindow =  
765 - timeService.createSubscriptionTimewindow(  
766 - widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow,  
767 - widgetContext.timeWindow.stDiff);  
768 - }  
769 - updateTimewindow();  
770 - return subscriptionTimewindow;  
771 - }  
772 -  
773 - function hasTimewindow() {  
774 - if (widgetContext.useDashboardTimewindow) {  
775 - return angular.isDefined(dashboardTimewindow);  
776 - } else {  
777 - return angular.isDefined(widget.config.timewindow);  
778 - }  
779 - }  
780 -  
781 - function updateDataKeyLabel(dataKey, deviceName, aliasName) {  
782 - var pattern = dataKey.pattern;  
783 - var label = dataKey.pattern;  
784 - var match = varsRegex.exec(pattern);  
785 - while (match !== null) {  
786 - var variable = match[0];  
787 - var variableName = match[1];  
788 - if (variableName === 'deviceName') {  
789 - label = label.split(variable).join(deviceName);  
790 - } else if (variableName === 'aliasName') {  
791 - label = label.split(variable).join(aliasName);  
792 - }  
793 - match = varsRegex.exec(pattern);  
794 - }  
795 - dataKey.label = label;  
796 - }  
797 -  
798 - function subscribe() {  
799 - if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {  
800 - notifyDataLoading();  
801 - if (widget.type === types.widgetType.timeseries.value &&  
802 - hasTimewindow()) {  
803 - updateRealtimeSubscription();  
804 - if (subscriptionTimewindow.fixedWindow) {  
805 - onDataUpdated();  
806 - }  
807 - }  
808 - var index = 0;  
809 - for (var i = 0; i < widgetContext.datasources.length; i++) {  
810 - var datasource = widgetContext.datasources[i];  
811 - if (angular.isFunction(datasource))  
812 - continue;  
813 - var deviceId = null;  
814 - if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {  
815 - if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) {  
816 - deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;  
817 - datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;  
818 - var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;  
819 - var deviceName = '';  
820 - var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];  
821 - for (var d=0;d<devicesInfo.length;d++) {  
822 - if (devicesInfo[d].id === deviceId) {  
823 - deviceName = devicesInfo[d].name;  
824 - break;  
825 - }  
826 - }  
827 - for (var dk = 0; dk < datasource.dataKeys.length; dk++) {  
828 - updateDataKeyLabel(datasource.dataKeys[dk], deviceName, aliasName);  
829 - }  
830 - }  
831 - } else {  
832 - datasource.name = types.datasourceType.function;  
833 - }  
834 - var listener = {  
835 - widget: widget,  
836 - subscriptionTimewindow: subscriptionTimewindow,  
837 - datasource: datasource,  
838 - deviceId: deviceId,  
839 - dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {  
840 - dataUpdated(data, datasourceIndex, dataKeyIndex, apply);  
841 - },  
842 - updateRealtimeSubscription: function() {  
843 - this.subscriptionTimewindow = updateRealtimeSubscription();  
844 - return this.subscriptionTimewindow;  
845 - },  
846 - setRealtimeSubscription: function(subscriptionTimewindow) {  
847 - updateRealtimeSubscription(angular.copy(subscriptionTimewindow));  
848 - },  
849 - datasourceIndex: index  
850 - };  
851 -  
852 - for (var a = 0; a < datasource.dataKeys.length; a++) {  
853 - widgetContext.data[index + a].data = [];  
854 - }  
855 -  
856 - index += datasource.dataKeys.length;  
857 -  
858 - datasourceListeners.push(listener);  
859 - datasourceService.subscribeToDatasource(listener);  
860 - }  
861 - } else {  
862 - notifyDataLoaded();  
863 - }  
864 - }  
865 -  
866 } 590 }
867 591
868 /* eslint-enable angular/angularelement */ 592 /* eslint-enable angular/angularelement */
@@ -15,4 +15,4 @@ @@ -15,4 +15,4 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="tb-uppercase">{{ item | contactShort }}</div>  
  18 +<div ng-show="item && (!item.additionalInfo || !item.additionalInfo.isPublic)" class="tb-uppercase">{{ item | contactShort }}</div>
@@ -15,13 +15,13 @@ @@ -15,13 +15,13 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button> 18 +<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
19 <md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button> 19 <md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button>
20 <md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button> 20 <md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button>
21 -<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button> 21 +<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
22 22
23 <md-content class="md-padding" layout="column"> 23 <md-content class="md-padding" layout="column">
24 - <fieldset ng-disabled="loading || !isEdit"> 24 + <fieldset ng-show="!isPublic" ng-disabled="loading || !isEdit">
25 <md-input-container class="md-block"> 25 <md-input-container class="md-block">
26 <label translate>customer.title</label> 26 <label translate>customer.title</label>
27 <input required name="title" ng-model="customer.title"> 27 <input required name="title" ng-model="customer.title">
@@ -30,14 +30,23 @@ export default function CustomerController(customerService, $state, $stateParams @@ -30,14 +30,23 @@ export default function CustomerController(customerService, $state, $stateParams
30 }, 30 },
31 name: function() { return $translate.instant('user.users') }, 31 name: function() { return $translate.instant('user.users') },
32 details: function() { return $translate.instant('customer.manage-customer-users') }, 32 details: function() { return $translate.instant('customer.manage-customer-users') },
33 - icon: "account_circle" 33 + icon: "account_circle",
  34 + isEnabled: function(customer) {
  35 + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
  36 + }
34 }, 37 },
35 { 38 {
36 onAction: function ($event, item) { 39 onAction: function ($event, item) {
37 openCustomerDevices($event, item); 40 openCustomerDevices($event, item);
38 }, 41 },
39 name: function() { return $translate.instant('device.devices') }, 42 name: function() { return $translate.instant('device.devices') },
40 - details: function() { return $translate.instant('customer.manage-customer-devices') }, 43 + details: function(customer) {
  44 + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) {
  45 + return $translate.instant('customer.manage-public-devices')
  46 + } else {
  47 + return $translate.instant('customer.manage-customer-devices')
  48 + }
  49 + },
41 icon: "devices_other" 50 icon: "devices_other"
42 }, 51 },
43 { 52 {
@@ -45,7 +54,13 @@ export default function CustomerController(customerService, $state, $stateParams @@ -45,7 +54,13 @@ export default function CustomerController(customerService, $state, $stateParams
45 openCustomerDashboards($event, item); 54 openCustomerDashboards($event, item);
46 }, 55 },
47 name: function() { return $translate.instant('dashboard.dashboards') }, 56 name: function() { return $translate.instant('dashboard.dashboards') },
48 - details: function() { return $translate.instant('customer.manage-customer-dashboards') }, 57 + details: function(customer) {
  58 + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) {
  59 + return $translate.instant('customer.manage-public-dashboards')
  60 + } else {
  61 + return $translate.instant('customer.manage-customer-dashboards')
  62 + }
  63 + },
49 icon: "dashboard" 64 icon: "dashboard"
50 }, 65 },
51 { 66 {
@@ -54,7 +69,10 @@ export default function CustomerController(customerService, $state, $stateParams @@ -54,7 +69,10 @@ export default function CustomerController(customerService, $state, $stateParams
54 }, 69 },
55 name: function() { return $translate.instant('action.delete') }, 70 name: function() { return $translate.instant('action.delete') },
56 details: function() { return $translate.instant('customer.delete') }, 71 details: function() { return $translate.instant('customer.delete') },
57 - icon: "delete" 72 + icon: "delete",
  73 + isEnabled: function(customer) {
  74 + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
  75 + }
58 } 76 }
59 ]; 77 ];
60 78
@@ -86,7 +104,19 @@ export default function CustomerController(customerService, $state, $stateParams @@ -86,7 +104,19 @@ export default function CustomerController(customerService, $state, $stateParams
86 104
87 addItemText: function() { return $translate.instant('customer.add-customer-text') }, 105 addItemText: function() { return $translate.instant('customer.add-customer-text') },
88 noItemsText: function() { return $translate.instant('customer.no-customers-text') }, 106 noItemsText: function() { return $translate.instant('customer.no-customers-text') },
89 - itemDetailsText: function() { return $translate.instant('customer.customer-details') } 107 + itemDetailsText: function(customer) {
  108 + if (customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic)) {
  109 + return $translate.instant('customer.customer-details')
  110 + } else {
  111 + return '';
  112 + }
  113 + },
  114 + isSelectionEnabled: function (customer) {
  115 + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
  116 + },
  117 + isDetailsReadOnly: function (customer) {
  118 + return customer && customer.additionalInfo && customer.additionalInfo.isPublic;
  119 + }
90 }; 120 };
91 121
92 if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { 122 if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
@@ -24,7 +24,21 @@ export default function CustomerDirective($compile, $templateCache) { @@ -24,7 +24,21 @@ export default function CustomerDirective($compile, $templateCache) {
24 var linker = function (scope, element) { 24 var linker = function (scope, element) {
25 var template = $templateCache.get(customerFieldsetTemplate); 25 var template = $templateCache.get(customerFieldsetTemplate);
26 element.html(template); 26 element.html(template);
  27 +
  28 + scope.isPublic = false;
  29 +
  30 + scope.$watch('customer', function(newVal) {
  31 + if (newVal) {
  32 + if (scope.customer.additionalInfo) {
  33 + scope.isPublic = scope.customer.additionalInfo.isPublic;
  34 + } else {
  35 + scope.isPublic = false;
  36 + }
  37 + }
  38 + });
  39 +
27 $compile(element.contents())(scope); 40 $compile(element.contents())(scope);
  41 +
28 } 42 }
29 return { 43 return {
30 restrict: "E", 44 restrict: "E",
@@ -15,5 +15,6 @@ @@ -15,5 +15,6 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</div> 18 +<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
  19 +<div class="tb-small" ng-show="vm.isPublic()">{{'dashboard.public' | translate}}</div>
19 20
@@ -15,25 +15,42 @@ @@ -15,25 +15,42 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
  18 +<md-button ng-click="onExportDashboard({event: $event})"
  19 + ng-show="!isEdit && dashboardScope === 'tenant'"
  20 + class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
  21 +<md-button ng-click="onMakePublic({event: $event})"
  22 + ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer && !isPublic"
  23 + class="md-raised md-primary">{{ 'dashboard.make-public' | translate }}</md-button>
18 <md-button ng-click="onAssignToCustomer({event: $event})" 24 <md-button ng-click="onAssignToCustomer({event: $event})"
19 ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer" 25 ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer"
20 class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button> 26 class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
21 -<md-button ng-click="onUnassignFromCustomer({event: $event})" 27 +<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
22 ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer" 28 ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer"
23 - class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button>  
24 -<md-button ng-click="onExportDashboard({event: $event})"  
25 - ng-show="!isEdit && dashboardScope === 'tenant'"  
26 - class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button> 29 + class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }}</md-button>
27 <md-button ng-click="onDeleteDashboard({event: $event})" 30 <md-button ng-click="onDeleteDashboard({event: $event})"
28 ng-show="!isEdit && dashboardScope === 'tenant'" 31 ng-show="!isEdit && dashboardScope === 'tenant'"
29 class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button> 32 class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
30 -  
31 <md-content class="md-padding" layout="column"> 33 <md-content class="md-padding" layout="column">
32 <md-input-container class="md-block" 34 <md-input-container class="md-block"
33 - ng-show="isAssignedToCustomer && dashboardScope === 'tenant'"> 35 + ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'">
34 <label translate>dashboard.assignedToCustomer</label> 36 <label translate>dashboard.assignedToCustomer</label>
35 <input ng-model="assignedCustomer.title" disabled> 37 <input ng-model="assignedCustomer.title" disabled>
36 </md-input-container> 38 </md-input-container>
  39 + <div layout="row" ng-show="!isEdit && isPublic && (dashboardScope === 'customer' || dashboardScope === 'tenant')">
  40 + <md-input-container class="md-block" flex>
  41 + <label translate>dashboard.public-link</label>
  42 + <input ng-model="publicLink" disabled>
  43 + </md-input-container>
  44 + <md-button class="md-icon-button" style="margin-top: 14px;"
  45 + ngclipboard
  46 + data-clipboard-text="{{ publicLink }}"
  47 + ngclipboard-success="onPublicLinkCopied(e)">
  48 + <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
  49 + <md-tooltip md-direction="top">
  50 + {{ 'dashboard.copy-public-link' | translate }}
  51 + </md-tooltip>
  52 + </md-button>
  53 + </div>
37 <fieldset ng-disabled="loading || !isEdit"> 54 <fieldset ng-disabled="loading || !isEdit">
38 <md-input-container class="md-block"> 55 <md-input-container class="md-block">
39 <label translate>dashboard.title</label> 56 <label translate>dashboard.title</label>
@@ -86,6 +86,7 @@ export default function DashboardController(types, widgetService, userService, @@ -86,6 +86,7 @@ export default function DashboardController(types, widgetService, userService,
86 vm.exportDashboard = exportDashboard; 86 vm.exportDashboard = exportDashboard;
87 vm.exportWidget = exportWidget; 87 vm.exportWidget = exportWidget;
88 vm.importWidget = importWidget; 88 vm.importWidget = importWidget;
  89 + vm.isPublicUser = isPublicUser;
89 vm.isTenantAdmin = isTenantAdmin; 90 vm.isTenantAdmin = isTenantAdmin;
90 vm.isSystemAdmin = isSystemAdmin; 91 vm.isSystemAdmin = isSystemAdmin;
91 vm.loadDashboard = loadDashboard; 92 vm.loadDashboard = loadDashboard;
@@ -273,6 +274,10 @@ export default function DashboardController(types, widgetService, userService, @@ -273,6 +274,10 @@ export default function DashboardController(types, widgetService, userService,
273 vm.dashboardInitComplete = true; 274 vm.dashboardInitComplete = true;
274 } 275 }
275 276
  277 + function isPublicUser() {
  278 + return vm.user.isPublic === true;
  279 + }
  280 +
276 function isTenantAdmin() { 281 function isTenantAdmin() {
277 return vm.user.authority === 'TENANT_ADMIN'; 282 return vm.user.authority === 'TENANT_ADMIN';
278 } 283 }
@@ -617,22 +622,8 @@ export default function DashboardController(types, widgetService, userService, @@ -617,22 +622,8 @@ export default function DashboardController(types, widgetService, userService,
617 sizeY: widgetTypeInfo.sizeY, 622 sizeY: widgetTypeInfo.sizeY,
618 config: config 623 config: config
619 }; 624 };
620 - $mdDialog.show({  
621 - controller: 'AddWidgetController',  
622 - controllerAs: 'vm',  
623 - templateUrl: addWidgetTemplate,  
624 - locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo},  
625 - parent: angular.element($document[0].body),  
626 - fullscreen: true,  
627 - skipHide: true,  
628 - targetEvent: event,  
629 - onComplete: function () {  
630 - var w = angular.element($window);  
631 - w.triggerHandler('resize');  
632 - }  
633 - }).then(function (result) {  
634 - var widget = result.widget;  
635 - vm.aliasesInfo = result.aliasesInfo; 625 +
  626 + function addWidget(widget) {
636 var columns = 24; 627 var columns = 24;
637 if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) { 628 if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
638 columns = vm.dashboard.configuration.gridSettings.columns; 629 columns = vm.dashboard.configuration.gridSettings.columns;
@@ -643,9 +634,37 @@ export default function DashboardController(types, widgetService, userService, @@ -643,9 +634,37 @@ export default function DashboardController(types, widgetService, userService,
643 widget.sizeY *= ratio; 634 widget.sizeY *= ratio;
644 } 635 }
645 vm.widgets.push(widget); 636 vm.widgets.push(widget);
646 - }, function (rejection) {  
647 - vm.aliasesInfo = rejection.aliasesInfo;  
648 - }); 637 + }
  638 +
  639 + if (widgetTypeInfo.useCustomDatasources) {
  640 + addWidget(newWidget);
  641 + } else {
  642 + $mdDialog.show({
  643 + controller: 'AddWidgetController',
  644 + controllerAs: 'vm',
  645 + templateUrl: addWidgetTemplate,
  646 + locals: {
  647 + dashboard: vm.dashboard,
  648 + aliasesInfo: vm.aliasesInfo,
  649 + widget: newWidget,
  650 + widgetInfo: widgetTypeInfo
  651 + },
  652 + parent: angular.element($document[0].body),
  653 + fullscreen: true,
  654 + skipHide: true,
  655 + targetEvent: event,
  656 + onComplete: function () {
  657 + var w = angular.element($window);
  658 + w.triggerHandler('resize');
  659 + }
  660 + }).then(function (result) {
  661 + var widget = result.widget;
  662 + vm.aliasesInfo = result.aliasesInfo;
  663 + addWidget(widget);
  664 + }, function (rejection) {
  665 + vm.aliasesInfo = rejection.aliasesInfo;
  666 + });
  667 + }
649 } 668 }
650 ); 669 );
651 } 670 }
@@ -20,30 +20,44 @@ import dashboardFieldsetTemplate from './dashboard-fieldset.tpl.html'; @@ -20,30 +20,44 @@ import dashboardFieldsetTemplate from './dashboard-fieldset.tpl.html';
20 /* eslint-enable import/no-unresolved, import/default */ 20 /* eslint-enable import/no-unresolved, import/default */
21 21
22 /*@ngInject*/ 22 /*@ngInject*/
23 -export default function DashboardDirective($compile, $templateCache, types, customerService) { 23 +export default function DashboardDirective($compile, $templateCache, $translate, types, toast, customerService, dashboardService) {
24 var linker = function (scope, element) { 24 var linker = function (scope, element) {
25 var template = $templateCache.get(dashboardFieldsetTemplate); 25 var template = $templateCache.get(dashboardFieldsetTemplate);
26 element.html(template); 26 element.html(template);
27 27
28 scope.isAssignedToCustomer = false; 28 scope.isAssignedToCustomer = false;
  29 + scope.isPublic = false;
29 scope.assignedCustomer = null; 30 scope.assignedCustomer = null;
  31 + scope.publicLink = null;
30 32
31 scope.$watch('dashboard', function(newVal) { 33 scope.$watch('dashboard', function(newVal) {
32 if (newVal) { 34 if (newVal) {
33 if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) { 35 if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) {
34 scope.isAssignedToCustomer = true; 36 scope.isAssignedToCustomer = true;
35 - customerService.getCustomer(scope.dashboard.customerId.id).then( 37 + customerService.getShortCustomerInfo(scope.dashboard.customerId.id).then(
36 function success(customer) { 38 function success(customer) {
37 scope.assignedCustomer = customer; 39 scope.assignedCustomer = customer;
  40 + scope.isPublic = customer.isPublic;
  41 + if (scope.isPublic) {
  42 + scope.publicLink = dashboardService.getPublicDashboardLink(scope.dashboard);
  43 + } else {
  44 + scope.publicLink = null;
  45 + }
38 } 46 }
39 ); 47 );
40 } else { 48 } else {
41 scope.isAssignedToCustomer = false; 49 scope.isAssignedToCustomer = false;
  50 + scope.isPublic = false;
  51 + scope.publicLink = null;
42 scope.assignedCustomer = null; 52 scope.assignedCustomer = null;
43 } 53 }
44 } 54 }
45 }); 55 });
46 56
  57 + scope.onPublicLinkCopied = function() {
  58 + toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element(element).parent().parent(), 'bottom left');
  59 + };
  60 +
47 $compile(element.contents())(scope); 61 $compile(element.contents())(scope);
48 } 62 }
49 return { 63 return {
@@ -55,6 +69,7 @@ export default function DashboardDirective($compile, $templateCache, types, cust @@ -55,6 +69,7 @@ export default function DashboardDirective($compile, $templateCache, types, cust
55 dashboardScope: '=', 69 dashboardScope: '=',
56 theForm: '=', 70 theForm: '=',
57 onAssignToCustomer: '&', 71 onAssignToCustomer: '&',
  72 + onMakePublic: '&',
58 onUnassignFromCustomer: '&', 73 onUnassignFromCustomer: '&',
59 onExportDashboard: '&', 74 onExportDashboard: '&',
60 onDeleteDashboard: '&' 75 onDeleteDashboard: '&'
@@ -62,7 +62,7 @@ export default function DashboardRoutes($stateProvider) { @@ -62,7 +62,7 @@ export default function DashboardRoutes($stateProvider) {
62 pageTitle: 'customer.dashboards' 62 pageTitle: 'customer.dashboards'
63 }, 63 },
64 ncyBreadcrumb: { 64 ncyBreadcrumb: {
65 - label: '{"icon": "dashboard", "label": "customer.dashboards"}' 65 + label: '{"icon": "dashboard", "label": "{{ vm.customerDashboardsTitle }}", "translate": "false"}'
66 } 66 }
67 }) 67 })
68 .state('home.dashboards.dashboard', { 68 .state('home.dashboards.dashboard', {
@@ -47,7 +47,7 @@ @@ -47,7 +47,7 @@
47 aria-label="{{ 'fullscreen.fullscreen' | translate }}" 47 aria-label="{{ 'fullscreen.fullscreen' | translate }}"
48 class="md-icon-button"> 48 class="md-icon-button">
49 </md-button> 49 </md-button>
50 - <tb-user-menu ng-show="forceFullscreen" display-user-info="true"> 50 + <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
51 </tb-user-menu> 51 </tb-user-menu>
52 <md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button" 52 <md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
53 ng-click="vm.exportDashboard($event)"> 53 ng-click="vm.exportDashboard($event)">
@@ -19,11 +19,33 @@ import addDashboardTemplate from './add-dashboard.tpl.html'; @@ -19,11 +19,33 @@ import addDashboardTemplate from './add-dashboard.tpl.html';
19 import dashboardCard from './dashboard-card.tpl.html'; 19 import dashboardCard from './dashboard-card.tpl.html';
20 import assignToCustomerTemplate from './assign-to-customer.tpl.html'; 20 import assignToCustomerTemplate from './assign-to-customer.tpl.html';
21 import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html'; 21 import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html';
  22 +import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html';
22 23
23 /* eslint-enable import/no-unresolved, import/default */ 24 /* eslint-enable import/no-unresolved, import/default */
24 25
25 /*@ngInject*/ 26 /*@ngInject*/
26 -export function DashboardCardController($scope, types, customerService) { 27 +export function MakeDashboardPublicDialogController($mdDialog, $translate, toast, dashboardService, dashboard) {
  28 +
  29 + var vm = this;
  30 +
  31 + vm.dashboard = dashboard;
  32 + vm.publicLink = dashboardService.getPublicDashboardLink(dashboard);
  33 +
  34 + vm.onPublicLinkCopied = onPublicLinkCopied;
  35 + vm.close = close;
  36 +
  37 + function onPublicLinkCopied(){
  38 + toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element('#make-dialog-public-content'), 'bottom left');
  39 + }
  40 +
  41 + function close() {
  42 + $mdDialog.hide();
  43 + }
  44 +
  45 +}
  46 +
  47 +/*@ngInject*/
  48 +export function DashboardCardController(types) {
27 49
28 var vm = this; 50 var vm = this;
29 51
@@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) { @@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) {
31 53
32 vm.isAssignedToCustomer = function() { 54 vm.isAssignedToCustomer = function() {
33 if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' && 55 if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' &&
34 - vm.item.customerId.id != vm.types.id.nullUid) { 56 + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
35 return true; 57 return true;
36 } 58 }
37 return false; 59 return false;
38 } 60 }
39 61
40 - $scope.$watch('vm.item',  
41 - function() {  
42 - if (vm.isAssignedToCustomer()) {  
43 - customerService.getCustomerTitle(vm.item.customerId.id).then(  
44 - function success(title) {  
45 - vm.customerTitle = title;  
46 - }  
47 - );  
48 - } 62 + vm.isPublic = function() {
  63 + if (vm.item && vm.item.assignedCustomer && vm.parentCtl.dashboardsScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
  64 + return true;
49 } 65 }
50 - ); 66 + return false;
  67 + }
51 } 68 }
52 69
53 /*@ngInject*/ 70 /*@ngInject*/
54 -export function DashboardsController(userService, dashboardService, customerService, importExport, types, $scope, $controller, 71 +export function DashboardsController(userService, dashboardService, customerService, importExport, types,
55 $state, $stateParams, $mdDialog, $document, $q, $translate) { 72 $state, $stateParams, $mdDialog, $document, $q, $translate) {
56 73
57 var customerId = $stateParams.customerId; 74 var customerId = $stateParams.customerId;
@@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ @@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ
119 vm.dashboardsScope = $state.$current.data.dashboardsType; 136 vm.dashboardsScope = $state.$current.data.dashboardsType;
120 137
121 vm.assignToCustomer = assignToCustomer; 138 vm.assignToCustomer = assignToCustomer;
  139 + vm.makePublic = makePublic;
122 vm.unassignFromCustomer = unassignFromCustomer; 140 vm.unassignFromCustomer = unassignFromCustomer;
123 vm.exportDashboard = exportDashboard; 141 vm.exportDashboard = exportDashboard;
124 142
@@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ @@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ
136 customerId = user.customerId; 154 customerId = user.customerId;
137 } 155 }
138 156
  157 + if (customerId) {
  158 + vm.customerDashboardsTitle = $translate.instant('customer.dashboards');
  159 + customerService.getShortCustomerInfo(customerId).then(
  160 + function success(info) {
  161 + if (info.isPublic) {
  162 + vm.customerDashboardsTitle = $translate.instant('customer.public-dashboards');
  163 + }
  164 + }
  165 + );
  166 + }
  167 +
139 if (vm.dashboardsScope === 'tenant') { 168 if (vm.dashboardsScope === 'tenant') {
140 fetchDashboardsFunction = function (pageLink) { 169 fetchDashboardsFunction = function (pageLink) {
141 return dashboardService.getTenantDashboards(pageLink); 170 return dashboardService.getTenantDashboards(pageLink);
@@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ @@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ
155 name: function() { $translate.instant('action.export') }, 184 name: function() { $translate.instant('action.export') },
156 details: function() { return $translate.instant('dashboard.export') }, 185 details: function() { return $translate.instant('dashboard.export') },
157 icon: "file_download" 186 icon: "file_download"
158 - },  
159 - { 187 + });
  188 +
  189 + dashboardActionsList.push({
  190 + onAction: function ($event, item) {
  191 + makePublic($event, item);
  192 + },
  193 + name: function() { return $translate.instant('action.share') },
  194 + details: function() { return $translate.instant('dashboard.make-public') },
  195 + icon: "share",
  196 + isEnabled: function(dashboard) {
  197 + return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
  198 + }
  199 + });
  200 +
  201 + dashboardActionsList.push({
160 onAction: function ($event, item) { 202 onAction: function ($event, item) {
161 assignToCustomer($event, [ item.id.id ]); 203 assignToCustomer($event, [ item.id.id ]);
162 }, 204 },
@@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ @@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ
166 isEnabled: function(dashboard) { 208 isEnabled: function(dashboard) {
167 return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); 209 return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
168 } 210 }
169 - },  
170 - { 211 + });
  212 + dashboardActionsList.push({
171 onAction: function ($event, item) { 213 onAction: function ($event, item) {
172 - unassignFromCustomer($event, item); 214 + unassignFromCustomer($event, item, false);
173 }, 215 },
174 name: function() { return $translate.instant('action.unassign') }, 216 name: function() { return $translate.instant('action.unassign') },
175 details: function() { return $translate.instant('dashboard.unassign-from-customer') }, 217 details: function() { return $translate.instant('dashboard.unassign-from-customer') },
176 icon: "assignment_return", 218 icon: "assignment_return",
177 isEnabled: function(dashboard) { 219 isEnabled: function(dashboard) {
178 - return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid; 220 + return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && !dashboard.assignedCustomer.isPublic;
179 } 221 }
180 - }  
181 - ); 222 + });
  223 + dashboardActionsList.push({
  224 + onAction: function ($event, item) {
  225 + unassignFromCustomer($event, item, true);
  226 + },
  227 + name: function() { return $translate.instant('action.unshare') },
  228 + details: function() { return $translate.instant('dashboard.make-private') },
  229 + icon: "reply",
  230 + isEnabled: function(dashboard) {
  231 + return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && dashboard.assignedCustomer.isPublic;
  232 + }
  233 + });
182 234
183 dashboardActionsList.push( 235 dashboardActionsList.push(
184 { 236 {
@@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ @@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ
262 dashboardActionsList.push( 314 dashboardActionsList.push(
263 { 315 {
264 onAction: function ($event, item) { 316 onAction: function ($event, item) {
265 - unassignFromCustomer($event, item); 317 + unassignFromCustomer($event, item, false);
266 }, 318 },
267 name: function() { return $translate.instant('action.unassign') }, 319 name: function() { return $translate.instant('action.unassign') },
268 details: function() { return $translate.instant('dashboard.unassign-from-customer') }, 320 details: function() { return $translate.instant('dashboard.unassign-from-customer') },
269 - icon: "assignment_return" 321 + icon: "assignment_return",
  322 + isEnabled: function(dashboard) {
  323 + return dashboard && !dashboard.assignedCustomer.isPublic;
  324 + }
  325 + }
  326 + );
  327 + dashboardActionsList.push(
  328 + {
  329 + onAction: function ($event, item) {
  330 + unassignFromCustomer($event, item, true);
  331 + },
  332 + name: function() { return $translate.instant('action.unshare') },
  333 + details: function() { return $translate.instant('dashboard.make-private') },
  334 + icon: "reply",
  335 + isEnabled: function(dashboard) {
  336 + return dashboard && dashboard.assignedCustomer.isPublic;
  337 + }
270 } 338 }
271 ); 339 );
272 340
@@ -418,15 +486,27 @@ export function DashboardsController(userService, dashboardService, customerServ @@ -418,15 +486,27 @@ export function DashboardsController(userService, dashboardService, customerServ
418 assignToCustomer($event, dashboardIds); 486 assignToCustomer($event, dashboardIds);
419 } 487 }
420 488
421 - function unassignFromCustomer($event, dashboard) { 489 + function unassignFromCustomer($event, dashboard, isPublic) {
422 if ($event) { 490 if ($event) {
423 $event.stopPropagation(); 491 $event.stopPropagation();
424 } 492 }
  493 + var title;
  494 + var content;
  495 + var label;
  496 + if (isPublic) {
  497 + title = $translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title});
  498 + content = $translate.instant('dashboard.make-private-dashboard-text');
  499 + label = $translate.instant('dashboard.make-private-dashboard');
  500 + } else {
  501 + title = $translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title});
  502 + content = $translate.instant('dashboard.unassign-dashboard-text');
  503 + label = $translate.instant('dashboard.unassign-dashboard');
  504 + }
425 var confirm = $mdDialog.confirm() 505 var confirm = $mdDialog.confirm()
426 .targetEvent($event) 506 .targetEvent($event)
427 - .title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}))  
428 - .htmlContent($translate.instant('dashboard.unassign-dashboard-text'))  
429 - .ariaLabel($translate.instant('dashboard.unassign-dashboard')) 507 + .title(title)
  508 + .htmlContent(content)
  509 + .ariaLabel(label)
430 .cancel($translate.instant('action.no')) 510 .cancel($translate.instant('action.no'))
431 .ok($translate.instant('action.yes')); 511 .ok($translate.instant('action.yes'));
432 $mdDialog.show(confirm).then(function () { 512 $mdDialog.show(confirm).then(function () {
@@ -436,6 +516,25 @@ export function DashboardsController(userService, dashboardService, customerServ @@ -436,6 +516,25 @@ export function DashboardsController(userService, dashboardService, customerServ
436 }); 516 });
437 } 517 }
438 518
  519 + function makePublic($event, dashboard) {
  520 + if ($event) {
  521 + $event.stopPropagation();
  522 + }
  523 + dashboardService.makeDashboardPublic(dashboard.id.id).then(function success(dashboard) {
  524 + $mdDialog.show({
  525 + controller: 'MakeDashboardPublicDialogController',
  526 + controllerAs: 'vm',
  527 + templateUrl: makeDashboardPublicDialogTemplate,
  528 + locals: {dashboard: dashboard},
  529 + parent: angular.element($document[0].body),
  530 + fullscreen: true,
  531 + targetEvent: $event
  532 + }).then(function () {
  533 + vm.grid.refreshList();
  534 + });
  535 + });
  536 + }
  537 +
439 function exportDashboard($event, dashboard) { 538 function exportDashboard($event, dashboard) {
440 $event.stopPropagation(); 539 $event.stopPropagation();
441 importExport.exportDashboard(dashboard.id.id); 540 importExport.exportDashboard(dashboard.id.id);
@@ -24,7 +24,8 @@ @@ -24,7 +24,8 @@
24 dashboard-scope="vm.dashboardsScope" 24 dashboard-scope="vm.dashboardsScope"
25 the-form="vm.grid.detailsForm" 25 the-form="vm.grid.detailsForm"
26 on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" 26 on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
27 - on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)" 27 + on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
  28 + on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
28 on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)" 29 on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
29 on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details> 30 on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
30 </tb-grid> 31 </tb-grid>
@@ -37,6 +37,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ @@ -37,6 +37,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
37 scope.widgetConfig = scope.widget.config; 37 scope.widgetConfig = scope.widget.config;
38 var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; 38 var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
39 var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; 39 var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
  40 + scope.isDataEnabled = !widgetInfo.useCustomDatasources;
40 if (!settingsSchema || settingsSchema === '') { 41 if (!settingsSchema || settingsSchema === '') {
41 scope.settingsSchema = {}; 42 scope.settingsSchema = {};
42 } else { 43 } else {
@@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
18 <fieldset ng-disabled="loading"> 18 <fieldset ng-disabled="loading">
19 <tb-widget-config widget-type="widget.type" 19 <tb-widget-config widget-type="widget.type"
20 ng-model="widgetConfig" 20 ng-model="widgetConfig"
  21 + is-data-enabled="isDataEnabled"
21 widget-settings-schema="settingsSchema" 22 widget-settings-schema="settingsSchema"
22 datakey-settings-schema="dataKeySettingsSchema" 23 datakey-settings-schema="dataKeySettingsSchema"
23 device-aliases="aliasesInfo.deviceAliases" 24 device-aliases="aliasesInfo.deviceAliases"
@@ -35,7 +35,7 @@ import thingsboardItemBuffer from '../services/item-buffer.service'; @@ -35,7 +35,7 @@ import thingsboardItemBuffer from '../services/item-buffer.service';
35 import thingsboardImportExport from '../import-export'; 35 import thingsboardImportExport from '../import-export';
36 36
37 import DashboardRoutes from './dashboard.routes'; 37 import DashboardRoutes from './dashboard.routes';
38 -import {DashboardsController, DashboardCardController} from './dashboards.controller'; 38 +import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
39 import DashboardController from './dashboard.controller'; 39 import DashboardController from './dashboard.controller';
40 import DeviceAliasesController from './device-aliases.controller'; 40 import DeviceAliasesController from './device-aliases.controller';
41 import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller'; 41 import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
@@ -69,6 +69,7 @@ export default angular.module('thingsboard.dashboard', [ @@ -69,6 +69,7 @@ export default angular.module('thingsboard.dashboard', [
69 .config(DashboardRoutes) 69 .config(DashboardRoutes)
70 .controller('DashboardsController', DashboardsController) 70 .controller('DashboardsController', DashboardsController)
71 .controller('DashboardCardController', DashboardCardController) 71 .controller('DashboardCardController', DashboardCardController)
  72 + .controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController)
72 .controller('DashboardController', DashboardController) 73 .controller('DashboardController', DashboardController)
73 .controller('DeviceAliasesController', DeviceAliasesController) 74 .controller('DeviceAliasesController', DeviceAliasesController)
74 .controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController) 75 .controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ 'dashboard.make-public' | translate }}" style="min-width: 400px;">
  19 + <form>
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate="dashboard.public-dashboard-title"></h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button" ng-click="vm.close()">
  25 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  26 + </md-button>
  27 + </div>
  28 + </md-toolbar>
  29 + <md-dialog-content>
  30 + <div id="make-dialog-public-content" class="md-dialog-content">
  31 + <md-content class="md-padding" layout="column">
  32 + <span translate="dashboard.public-dashboard-text" translate-values="{dashboardTitle: vm.dashboard.title, publicLink: vm.publicLink}"></span>
  33 + <div layout="row" layout-align="start center">
  34 + <pre class="tb-highlight" flex><code>{{ vm.publicLink }}</code></pre>
  35 + <md-button class="md-icon-button"
  36 + ngclipboard
  37 + data-clipboard-text="{{ vm.publicLink }}"
  38 + ngclipboard-success="vm.onPublicLinkCopied(e)">
  39 + <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
  40 + <md-tooltip md-direction="top">
  41 + {{ 'dashboard.copy-public-link' | translate }}
  42 + </md-tooltip>
  43 + </md-button>
  44 + </div>
  45 + <div class="tb-notice" translate>dashboard.public-dashboard-notice</div>
  46 + </md-content>
  47 + </div>
  48 + </md-dialog-content>
  49 + <md-dialog-actions layout="row">
  50 + <span flex></span>
  51 + <md-button ng-click="vm.close()">{{ 'action.ok' |
  52 + translate }}
  53 + </md-button>
  54 + </md-dialog-actions>
  55 + </form>
  56 +</md-dialog>
@@ -52,7 +52,7 @@ export default function AddDevicesToCustomerController(deviceService, $mdDialog, @@ -52,7 +52,7 @@ export default function AddDevicesToCustomerController(deviceService, $mdDialog,
52 fetchMoreItems_: function () { 52 fetchMoreItems_: function () {
53 if (vm.devices.hasNext && !vm.devices.pending) { 53 if (vm.devices.hasNext && !vm.devices.pending) {
54 vm.devices.pending = true; 54 vm.devices.pending = true;
55 - deviceService.getTenantDevices(vm.devices.nextPageLink).then( 55 + deviceService.getTenantDevices(vm.devices.nextPageLink, false).then(
56 function success(devices) { 56 function success(devices) {
57 vm.devices.data = vm.devices.data.concat(devices.data); 57 vm.devices.data = vm.devices.data.concat(devices.data);
58 vm.devices.nextPageLink = devices.nextPageLink; 58 vm.devices.nextPageLink = devices.nextPageLink;
@@ -15,4 +15,5 @@ @@ -15,4 +15,5 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</div> 18 +<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
  19 +<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
@@ -15,12 +15,15 @@ @@ -15,12 +15,15 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
  18 +<md-button ng-click="onMakePublic({event: $event})"
  19 + ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer && !isPublic"
  20 + class="md-raised md-primary">{{ 'device.make-public' | translate }}</md-button>
18 <md-button ng-click="onAssignToCustomer({event: $event})" 21 <md-button ng-click="onAssignToCustomer({event: $event})"
19 ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer" 22 ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer"
20 class="md-raised md-primary">{{ 'device.assign-to-customer' | translate }}</md-button> 23 class="md-raised md-primary">{{ 'device.assign-to-customer' | translate }}</md-button>
21 -<md-button ng-click="onUnassignFromCustomer({event: $event})" 24 +<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
22 ng-show="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer" 25 ng-show="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer"
23 - class="md-raised md-primary">{{ 'device.unassign-from-customer' | translate }}</md-button> 26 + class="md-raised md-primary">{{ isPublic ? 'device.make-private' : 'device.unassign-from-customer' | translate }}</md-button>
24 <md-button ng-click="onManageCredentials({event: $event})" 27 <md-button ng-click="onManageCredentials({event: $event})"
25 ng-show="!isEdit" 28 ng-show="!isEdit"
26 class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button> 29 class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button>
@@ -47,10 +50,14 @@ @@ -47,10 +50,14 @@
47 50
48 <md-content class="md-padding" layout="column"> 51 <md-content class="md-padding" layout="column">
49 <md-input-container class="md-block" 52 <md-input-container class="md-block"
50 - ng-show="isAssignedToCustomer && deviceScope === 'tenant'"> 53 + ng-show="!isEdit && isAssignedToCustomer && !isPublic && deviceScope === 'tenant'">
51 <label translate>device.assignedToCustomer</label> 54 <label translate>device.assignedToCustomer</label>
52 <input ng-model="assignedCustomer.title" disabled> 55 <input ng-model="assignedCustomer.title" disabled>
53 </md-input-container> 56 </md-input-container>
  57 + <div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
  58 + ng-show="!isEdit && isPublic && (deviceScope === 'customer' || deviceScope === 'tenant')">
  59 + {{ 'device.device-public' | translate }}
  60 + </div>
54 <fieldset ng-disabled="loading || !isEdit"> 61 <fieldset ng-disabled="loading || !isEdit">
55 <md-input-container class="md-block"> 62 <md-input-container class="md-block">
56 <label translate>device.name</label> 63 <label translate>device.name</label>
@@ -24,7 +24,7 @@ import deviceCredentialsTemplate from './device-credentials.tpl.html'; @@ -24,7 +24,7 @@ import deviceCredentialsTemplate from './device-credentials.tpl.html';
24 /* eslint-enable import/no-unresolved, import/default */ 24 /* eslint-enable import/no-unresolved, import/default */
25 25
26 /*@ngInject*/ 26 /*@ngInject*/
27 -export function DeviceCardController($scope, types, customerService) { 27 +export function DeviceCardController(types) {
28 28
29 var vm = this; 29 var vm = this;
30 30
@@ -32,28 +32,23 @@ export function DeviceCardController($scope, types, customerService) { @@ -32,28 +32,23 @@ export function DeviceCardController($scope, types, customerService) {
32 32
33 vm.isAssignedToCustomer = function() { 33 vm.isAssignedToCustomer = function() {
34 if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === 'tenant' && 34 if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === 'tenant' &&
35 - vm.item.customerId.id != vm.types.id.nullUid) { 35 + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
36 return true; 36 return true;
37 } 37 }
38 return false; 38 return false;
39 } 39 }
40 40
41 - $scope.$watch('vm.item',  
42 - function() {  
43 - if (vm.isAssignedToCustomer()) {  
44 - customerService.getCustomerTitle(vm.item.customerId.id).then(  
45 - function success(title) {  
46 - vm.customerTitle = title;  
47 - }  
48 - );  
49 - } 41 + vm.isPublic = function() {
  42 + if (vm.item && vm.item.assignedCustomer && vm.parentCtl.devicesScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
  43 + return true;
50 } 44 }
51 - ); 45 + return false;
  46 + }
52 } 47 }
53 48
54 49
55 /*@ngInject*/ 50 /*@ngInject*/
56 -export function DeviceController(userService, deviceService, customerService, $scope, $controller, $state, $stateParams, $document, $mdDialog, $q, $translate, types) { 51 +export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
57 52
58 var customerId = $stateParams.customerId; 53 var customerId = $stateParams.customerId;
59 54
@@ -107,6 +102,7 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -107,6 +102,7 @@ export function DeviceController(userService, deviceService, customerService, $s
107 vm.devicesScope = $state.$current.data.devicesType; 102 vm.devicesScope = $state.$current.data.devicesType;
108 103
109 vm.assignToCustomer = assignToCustomer; 104 vm.assignToCustomer = assignToCustomer;
  105 + vm.makePublic = makePublic;
110 vm.unassignFromCustomer = unassignFromCustomer; 106 vm.unassignFromCustomer = unassignFromCustomer;
111 vm.manageCredentials = manageCredentials; 107 vm.manageCredentials = manageCredentials;
112 108
@@ -123,10 +119,20 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -123,10 +119,20 @@ export function DeviceController(userService, deviceService, customerService, $s
123 vm.devicesScope = 'customer_user'; 119 vm.devicesScope = 'customer_user';
124 customerId = user.customerId; 120 customerId = user.customerId;
125 } 121 }
  122 + if (customerId) {
  123 + vm.customerDevicesTitle = $translate.instant('customer.devices');
  124 + customerService.getShortCustomerInfo(customerId).then(
  125 + function success(info) {
  126 + if (info.isPublic) {
  127 + vm.customerDevicesTitle = $translate.instant('customer.public-devices');
  128 + }
  129 + }
  130 + );
  131 + }
126 132
127 if (vm.devicesScope === 'tenant') { 133 if (vm.devicesScope === 'tenant') {
128 fetchDevicesFunction = function (pageLink) { 134 fetchDevicesFunction = function (pageLink) {
129 - return deviceService.getTenantDevices(pageLink); 135 + return deviceService.getTenantDevices(pageLink, true);
130 }; 136 };
131 deleteDeviceFunction = function (deviceId) { 137 deleteDeviceFunction = function (deviceId) {
132 return deviceService.deleteDevice(deviceId); 138 return deviceService.deleteDevice(deviceId);
@@ -135,6 +141,18 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -135,6 +141,18 @@ export function DeviceController(userService, deviceService, customerService, $s
135 return {"topIndex": vm.topIndex}; 141 return {"topIndex": vm.topIndex};
136 }; 142 };
137 143
  144 + deviceActionsList.push({
  145 + onAction: function ($event, item) {
  146 + makePublic($event, item);
  147 + },
  148 + name: function() { return $translate.instant('action.share') },
  149 + details: function() { return $translate.instant('device.make-public') },
  150 + icon: "share",
  151 + isEnabled: function(device) {
  152 + return device && (!device.customerId || device.customerId.id === types.id.nullUid);
  153 + }
  154 + });
  155 +
138 deviceActionsList.push( 156 deviceActionsList.push(
139 { 157 {
140 onAction: function ($event, item) { 158 onAction: function ($event, item) {
@@ -152,17 +170,29 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -152,17 +170,29 @@ export function DeviceController(userService, deviceService, customerService, $s
152 deviceActionsList.push( 170 deviceActionsList.push(
153 { 171 {
154 onAction: function ($event, item) { 172 onAction: function ($event, item) {
155 - unassignFromCustomer($event, item); 173 + unassignFromCustomer($event, item, false);
156 }, 174 },
157 name: function() { return $translate.instant('action.unassign') }, 175 name: function() { return $translate.instant('action.unassign') },
158 details: function() { return $translate.instant('device.unassign-from-customer') }, 176 details: function() { return $translate.instant('device.unassign-from-customer') },
159 icon: "assignment_return", 177 icon: "assignment_return",
160 isEnabled: function(device) { 178 isEnabled: function(device) {
161 - return device && device.customerId && device.customerId.id !== types.id.nullUid; 179 + return device && device.customerId && device.customerId.id !== types.id.nullUid && !device.assignedCustomer.isPublic;
162 } 180 }
163 } 181 }
164 ); 182 );
165 183
  184 + deviceActionsList.push({
  185 + onAction: function ($event, item) {
  186 + unassignFromCustomer($event, item, true);
  187 + },
  188 + name: function() { return $translate.instant('action.unshare') },
  189 + details: function() { return $translate.instant('device.make-private') },
  190 + icon: "reply",
  191 + isEnabled: function(device) {
  192 + return device && device.customerId && device.customerId.id !== types.id.nullUid && device.assignedCustomer.isPublic;
  193 + }
  194 + });
  195 +
166 deviceActionsList.push( 196 deviceActionsList.push(
167 { 197 {
168 onAction: function ($event, item) { 198 onAction: function ($event, item) {
@@ -213,7 +243,7 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -213,7 +243,7 @@ export function DeviceController(userService, deviceService, customerService, $s
213 243
214 } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') { 244 } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
215 fetchDevicesFunction = function (pageLink) { 245 fetchDevicesFunction = function (pageLink) {
216 - return deviceService.getCustomerDevices(customerId, pageLink); 246 + return deviceService.getCustomerDevices(customerId, pageLink, true);
217 }; 247 };
218 deleteDeviceFunction = function (deviceId) { 248 deleteDeviceFunction = function (deviceId) {
219 return deviceService.unassignDeviceFromCustomer(deviceId); 249 return deviceService.unassignDeviceFromCustomer(deviceId);
@@ -226,16 +256,33 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -226,16 +256,33 @@ export function DeviceController(userService, deviceService, customerService, $s
226 deviceActionsList.push( 256 deviceActionsList.push(
227 { 257 {
228 onAction: function ($event, item) { 258 onAction: function ($event, item) {
229 - unassignFromCustomer($event, item); 259 + unassignFromCustomer($event, item, false);
230 }, 260 },
231 name: function() { return $translate.instant('action.unassign') }, 261 name: function() { return $translate.instant('action.unassign') },
232 details: function() { return $translate.instant('device.unassign-from-customer') }, 262 details: function() { return $translate.instant('device.unassign-from-customer') },
233 - icon: "assignment_return" 263 + icon: "assignment_return",
  264 + isEnabled: function(device) {
  265 + return device && !device.assignedCustomer.isPublic;
  266 + }
234 } 267 }
235 ); 268 );
236 deviceActionsList.push( 269 deviceActionsList.push(
237 { 270 {
238 onAction: function ($event, item) { 271 onAction: function ($event, item) {
  272 + unassignFromCustomer($event, item, true);
  273 + },
  274 + name: function() { return $translate.instant('action.unshare') },
  275 + details: function() { return $translate.instant('device.make-private') },
  276 + icon: "reply",
  277 + isEnabled: function(device) {
  278 + return device && device.assignedCustomer.isPublic;
  279 + }
  280 + }
  281 + );
  282 +
  283 + deviceActionsList.push(
  284 + {
  285 + onAction: function ($event, item) {
239 manageCredentials($event, item); 286 manageCredentials($event, item);
240 }, 287 },
241 name: function() { return $translate.instant('device.credentials') }, 288 name: function() { return $translate.instant('device.credentials') },
@@ -365,7 +412,7 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -365,7 +412,7 @@ export function DeviceController(userService, deviceService, customerService, $s
365 $event.stopPropagation(); 412 $event.stopPropagation();
366 } 413 }
367 var pageSize = 10; 414 var pageSize = 10;
368 - deviceService.getTenantDevices({limit: pageSize, textSearch: ''}).then( 415 + deviceService.getTenantDevices({limit: pageSize, textSearch: ''}, false).then(
369 function success(_devices) { 416 function success(_devices) {
370 var devices = { 417 var devices = {
371 pageSize: pageSize, 418 pageSize: pageSize,
@@ -404,15 +451,27 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -404,15 +451,27 @@ export function DeviceController(userService, deviceService, customerService, $s
404 assignToCustomer($event, deviceIds); 451 assignToCustomer($event, deviceIds);
405 } 452 }
406 453
407 - function unassignFromCustomer($event, device) { 454 + function unassignFromCustomer($event, device, isPublic) {
408 if ($event) { 455 if ($event) {
409 $event.stopPropagation(); 456 $event.stopPropagation();
410 } 457 }
  458 + var title;
  459 + var content;
  460 + var label;
  461 + if (isPublic) {
  462 + title = $translate.instant('device.make-private-device-title', {deviceName: device.name});
  463 + content = $translate.instant('device.make-private-device-text');
  464 + label = $translate.instant('device.make-private');
  465 + } else {
  466 + title = $translate.instant('device.unassign-device-title', {deviceName: device.name});
  467 + content = $translate.instant('device.unassign-device-text');
  468 + label = $translate.instant('device.unassign-device');
  469 + }
411 var confirm = $mdDialog.confirm() 470 var confirm = $mdDialog.confirm()
412 .targetEvent($event) 471 .targetEvent($event)
413 - .title($translate.instant('device.unassign-device-title', {deviceName: device.name}))  
414 - .htmlContent($translate.instant('device.unassign-device-text'))  
415 - .ariaLabel($translate.instant('device.unassign-device')) 472 + .title(title)
  473 + .htmlContent(content)
  474 + .ariaLabel(label)
416 .cancel($translate.instant('action.no')) 475 .cancel($translate.instant('action.no'))
417 .ok($translate.instant('action.yes')); 476 .ok($translate.instant('action.yes'));
418 $mdDialog.show(confirm).then(function () { 477 $mdDialog.show(confirm).then(function () {
@@ -441,6 +500,24 @@ export function DeviceController(userService, deviceService, customerService, $s @@ -441,6 +500,24 @@ export function DeviceController(userService, deviceService, customerService, $s
441 }); 500 });
442 } 501 }
443 502
  503 + function makePublic($event, device) {
  504 + if ($event) {
  505 + $event.stopPropagation();
  506 + }
  507 + var confirm = $mdDialog.confirm()
  508 + .targetEvent($event)
  509 + .title($translate.instant('device.make-public-device-title', {deviceName: device.name}))
  510 + .htmlContent($translate.instant('device.make-public-device-text'))
  511 + .ariaLabel($translate.instant('device.make-public'))
  512 + .cancel($translate.instant('action.no'))
  513 + .ok($translate.instant('action.yes'));
  514 + $mdDialog.show(confirm).then(function () {
  515 + deviceService.makeDevicePublic(device.id.id).then(function success() {
  516 + vm.grid.refreshList();
  517 + });
  518 + });
  519 + }
  520 +
444 function manageCredentials($event, device) { 521 function manageCredentials($event, device) {
445 if ($event) { 522 if ($event) {
446 $event.stopPropagation(); 523 $event.stopPropagation();
@@ -26,6 +26,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl @@ -26,6 +26,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
26 element.html(template); 26 element.html(template);
27 27
28 scope.isAssignedToCustomer = false; 28 scope.isAssignedToCustomer = false;
  29 + scope.isPublic = false;
29 scope.assignedCustomer = null; 30 scope.assignedCustomer = null;
30 31
31 scope.deviceCredentials = null; 32 scope.deviceCredentials = null;
@@ -41,13 +42,15 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl @@ -41,13 +42,15 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
41 } 42 }
42 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) { 43 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
43 scope.isAssignedToCustomer = true; 44 scope.isAssignedToCustomer = true;
44 - customerService.getCustomer(scope.device.customerId.id).then( 45 + customerService.getShortCustomerInfo(scope.device.customerId.id).then(
45 function success(customer) { 46 function success(customer) {
46 scope.assignedCustomer = customer; 47 scope.assignedCustomer = customer;
  48 + scope.isPublic = customer.isPublic;
47 } 49 }
48 ); 50 );
49 } else { 51 } else {
50 scope.isAssignedToCustomer = false; 52 scope.isAssignedToCustomer = false;
  53 + scope.isPublic = false;
51 scope.assignedCustomer = null; 54 scope.assignedCustomer = null;
52 } 55 }
53 } 56 }
@@ -72,6 +75,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl @@ -72,6 +75,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
72 deviceScope: '=', 75 deviceScope: '=',
73 theForm: '=', 76 theForm: '=',
74 onAssignToCustomer: '&', 77 onAssignToCustomer: '&',
  78 + onMakePublic: '&',
75 onUnassignFromCustomer: '&', 79 onUnassignFromCustomer: '&',
76 onManageCredentials: '&', 80 onManageCredentials: '&',
77 onDeleteDevice: '&' 81 onDeleteDevice: '&'
@@ -61,7 +61,7 @@ export default function DeviceRoutes($stateProvider) { @@ -61,7 +61,7 @@ export default function DeviceRoutes($stateProvider) {
61 pageTitle: 'customer.devices' 61 pageTitle: 'customer.devices'
62 }, 62 },
63 ncyBreadcrumb: { 63 ncyBreadcrumb: {
64 - label: '{"icon": "devices_other", "label": "customer.devices"}' 64 + label: '{"icon": "devices_other", "label": "{{ vm.customerDevicesTitle }}", "translate": "false"}'
65 } 65 }
66 }); 66 });
67 67
@@ -27,7 +27,8 @@ @@ -27,7 +27,8 @@
27 device-scope="vm.devicesScope" 27 device-scope="vm.devicesScope"
28 the-form="vm.grid.detailsForm" 28 the-form="vm.grid.detailsForm"
29 on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" 29 on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
30 - on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)" 30 + on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
  31 + on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
31 on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)" 32 on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
32 on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device> 33 on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
33 </md-tab> 34 </md-tab>