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 39 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
40 40 import org.thingsboard.server.service.security.auth.jwt.*;
41 41 import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
  42 +import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
42 43
43 44 import java.util.ArrayList;
44 45 import java.util.Arrays;
... ... @@ -56,6 +57,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
56 57 public static final String WEBJARS_ENTRY_POINT = "/webjars/**";
57 58 public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
58 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 61 public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
60 62 public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};
61 63 public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
... ... @@ -88,9 +90,17 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
88 90 }
89 91
90 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 100 protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
92 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 104 SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
95 105 JwtTokenAuthenticationProcessingFilter filter
96 106 = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
... ... @@ -146,6 +156,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
146 156 .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
147 157 .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
148 158 .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
  159 + .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
149 160 .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
150 161 .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
151 162 .and()
... ... @@ -156,6 +167,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
156 167 .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
157 168 .and()
158 169 .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
  170 + .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
159 171 .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
160 172 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
161 173 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
... ...
... ... @@ -36,6 +36,7 @@ import org.thingsboard.server.exception.ThingsboardException;
36 36 import org.thingsboard.server.service.mail.MailService;
37 37 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
38 38 import org.thingsboard.server.service.security.model.SecurityUser;
  39 +import org.thingsboard.server.service.security.model.UserPrincipal;
39 40 import org.thingsboard.server.service.security.model.token.JwtToken;
40 41 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
41 42
... ... @@ -167,7 +168,8 @@ public class AuthController extends BaseController {
167 168 String encodedPassword = passwordEncoder.encode(password);
168 169 UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
169 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 173 String baseUrl = constructBaseUrl(request);
172 174 String loginUrl = String.format("%s/login", baseUrl);
173 175 String email = user.getEmail();
... ... @@ -201,7 +203,8 @@ public class AuthController extends BaseController {
201 203 userCredentials.setResetToken(null);
202 204 userCredentials = userService.saveUserCredentials(userCredentials);
203 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 208 String baseUrl = constructBaseUrl(request);
206 209 String loginUrl = String.format("%s/login", baseUrl);
207 210 String email = user.getEmail();
... ...
... ... @@ -15,6 +15,9 @@
15 15 */
16 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 21 import org.springframework.http.HttpStatus;
19 22 import org.springframework.security.access.prepost.PreAuthorize;
20 23 import org.springframework.web.bind.annotation.*;
... ... @@ -43,14 +46,22 @@ public class CustomerController extends BaseController {
43 46 }
44 47
45 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 50 @ResponseBody
48   - public String getCustomerTitleById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
  51 + public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
49 52 checkParameter("customerId", strCustomerId);
50 53 try {
51 54 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
52 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 65 } catch (Exception e) {
55 66 throw handleException(e);
56 67 }
... ...
... ... @@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
18 18 import org.springframework.http.HttpStatus;
19 19 import org.springframework.security.access.prepost.PreAuthorize;
20 20 import org.springframework.web.bind.annotation.*;
  21 +import org.thingsboard.server.common.data.Customer;
21 22 import org.thingsboard.server.common.data.Dashboard;
22 23 import org.thingsboard.server.common.data.DashboardInfo;
23 24 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -117,6 +118,21 @@ public class DashboardController extends BaseController {
117 118 }
118 119
119 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 136 @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
121 137 @ResponseBody
122 138 public TextPageData<DashboardInfo> getTenantDashboards(
... ...
... ... @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
19 19 import org.springframework.http.HttpStatus;
20 20 import org.springframework.security.access.prepost.PreAuthorize;
21 21 import org.springframework.web.bind.annotation.*;
  22 +import org.thingsboard.server.common.data.Customer;
22 23 import org.thingsboard.server.common.data.Device;
23 24 import org.thingsboard.server.common.data.id.CustomerId;
24 25 import org.thingsboard.server.common.data.id.DeviceId;
... ... @@ -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 136 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
121 137 @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET)
122 138 @ResponseBody
... ...
... ... @@ -16,32 +16,40 @@
16 16 package org.thingsboard.server.service.security.auth.jwt;
17 17
18 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 20 import org.springframework.security.core.Authentication;
24 21 import org.springframework.security.core.AuthenticationException;
25 22 import org.springframework.security.core.userdetails.UsernameNotFoundException;
26 23 import org.springframework.stereotype.Component;
27 24 import org.springframework.util.Assert;
  25 +import org.thingsboard.server.common.data.Customer;
28 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 31 import org.thingsboard.server.common.data.security.UserCredentials;
  32 +import org.thingsboard.server.dao.customer.CustomerService;
30 33 import org.thingsboard.server.dao.user.UserService;
31 34 import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
32 35 import org.thingsboard.server.service.security.model.SecurityUser;
  36 +import org.thingsboard.server.service.security.model.UserPrincipal;
33 37 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
34 38 import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
35 39
  40 +import java.util.UUID;
  41 +
36 42 @Component
37 43 public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
38 44
39 45 private final JwtTokenFactory tokenFactory;
40 46 private final UserService userService;
  47 + private final CustomerService customerService;
41 48
42 49 @Autowired
43   - public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) {
  50 + public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) {
44 51 this.userService = userService;
  52 + this.customerService = customerService;
45 53 this.tokenFactory = tokenFactory;
46 54 }
47 55
... ... @@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
50 58 Assert.notNull(authentication, "No authentication data provided");
51 59 RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
52 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 73 if (user == null) {
56 74 throw new UsernameNotFoundException("User not found by refresh token");
57 75 }
... ... @@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
67 85
68 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 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 23 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
24 24 import org.springframework.stereotype.Component;
25 25 import org.springframework.util.Assert;
  26 +import org.thingsboard.server.common.data.Customer;
26 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 32 import org.thingsboard.server.common.data.security.UserCredentials;
  33 +import org.thingsboard.server.dao.customer.CustomerService;
28 34 import org.thingsboard.server.dao.user.UserService;
29 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 40 @Component
32 41 public class RestAuthenticationProvider implements AuthenticationProvider {
33 42
34 43 private final BCryptPasswordEncoder encoder;
35 44 private final UserService userService;
  45 + private final CustomerService customerService;
36 46
37 47 @Autowired
38   - public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) {
  48 + public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) {
39 49 this.userService = userService;
  50 + this.customerService = customerService;
40 51 this.encoder = encoder;
41 52 }
42 53
... ... @@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
44 55 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
45 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 75 User user = userService.findUserByEmail(username);
51 76 if (user == null) {
52 77 throw new UsernameNotFoundException("User not found: " + username);
... ... @@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
67 92
68 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 128 return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
73 129 }
... ...
... ... @@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
29 29 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
30 30 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
31 31 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
32 33
33 34 import javax.servlet.FilterChain;
34 35 import javax.servlet.ServletException;
... ... @@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
73 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 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 30
31 31 private Collection<GrantedAuthority> authorities;
32 32 private boolean enabled;
  33 + private UserPrincipal userPrincipal;
33 34
34 35 public SecurityUser() {
35 36 super();
... ... @@ -39,9 +40,10 @@ public class SecurityUser extends User {
39 40 super(id);
40 41 }
41 42
42   - public SecurityUser(User user, boolean enabled) {
  43 + public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) {
43 44 super(user);
44 45 this.enabled = enabled;
  46 + this.userPrincipal = userPrincipal;
45 47 }
46 48
47 49 public Collection<? extends GrantedAuthority> getAuthorities() {
... ... @@ -57,8 +59,16 @@ public class SecurityUser extends User {
57 59 return enabled;
58 60 }
59 61
  62 + public UserPrincipal getUserPrincipal() {
  63 + return userPrincipal;
  64 + }
  65 +
60 66 public void setEnabled(boolean enabled) {
61 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 29 import org.thingsboard.server.common.data.security.Authority;
30 30 import org.thingsboard.server.config.JwtSettings;
31 31 import org.thingsboard.server.service.security.model.SecurityUser;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
32 33
33 34 import java.util.Arrays;
34 35 import java.util.List;
... ... @@ -43,6 +44,7 @@ public class JwtTokenFactory {
43 44 private static final String FIRST_NAME = "firstName";
44 45 private static final String LAST_NAME = "lastName";
45 46 private static final String ENABLED = "enabled";
  47 + private static final String IS_PUBLIC = "isPublic";
46 48 private static final String TENANT_ID = "tenantId";
47 49 private static final String CUSTOMER_ID = "customerId";
48 50
... ... @@ -63,12 +65,15 @@ public class JwtTokenFactory {
63 65 if (securityUser.getAuthority() == null)
64 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 71 claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
68 72 claims.put(USER_ID, securityUser.getId().getId().toString());
69 73 claims.put(FIRST_NAME, securityUser.getFirstName());
70 74 claims.put(LAST_NAME, securityUser.getLastName());
71 75 claims.put(ENABLED, securityUser.isEnabled());
  76 + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
72 77 if (securityUser.getTenantId() != null) {
73 78 claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
74 79 }
... ... @@ -104,6 +109,9 @@ public class JwtTokenFactory {
104 109 securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
105 110 securityUser.setLastName(claims.get(LAST_NAME, String.class));
106 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 115 String tenantId = claims.get(TENANT_ID, String.class);
108 116 if (tenantId != null) {
109 117 securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
... ... @@ -123,9 +131,11 @@ public class JwtTokenFactory {
123 131
124 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 136 claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name()));
128 137 claims.put(USER_ID, securityUser.getId().getId().toString());
  138 + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
129 139
130 140 String token = Jwts.builder()
131 141 .setClaims(claims)
... ... @@ -150,8 +160,10 @@ public class JwtTokenFactory {
150 160 if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
151 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 165 SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
154   - securityUser.setEmail(subject);
  166 + securityUser.setUserPrincipal(principal);
155 167 return securityUser;
156 168 }
157 169
... ...
... ... @@ -16,12 +16,14 @@
16 16 package org.thingsboard.server.dao.customer;
17 17
18 18 import java.util.List;
  19 +import java.util.Optional;
19 20 import java.util.UUID;
20 21
21 22 import org.thingsboard.server.common.data.Customer;
22 23 import org.thingsboard.server.common.data.page.TextPageLink;
23 24 import org.thingsboard.server.dao.Dao;
24 25 import org.thingsboard.server.dao.model.CustomerEntity;
  26 +import org.thingsboard.server.dao.model.DeviceEntity;
25 27
26 28 /**
27 29 * The Interface CustomerDao.
... ... @@ -44,5 +46,14 @@ public interface CustomerDao extends Dao<CustomerEntity> {
44 46 * @return the list of customer objects
45 47 */
46 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 16 package org.thingsboard.server.dao.customer;
17 17
18 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 25 import java.util.Arrays;
21 26 import java.util.List;
  27 +import java.util.Optional;
22 28 import java.util.UUID;
23 29
  30 +import com.datastax.driver.core.querybuilder.Select;
24 31 import lombok.extern.slf4j.Slf4j;
25 32 import org.springframework.stereotype.Component;
26 33 import org.thingsboard.server.common.data.Customer;
... ... @@ -60,4 +67,13 @@ public class CustomerDaoImpl extends AbstractSearchTextDao<CustomerEntity> imple
60 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 28 public Customer saveCustomer(Customer customer);
29 29
30 30 public void deleteCustomer(CustomerId customerId);
  31 +
  32 + public Customer findOrCreatePublicCustomer(TenantId tenantId);
31 33
32 34 public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink);
33 35
... ...
... ... @@ -18,8 +18,12 @@ package org.thingsboard.server.dao.customer;
18 18 import static org.thingsboard.server.dao.DaoUtil.convertDataList;
19 19 import static org.thingsboard.server.dao.DaoUtil.getData;
20 20
  21 +import java.io.IOException;
21 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 27 import lombok.extern.slf4j.Slf4j;
24 28 import org.apache.commons.lang3.StringUtils;
25 29 import org.thingsboard.server.common.data.Customer;
... ... @@ -46,6 +50,8 @@ import org.thingsboard.server.dao.service.Validator;
46 50 @Slf4j
47 51 public class CustomerServiceImpl implements CustomerService {
48 52
  53 + private static final String PUBLIC_CUSTOMER_TITLE = "Public";
  54 +
49 55 @Autowired
50 56 private CustomerDao customerDao;
51 57
... ... @@ -80,7 +86,7 @@ public class CustomerServiceImpl implements CustomerService {
80 86 @Override
81 87 public void deleteCustomer(CustomerId customerId) {
82 88 log.trace("Executing deleteCustomer [{}]", customerId);
83   - Validator.validateId(customerId, "Incorrect tenantId " + customerId);
  89 + Validator.validateId(customerId, "Incorrect customerId " + customerId);
84 90 Customer customer = findCustomerById(customerId);
85 91 if (customer == null) {
86 92 throw new IncorrectParameterException("Unable to delete non-existent customer.");
... ... @@ -92,6 +98,27 @@ public class CustomerServiceImpl implements CustomerService {
92 98 }
93 99
94 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 122 public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) {
96 123 log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
97 124 Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
... ... @@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService {
110 137
111 138 private DataValidator<Customer> customerValidator =
112 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 161 @Override
114 162 protected void validateDataImpl(Customer customer) {
115 163 if (StringUtils.isEmpty(customer.getTitle())) {
116 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 169 if (!StringUtils.isEmpty(customer.getEmail())) {
119 170 validateEmail(customer.getEmail());
120 171 }
... ...
... ... @@ -111,6 +111,7 @@ public class ModelConstants {
111 111 public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
112 112
113 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 117 * Cassandra device constants.
... ...
... ... @@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer (
137 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 147 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS
141 148 SELECT *
142 149 from thingsboard.customer
... ...
... ... @@ -80,7 +80,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel',
80 80
81 81 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
82 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 84 'Timeseries table' );
85 85
86 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 18 .name;
19 19
20 20 /*@ngInject*/
21   -function CustomerService($http, $q) {
  21 +function CustomerService($http, $q, types) {
22 22
23 23 var service = {
24 24 getCustomers: getCustomers,
25 25 getCustomer: getCustomer,
26   - getCustomerTitle: getCustomerTitle,
  26 + getShortCustomerInfo: getShortCustomerInfo,
  27 + applyAssignedCustomersInfo: applyAssignedCustomersInfo,
  28 + applyAssignedCustomerInfo: applyAssignedCustomerInfo,
27 29 deleteCustomer: deleteCustomer,
28 30 saveCustomer: saveCustomer
29 31 }
... ... @@ -61,9 +63,9 @@ function CustomerService($http, $q) {
61 63 return deferred.promise;
62 64 }
63 65
64   - function getCustomerTitle(customerId) {
  66 + function getShortCustomerInfo(customerId) {
65 67 var deferred = $q.defer();
66   - var url = '/api/customer/' + customerId + '/title';
  68 + var url = '/api/customer/' + customerId + '/shortInfo';
67 69 $http.get(url, null).then(function success(response) {
68 70 deferred.resolve(response.data);
69 71 }, function fail(response) {
... ... @@ -72,6 +74,77 @@ function CustomerService($http, $q) {
72 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 148 function saveCustomer(customer) {
76 149 var deferred = $q.defer();
77 150 var url = '/api/customer';
... ...
... ... @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', [])
17 17 .factory('dashboardService', DashboardService).name;
18 18
19 19 /*@ngInject*/
20   -function DashboardService($http, $q) {
  20 +function DashboardService($http, $q, $location, customerService) {
21 21
22 22 var service = {
23 23 assignDashboardToCustomer: assignDashboardToCustomer,
... ... @@ -27,7 +27,9 @@ function DashboardService($http, $q) {
27 27 getTenantDashboards: getTenantDashboards,
28 28 deleteDashboard: deleteDashboard,
29 29 saveDashboard: saveDashboard,
30   - unassignDashboardFromCustomer: unassignDashboardFromCustomer
  30 + unassignDashboardFromCustomer: unassignDashboardFromCustomer,
  31 + makeDashboardPublic: makeDashboardPublic,
  32 + getPublicDashboardLink: getPublicDashboardLink
31 33 }
32 34
33 35 return service;
... ... @@ -45,7 +47,15 @@ function DashboardService($http, $q) {
45 47 url += '&textOffset=' + pageLink.textOffset;
46 48 }
47 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 59 }, function fail() {
50 60 deferred.reject();
51 61 });
... ... @@ -65,7 +75,15 @@ function DashboardService($http, $q) {
65 75 url += '&textOffset=' + pageLink.textOffset;
66 76 }
67 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 87 }, function fail() {
70 88 deferred.reject();
71 89 });
... ... @@ -92,8 +110,8 @@ function DashboardService($http, $q) {
92 110 var url = '/api/dashboard/' + dashboardId;
93 111 $http.get(url, null).then(function success(response) {
94 112 deferred.resolve(response.data);
95   - }, function fail(response) {
96   - deferred.reject(response.data);
  113 + }, function fail() {
  114 + deferred.reject();
97 115 });
98 116 return deferred.promise;
99 117 }
... ... @@ -103,8 +121,8 @@ function DashboardService($http, $q) {
103 121 var url = '/api/dashboard';
104 122 $http.post(url, dashboard).then(function success(response) {
105 123 deferred.resolve(response.data);
106   - }, function fail(response) {
107   - deferred.reject(response.data);
  124 + }, function fail() {
  125 + deferred.reject();
108 126 });
109 127 return deferred.promise;
110 128 }
... ... @@ -114,8 +132,8 @@ function DashboardService($http, $q) {
114 132 var url = '/api/dashboard/' + dashboardId;
115 133 $http.delete(url).then(function success() {
116 134 deferred.resolve();
117   - }, function fail(response) {
118   - deferred.reject(response.data);
  135 + }, function fail() {
  136 + deferred.reject();
119 137 });
120 138 return deferred.promise;
121 139 }
... ... @@ -123,10 +141,10 @@ function DashboardService($http, $q) {
123 141 function assignDashboardToCustomer(customerId, dashboardId) {
124 142 var deferred = $q.defer();
125 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 149 return deferred.promise;
132 150 }
... ... @@ -134,12 +152,33 @@ function DashboardService($http, $q) {
134 152 function unassignDashboardFromCustomer(dashboardId) {
135 153 var deferred = $q.defer();
136 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 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 58 var datasourceSubscription = {
59 59 datasourceType: datasource.type,
60 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 65 datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow);
66 66 }
67 67 if (datasourceSubscription.datasourceType === types.datasourceType.device) {
... ...
... ... @@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
20 20 .name;
21 21
22 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 26 var deviceAttributesSubscriptionMap = {};
... ... @@ -33,6 +33,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
33 33 getDevices: getDevices,
34 34 processDeviceAliases: processDeviceAliases,
35 35 checkDeviceAlias: checkDeviceAlias,
  36 + fetchAliasDeviceByNameFilter: fetchAliasDeviceByNameFilter,
36 37 getDeviceCredentials: getDeviceCredentials,
37 38 getDeviceKeys: getDeviceKeys,
38 39 getDeviceTimeseriesValues: getDeviceTimeseriesValues,
... ... @@ -40,6 +41,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
40 41 saveDevice: saveDevice,
41 42 saveDeviceCredentials: saveDeviceCredentials,
42 43 unassignDeviceFromCustomer: unassignDeviceFromCustomer,
  44 + makeDevicePublic: makeDevicePublic,
43 45 getDeviceAttributes: getDeviceAttributes,
44 46 subscribeForDeviceAttributes: subscribeForDeviceAttributes,
45 47 unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes,
... ... @@ -51,7 +53,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
51 53
52 54 return service;
53 55
54   - function getTenantDevices(pageLink, config) {
  56 + function getTenantDevices(pageLink, applyCustomersInfo, config) {
55 57 var deferred = $q.defer();
56 58 var url = '/api/tenant/devices?limit=' + pageLink.limit;
57 59 if (angular.isDefined(pageLink.textSearch)) {
... ... @@ -64,14 +66,26 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
64 66 url += '&textOffset=' + pageLink.textOffset;
65 67 }
66 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 82 }, function fail() {
69 83 deferred.reject();
70 84 });
71 85 return deferred.promise;
72 86 }
73 87
74   - function getCustomerDevices(customerId, pageLink) {
  88 + function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
75 89 var deferred = $q.defer();
76 90 var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
77 91 if (angular.isDefined(pageLink.textSearch)) {
... ... @@ -83,18 +97,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
83 97 if (angular.isDefined(pageLink.textOffset)) {
84 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 114 }, function fail() {
89 115 deferred.reject();
90 116 });
  117 +
91 118 return deferred.promise;
92 119 }
93 120
94   - function getDevice(deviceId, ignoreErrors) {
  121 + function getDevice(deviceId, ignoreErrors, config) {
95 122 var deferred = $q.defer();
96 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 129 deferred.resolve(response.data);
99 130 }, function fail(response) {
100 131 deferred.reject(response.data);
... ... @@ -102,7 +133,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
102 133 return deferred.promise;
103 134 }
104 135
105   - function getDevices(deviceIds) {
  136 + function getDevices(deviceIds, config) {
106 137 var deferred = $q.defer();
107 138 var ids = '';
108 139 for (var i=0;i<deviceIds.length;i++) {
... ... @@ -112,7 +143,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
112 143 ids += deviceIds[i];
113 144 }
114 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 147 var devices = response.data;
117 148 devices.sort(function (device1, device2) {
118 149 var id1 = device1.id.id;
... ... @@ -128,16 +159,16 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
128 159 return deferred.promise;
129 160 }
130 161
131   - function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
  162 + function fetchAliasDeviceByNameFilter(deviceNameFilter, limit, applyCustomersInfo, config) {
132 163 var deferred = $q.defer();
133 164 var user = userService.getCurrentUser();
134 165 var promise;
135 166 var pageLink = {limit: limit, textSearch: deviceNameFilter};
136 167 if (user.authority === 'CUSTOMER_USER') {
137 168 var customerId = user.customerId;
138   - promise = getCustomerDevices(customerId, pageLink);
  169 + promise = getCustomerDevices(customerId, pageLink, applyCustomersInfo, config);
139 170 } else {
140   - promise = getTenantDevices(pageLink);
  171 + promise = getTenantDevices(pageLink, applyCustomersInfo, config);
141 172 }
142 173 promise.then(
143 174 function success(result) {
... ... @@ -194,7 +225,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
194 225 var deviceFilter = deviceAlias.deviceFilter;
195 226 if (deviceFilter.useFilter) {
196 227 var deviceNameFilter = deviceFilter.deviceNameFilter;
197   - fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
  228 + fetchAliasDeviceByNameFilter(deviceNameFilter, 100, false).then(
198 229 function(devices) {
199 230 if (devices && devices != null) {
200 231 var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
... ... @@ -276,7 +307,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
276 307 var promise;
277 308 if (deviceFilter.useFilter) {
278 309 var deviceNameFilter = deviceFilter.deviceNameFilter;
279   - promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
  310 + promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1, false);
280 311 } else {
281 312 var deviceList = deviceFilter.deviceList;
282 313 promise = getDevices(deviceList);
... ... @@ -301,8 +332,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
301 332 var url = '/api/device';
302 333 $http.post(url, device).then(function success(response) {
303 334 deferred.resolve(response.data);
304   - }, function fail(response) {
305   - deferred.reject(response.data);
  335 + }, function fail() {
  336 + deferred.reject();
306 337 });
307 338 return deferred.promise;
308 339 }
... ... @@ -312,8 +343,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
312 343 var url = '/api/device/' + deviceId;
313 344 $http.delete(url).then(function success() {
314 345 deferred.resolve();
315   - }, function fail(response) {
316   - deferred.reject(response.data);
  346 + }, function fail() {
  347 + deferred.reject();
317 348 });
318 349 return deferred.promise;
319 350 }
... ... @@ -323,8 +354,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
323 354 var url = '/api/device/' + deviceId + '/credentials';
324 355 $http.get(url, null).then(function success(response) {
325 356 deferred.resolve(response.data);
326   - }, function fail(response) {
327   - deferred.reject(response.data);
  357 + }, function fail() {
  358 + deferred.reject();
328 359 });
329 360 return deferred.promise;
330 361 }
... ... @@ -334,8 +365,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
334 365 var url = '/api/device/credentials';
335 366 $http.post(url, deviceCredentials).then(function success(response) {
336 367 deferred.resolve(response.data);
337   - }, function fail(response) {
338   - deferred.reject(response.data);
  368 + }, function fail() {
  369 + deferred.reject();
339 370 });
340 371 return deferred.promise;
341 372 }
... ... @@ -343,10 +374,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
343 374 function assignDeviceToCustomer(customerId, deviceId) {
344 375 var deferred = $q.defer();
345 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 382 return deferred.promise;
352 383 }
... ... @@ -354,10 +385,21 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
354 385 function unassignDeviceFromCustomer(deviceId) {
355 386 var deferred = $q.defer();
356 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 404 return deferred.promise;
363 405 }
... ...
... ... @@ -25,6 +25,7 @@ function LoginService($http, $q) {
25 25 changePassword: changePassword,
26 26 hasUser: hasUser,
27 27 login: login,
  28 + publicLogin: publicLogin,
28 29 resetPassword: resetPassword,
29 30 sendResetPasswordLink: sendResetPasswordLink,
30 31 }
... ... @@ -49,6 +50,19 @@ function LoginService($http, $q) {
49 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 66 function sendResetPasswordLink(email) {
53 67 var deferred = $q.defer();
54 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 22 .name;
23 23
24 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 26 var currentUser = null,
27 27 currentUserDetails = null,
  28 + lastPublicDashboardId = null,
28 29 allowedDashboardIds = [],
29 30 userLoaded = false;
30 31
... ... @@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
33 34 var service = {
34 35 deleteUser: deleteUser,
35 36 getAuthority: getAuthority,
  37 + isPublic: isPublic,
  38 + getPublicId: getPublicId,
  39 + parsePublicId: parsePublicId,
36 40 isAuthenticated: isAuthenticated,
37 41 getCurrentUser: getCurrentUser,
38 42 getCustomerUsers: getCustomerUsers,
... ... @@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
51 55 updateAuthorizationHeader: updateAuthorizationHeader,
52 56 gotoDefaultPlace: gotoDefaultPlace,
53 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 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 77 var valid = false;
67 78 var tokenData = jwtHelper.decodeToken(token);
68 79 var issuedAt = tokenData.iat;
... ... @@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
76 87 valid = true;
77 88 }
78 89 }
79   - if (!valid) {
  90 + if (!valid && notify) {
80 91 $rootScope.$broadcast('unauthenticated');
81 92 }
82 93 }
... ... @@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
91 102 function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
92 103 currentUser = null;
93 104 currentUserDetails = null;
  105 + lastPublicDashboardId = null;
94 106 allowedDashboardIds = [];
95 107 if (!jwtToken) {
96 108 clearTokenData();
... ... @@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
98 110 $rootScope.$broadcast('unauthenticated', doLogout);
99 111 }
100 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 115 if (notify) {
104 116 loadUser(false).then(function success() {
105 117 $rootScope.$broadcast('authenticated');
... ... @@ -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 255 function isUserLoaded() {
217 256 return userLoaded;
218 257 }
219 258
220 259 function loadUser(doTokenRefresh) {
  260 +
221 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 280 validateJwtToken(doTokenRefresh).then(function success() {
224 281 var jwtToken = store.get('jwt_token');
225 282 currentUser = jwtHelper.decodeToken(jwtToken);
... ... @@ -228,29 +285,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
228 285 } else if (currentUser) {
229 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 292 getUser(currentUser.userId).then(
233 293 function success(user) {
234 294 currentUserDetails = user;
235 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 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 301 } else {
255 302 deferred.resolve();
256 303 }
... ... @@ -265,6 +312,23 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
265 312 }, function fail() {
266 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 332 } else {
269 333 deferred.resolve();
270 334 }
... ... @@ -373,17 +437,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
373 437 function forceDefaultPlace(to, params) {
374 438 if (currentUser && isAuthenticated()) {
375 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 443 return false;
384 444 } else {
385 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 459 if (currentUser && isAuthenticated()) {
396 460 var place = 'home.links';
397 461 if (currentUser.authority === 'CUSTOMER_USER') {
398   - if (currentUserDetails &&
399   - currentUserDetails.additionalInfo &&
400   - currentUserDetails.additionalInfo.defaultDashboardId) {
  462 + if (userHasDefaultDashboard()) {
401 463 place = 'home.dashboards.dashboard';
402 464 params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
  465 + } else if (isPublic()) {
  466 + place = 'home.dashboards.dashboard';
  467 + params = {dashboardId: lastPublicDashboardId};
403 468 }
404 469 } else if (currentUser.authority === 'SYS_ADMIN') {
405 470 adminService.checkUpdates().then(
... ... @@ -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 17 import moment from 'moment';
18 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 23 import TbFlot from '../widget/lib/flot-widget';
23 24 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
... ... @@ -31,7 +32,8 @@ import cssjs from '../../vendor/css.js/css';
31 32 import thingsboardTypes from '../common/types.constant';
32 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 37 .factory('widgetService', WidgetService)
36 38 .name;
37 39
... ... @@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
539 541
540 542 ' }\n\n' +
541 543
  544 + ' self.useCustomDatasources = function() {\n\n' +
  545 +
  546 + ' }\n\n' +
  547 +
542 548 ' self.onResize = function() {\n\n' +
543 549
544 550 ' }\n\n' +
... ... @@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
579 585 if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
580 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 593 return result;
583 594 } catch (e) {
584 595 utils.processWidgetException(e);
... ... @@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
617 628 if (widgetType.dataKeySettingsSchema) {
618 629 widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
619 630 }
  631 + widgetInfo.useCustomDatasources = widgetType.useCustomDatasources;
620 632 putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
621 633 putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
622 634 deferred.resolve(widgetInfo);
... ...
... ... @@ -55,8 +55,39 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
55 55 });
56 56
57 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 78 if (userService.isUserLoaded() === true) {
59 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 91 if (userService.forceDefaultPlace(to, params)) {
61 92 evt.preventDefault();
62 93 gotoDefaultPlace(params);
... ... @@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
75 106 }
76 107 }
77 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 113 evt.preventDefault();
80 114 if (to.url === '/home' || to.url === '/') {
81 115 $state.go('login', params);
... ... @@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
86 120 }
87 121 } else {
88 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 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 134 if (angular.isDefined(to.data.pageTitle)) {
103 135 $translate(to.data.pageTitle).then(function (translation) {
104 136 $rootScope.pageTitle = 'Thingsboard | ' + translation;
... ...
... ... @@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
22 22 .name;
23 23
24 24 /*@ngInject*/
25   -function Utils($mdColorPalette, $rootScope, $window, types) {
  25 +function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) {
26 26
27 27 var predefinedFunctions = {},
28 28 predefinedFunctionsList = [],
... ... @@ -104,7 +104,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
104 104 parseException: parseException,
105 105 processWidgetException: processWidgetException,
106 106 isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
107   - filterSearchTextEntities: filterSearchTextEntities
  107 + filterSearchTextEntities: filterSearchTextEntities,
  108 + guid: guid,
  109 + createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
108 110 }
109 111
110 112 return service;
... ... @@ -276,4 +278,153 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
276 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 182
183 183 vm.dashboardTimewindowApi = {
184 184 onResetTimewindow: function() {
185   - if (vm.originalDashboardTimewindow) {
186   - $timeout(function() {
  185 + $timeout(function() {
  186 + if (vm.originalDashboardTimewindow) {
187 187 vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
188 188 vm.originalDashboardTimewindow = null;
189   - }, 0);
190   - }
  189 + }
  190 + }, 0);
191 191 },
192 192 onUpdateTimewindow: function(startTimeMs, endTimeMs) {
193 193 if (!vm.originalDashboardTimewindow) {
... ...
... ... @@ -41,7 +41,7 @@ function DeviceFilter($compile, $templateCache, $q, deviceService) {
41 41
42 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 45 deferred.resolve(result.data);
46 46 }, function fail() {
47 47 deferred.reject();
... ...
... ... @@ -49,7 +49,7 @@
49 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 50 ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
51 51 <md-tooltip md-direction="top">
52   - {{ action.details() }}
  52 + {{ action.details( rowItem[n] ) }}
53 53 </md-tooltip>
54 54 <ng-md-icon icon="{{action.icon}}"></ng-md-icon>
55 55 </md-button>
... ... @@ -62,7 +62,7 @@
62 62 </div>
63 63 <tb-details-sidenav
64 64 header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}"
65   - header-subtitle="{{vm.itemDetailsText()}}"
  65 + header-subtitle="{{vm.itemDetailsText(vm.operatingItem())}}"
66 66 is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
67 67 is-open="vm.detailsConfig.isDetailsOpen"
68 68 is-edit="vm.detailsConfig.isDetailsEditMode"
... ...
... ... @@ -44,15 +44,8 @@ function Legend($compile, $templateCache, types) {
44 44 scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
45 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 47 scope.toggleHideData = function(index) {
54 48 scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
55   - scope.$emit('legendDataHiddenChanged', index);
56 49 }
57 50
58 51 $compile(element.contents())(scope);
... ...
... ... @@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
76 76 scope.forceExpandDatasources = false;
77 77 }
78 78
  79 + if (angular.isUndefined(scope.isDataEnabled)) {
  80 + scope.isDataEnabled = true;
  81 + }
  82 +
79 83 scope.currentSettingsSchema = {};
80 84 scope.currentSettings = angular.copy(scope.emptySettingsSchema);
81 85
... ... @@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
108 112 scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ?
109 113 ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
110 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 117 if (scope.datasources) {
113 118 scope.datasources.splice(0, scope.datasources.length);
114 119 } else {
... ... @@ -119,7 +124,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
119 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 128 if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
124 129 var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
125 130 if (scope.deviceAliases[aliasId]) {
... ... @@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
159 164 if (ngModelCtrl.$viewValue) {
160 165 var value = ngModelCtrl.$viewValue;
161 166 var valid;
162   - if (scope.widgetType === types.widgetType.rpc.value) {
  167 + if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
163 168 valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
164 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 171 valid = value && value.datasources && value.datasources.length > 0;
167 172 ngModelCtrl.$setValidity('datasources', valid);
168 173 }
... ... @@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
228 233
229 234 scope.$watch('datasources', function () {
230 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 237 var value = ngModelCtrl.$viewValue;
233 238 if (value.datasources) {
234 239 value.datasources.splice(0, value.datasources.length);
... ... @@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
246 251 }, true);
247 252
248 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 255 var value = ngModelCtrl.$viewValue;
251 256 if (scope.targetDeviceAlias.value) {
252 257 value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
... ... @@ -359,6 +364,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
359 364 require: "^ngModel",
360 365 scope: {
361 366 forceExpandDatasources: '=?',
  367 + isDataEnabled: '=?',
362 368 widgetType: '=',
363 369 widgetSettingsSchema: '=',
364 370 datakeySettingsSchema: '=',
... ...
... ... @@ -31,7 +31,7 @@
31 31 </section>
32 32 </div>
33 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 35 <v-pane id="datasources-pane" expanded="true">
36 36 <v-pane-header>
37 37 {{ 'widget-config.datasources' | translate }}
... ... @@ -96,7 +96,7 @@
96 96 </v-pane>
97 97 </v-accordion>
98 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 100 <v-pane id="target-devices-pane" expanded="true">
101 101 <v-pane-header>
102 102 {{ 'widget-config.target-device' | translate }}
... ...
... ... @@ -15,6 +15,7 @@
15 15 */
16 16 import $ from 'jquery';
17 17 import 'javascript-detect-element-resize/detect-element-resize';
  18 +import Subscription from '../api/subscription';
18 19
19 20 /* eslint-disable angular/angularelement */
20 21
... ... @@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
34 35 $scope.rpcErrorText = null;
35 36 $scope.rpcEnabled = false;
36 37 $scope.executingRpcRequest = false;
37   - $scope.executingPromises = [];
38 38
39 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 41 var cafs = {};
47 42
48   - var varsRegex = /\$\{([^\}]*)\}/g;
49   -
50 43 /*
51 44 * data = array of datasourceData
52 45 * datasourceData = {
... ... @@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
54 47 * dataKey, { name, config }
55 48 * data = array of [time, value]
56 49 * }
57   - *
58   - *
59 50 */
60 51
61 52 var widgetContext = {
... ... @@ -71,22 +62,70 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
71 62 settings: widget.config.settings,
72 63 units: widget.config.units || '',
73 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 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 117 controlApi: {
85 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 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 131 utils: {
... ... @@ -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 153 var widgetTypeInstance;
  154 +
  155 + vm.useCustomDatasources = false;
  156 +
98 157 try {
99 158 widgetTypeInstance = new widgetType(widgetContext);
100 159 } catch (e) {
... ... @@ -119,19 +178,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
119 178 if (!widgetTypeInstance.onDestroy) {
120 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 185 //TODO: widgets visibility
  186 +
  187 + //var bounds = {top: 0, left: 0, bottom: 0, right: 0};
  188 + /*var visible = false;*/
135 189 /*vm.visibleRectChanged = visibleRectChanged;
136 190
137 191 function visibleRectChanged(newVisibleRect) {
... ... @@ -139,23 +193,216 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
139 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 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 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 408 function onInit() {
... ... @@ -166,42 +413,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
166 413 } catch (e) {
167 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 422 function checkSize() {
206 423 var width = widgetContext.$containerParent.width();
207 424 var height = widgetContext.$containerParent.height();
... ... @@ -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 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 536 if (widgetContext.inited) {
295 537 widgetContext.inited = false;
296   - widgetContext.dataUpdatePending = false;
297 538 for (var cafId in cafs) {
298 539 if (cafs[cafId]) {
299 540 cafs[cafId]();
... ... @@ -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 552 //TODO: widgets visibility
550 553 /*function updateVisibility(forceRedraw) {
551 554 if (visibleRect) {
... ... @@ -584,285 +587,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
584 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 592 /* eslint-enable angular/angularelement */
\ No newline at end of file
... ...
... ... @@ -15,4 +15,4 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<div class="tb-uppercase">{{ item | contactShort }}</div>
\ No newline at end of file
  18 +<div ng-show="item && (!item.additionalInfo || !item.additionalInfo.isPublic)" class="tb-uppercase">{{ item | contactShort }}</div>
\ No newline at end of file
... ...
... ... @@ -15,13 +15,13 @@
15 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 19 <md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button>
20 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 23 <md-content class="md-padding" layout="column">
24   - <fieldset ng-disabled="loading || !isEdit">
  24 + <fieldset ng-show="!isPublic" ng-disabled="loading || !isEdit">
25 25 <md-input-container class="md-block">
26 26 <label translate>customer.title</label>
27 27 <input required name="title" ng-model="customer.title">
... ...
... ... @@ -30,14 +30,23 @@ export default function CustomerController(customerService, $state, $stateParams
30 30 },
31 31 name: function() { return $translate.instant('user.users') },
32 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 39 onAction: function ($event, item) {
37 40 openCustomerDevices($event, item);
38 41 },
39 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 50 icon: "devices_other"
42 51 },
43 52 {
... ... @@ -45,7 +54,13 @@ export default function CustomerController(customerService, $state, $stateParams
45 54 openCustomerDashboards($event, item);
46 55 },
47 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 64 icon: "dashboard"
50 65 },
51 66 {
... ... @@ -54,7 +69,10 @@ export default function CustomerController(customerService, $state, $stateParams
54 69 },
55 70 name: function() { return $translate.instant('action.delete') },
56 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 104
87 105 addItemText: function() { return $translate.instant('customer.add-customer-text') },
88 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 122 if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
... ...
... ... @@ -24,7 +24,21 @@ export default function CustomerDirective($compile, $templateCache) {
24 24 var linker = function (scope, element) {
25 25 var template = $templateCache.get(customerFieldsetTemplate);
26 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 40 $compile(element.contents())(scope);
  41 +
28 42 }
29 43 return {
30 44 restrict: "E",
... ...
... ... @@ -15,5 +15,6 @@
15 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 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 24 <md-button ng-click="onAssignToCustomer({event: $event})"
19 25 ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer"
20 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 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 30 <md-button ng-click="onDeleteDashboard({event: $event})"
28 31 ng-show="!isEdit && dashboardScope === 'tenant'"
29 32 class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
30   -
31 33 <md-content class="md-padding" layout="column">
32 34 <md-input-container class="md-block"
33   - ng-show="isAssignedToCustomer && dashboardScope === 'tenant'">
  35 + ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'">
34 36 <label translate>dashboard.assignedToCustomer</label>
35 37 <input ng-model="assignedCustomer.title" disabled>
36 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 54 <fieldset ng-disabled="loading || !isEdit">
38 55 <md-input-container class="md-block">
39 56 <label translate>dashboard.title</label>
... ...
... ... @@ -86,6 +86,7 @@ export default function DashboardController(types, widgetService, userService,
86 86 vm.exportDashboard = exportDashboard;
87 87 vm.exportWidget = exportWidget;
88 88 vm.importWidget = importWidget;
  89 + vm.isPublicUser = isPublicUser;
89 90 vm.isTenantAdmin = isTenantAdmin;
90 91 vm.isSystemAdmin = isSystemAdmin;
91 92 vm.loadDashboard = loadDashboard;
... ... @@ -273,6 +274,10 @@ export default function DashboardController(types, widgetService, userService,
273 274 vm.dashboardInitComplete = true;
274 275 }
275 276
  277 + function isPublicUser() {
  278 + return vm.user.isPublic === true;
  279 + }
  280 +
276 281 function isTenantAdmin() {
277 282 return vm.user.authority === 'TENANT_ADMIN';
278 283 }
... ... @@ -617,22 +622,8 @@ export default function DashboardController(types, widgetService, userService,
617 622 sizeY: widgetTypeInfo.sizeY,
618 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 627 var columns = 24;
637 628 if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
638 629 columns = vm.dashboard.configuration.gridSettings.columns;
... ... @@ -643,9 +634,37 @@ export default function DashboardController(types, widgetService, userService,
643 634 widget.sizeY *= ratio;
644 635 }
645 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 20 /* eslint-enable import/no-unresolved, import/default */
21 21
22 22 /*@ngInject*/
23   -export default function DashboardDirective($compile, $templateCache, types, customerService) {
  23 +export default function DashboardDirective($compile, $templateCache, $translate, types, toast, customerService, dashboardService) {
24 24 var linker = function (scope, element) {
25 25 var template = $templateCache.get(dashboardFieldsetTemplate);
26 26 element.html(template);
27 27
28 28 scope.isAssignedToCustomer = false;
  29 + scope.isPublic = false;
29 30 scope.assignedCustomer = null;
  31 + scope.publicLink = null;
30 32
31 33 scope.$watch('dashboard', function(newVal) {
32 34 if (newVal) {
33 35 if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) {
34 36 scope.isAssignedToCustomer = true;
35   - customerService.getCustomer(scope.dashboard.customerId.id).then(
  37 + customerService.getShortCustomerInfo(scope.dashboard.customerId.id).then(
36 38 function success(customer) {
37 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 48 } else {
41 49 scope.isAssignedToCustomer = false;
  50 + scope.isPublic = false;
  51 + scope.publicLink = null;
42 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 61 $compile(element.contents())(scope);
48 62 }
49 63 return {
... ... @@ -55,6 +69,7 @@ export default function DashboardDirective($compile, $templateCache, types, cust
55 69 dashboardScope: '=',
56 70 theForm: '=',
57 71 onAssignToCustomer: '&',
  72 + onMakePublic: '&',
58 73 onUnassignFromCustomer: '&',
59 74 onExportDashboard: '&',
60 75 onDeleteDashboard: '&'
... ...
... ... @@ -62,7 +62,7 @@ export default function DashboardRoutes($stateProvider) {
62 62 pageTitle: 'customer.dashboards'
63 63 },
64 64 ncyBreadcrumb: {
65   - label: '{"icon": "dashboard", "label": "customer.dashboards"}'
  65 + label: '{"icon": "dashboard", "label": "{{ vm.customerDashboardsTitle }}", "translate": "false"}'
66 66 }
67 67 })
68 68 .state('home.dashboards.dashboard', {
... ...
... ... @@ -47,7 +47,7 @@
47 47 aria-label="{{ 'fullscreen.fullscreen' | translate }}"
48 48 class="md-icon-button">
49 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 51 </tb-user-menu>
52 52 <md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
53 53 ng-click="vm.exportDashboard($event)">
... ...
... ... @@ -19,11 +19,33 @@ import addDashboardTemplate from './add-dashboard.tpl.html';
19 19 import dashboardCard from './dashboard-card.tpl.html';
20 20 import assignToCustomerTemplate from './assign-to-customer.tpl.html';
21 21 import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html';
  22 +import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html';
22 23
23 24 /* eslint-enable import/no-unresolved, import/default */
24 25
25 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 50 var vm = this;
29 51
... ... @@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) {
31 53
32 54 vm.isAssignedToCustomer = function() {
33 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 57 return true;
36 58 }
37 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 70 /*@ngInject*/
54   -export function DashboardsController(userService, dashboardService, customerService, importExport, types, $scope, $controller,
  71 +export function DashboardsController(userService, dashboardService, customerService, importExport, types,
55 72 $state, $stateParams, $mdDialog, $document, $q, $translate) {
56 73
57 74 var customerId = $stateParams.customerId;
... ... @@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ
119 136 vm.dashboardsScope = $state.$current.data.dashboardsType;
120 137
121 138 vm.assignToCustomer = assignToCustomer;
  139 + vm.makePublic = makePublic;
122 140 vm.unassignFromCustomer = unassignFromCustomer;
123 141 vm.exportDashboard = exportDashboard;
124 142
... ... @@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ
136 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 168 if (vm.dashboardsScope === 'tenant') {
140 169 fetchDashboardsFunction = function (pageLink) {
141 170 return dashboardService.getTenantDashboards(pageLink);
... ... @@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ
155 184 name: function() { $translate.instant('action.export') },
156 185 details: function() { return $translate.instant('dashboard.export') },
157 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 202 onAction: function ($event, item) {
161 203 assignToCustomer($event, [ item.id.id ]);
162 204 },
... ... @@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ
166 208 isEnabled: function(dashboard) {
167 209 return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
168 210 }
169   - },
170   - {
  211 + });
  212 + dashboardActionsList.push({
171 213 onAction: function ($event, item) {
172   - unassignFromCustomer($event, item);
  214 + unassignFromCustomer($event, item, false);
173 215 },
174 216 name: function() { return $translate.instant('action.unassign') },
175 217 details: function() { return $translate.instant('dashboard.unassign-from-customer') },
176 218 icon: "assignment_return",
177 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 235 dashboardActionsList.push(
184 236 {
... ... @@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ
262 314 dashboardActionsList.push(
263 315 {
264 316 onAction: function ($event, item) {
265   - unassignFromCustomer($event, item);
  317 + unassignFromCustomer($event, item, false);
266 318 },
267 319 name: function() { return $translate.instant('action.unassign') },
268 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 486 assignToCustomer($event, dashboardIds);
419 487 }
420 488
421   - function unassignFromCustomer($event, dashboard) {
  489 + function unassignFromCustomer($event, dashboard, isPublic) {
422 490 if ($event) {
423 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 505 var confirm = $mdDialog.confirm()
426 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 510 .cancel($translate.instant('action.no'))
431 511 .ok($translate.instant('action.yes'));
432 512 $mdDialog.show(confirm).then(function () {
... ... @@ -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 538 function exportDashboard($event, dashboard) {
440 539 $event.stopPropagation();
441 540 importExport.exportDashboard(dashboard.id.id);
... ...
... ... @@ -24,7 +24,8 @@
24 24 dashboard-scope="vm.dashboardsScope"
25 25 the-form="vm.grid.detailsForm"
26 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 29 on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
29 30 on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
30 31 </tb-grid>
... ...
... ... @@ -37,6 +37,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
37 37 scope.widgetConfig = scope.widget.config;
38 38 var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
39 39 var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
  40 + scope.isDataEnabled = !widgetInfo.useCustomDatasources;
40 41 if (!settingsSchema || settingsSchema === '') {
41 42 scope.settingsSchema = {};
42 43 } else {
... ...
... ... @@ -18,6 +18,7 @@
18 18 <fieldset ng-disabled="loading">
19 19 <tb-widget-config widget-type="widget.type"
20 20 ng-model="widgetConfig"
  21 + is-data-enabled="isDataEnabled"
21 22 widget-settings-schema="settingsSchema"
22 23 datakey-settings-schema="dataKeySettingsSchema"
23 24 device-aliases="aliasesInfo.deviceAliases"
... ...
... ... @@ -35,7 +35,7 @@ import thingsboardItemBuffer from '../services/item-buffer.service';
35 35 import thingsboardImportExport from '../import-export';
36 36
37 37 import DashboardRoutes from './dashboard.routes';
38   -import {DashboardsController, DashboardCardController} from './dashboards.controller';
  38 +import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
39 39 import DashboardController from './dashboard.controller';
40 40 import DeviceAliasesController from './device-aliases.controller';
41 41 import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
... ... @@ -69,6 +69,7 @@ export default angular.module('thingsboard.dashboard', [
69 69 .config(DashboardRoutes)
70 70 .controller('DashboardsController', DashboardsController)
71 71 .controller('DashboardCardController', DashboardCardController)
  72 + .controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController)
72 73 .controller('DashboardController', DashboardController)
73 74 .controller('DeviceAliasesController', DeviceAliasesController)
74 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 52 fetchMoreItems_: function () {
53 53 if (vm.devices.hasNext && !vm.devices.pending) {
54 54 vm.devices.pending = true;
55   - deviceService.getTenantDevices(vm.devices.nextPageLink).then(
  55 + deviceService.getTenantDevices(vm.devices.nextPageLink, false).then(
56 56 function success(devices) {
57 57 vm.devices.data = vm.devices.data.concat(devices.data);
58 58 vm.devices.nextPageLink = devices.nextPageLink;
... ...
... ... @@ -15,4 +15,5 @@
15 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 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 21 <md-button ng-click="onAssignToCustomer({event: $event})"
19 22 ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer"
20 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 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 27 <md-button ng-click="onManageCredentials({event: $event})"
25 28 ng-show="!isEdit"
26 29 class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button>
... ... @@ -47,10 +50,14 @@
47 50
48 51 <md-content class="md-padding" layout="column">
49 52 <md-input-container class="md-block"
50   - ng-show="isAssignedToCustomer && deviceScope === 'tenant'">
  53 + ng-show="!isEdit && isAssignedToCustomer && !isPublic && deviceScope === 'tenant'">
51 54 <label translate>device.assignedToCustomer</label>
52 55 <input ng-model="assignedCustomer.title" disabled>
53 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 61 <fieldset ng-disabled="loading || !isEdit">
55 62 <md-input-container class="md-block">
56 63 <label translate>device.name</label>
... ...
... ... @@ -24,7 +24,7 @@ import deviceCredentialsTemplate from './device-credentials.tpl.html';
24 24 /* eslint-enable import/no-unresolved, import/default */
25 25
26 26 /*@ngInject*/
27   -export function DeviceCardController($scope, types, customerService) {
  27 +export function DeviceCardController(types) {
28 28
29 29 var vm = this;
30 30
... ... @@ -32,28 +32,23 @@ export function DeviceCardController($scope, types, customerService) {
32 32
33 33 vm.isAssignedToCustomer = function() {
34 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 36 return true;
37 37 }
38 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 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 53 var customerId = $stateParams.customerId;
59 54
... ... @@ -107,6 +102,7 @@ export function DeviceController(userService, deviceService, customerService, $s
107 102 vm.devicesScope = $state.$current.data.devicesType;
108 103
109 104 vm.assignToCustomer = assignToCustomer;
  105 + vm.makePublic = makePublic;
110 106 vm.unassignFromCustomer = unassignFromCustomer;
111 107 vm.manageCredentials = manageCredentials;
112 108
... ... @@ -123,10 +119,20 @@ export function DeviceController(userService, deviceService, customerService, $s
123 119 vm.devicesScope = 'customer_user';
124 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 133 if (vm.devicesScope === 'tenant') {
128 134 fetchDevicesFunction = function (pageLink) {
129   - return deviceService.getTenantDevices(pageLink);
  135 + return deviceService.getTenantDevices(pageLink, true);
130 136 };
131 137 deleteDeviceFunction = function (deviceId) {
132 138 return deviceService.deleteDevice(deviceId);
... ... @@ -135,6 +141,18 @@ export function DeviceController(userService, deviceService, customerService, $s
135 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 156 deviceActionsList.push(
139 157 {
140 158 onAction: function ($event, item) {
... ... @@ -152,17 +170,29 @@ export function DeviceController(userService, deviceService, customerService, $s
152 170 deviceActionsList.push(
153 171 {
154 172 onAction: function ($event, item) {
155   - unassignFromCustomer($event, item);
  173 + unassignFromCustomer($event, item, false);
156 174 },
157 175 name: function() { return $translate.instant('action.unassign') },
158 176 details: function() { return $translate.instant('device.unassign-from-customer') },
159 177 icon: "assignment_return",
160 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 196 deviceActionsList.push(
167 197 {
168 198 onAction: function ($event, item) {
... ... @@ -213,7 +243,7 @@ export function DeviceController(userService, deviceService, customerService, $s
213 243
214 244 } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
215 245 fetchDevicesFunction = function (pageLink) {
216   - return deviceService.getCustomerDevices(customerId, pageLink);
  246 + return deviceService.getCustomerDevices(customerId, pageLink, true);
217 247 };
218 248 deleteDeviceFunction = function (deviceId) {
219 249 return deviceService.unassignDeviceFromCustomer(deviceId);
... ... @@ -226,16 +256,33 @@ export function DeviceController(userService, deviceService, customerService, $s
226 256 deviceActionsList.push(
227 257 {
228 258 onAction: function ($event, item) {
229   - unassignFromCustomer($event, item);
  259 + unassignFromCustomer($event, item, false);
230 260 },
231 261 name: function() { return $translate.instant('action.unassign') },
232 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 269 deviceActionsList.push(
237 270 {
238 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 286 manageCredentials($event, item);
240 287 },
241 288 name: function() { return $translate.instant('device.credentials') },
... ... @@ -365,7 +412,7 @@ export function DeviceController(userService, deviceService, customerService, $s
365 412 $event.stopPropagation();
366 413 }
367 414 var pageSize = 10;
368   - deviceService.getTenantDevices({limit: pageSize, textSearch: ''}).then(
  415 + deviceService.getTenantDevices({limit: pageSize, textSearch: ''}, false).then(
369 416 function success(_devices) {
370 417 var devices = {
371 418 pageSize: pageSize,
... ... @@ -404,15 +451,27 @@ export function DeviceController(userService, deviceService, customerService, $s
404 451 assignToCustomer($event, deviceIds);
405 452 }
406 453
407   - function unassignFromCustomer($event, device) {
  454 + function unassignFromCustomer($event, device, isPublic) {
408 455 if ($event) {
409 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 470 var confirm = $mdDialog.confirm()
412 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 475 .cancel($translate.instant('action.no'))
417 476 .ok($translate.instant('action.yes'));
418 477 $mdDialog.show(confirm).then(function () {
... ... @@ -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 521 function manageCredentials($event, device) {
445 522 if ($event) {
446 523 $event.stopPropagation();
... ...
... ... @@ -26,6 +26,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
26 26 element.html(template);
27 27
28 28 scope.isAssignedToCustomer = false;
  29 + scope.isPublic = false;
29 30 scope.assignedCustomer = null;
30 31
31 32 scope.deviceCredentials = null;
... ... @@ -41,13 +42,15 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
41 42 }
42 43 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
43 44 scope.isAssignedToCustomer = true;
44   - customerService.getCustomer(scope.device.customerId.id).then(
  45 + customerService.getShortCustomerInfo(scope.device.customerId.id).then(
45 46 function success(customer) {
46 47 scope.assignedCustomer = customer;
  48 + scope.isPublic = customer.isPublic;
47 49 }
48 50 );
49 51 } else {
50 52 scope.isAssignedToCustomer = false;
  53 + scope.isPublic = false;
51 54 scope.assignedCustomer = null;
52 55 }
53 56 }
... ... @@ -72,6 +75,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
72 75 deviceScope: '=',
73 76 theForm: '=',
74 77 onAssignToCustomer: '&',
  78 + onMakePublic: '&',
75 79 onUnassignFromCustomer: '&',
76 80 onManageCredentials: '&',
77 81 onDeleteDevice: '&'
... ...
... ... @@ -61,7 +61,7 @@ export default function DeviceRoutes($stateProvider) {
61 61 pageTitle: 'customer.devices'
62 62 },
63 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 27 device-scope="vm.devicesScope"
28 28 the-form="vm.grid.detailsForm"
29 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 32 on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
32 33 on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
33 34 </md-tab>
... ...