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 | ... | ... |
application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java
0 → 100644
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 | } | ... | ... |
application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java
0 → 100644
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; | ... | ... |
ui/src/app/api/subscription.js
0 → 100644
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> | ... | ... |