Commit 71c4e0a48c3618644a17afaec3486c5ed30e5a9b
Committed by
GitHub
Merge pull request #135 from thingsboard/master
Merge 1.2.3 to release branch
Showing
83 changed files
with
2236 additions
and
388 deletions
Too many changes to show.
To preserve performance only 83 of 130 files are displayed.
... | ... | @@ -10,16 +10,16 @@ Thingsboard is an open-source IoT platform for data collection, processing, visu |
10 | 10 | |
11 | 11 | Thingsboard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs). |
12 | 12 | |
13 | -## Sample Dashboards | |
13 | +## IoT use cases | |
14 | 14 | |
15 | -[**Smart energy monitoring**](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github) | |
16 | -[](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github) | |
15 | +[**Smart energy**](https://thingsboard.io/smart-energy/) | |
16 | +[](https://thingsboard.io/smart-energy/) | |
17 | 17 | |
18 | -[**Silos monitoring**](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github) | |
19 | -[](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github) | |
18 | +[**Smart farming**](https://thingsboard.io/smart-farming/) | |
19 | +[](https://thingsboard.io/smart-farming/) | |
20 | 20 | |
21 | -[**Smart bus tracking**](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github) | |
22 | -[](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github) | |
21 | +[**Fleet tracking**](https://thingsboard.io/fleet-tracking/) | |
22 | +[](https://thingsboard.io/fleet-tracking/) | |
23 | 23 | |
24 | 24 | ## Getting Started |
25 | 25 | ... | ... |
... | ... | @@ -51,13 +51,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody; |
51 | 51 | import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg; |
52 | 52 | import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg; |
53 | 53 | |
54 | -import java.util.HashMap; | |
55 | -import java.util.HashSet; | |
56 | -import java.util.List; | |
57 | -import java.util.Map; | |
58 | -import java.util.Optional; | |
59 | -import java.util.Set; | |
60 | -import java.util.UUID; | |
54 | +import java.util.*; | |
61 | 55 | import java.util.concurrent.ExecutionException; |
62 | 56 | import java.util.concurrent.TimeoutException; |
63 | 57 | import java.util.function.Consumer; |
... | ... | @@ -205,25 +199,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso |
205 | 199 | |
206 | 200 | void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) { |
207 | 201 | refreshAttributes(msg); |
208 | - Set<AttributeKey> keys = msg.getDeletedKeys(); | |
209 | 202 | if (attributeSubscriptions.size() > 0) { |
210 | 203 | ToDeviceMsg notification = null; |
211 | 204 | if (msg.isDeleted()) { |
212 | - List<AttributeKey> sharedKeys = keys.stream() | |
205 | + List<AttributeKey> sharedKeys = msg.getDeletedKeys().stream() | |
213 | 206 | .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) |
214 | 207 | .collect(Collectors.toList()); |
215 | 208 | notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys)); |
216 | 209 | } else { |
217 | - List<AttributeKvEntry> attributes = keys.stream() | |
218 | - .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) | |
219 | - .map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey())) | |
220 | - .filter(Optional::isPresent) | |
221 | - .map(Optional::get) | |
222 | - .collect(Collectors.toList()); | |
223 | - if (attributes.size() > 0) { | |
224 | - notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes)); | |
225 | - } else { | |
226 | - logger.debug("[{}] No public server side attributes changed!", deviceId); | |
210 | + if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) { | |
211 | + List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues()); | |
212 | + if (attributes.size() > 0) { | |
213 | + notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes)); | |
214 | + } else { | |
215 | + logger.debug("[{}] No public server side attributes changed!", deviceId); | |
216 | + } | |
227 | 217 | } |
228 | 218 | } |
229 | 219 | if (notification != null) { | ... | ... |
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.config; | |
17 | + | |
18 | +import org.springframework.boot.context.properties.ConfigurationProperties; | |
19 | +import org.springframework.context.annotation.Configuration; | |
20 | +import org.springframework.web.cors.CorsConfiguration; | |
21 | + | |
22 | +import java.util.HashMap; | |
23 | +import java.util.Map; | |
24 | + | |
25 | +/** | |
26 | + * Created by yyh on 2017/5/2. | |
27 | + * CORS configuration | |
28 | + */ | |
29 | +@Configuration | |
30 | +@ConfigurationProperties(prefix = "spring.mvc.cors") | |
31 | +public class MvcCorsProperties { | |
32 | + | |
33 | + private Map<String, CorsConfiguration> mappings = new HashMap<>(); | |
34 | + | |
35 | + public MvcCorsProperties() { | |
36 | + } | |
37 | + | |
38 | + public Map<String, CorsConfiguration> getMappings() { | |
39 | + return mappings; | |
40 | + } | |
41 | + | |
42 | + public void setMappings(Map<String, CorsConfiguration> mappings) { | |
43 | + this.mappings = mappings; | |
44 | + } | |
45 | +} | ... | ... |
... | ... | @@ -18,7 +18,9 @@ package org.thingsboard.server.config; |
18 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; |
19 | 19 | import org.springframework.beans.factory.annotation.Autowired; |
20 | 20 | import org.springframework.beans.factory.annotation.Qualifier; |
21 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | |
21 | 22 | import org.springframework.boot.autoconfigure.security.SecurityProperties; |
23 | +import org.springframework.boot.context.properties.EnableConfigurationProperties; | |
22 | 24 | import org.springframework.context.annotation.Bean; |
23 | 25 | import org.springframework.context.annotation.Configuration; |
24 | 26 | import org.springframework.core.annotation.Order; |
... | ... | @@ -34,11 +36,15 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand |
34 | 36 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
35 | 37 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
36 | 38 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
39 | +import org.springframework.web.cors.CorsUtils; | |
40 | +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | |
41 | +import org.springframework.web.filter.CorsFilter; | |
37 | 42 | import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; |
38 | 43 | import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; |
39 | 44 | import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; |
40 | 45 | import org.thingsboard.server.service.security.auth.jwt.*; |
41 | 46 | import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; |
47 | +import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; | |
42 | 48 | |
43 | 49 | import java.util.ArrayList; |
44 | 50 | import java.util.Arrays; |
... | ... | @@ -56,6 +62,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt |
56 | 62 | public static final String WEBJARS_ENTRY_POINT = "/webjars/**"; |
57 | 63 | public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; |
58 | 64 | public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; |
65 | + public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; | |
59 | 66 | public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; |
60 | 67 | public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; |
61 | 68 | public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; |
... | ... | @@ -88,9 +95,17 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt |
88 | 95 | } |
89 | 96 | |
90 | 97 | @Bean |
98 | + protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception { | |
99 | + RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper); | |
100 | + filter.setAuthenticationManager(this.authenticationManager); | |
101 | + return filter; | |
102 | + } | |
103 | + | |
104 | + @Bean | |
91 | 105 | protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { |
92 | 106 | 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)); | |
107 | + pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, | |
108 | + PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); | |
94 | 109 | SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); |
95 | 110 | JwtTokenAuthenticationProcessingFilter filter |
96 | 111 | = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); |
... | ... | @@ -136,6 +151,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt |
136 | 151 | protected void configure(HttpSecurity http) throws Exception { |
137 | 152 | http.headers().cacheControl().disable().frameOptions().disable() |
138 | 153 | .and() |
154 | + .cors() | |
155 | + .and() | |
139 | 156 | .csrf().disable() |
140 | 157 | .exceptionHandling() |
141 | 158 | .and() |
... | ... | @@ -146,6 +163,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt |
146 | 163 | .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars |
147 | 164 | .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API |
148 | 165 | .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point |
166 | + .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point | |
149 | 167 | .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point |
150 | 168 | .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points |
151 | 169 | .and() |
... | ... | @@ -156,8 +174,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt |
156 | 174 | .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler) |
157 | 175 | .and() |
158 | 176 | .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) |
177 | + .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) | |
159 | 178 | .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) |
160 | 179 | .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) |
161 | 180 | .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); |
162 | 181 | } |
182 | + | |
183 | + | |
184 | + @Bean | |
185 | + @ConditionalOnMissingBean(CorsFilter.class) | |
186 | + public CorsFilter corsFilter(@Autowired MvcCorsProperties mvcCorsProperties) { | |
187 | + if (mvcCorsProperties.getMappings().size() == 0) { | |
188 | + return new CorsFilter(new UrlBasedCorsConfigurationSource()); | |
189 | + } else { | |
190 | + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | |
191 | + source.setCorsConfigurations(mvcCorsProperties.getMappings()); | |
192 | + return new CorsFilter(source); | |
193 | + } | |
194 | + } | |
163 | 195 | } | ... | ... |
... | ... | @@ -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.*; |
... | ... | @@ -42,6 +45,28 @@ public class CustomerController extends BaseController { |
42 | 45 | } |
43 | 46 | } |
44 | 47 | |
48 | + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") | |
49 | + @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET) | |
50 | + @ResponseBody | |
51 | + public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException { | |
52 | + checkParameter("customerId", strCustomerId); | |
53 | + try { | |
54 | + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); | |
55 | + Customer customer = checkCustomerId(customerId); | |
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; | |
65 | + } catch (Exception e) { | |
66 | + throw handleException(e); | |
67 | + } | |
68 | + } | |
69 | + | |
45 | 70 | @PreAuthorize("hasAuthority('TENANT_ADMIN')") |
46 | 71 | @RequestMapping(value = "/customer", method = RequestMethod.POST) |
47 | 72 | @ResponseBody | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -188,3 +188,25 @@ cache: |
188 | 188 | updates: |
189 | 189 | # Enable/disable updates checking. |
190 | 190 | enabled: "${UPDATES_ENABLED:true}" |
191 | + | |
192 | + # spring CORS configuration | |
193 | +spring.mvc.cors: | |
194 | + mappings: | |
195 | + # Intercept path | |
196 | + "/api/auth/**": | |
197 | + #Comma-separated list of origins to allow. '*' allows all origins. When not set,CORS support is disabled. | |
198 | + allowed-origins: "*" | |
199 | + #Comma-separated list of methods to allow. '*' allows all methods. | |
200 | + allowed-methods: "POST,GET,OPTIONS" | |
201 | + #Comma-separated list of headers to allow in a request. '*' allows all headers. | |
202 | + allowed-headers: "*" | |
203 | + #How long, in seconds, the response from a pre-flight request can be cached by clients. | |
204 | + max-age: "1800" | |
205 | + #Set whether credentials are supported. When not set, credentials are not supported. | |
206 | + allow-credentials: "true" | |
207 | + "/api/v1/**": | |
208 | + allowed-origins: "*" | |
209 | + allowed-methods: "*" | |
210 | + allowed-headers: "*" | |
211 | + max-age: "1800" | |
212 | + allow-credentials: "true" | ... | ... |
... | ... | @@ -2,9 +2,6 @@ |
2 | 2 | |
3 | 3 | setlocal ENABLEEXTENSIONS |
4 | 4 | |
5 | -IF %PROCESSOR_ARCHITECTURE%==AMD64 GOTO CHECK_JAVA_64 | |
6 | -IF %PROCESSOR_ARCHITECTURE%==x86 GOTO CHECK_JAVA_32 | |
7 | - | |
8 | 5 | @ECHO Detecting Java version installed. |
9 | 6 | :CHECK_JAVA_64 |
10 | 7 | @ECHO Detecting if it is 64 bit machine |
... | ... | @@ -36,7 +33,6 @@ if defined ValueName ( |
36 | 33 | ) |
37 | 34 | |
38 | 35 | IF NOT "%JRE_PATH2%" == "" GOTO JAVA_INSTALLED |
39 | -IF "%JRE_PATH2%" == "" GOTO JAVA_NOT_INSTALLED | |
40 | 36 | |
41 | 37 | :CHECK_JAVA_32 |
42 | 38 | @ECHO Detecting if it is 32 bit machine | ... | ... |
... | ... | @@ -288,7 +288,7 @@ public class CustomerControllerTest extends AbstractControllerTest { |
288 | 288 | for (int i=0;i<143;i++) { |
289 | 289 | Customer customer = new Customer(); |
290 | 290 | customer.setTenantId(tenantId); |
291 | - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); | |
291 | + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); | |
292 | 292 | String title = title1+suffix; |
293 | 293 | title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); |
294 | 294 | customer.setTitle(title); |
... | ... | @@ -299,7 +299,7 @@ public class CustomerControllerTest extends AbstractControllerTest { |
299 | 299 | for (int i=0;i<175;i++) { |
300 | 300 | Customer customer = new Customer(); |
301 | 301 | customer.setTenantId(tenantId); |
302 | - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); | |
302 | + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); | |
303 | 303 | String title = title2+suffix; |
304 | 304 | title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); |
305 | 305 | customer.setTitle(title); | ... | ... |
... | ... | @@ -149,7 +149,7 @@ public class TenantControllerTest extends AbstractControllerTest { |
149 | 149 | List<Tenant> tenantsTitle1 = new ArrayList<>(); |
150 | 150 | for (int i=0;i<134;i++) { |
151 | 151 | Tenant tenant = new Tenant(); |
152 | - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); | |
152 | + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); | |
153 | 153 | String title = title1+suffix; |
154 | 154 | title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); |
155 | 155 | tenant.setTitle(title); |
... | ... | @@ -159,7 +159,7 @@ public class TenantControllerTest extends AbstractControllerTest { |
159 | 159 | List<Tenant> tenantsTitle2 = new ArrayList<>(); |
160 | 160 | for (int i=0;i<127;i++) { |
161 | 161 | Tenant tenant = new Tenant(); |
162 | - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); | |
162 | + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); | |
163 | 163 | String title = title2+suffix; |
164 | 164 | title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); |
165 | 165 | tenant.setTitle(title); | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>common</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard.common</groupId> | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>common</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard.common</groupId> | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>common</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard.common</groupId> | ... | ... |
... | ... | @@ -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" ) | ... | ... |
... | ... | @@ -34,11 +34,11 @@ import org.junit.Test; |
34 | 34 | import com.datastax.driver.core.utils.UUIDs; |
35 | 35 | |
36 | 36 | public class CustomerServiceImplTest extends AbstractServiceTest { |
37 | - | |
37 | + | |
38 | 38 | private IdComparator<Customer> idComparator = new IdComparator<>(); |
39 | - | |
39 | + | |
40 | 40 | private TenantId tenantId; |
41 | - | |
41 | + | |
42 | 42 | @Before |
43 | 43 | public void before() { |
44 | 44 | Tenant tenant = new Tenant(); |
... | ... | @@ -59,23 +59,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
59 | 59 | customer.setTenantId(tenantId); |
60 | 60 | customer.setTitle("My customer"); |
61 | 61 | Customer savedCustomer = customerService.saveCustomer(customer); |
62 | - | |
62 | + | |
63 | 63 | Assert.assertNotNull(savedCustomer); |
64 | 64 | Assert.assertNotNull(savedCustomer.getId()); |
65 | 65 | Assert.assertTrue(savedCustomer.getCreatedTime() > 0); |
66 | 66 | Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId()); |
67 | 67 | Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle()); |
68 | - | |
69 | - | |
68 | + | |
69 | + | |
70 | 70 | savedCustomer.setTitle("My new customer"); |
71 | - | |
71 | + | |
72 | 72 | customerService.saveCustomer(savedCustomer); |
73 | 73 | Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); |
74 | 74 | Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle()); |
75 | - | |
75 | + | |
76 | 76 | customerService.deleteCustomer(savedCustomer.getId()); |
77 | 77 | } |
78 | - | |
78 | + | |
79 | 79 | @Test |
80 | 80 | public void testFindCustomerById() { |
81 | 81 | Customer customer = new Customer(); |
... | ... | @@ -87,21 +87,21 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
87 | 87 | Assert.assertEquals(savedCustomer, foundCustomer); |
88 | 88 | customerService.deleteCustomer(savedCustomer.getId()); |
89 | 89 | } |
90 | - | |
90 | + | |
91 | 91 | @Test(expected = DataValidationException.class) |
92 | 92 | public void testSaveCustomerWithEmptyTitle() { |
93 | 93 | Customer customer = new Customer(); |
94 | 94 | customer.setTenantId(tenantId); |
95 | 95 | customerService.saveCustomer(customer); |
96 | 96 | } |
97 | - | |
97 | + | |
98 | 98 | @Test(expected = DataValidationException.class) |
99 | 99 | public void testSaveCustomerWithEmptyTenant() { |
100 | 100 | Customer customer = new Customer(); |
101 | 101 | customer.setTitle("My customer"); |
102 | 102 | customerService.saveCustomer(customer); |
103 | 103 | } |
104 | - | |
104 | + | |
105 | 105 | @Test(expected = DataValidationException.class) |
106 | 106 | public void testSaveCustomerWithInvalidTenant() { |
107 | 107 | Customer customer = new Customer(); |
... | ... | @@ -109,7 +109,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
109 | 109 | customer.setTenantId(new TenantId(UUIDs.timeBased())); |
110 | 110 | customerService.saveCustomer(customer); |
111 | 111 | } |
112 | - | |
112 | + | |
113 | 113 | @Test(expected = DataValidationException.class) |
114 | 114 | public void testSaveCustomerWithInvalidEmail() { |
115 | 115 | Customer customer = new Customer(); |
... | ... | @@ -118,7 +118,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
118 | 118 | customer.setEmail("invalid@mail"); |
119 | 119 | customerService.saveCustomer(customer); |
120 | 120 | } |
121 | - | |
121 | + | |
122 | 122 | @Test |
123 | 123 | public void testDeleteCustomer() { |
124 | 124 | Customer customer = new Customer(); |
... | ... | @@ -129,23 +129,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
129 | 129 | Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); |
130 | 130 | Assert.assertNull(foundCustomer); |
131 | 131 | } |
132 | - | |
132 | + | |
133 | 133 | @Test |
134 | 134 | public void testFindCustomersByTenantId() { |
135 | 135 | Tenant tenant = new Tenant(); |
136 | 136 | tenant.setTitle("Test tenant"); |
137 | 137 | tenant = tenantService.saveTenant(tenant); |
138 | - | |
138 | + | |
139 | 139 | TenantId tenantId = tenant.getId(); |
140 | - | |
140 | + | |
141 | 141 | List<Customer> customers = new ArrayList<>(); |
142 | - for (int i=0;i<135;i++) { | |
142 | + for (int i = 0; i < 135; i++) { | |
143 | 143 | Customer customer = new Customer(); |
144 | 144 | customer.setTenantId(tenantId); |
145 | - customer.setTitle("Customer"+i); | |
145 | + customer.setTitle("Customer" + i); | |
146 | 146 | customers.add(customerService.saveCustomer(customer)); |
147 | 147 | } |
148 | - | |
148 | + | |
149 | 149 | List<Customer> loadedCustomers = new ArrayList<>(); |
150 | 150 | TextPageLink pageLink = new TextPageLink(23); |
151 | 151 | TextPageData<Customer> pageData = null; |
... | ... | @@ -156,47 +156,47 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
156 | 156 | pageLink = pageData.getNextPageLink(); |
157 | 157 | } |
158 | 158 | } while (pageData.hasNext()); |
159 | - | |
159 | + | |
160 | 160 | Collections.sort(customers, idComparator); |
161 | 161 | Collections.sort(loadedCustomers, idComparator); |
162 | - | |
162 | + | |
163 | 163 | Assert.assertEquals(customers, loadedCustomers); |
164 | - | |
164 | + | |
165 | 165 | customerService.deleteCustomersByTenantId(tenantId); |
166 | 166 | |
167 | 167 | pageLink = new TextPageLink(33); |
168 | 168 | pageData = customerService.findCustomersByTenantId(tenantId, pageLink); |
169 | 169 | Assert.assertFalse(pageData.hasNext()); |
170 | 170 | Assert.assertTrue(pageData.getData().isEmpty()); |
171 | - | |
171 | + | |
172 | 172 | tenantService.deleteTenant(tenantId); |
173 | 173 | } |
174 | - | |
174 | + | |
175 | 175 | @Test |
176 | 176 | public void testFindCustomersByTenantIdAndTitle() { |
177 | 177 | String title1 = "Customer title 1"; |
178 | 178 | List<Customer> customersTitle1 = new ArrayList<>(); |
179 | - for (int i=0;i<143;i++) { | |
179 | + for (int i = 0; i < 143; i++) { | |
180 | 180 | Customer customer = new Customer(); |
181 | 181 | customer.setTenantId(tenantId); |
182 | - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); | |
183 | - String title = title1+suffix; | |
182 | + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); | |
183 | + String title = title1 + suffix; | |
184 | 184 | title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); |
185 | 185 | customer.setTitle(title); |
186 | 186 | customersTitle1.add(customerService.saveCustomer(customer)); |
187 | 187 | } |
188 | 188 | String title2 = "Customer title 2"; |
189 | 189 | List<Customer> customersTitle2 = new ArrayList<>(); |
190 | - for (int i=0;i<175;i++) { | |
190 | + for (int i = 0; i < 175; i++) { | |
191 | 191 | Customer customer = new Customer(); |
192 | 192 | customer.setTenantId(tenantId); |
193 | - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); | |
194 | - String title = title2+suffix; | |
193 | + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); | |
194 | + String title = title2 + suffix; | |
195 | 195 | title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); |
196 | 196 | customer.setTitle(title); |
197 | 197 | customersTitle2.add(customerService.saveCustomer(customer)); |
198 | 198 | } |
199 | - | |
199 | + | |
200 | 200 | List<Customer> loadedCustomersTitle1 = new ArrayList<>(); |
201 | 201 | TextPageLink pageLink = new TextPageLink(15, title1); |
202 | 202 | TextPageData<Customer> pageData = null; |
... | ... | @@ -207,12 +207,12 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
207 | 207 | pageLink = pageData.getNextPageLink(); |
208 | 208 | } |
209 | 209 | } while (pageData.hasNext()); |
210 | - | |
210 | + | |
211 | 211 | Collections.sort(customersTitle1, idComparator); |
212 | 212 | Collections.sort(loadedCustomersTitle1, idComparator); |
213 | - | |
213 | + | |
214 | 214 | Assert.assertEquals(customersTitle1, loadedCustomersTitle1); |
215 | - | |
215 | + | |
216 | 216 | List<Customer> loadedCustomersTitle2 = new ArrayList<>(); |
217 | 217 | pageLink = new TextPageLink(4, title2); |
218 | 218 | do { |
... | ... | @@ -225,22 +225,22 @@ public class CustomerServiceImplTest extends AbstractServiceTest { |
225 | 225 | |
226 | 226 | Collections.sort(customersTitle2, idComparator); |
227 | 227 | Collections.sort(loadedCustomersTitle2, idComparator); |
228 | - | |
228 | + | |
229 | 229 | Assert.assertEquals(customersTitle2, loadedCustomersTitle2); |
230 | 230 | |
231 | 231 | for (Customer customer : loadedCustomersTitle1) { |
232 | 232 | customerService.deleteCustomer(customer.getId()); |
233 | 233 | } |
234 | - | |
234 | + | |
235 | 235 | pageLink = new TextPageLink(4, title1); |
236 | 236 | pageData = customerService.findCustomersByTenantId(tenantId, pageLink); |
237 | 237 | Assert.assertFalse(pageData.hasNext()); |
238 | 238 | Assert.assertEquals(0, pageData.getData().size()); |
239 | - | |
239 | + | |
240 | 240 | for (Customer customer : loadedCustomersTitle2) { |
241 | 241 | customerService.deleteCustomer(customer.getId()); |
242 | 242 | } |
243 | - | |
243 | + | |
244 | 244 | pageLink = new TextPageLink(4, title2); |
245 | 245 | pageData = customerService.findCustomersByTenantId(tenantId, pageLink); |
246 | 246 | Assert.assertFalse(pageData.hasNext()); | ... | ... |
... | ... | @@ -15,7 +15,6 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.extensions.core.plugin.telemetry; |
17 | 17 | |
18 | -import com.sun.javafx.collections.MappingChange; | |
19 | 18 | import lombok.Setter; |
20 | 19 | import lombok.extern.slf4j.Slf4j; |
21 | 20 | import org.thingsboard.server.common.data.DataConstants; |
... | ... | @@ -292,7 +291,6 @@ public class SubscriptionManager { |
292 | 291 | } else { |
293 | 292 | log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress); |
294 | 293 | subscriptionIterator.remove(); |
295 | - | |
296 | 294 | //TODO: onUpdate state of subscription by WsSessionId and other maps. |
297 | 295 | } |
298 | 296 | } | ... | ... |
... | ... | @@ -139,7 +139,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { |
139 | 139 | public void onSuccess(PluginContext ctx, List<TsKvEntry> data) { |
140 | 140 | Map<String, List<TsData>> result = new LinkedHashMap<>(); |
141 | 141 | for (TsKvEntry entry : data) { |
142 | - result.put(entry.getKey(), data.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList())); | |
142 | + List<TsData> vList = result.get(entry.getKey()); | |
143 | + if (vList == null) { | |
144 | + vList = new ArrayList<>(); | |
145 | + result.put(entry.getKey(), vList); | |
146 | + } | |
147 | + vList.add(new TsData(entry.getTs(), entry.getValueAsString())); | |
143 | 148 | } |
144 | 149 | msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK)); |
145 | 150 | } | ... | ... |
... | ... | @@ -30,7 +30,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg; |
30 | 30 | import org.thingsboard.server.extensions.api.rules.RuleException; |
31 | 31 | import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction; |
32 | 32 | |
33 | -import java.time.LocalDateTime; | |
33 | +import java.time.ZonedDateTime; | |
34 | 34 | import java.time.format.DateTimeFormatter; |
35 | 35 | |
36 | 36 | /** |
... | ... | @@ -51,7 +51,7 @@ public class TimePlugin extends AbstractPlugin<TimePluginConfiguration> implemen |
51 | 51 | |
52 | 52 | String reply; |
53 | 53 | if (!StringUtils.isEmpty(format)) { |
54 | - reply = "\"" + formatter.format(LocalDateTime.now()) + "\""; | |
54 | + reply = "\"" + formatter.format(ZonedDateTime.now()) + "\""; | |
55 | 55 | } else { |
56 | 56 | reply = Long.toString(System.currentTimeMillis()); |
57 | 57 | } | ... | ... |
... | ... | @@ -22,7 +22,7 @@ |
22 | 22 | <modelVersion>4.0.0</modelVersion> |
23 | 23 | <parent> |
24 | 24 | <groupId>org.thingsboard</groupId> |
25 | - <version>1.2.2</version> | |
25 | + <version>1.2.3-SNAPSHOT</version> | |
26 | 26 | <artifactId>extensions</artifactId> |
27 | 27 | </parent> |
28 | 28 | <groupId>org.thingsboard.extensions</groupId> | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>extensions</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard.extensions</groupId> | ... | ... |
... | ... | @@ -22,7 +22,7 @@ |
22 | 22 | <modelVersion>4.0.0</modelVersion> |
23 | 23 | <parent> |
24 | 24 | <groupId>org.thingsboard</groupId> |
25 | - <version>1.2.2</version> | |
25 | + <version>1.2.3-SNAPSHOT</version> | |
26 | 26 | <artifactId>extensions</artifactId> |
27 | 27 | </parent> |
28 | 28 | <groupId>org.thingsboard.extensions</groupId> | ... | ... |
... | ... | @@ -55,6 +55,13 @@ public class RestApiCallPlugin extends AbstractPlugin<RestApiCallPluginConfigura |
55 | 55 | this.headers.add(AUTHORIZATION_HEADER_NAME, String.format(AUTHORIZATION_HEADER_FORMAT, new String(token))); |
56 | 56 | } |
57 | 57 | |
58 | + if (configuration.getHeaders() != null) { | |
59 | + configuration.getHeaders().forEach(h -> { | |
60 | + log.debug("Adding header to request object. Key = {}, Value = {}", h.getKey(), h.getValue()); | |
61 | + this.headers.add(h.getKey(), h.getValue()); | |
62 | + }); | |
63 | + } | |
64 | + | |
58 | 65 | init(); |
59 | 66 | } |
60 | 67 | ... | ... |
... | ... | @@ -16,6 +16,9 @@ |
16 | 16 | package org.thingsboard.server.extensions.rest.plugin; |
17 | 17 | |
18 | 18 | import lombok.Data; |
19 | +import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties; | |
20 | + | |
21 | +import java.util.List; | |
19 | 22 | |
20 | 23 | @Data |
21 | 24 | public class RestApiCallPluginConfiguration { |
... | ... | @@ -27,4 +30,6 @@ public class RestApiCallPluginConfiguration { |
27 | 30 | |
28 | 31 | private String userName; |
29 | 32 | private String password; |
33 | + | |
34 | + private List<KeyValuePluginProperties> headers; | |
30 | 35 | } | ... | ... |
... | ... | @@ -30,6 +30,24 @@ |
30 | 30 | "password": { |
31 | 31 | "title": "Password", |
32 | 32 | "type": "string" |
33 | + }, | |
34 | + "headers": { | |
35 | + "title": "Request Headers", | |
36 | + "type": "array", | |
37 | + "items": { | |
38 | + "title": "Request Header", | |
39 | + "type": "object", | |
40 | + "properties": { | |
41 | + "key": { | |
42 | + "title": "Key", | |
43 | + "type": "string" | |
44 | + }, | |
45 | + "value": { | |
46 | + "title": "Value", | |
47 | + "type": "string" | |
48 | + } | |
49 | + } | |
50 | + } | |
33 | 51 | } |
34 | 52 | }, |
35 | 53 | "required": [ |
... | ... | @@ -62,6 +80,7 @@ |
62 | 80 | { |
63 | 81 | "key": "password", |
64 | 82 | "type": "password" |
65 | - } | |
83 | + }, | |
84 | + "headers" | |
66 | 85 | ] |
67 | 86 | } |
\ No newline at end of file | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>thingsboard</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard</groupId> |
... | ... | @@ -54,40 +54,4 @@ |
54 | 54 | </dependency> |
55 | 55 | </dependencies> |
56 | 56 | |
57 | - <build> | |
58 | - <plugins> | |
59 | - <plugin> | |
60 | - <groupId>org.apache.maven.plugins</groupId> | |
61 | - <artifactId>maven-shade-plugin</artifactId> | |
62 | - <executions> | |
63 | - <execution> | |
64 | - <phase>package</phase> | |
65 | - <goals> | |
66 | - <goal>shade</goal> | |
67 | - </goals> | |
68 | - <configuration> | |
69 | - <filters> | |
70 | - <filter> | |
71 | - <artifact>*:*</artifact> | |
72 | - <excludes> | |
73 | - <exclude>META-INF/*.SF</exclude> | |
74 | - <exclude>META-INF/*.DSA</exclude> | |
75 | - <exclude>META-INF/*.RSA</exclude> | |
76 | - </excludes> | |
77 | - </filter> | |
78 | - </filters> | |
79 | - <transformers> | |
80 | - <transformer | |
81 | - implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> | |
82 | - <manifestEntries> | |
83 | - <Main-Class>org.thingsboard.client.tools.MqttStressTestTool</Main-Class> | |
84 | - </manifestEntries> | |
85 | - </transformer> | |
86 | - </transformers> | |
87 | - </configuration> | |
88 | - </execution> | |
89 | - </executions> | |
90 | - </plugin> | |
91 | - </plugins> | |
92 | - </build> | |
93 | 57 | </project> | ... | ... |
... | ... | @@ -27,6 +27,7 @@ import org.springframework.http.client.support.HttpRequestWrapper; |
27 | 27 | import org.springframework.web.client.HttpClientErrorException; |
28 | 28 | import org.springframework.web.client.RestTemplate; |
29 | 29 | import org.thingsboard.server.common.data.Device; |
30 | +import org.thingsboard.server.common.data.id.CustomerId; | |
30 | 31 | import org.thingsboard.server.common.data.id.DeviceId; |
31 | 32 | import org.thingsboard.server.common.data.security.DeviceCredentials; |
32 | 33 | |
... | ... | @@ -76,6 +77,12 @@ public class RestClient implements ClientHttpRequestInterceptor { |
76 | 77 | return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); |
77 | 78 | } |
78 | 79 | |
80 | + | |
81 | + public Device assignDevice(CustomerId customerId, DeviceId deviceId) { | |
82 | + return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class, | |
83 | + customerId.toString(), deviceId.toString()).getBody(); | |
84 | + } | |
85 | + | |
79 | 86 | public DeviceCredentials getCredentials(DeviceId id) { |
80 | 87 | return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody(); |
81 | 88 | } | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>transport</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard.transport</groupId> | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>transport</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard.transport</groupId> | ... | ... |
... | ... | @@ -20,7 +20,7 @@ |
20 | 20 | <modelVersion>4.0.0</modelVersion> |
21 | 21 | <parent> |
22 | 22 | <groupId>org.thingsboard</groupId> |
23 | - <version>1.2.2</version> | |
23 | + <version>1.2.3-SNAPSHOT</version> | |
24 | 24 | <artifactId>transport</artifactId> |
25 | 25 | </parent> |
26 | 26 | <groupId>org.thingsboard.transport</groupId> | ... | ... |
... | ... | @@ -247,7 +247,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
247 | 247 | |
248 | 248 | private MqttMessage createUnSubAckMessage(int msgId) { |
249 | 249 | MqttFixedHeader mqttFixedHeader = |
250 | - new MqttFixedHeader(SUBACK, false, AT_LEAST_ONCE, false, 0); | |
250 | + new MqttFixedHeader(UNSUBACK, false, AT_LEAST_ONCE, false, 0); | |
251 | 251 | MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); |
252 | 252 | return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); |
253 | 253 | } | ... | ... |
... | ... | @@ -40,6 +40,8 @@ import java.security.cert.CertificateException; |
40 | 40 | */ |
41 | 41 | public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> { |
42 | 42 | |
43 | + private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024; | |
44 | + | |
43 | 45 | private final SessionMsgProcessor processor; |
44 | 46 | private final DeviceService deviceService; |
45 | 47 | private final DeviceAuthService authService; |
... | ... | @@ -63,7 +65,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha |
63 | 65 | sslHandler = sslHandlerProvider.getSslHandler(); |
64 | 66 | pipeline.addLast(sslHandler); |
65 | 67 | } |
66 | - pipeline.addLast("decoder", new MqttDecoder()); | |
68 | + pipeline.addLast("decoder", new MqttDecoder(MAX_PAYLOAD_SIZE)); | |
67 | 69 | pipeline.addLast("encoder", MqttEncoder.INSTANCE); |
68 | 70 | |
69 | 71 | MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, adaptor, sslHandler); | ... | ... |
... | ... | @@ -134,7 +134,7 @@ public class GatewaySessionCtx { |
134 | 134 | JsonObject jsonObj = json.getAsJsonObject(); |
135 | 135 | String deviceName = checkDeviceConnected(jsonObj.get("device").getAsString()); |
136 | 136 | Integer requestId = jsonObj.get("id").getAsInt(); |
137 | - String data = jsonObj.get("data").getAsString(); | |
137 | + String data = jsonObj.get("data").toString(); | |
138 | 138 | GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); |
139 | 139 | processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), |
140 | 140 | new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data)))); | ... | ... |
1 | 1 | { |
2 | 2 | "name": "thingsboard", |
3 | 3 | "private": true, |
4 | - "version": "1.2.2", | |
4 | + "version": "1.2.3", | |
5 | 5 | "description": "Thingsboard UI", |
6 | 6 | "licenses": [ |
7 | 7 | { |
... | ... | @@ -33,6 +33,7 @@ |
33 | 33 | "angular-messages": "1.5.8", |
34 | 34 | "angular-route": "1.5.8", |
35 | 35 | "angular-sanitize": "1.5.8", |
36 | + "angular-socialshare": "^2.3.8", | |
36 | 37 | "angular-storage": "0.0.15", |
37 | 38 | "angular-touch": "1.5.8", |
38 | 39 | "angular-translate": "2.13.1", |
... | ... | @@ -49,6 +50,7 @@ |
49 | 50 | "clipboard": "^1.5.15", |
50 | 51 | "compass-sass-mixins": "^0.12.7", |
51 | 52 | "flot": "git://github.com/flot/flot.git#0.9-work", |
53 | + "flot-curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", | |
52 | 54 | "font-awesome": "^4.6.3", |
53 | 55 | "javascript-detect-element-resize": "^0.5.3", |
54 | 56 | "jquery": "^3.1.0", | ... | ... |
... | ... | @@ -18,11 +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 | + getShortCustomerInfo: getShortCustomerInfo, | |
27 | + applyAssignedCustomersInfo: applyAssignedCustomersInfo, | |
28 | + applyAssignedCustomerInfo: applyAssignedCustomerInfo, | |
26 | 29 | deleteCustomer: deleteCustomer, |
27 | 30 | saveCustomer: saveCustomer |
28 | 31 | } |
... | ... | @@ -60,6 +63,88 @@ function CustomerService($http, $q) { |
60 | 63 | return deferred.promise; |
61 | 64 | } |
62 | 65 | |
66 | + function getShortCustomerInfo(customerId) { | |
67 | + var deferred = $q.defer(); | |
68 | + var url = '/api/customer/' + customerId + '/shortInfo'; | |
69 | + $http.get(url, null).then(function success(response) { | |
70 | + deferred.resolve(response.data); | |
71 | + }, function fail(response) { | |
72 | + deferred.reject(response.data); | |
73 | + }); | |
74 | + return deferred.promise; | |
75 | + } | |
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 | + | |
63 | 148 | function saveCustomer(customer) { |
64 | 149 | var deferred = $q.defer(); |
65 | 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) { | |
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)) { |
... | ... | @@ -63,15 +65,27 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic |
63 | 65 | if (angular.isDefined(pageLink.textOffset)) { |
64 | 66 | url += '&textOffset=' + pageLink.textOffset; |
65 | 67 | } |
66 | - $http.get(url, null).then(function success(response) { | |
67 | - deferred.resolve(response.data); | |
68 | + $http.get(url, config).then(function success(response) { | |
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 | } |
... | ... | @@ -425,7 +467,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic |
425 | 467 | } |
426 | 468 | } |
427 | 469 | |
428 | - function getDeviceAttributes(deviceId, attributeScope, query, successCallback) { | |
470 | + function getDeviceAttributes(deviceId, attributeScope, query, successCallback, config) { | |
429 | 471 | var deferred = $q.defer(); |
430 | 472 | var subscriptionId = deviceId + attributeScope; |
431 | 473 | var das = deviceAttributesSubscriptionMap[subscriptionId]; |
... | ... | @@ -445,7 +487,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic |
445 | 487 | } |
446 | 488 | } else { |
447 | 489 | var url = '/api/plugins/telemetry/' + deviceId + '/values/attributes/' + attributeScope; |
448 | - $http.get(url, null).then(function success(response) { | |
490 | + $http.get(url, config).then(function success(response) { | |
449 | 491 | processDeviceAttributes(response.data, query, deferred, successCallback); |
450 | 492 | }, function fail() { |
451 | 493 | deferred.reject(); | ... | ... |
... | ... | @@ -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 | + this.startWatchingTimewindow(); | |
173 | + } | |
174 | + } | |
175 | + | |
176 | + registration = this.ctx.$scope.$on('deviceAliasListChanged', function () { | |
177 | + subscription.checkSubscriptions(); | |
178 | + }); | |
179 | + | |
180 | + this.registrations.push(registration); | |
181 | + } | |
182 | + | |
183 | + startWatchingTimewindow() { | |
184 | + var subscription = this; | |
185 | + this.timeWindowWatchRegistration = this.ctx.$scope.$watch(function () { | |
186 | + return subscription.timeWindowConfig; | |
187 | + }, function (newTimewindow, prevTimewindow) { | |
188 | + if (!angular.equals(newTimewindow, prevTimewindow)) { | |
189 | + subscription.unsubscribe(); | |
190 | + subscription.subscribe(); | |
191 | + } | |
192 | + }, true); | |
193 | + this.registrations.push(this.timeWindowWatchRegistration); | |
194 | + } | |
195 | + | |
196 | + stopWatchingTimewindow() { | |
197 | + if (this.timeWindowWatchRegistration) { | |
198 | + this.timeWindowWatchRegistration(); | |
199 | + var index = this.registrations.indexOf(this.timeWindowWatchRegistration); | |
200 | + if (index > -1) { | |
201 | + this.registrations.splice(index, 1); | |
202 | + } | |
203 | + } | |
204 | + } | |
205 | + | |
206 | + initRpc() { | |
207 | + | |
208 | + if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { | |
209 | + this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; | |
210 | + if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) { | |
211 | + this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId; | |
212 | + } | |
213 | + var subscription = this; | |
214 | + var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () { | |
215 | + var deviceId = null; | |
216 | + if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) { | |
217 | + deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId; | |
218 | + } | |
219 | + if (!angular.equals(deviceId, subscription.targetDeviceId)) { | |
220 | + subscription.targetDeviceId = deviceId; | |
221 | + if (subscription.targetDeviceId) { | |
222 | + subscription.rpcEnabled = true; | |
223 | + } else { | |
224 | + subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false; | |
225 | + } | |
226 | + subscription.callbacks.rpcStateChanged(subscription); | |
227 | + } | |
228 | + }); | |
229 | + this.registrations.push(registration); | |
230 | + } else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) { | |
231 | + this.targetDeviceId = this.targetDeviceIds[0]; | |
232 | + } | |
233 | + | |
234 | + if (this.targetDeviceId) { | |
235 | + this.rpcEnabled = true; | |
236 | + } else { | |
237 | + this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false; | |
238 | + } | |
239 | + this.callbacks.rpcStateChanged(this); | |
240 | + } | |
241 | + | |
242 | + clearRpcError() { | |
243 | + this.rpcRejection = null; | |
244 | + this.rpcErrorText = null; | |
245 | + this.callbacks.onRpcErrorCleared(this); | |
246 | + } | |
247 | + | |
248 | + sendOneWayCommand(method, params, timeout) { | |
249 | + return this.sendCommand(true, method, params, timeout); | |
250 | + } | |
251 | + | |
252 | + sendTwoWayCommand(method, params, timeout) { | |
253 | + return this.sendCommand(false, method, params, timeout); | |
254 | + } | |
255 | + | |
256 | + sendCommand(oneWayElseTwoWay, method, params, timeout) { | |
257 | + if (!this.rpcEnabled) { | |
258 | + return this.ctx.$q.reject(); | |
259 | + } | |
260 | + | |
261 | + if (this.rpcRejection && this.rpcRejection.status !== 408) { | |
262 | + this.rpcRejection = null; | |
263 | + this.rpcErrorText = null; | |
264 | + this.callbacks.onRpcErrorCleared(this); | |
265 | + } | |
266 | + | |
267 | + var subscription = this; | |
268 | + | |
269 | + var requestBody = { | |
270 | + method: method, | |
271 | + params: params | |
272 | + }; | |
273 | + | |
274 | + if (timeout && timeout > 0) { | |
275 | + requestBody.timeout = timeout; | |
276 | + } | |
277 | + | |
278 | + var deferred = this.ctx.$q.defer(); | |
279 | + this.executingRpcRequest = true; | |
280 | + this.callbacks.rpcStateChanged(this); | |
281 | + if (this.ctx.$scope.widgetEditMode) { | |
282 | + this.ctx.$timeout(function() { | |
283 | + subscription.executingRpcRequest = false; | |
284 | + subscription.callbacks.rpcStateChanged(subscription); | |
285 | + if (oneWayElseTwoWay) { | |
286 | + deferred.resolve(); | |
287 | + } else { | |
288 | + deferred.resolve(requestBody); | |
289 | + } | |
290 | + }, 500); | |
291 | + } else { | |
292 | + this.executingPromises.push(deferred.promise); | |
293 | + var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand; | |
294 | + targetSendFunction(this.targetDeviceId, requestBody).then( | |
295 | + function success(responseBody) { | |
296 | + subscription.rpcRejection = null; | |
297 | + subscription.rpcErrorText = null; | |
298 | + var index = subscription.executingPromises.indexOf(deferred.promise); | |
299 | + if (index >= 0) { | |
300 | + subscription.executingPromises.splice( index, 1 ); | |
301 | + } | |
302 | + subscription.executingRpcRequest = subscription.executingPromises.length > 0; | |
303 | + subscription.callbacks.onRpcSuccess(subscription); | |
304 | + deferred.resolve(responseBody); | |
305 | + }, | |
306 | + function fail(rejection) { | |
307 | + var index = subscription.executingPromises.indexOf(deferred.promise); | |
308 | + if (index >= 0) { | |
309 | + subscription.executingPromises.splice( index, 1 ); | |
310 | + } | |
311 | + subscription.executingRpcRequest = subscription.executingPromises.length > 0; | |
312 | + subscription.callbacks.rpcStateChanged(subscription); | |
313 | + if (!subscription.executingRpcRequest || rejection.status === 408) { | |
314 | + subscription.rpcRejection = rejection; | |
315 | + if (rejection.status === 408) { | |
316 | + subscription.rpcErrorText = 'Device is offline.'; | |
317 | + } else { | |
318 | + subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; | |
319 | + if (rejection.data && rejection.data.length > 0) { | |
320 | + subscription.rpcErrorText += '</br>'; | |
321 | + subscription.rpcErrorText += rejection.data; | |
322 | + } | |
323 | + } | |
324 | + subscription.callbacks.onRpcFailed(subscription); | |
325 | + } | |
326 | + deferred.reject(rejection); | |
327 | + } | |
328 | + ); | |
329 | + } | |
330 | + return deferred.promise; | |
331 | + } | |
332 | + | |
333 | + updateDataVisibility(index) { | |
334 | + var hidden = this.legendData.data[index].hidden; | |
335 | + if (hidden) { | |
336 | + this.hiddenData[index].data = this.data[index].data; | |
337 | + this.data[index].data = []; | |
338 | + } else { | |
339 | + this.data[index].data = this.hiddenData[index].data; | |
340 | + this.hiddenData[index].data = []; | |
341 | + } | |
342 | + this.onDataUpdated(); | |
343 | + } | |
344 | + | |
345 | + onDataUpdated(apply) { | |
346 | + if (this.cafs['dataUpdated']) { | |
347 | + this.cafs['dataUpdated'](); | |
348 | + this.cafs['dataUpdated'] = null; | |
349 | + } | |
350 | + var subscription = this; | |
351 | + this.cafs['dataUpdated'] = this.ctx.tbRaf(function() { | |
352 | + try { | |
353 | + subscription.callbacks.onDataUpdated(subscription, apply); | |
354 | + } catch (e) { | |
355 | + subscription.callbacks.onDataUpdateError(subscription, e); | |
356 | + } | |
357 | + }); | |
358 | + if (apply) { | |
359 | + this.ctx.$scope.$digest(); | |
360 | + } | |
361 | + } | |
362 | + | |
363 | + updateTimewindowConfig(newTimewindow) { | |
364 | + this.timeWindowConfig = newTimewindow; | |
365 | + } | |
366 | + | |
367 | + onResetTimewindow() { | |
368 | + if (this.useDashboardTimewindow) { | |
369 | + this.ctx.dashboardTimewindowApi.onResetTimewindow(); | |
370 | + } else { | |
371 | + if (this.originalTimewindow) { | |
372 | + this.stopWatchingTimewindow(); | |
373 | + this.timeWindowConfig = angular.copy(this.originalTimewindow); | |
374 | + this.originalTimewindow = null; | |
375 | + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); | |
376 | + this.unsubscribe(); | |
377 | + this.subscribe(); | |
378 | + this.startWatchingTimewindow(); | |
379 | + } | |
380 | + } | |
381 | + } | |
382 | + | |
383 | + onUpdateTimewindow(startTimeMs, endTimeMs) { | |
384 | + if (this.useDashboardTimewindow) { | |
385 | + this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); | |
386 | + } else { | |
387 | + this.stopWatchingTimewindow(); | |
388 | + if (!this.originalTimewindow) { | |
389 | + this.originalTimewindow = angular.copy(this.timeWindowConfig); | |
390 | + } | |
391 | + this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); | |
392 | + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); | |
393 | + this.unsubscribe(); | |
394 | + this.subscribe(); | |
395 | + this.startWatchingTimewindow(); | |
396 | + } | |
397 | + } | |
398 | + | |
399 | + notifyDataLoading() { | |
400 | + this.loadingData = true; | |
401 | + this.callbacks.dataLoading(this); | |
402 | + } | |
403 | + | |
404 | + notifyDataLoaded() { | |
405 | + this.loadingData = false; | |
406 | + this.callbacks.dataLoading(this); | |
407 | + } | |
408 | + | |
409 | + updateTimewindow() { | |
410 | + this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; | |
411 | + if (this.subscriptionTimewindow.realtimeWindowMs) { | |
412 | + this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff; | |
413 | + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; | |
414 | + } else if (this.subscriptionTimewindow.fixedWindow) { | |
415 | + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; | |
416 | + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; | |
417 | + } | |
418 | + } | |
419 | + | |
420 | + updateRealtimeSubscription(subscriptionTimewindow) { | |
421 | + if (subscriptionTimewindow) { | |
422 | + this.subscriptionTimewindow = subscriptionTimewindow; | |
423 | + } else { | |
424 | + this.subscriptionTimewindow = | |
425 | + this.ctx.timeService.createSubscriptionTimewindow( | |
426 | + this.timeWindowConfig, | |
427 | + this.timeWindow.stDiff); | |
428 | + } | |
429 | + this.updateTimewindow(); | |
430 | + return this.subscriptionTimewindow; | |
431 | + } | |
432 | + | |
433 | + dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { | |
434 | + this.notifyDataLoaded(); | |
435 | + var update = true; | |
436 | + var currentData; | |
437 | + if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) { | |
438 | + currentData = this.hiddenData[datasourceIndex + dataKeyIndex]; | |
439 | + } else { | |
440 | + currentData = this.data[datasourceIndex + dataKeyIndex]; | |
441 | + } | |
442 | + if (this.type === this.ctx.types.widgetType.latest.value) { | |
443 | + var prevData = currentData.data; | |
444 | + if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { | |
445 | + var prevValue = prevData[0][1]; | |
446 | + if (prevValue === sourceData.data[0][1]) { | |
447 | + update = false; | |
448 | + } | |
449 | + } | |
450 | + } | |
451 | + if (update) { | |
452 | + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { | |
453 | + this.updateTimewindow(); | |
454 | + } | |
455 | + currentData.data = sourceData.data; | |
456 | + if (this.caulculateLegendData) { | |
457 | + this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); | |
458 | + } | |
459 | + this.onDataUpdated(apply); | |
460 | + } | |
461 | + } | |
462 | + | |
463 | + updateLegend(dataIndex, data, apply) { | |
464 | + var legendKeyData = this.legendData.data[dataIndex]; | |
465 | + if (this.legendConfig.showMin) { | |
466 | + legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units); | |
467 | + } | |
468 | + if (this.legendConfig.showMax) { | |
469 | + legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units); | |
470 | + } | |
471 | + if (this.legendConfig.showAvg) { | |
472 | + legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units); | |
473 | + } | |
474 | + if (this.legendConfig.showTotal) { | |
475 | + legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units); | |
476 | + } | |
477 | + this.callbacks.legendDataUpdated(this, apply !== false); | |
478 | + } | |
479 | + | |
480 | + subscribe() { | |
481 | + if (this.type === this.ctx.types.widgetType.rpc.value) { | |
482 | + return; | |
483 | + } | |
484 | + this.notifyDataLoading(); | |
485 | + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { | |
486 | + this.updateRealtimeSubscription(); | |
487 | + if (this.subscriptionTimewindow.fixedWindow) { | |
488 | + this.onDataUpdated(); | |
489 | + } | |
490 | + } | |
491 | + var index = 0; | |
492 | + for (var i = 0; i < this.datasources.length; i++) { | |
493 | + var datasource = this.datasources[i]; | |
494 | + if (angular.isFunction(datasource)) | |
495 | + continue; | |
496 | + var deviceId = null; | |
497 | + if (datasource.type === this.ctx.types.datasourceType.device) { | |
498 | + var aliasName = null; | |
499 | + var deviceName = null; | |
500 | + if (datasource.deviceId) { | |
501 | + deviceId = datasource.deviceId; | |
502 | + datasource.name = datasource.deviceName; | |
503 | + aliasName = datasource.deviceName; | |
504 | + deviceName = datasource.deviceName; | |
505 | + } else if (datasource.deviceAliasId && this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId]) { | |
506 | + deviceId = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId; | |
507 | + datasource.name = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; | |
508 | + aliasName = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; | |
509 | + deviceName = ''; | |
510 | + var devicesInfo = this.ctx.aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId]; | |
511 | + for (var d = 0; d < devicesInfo.length; d++) { | |
512 | + if (devicesInfo[d].id === deviceId) { | |
513 | + deviceName = devicesInfo[d].name; | |
514 | + break; | |
515 | + } | |
516 | + } | |
517 | + } | |
518 | + } else { | |
519 | + datasource.name = datasource.name || this.ctx.types.datasourceType.function; | |
520 | + } | |
521 | + for (var dk = 0; dk < datasource.dataKeys.length; dk++) { | |
522 | + updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, deviceName, aliasName); | |
523 | + } | |
524 | + | |
525 | + var subscription = this; | |
526 | + | |
527 | + var listener = { | |
528 | + subscriptionType: this.type, | |
529 | + subscriptionTimewindow: this.subscriptionTimewindow, | |
530 | + datasource: datasource, | |
531 | + deviceId: deviceId, | |
532 | + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { | |
533 | + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); | |
534 | + }, | |
535 | + updateRealtimeSubscription: function () { | |
536 | + this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); | |
537 | + return this.subscriptionTimewindow; | |
538 | + }, | |
539 | + setRealtimeSubscription: function (subscriptionTimewindow) { | |
540 | + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); | |
541 | + }, | |
542 | + datasourceIndex: index | |
543 | + }; | |
544 | + | |
545 | + for (var a = 0; a < datasource.dataKeys.length; a++) { | |
546 | + this.data[index + a].data = []; | |
547 | + } | |
548 | + | |
549 | + index += datasource.dataKeys.length; | |
550 | + | |
551 | + this.datasourceListeners.push(listener); | |
552 | + this.ctx.datasourceService.subscribeToDatasource(listener); | |
553 | + } | |
554 | + } | |
555 | + | |
556 | + unsubscribe() { | |
557 | + if (this.type !== this.ctx.types.widgetType.rpc.value) { | |
558 | + for (var i = 0; i < this.datasourceListeners.length; i++) { | |
559 | + var listener = this.datasourceListeners[i]; | |
560 | + this.ctx.datasourceService.unsubscribeFromDatasource(listener); | |
561 | + } | |
562 | + this.datasourceListeners = []; | |
563 | + } | |
564 | + } | |
565 | + | |
566 | + checkSubscriptions() { | |
567 | + var subscriptionsChanged = false; | |
568 | + for (var i = 0; i < this.datasourceListeners.length; i++) { | |
569 | + var listener = this.datasourceListeners[i]; | |
570 | + var deviceId = null; | |
571 | + var aliasName = null; | |
572 | + if (listener.datasource.type === this.ctx.types.datasourceType.device) { | |
573 | + if (listener.datasource.deviceAliasId && | |
574 | + this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) { | |
575 | + deviceId = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId; | |
576 | + aliasName = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias; | |
577 | + } | |
578 | + if (!angular.equals(deviceId, listener.deviceId) || | |
579 | + !angular.equals(aliasName, listener.datasource.name)) { | |
580 | + subscriptionsChanged = true; | |
581 | + break; | |
582 | + } | |
583 | + } | |
584 | + } | |
585 | + if (subscriptionsChanged) { | |
586 | + this.unsubscribe(); | |
587 | + this.subscribe(); | |
588 | + } | |
589 | + } | |
590 | + | |
591 | + destroy() { | |
592 | + this.unsubscribe(); | |
593 | + for (var cafId in this.cafs) { | |
594 | + if (this.cafs[cafId]) { | |
595 | + this.cafs[cafId](); | |
596 | + this.cafs[cafId] = null; | |
597 | + } | |
598 | + } | |
599 | + this.registrations.forEach(function (registration) { | |
600 | + registration(); | |
601 | + }); | |
602 | + this.registrations = []; | |
603 | + } | |
604 | + | |
605 | +} | |
606 | + | |
607 | +const varsRegex = /\$\{([^\}]*)\}/g; | |
608 | + | |
609 | +function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) { | |
610 | + var pattern = dataKey.pattern; | |
611 | + var label = dataKey.pattern; | |
612 | + var match = varsRegex.exec(pattern); | |
613 | + while (match !== null) { | |
614 | + var variable = match[0]; | |
615 | + var variableName = match[1]; | |
616 | + if (variableName === 'dsName') { | |
617 | + label = label.split(variable).join(dsName); | |
618 | + } else if (variableName === 'deviceName') { | |
619 | + label = label.split(variable).join(deviceName); | |
620 | + } else if (variableName === 'aliasName') { | |
621 | + label = label.split(variable).join(aliasName); | |
622 | + } | |
623 | + match = varsRegex.exec(pattern); | |
624 | + } | |
625 | + dataKey.label = label; | |
626 | +} | |
627 | + | |
628 | +function calculateMin(data) { | |
629 | + if (data.length > 0) { | |
630 | + var result = Number(data[0][1]); | |
631 | + for (var i=1;i<data.length;i++) { | |
632 | + result = Math.min(result, Number(data[i][1])); | |
633 | + } | |
634 | + return result; | |
635 | + } else { | |
636 | + return null; | |
637 | + } | |
638 | +} | |
639 | + | |
640 | +function calculateMax(data) { | |
641 | + if (data.length > 0) { | |
642 | + var result = Number(data[0][1]); | |
643 | + for (var i=1;i<data.length;i++) { | |
644 | + result = Math.max(result, Number(data[i][1])); | |
645 | + } | |
646 | + return result; | |
647 | + } else { | |
648 | + return null; | |
649 | + } | |
650 | +} | |
651 | + | |
652 | +function calculateAvg(data) { | |
653 | + if (data.length > 0) { | |
654 | + return calculateTotal(data)/data.length; | |
655 | + } else { | |
656 | + return null; | |
657 | + } | |
658 | +} | |
659 | + | |
660 | +function calculateTotal(data) { | |
661 | + if (data.length > 0) { | |
662 | + var result = 0; | |
663 | + for (var i = 0; i < data.length; i++) { | |
664 | + result += Number(data[i][1]); | |
665 | + } | |
666 | + return result; | |
667 | + } else { | |
668 | + return null; | |
669 | + } | |
670 | +} | ... | ... |
... | ... | @@ -45,15 +45,21 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty |
45 | 45 | socketCloseTimer, |
46 | 46 | reconnectTimer; |
47 | 47 | |
48 | + var port = location.port; | |
48 | 49 | if (location.protocol === "https:") { |
50 | + if (!port) { | |
51 | + port = "443"; | |
52 | + } | |
49 | 53 | telemetryUri = "wss:"; |
50 | 54 | } else { |
55 | + if (!port) { | |
56 | + port = "80"; | |
57 | + } | |
51 | 58 | telemetryUri = "ws:"; |
52 | 59 | } |
53 | - telemetryUri += "//" + location.hostname + ":" + location.port; | |
60 | + telemetryUri += "//" + location.hostname + ":" + port; | |
54 | 61 | telemetryUri += "/api/ws/plugins/telemetry"; |
55 | 62 | |
56 | - | |
57 | 63 | var service = { |
58 | 64 | subscribe: subscribe, |
59 | 65 | unsubscribe: unsubscribe | ... | ... |
... | ... | @@ -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); | ... | ... |
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | import injectTapEventPlugin from 'react-tap-event-plugin'; |
17 | 17 | import UrlHandler from './url.handler'; |
18 | 18 | import addLocaleKorean from './locale/locale.constant-ko'; |
19 | +import addLocaleChinese from './locale/locale.constant-zh'; | |
19 | 20 | |
20 | 21 | /* eslint-disable import/no-unresolved, import/default */ |
21 | 22 | |
... | ... | @@ -50,11 +51,15 @@ export default function AppConfig($provide, |
50 | 51 | $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); |
51 | 52 | |
52 | 53 | addLocaleKorean(locales); |
54 | + addLocaleChinese(locales); | |
53 | 55 | var $window = angular.injector(['ng']).get('$window'); |
54 | 56 | var lang = $window.navigator.language || $window.navigator.userLanguage; |
55 | 57 | if (lang === 'ko') { |
56 | 58 | $translateProvider.useSanitizeValueStrategy(null); |
57 | 59 | $translateProvider.preferredLanguage('ko_KR'); |
60 | + } else if (lang === 'zh') { | |
61 | + $translateProvider.useSanitizeValueStrategy(null); | |
62 | + $translateProvider.preferredLanguage('zh_CN'); | |
58 | 63 | } |
59 | 64 | |
60 | 65 | for (var langKey in locales) { | ... | ... |
... | ... | @@ -19,6 +19,7 @@ import angular from 'angular'; |
19 | 19 | import ngMaterial from 'angular-material'; |
20 | 20 | import ngMdIcons from 'angular-material-icons'; |
21 | 21 | import ngCookies from 'angular-cookies'; |
22 | +import angularSocialshare from 'angular-socialshare'; | |
22 | 23 | import 'angular-translate'; |
23 | 24 | import 'angular-translate-loader-static-files'; |
24 | 25 | import 'angular-translate-storage-local'; |
... | ... | @@ -82,6 +83,7 @@ angular.module('thingsboard', [ |
82 | 83 | ngMaterial, |
83 | 84 | ngMdIcons, |
84 | 85 | ngCookies, |
86 | + angularSocialshare, | |
85 | 87 | 'pascalprecht.translate', |
86 | 88 | 'mdColorPicker', |
87 | 89 | mdPickers, | ... | ... |
... | ... | @@ -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,10 @@ 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, | |
110 | + isLocalUrl: isLocalUrl | |
108 | 111 | } |
109 | 112 | |
110 | 113 | return service; |
... | ... | @@ -276,4 +279,165 @@ function Utils($mdColorPalette, $rootScope, $window, types) { |
276 | 279 | deferred.resolve(response); |
277 | 280 | } |
278 | 281 | |
282 | + function guid() { | |
283 | + function s4() { | |
284 | + return Math.floor((1 + Math.random()) * 0x10000) | |
285 | + .toString(16) | |
286 | + .substring(1); | |
287 | + } | |
288 | + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + | |
289 | + s4() + '-' + s4() + s4() + s4(); | |
290 | + } | |
291 | + | |
292 | + function genNextColor(datasources) { | |
293 | + var index = 0; | |
294 | + if (datasources) { | |
295 | + for (var i = 0; i < datasources.length; i++) { | |
296 | + var datasource = datasources[i]; | |
297 | + index += datasource.dataKeys.length; | |
298 | + } | |
299 | + } | |
300 | + return getMaterialColor(index); | |
301 | + } | |
302 | + | |
303 | + /*var defaultDataKey = { | |
304 | + name: 'f(x)', | |
305 | + type: types.dataKeyType.function, | |
306 | + label: 'Sin', | |
307 | + color: getMaterialColor(0), | |
308 | + funcBody: getPredefinedFunctionBody('Sin'), | |
309 | + settings: {}, | |
310 | + _hash: Math.random() | |
311 | + }; | |
312 | + | |
313 | + var defaultDatasource = { | |
314 | + type: types.datasourceType.function, | |
315 | + name: types.datasourceType.function, | |
316 | + dataKeys: [angular.copy(defaultDataKey)] | |
317 | + };*/ | |
318 | + | |
319 | + function createKey(keyInfo, type, datasources) { | |
320 | + var dataKey = { | |
321 | + name: keyInfo.name, | |
322 | + type: type, | |
323 | + label: keyInfo.label || keyInfo.name, | |
324 | + color: genNextColor(datasources), | |
325 | + funcBody: keyInfo.funcBody, | |
326 | + settings: {}, | |
327 | + _hash: Math.random() | |
328 | + } | |
329 | + return dataKey; | |
330 | + } | |
331 | + | |
332 | + function createDatasourceKeys(keyInfos, type, datasource, datasources) { | |
333 | + for (var i=0;i<keyInfos.length;i++) { | |
334 | + var keyInfo = keyInfos[i]; | |
335 | + var dataKey = createKey(keyInfo, type, datasources); | |
336 | + datasource.dataKeys.push(dataKey); | |
337 | + } | |
338 | + } | |
339 | + | |
340 | + function createDatasourceFromSubscription(subscriptionInfo, datasources, device) { | |
341 | + var datasource; | |
342 | + if (subscriptionInfo.type === types.datasourceType.device) { | |
343 | + datasource = { | |
344 | + type: subscriptionInfo.type, | |
345 | + deviceName: device.name, | |
346 | + name: device.name, | |
347 | + deviceId: device.id.id, | |
348 | + dataKeys: [] | |
349 | + } | |
350 | + } else if (subscriptionInfo.type === types.datasourceType.function) { | |
351 | + datasource = { | |
352 | + type: subscriptionInfo.type, | |
353 | + name: subscriptionInfo.name || types.datasourceType.function, | |
354 | + dataKeys: [] | |
355 | + } | |
356 | + } | |
357 | + datasources.push(datasource); | |
358 | + if (subscriptionInfo.timeseries) { | |
359 | + createDatasourceKeys(subscriptionInfo.timeseries, types.dataKeyType.timeseries, datasource, datasources); | |
360 | + } | |
361 | + if (subscriptionInfo.attributes) { | |
362 | + createDatasourceKeys(subscriptionInfo.attributes, types.dataKeyType.attribute, datasource, datasources); | |
363 | + } | |
364 | + if (subscriptionInfo.functions) { | |
365 | + createDatasourceKeys(subscriptionInfo.functions, types.dataKeyType.function, datasource, datasources); | |
366 | + } | |
367 | + } | |
368 | + | |
369 | + function processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred) { | |
370 | + if (index < subscriptionsInfo.length) { | |
371 | + var subscriptionInfo = subscriptionsInfo[index]; | |
372 | + if (subscriptionInfo.type === types.datasourceType.device) { | |
373 | + if (subscriptionInfo.deviceId) { | |
374 | + deviceService.getDevice(subscriptionInfo.deviceId, true, {ignoreLoading: true}).then( | |
375 | + function success(device) { | |
376 | + createDatasourceFromSubscription(subscriptionInfo, datasources, device); | |
377 | + index++; | |
378 | + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); | |
379 | + }, | |
380 | + function fail() { | |
381 | + index++; | |
382 | + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); | |
383 | + } | |
384 | + ); | |
385 | + } else if (subscriptionInfo.deviceName || subscriptionInfo.deviceNamePrefix | |
386 | + || subscriptionInfo.deviceIds) { | |
387 | + var promise; | |
388 | + if (subscriptionInfo.deviceName) { | |
389 | + promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceName, 1, false, {ignoreLoading: true}); | |
390 | + } else if (subscriptionInfo.deviceNamePrefix) { | |
391 | + promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceNamePrefix, 100, false, {ignoreLoading: true}); | |
392 | + } else if (subscriptionInfo.deviceIds) { | |
393 | + promise = deviceService.getDevices(subscriptionInfo.deviceIds, {ignoreLoading: true}); | |
394 | + } | |
395 | + promise.then( | |
396 | + function success(devices) { | |
397 | + if (devices && devices.length > 0) { | |
398 | + for (var i = 0; i < devices.length; i++) { | |
399 | + var device = devices[i]; | |
400 | + createDatasourceFromSubscription(subscriptionInfo, datasources, device); | |
401 | + } | |
402 | + } | |
403 | + index++; | |
404 | + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); | |
405 | + }, | |
406 | + function fail() { | |
407 | + index++; | |
408 | + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); | |
409 | + } | |
410 | + ) | |
411 | + } else { | |
412 | + index++; | |
413 | + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); | |
414 | + } | |
415 | + } else if (subscriptionInfo.type === types.datasourceType.function) { | |
416 | + createDatasourceFromSubscription(subscriptionInfo, datasources); | |
417 | + index++; | |
418 | + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); | |
419 | + } | |
420 | + } else { | |
421 | + deferred.resolve(datasources); | |
422 | + } | |
423 | + } | |
424 | + | |
425 | + function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) { | |
426 | + var deferred = $q.defer(); | |
427 | + var datasources = []; | |
428 | + processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred); | |
429 | + return deferred.promise; | |
430 | + } | |
431 | + | |
432 | + function isLocalUrl(url) { | |
433 | + var parser = document.createElement('a'); //eslint-disable-line | |
434 | + parser.href = url; | |
435 | + var host = parser.hostname; | |
436 | + if (host === "localhost" || host === "127.0.0.1") { | |
437 | + return true; | |
438 | + } else { | |
439 | + return false; | |
440 | + } | |
441 | + } | |
442 | + | |
279 | 443 | } | ... | ... |
... | ... | @@ -87,8 +87,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
87 | 87 | var highlightedMode = false; |
88 | 88 | var highlightedWidget = null; |
89 | 89 | var selectedWidget = null; |
90 | - var mouseDownWidget = -1; | |
91 | - var widgetMouseMoved = false; | |
92 | 90 | |
93 | 91 | var gridsterParent = $('#gridster-parent', $element); |
94 | 92 | var gridsterElement = angular.element($('#gridster-child', gridsterParent)); |
... | ... | @@ -152,9 +150,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
152 | 150 | vm.resetHighlight = resetHighlight; |
153 | 151 | |
154 | 152 | vm.onWidgetFullscreenChanged = onWidgetFullscreenChanged; |
153 | + | |
155 | 154 | vm.widgetMouseDown = widgetMouseDown; |
156 | - vm.widgetMouseMove = widgetMouseMove; | |
157 | - vm.widgetMouseUp = widgetMouseUp; | |
155 | + vm.widgetClicked = widgetClicked; | |
158 | 156 | |
159 | 157 | vm.widgetSizeX = widgetSizeX; |
160 | 158 | vm.widgetSizeY = widgetSizeY; |
... | ... | @@ -184,23 +182,23 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
184 | 182 | |
185 | 183 | vm.dashboardTimewindowApi = { |
186 | 184 | onResetTimewindow: function() { |
187 | - if (vm.originalDashboardTimewindow) { | |
188 | - vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); | |
189 | - vm.originalDashboardTimewindow = null; | |
190 | - } | |
185 | + $timeout(function() { | |
186 | + if (vm.originalDashboardTimewindow) { | |
187 | + vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); | |
188 | + vm.originalDashboardTimewindow = null; | |
189 | + } | |
190 | + }, 0); | |
191 | 191 | }, |
192 | 192 | onUpdateTimewindow: function(startTimeMs, endTimeMs) { |
193 | 193 | if (!vm.originalDashboardTimewindow) { |
194 | 194 | vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow); |
195 | 195 | } |
196 | - vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs); | |
196 | + $timeout(function() { | |
197 | + vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs); | |
198 | + }, 0); | |
197 | 199 | } |
198 | 200 | }; |
199 | 201 | |
200 | - //$element[0].onmousemove=function(){ | |
201 | - // widgetMouseMove(); | |
202 | - // } | |
203 | - | |
204 | 202 | //TODO: widgets visibility |
205 | 203 | /*gridsterParent.scroll(function () { |
206 | 204 | updateVisibleRect(); |
... | ... | @@ -350,7 +348,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
350 | 348 | } |
351 | 349 | |
352 | 350 | function loadDashboard() { |
353 | - resetWidgetClick(); | |
354 | 351 | $timeout(function () { |
355 | 352 | if (vm.loadWidgets) { |
356 | 353 | var promise = vm.loadWidgets(); |
... | ... | @@ -434,42 +431,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
434 | 431 | return gridsterElement && gridsterElement[0] === gridster.$element[0]; |
435 | 432 | } |
436 | 433 | |
437 | - function resetWidgetClick () { | |
438 | - mouseDownWidget = -1; | |
439 | - widgetMouseMoved = false; | |
440 | - } | |
441 | - | |
442 | 434 | function onWidgetFullscreenChanged(expanded, widget) { |
443 | 435 | vm.isWidgetExpanded = expanded; |
444 | 436 | $scope.$broadcast('onWidgetFullscreenChanged', vm.isWidgetExpanded, widget); |
445 | 437 | } |
446 | 438 | |
447 | 439 | function widgetMouseDown ($event, widget) { |
448 | - mouseDownWidget = widget; | |
449 | - widgetMouseMoved = false; | |
450 | 440 | if (vm.onWidgetMouseDown) { |
451 | 441 | vm.onWidgetMouseDown({event: $event, widget: widget}); |
452 | 442 | } |
453 | 443 | } |
454 | 444 | |
455 | - function widgetMouseMove () { | |
456 | - if (mouseDownWidget) { | |
457 | - widgetMouseMoved = true; | |
458 | - } | |
459 | - } | |
460 | - | |
461 | - function widgetMouseUp ($event, widget) { | |
462 | - $timeout(function () { | |
463 | - if (!widgetMouseMoved && mouseDownWidget) { | |
464 | - if (widget === mouseDownWidget) { | |
465 | - widgetClicked($event, widget); | |
466 | - } | |
467 | - } | |
468 | - mouseDownWidget = null; | |
469 | - widgetMouseMoved = false; | |
470 | - }, 0); | |
471 | - } | |
472 | - | |
473 | 445 | function widgetClicked ($event, widget) { |
474 | 446 | if ($event) { |
475 | 447 | $event.stopPropagation(); |
... | ... | @@ -518,7 +490,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
518 | 490 | } |
519 | 491 | |
520 | 492 | function editWidget ($event, widget) { |
521 | - resetWidgetClick(); | |
522 | 493 | if ($event) { |
523 | 494 | $event.stopPropagation(); |
524 | 495 | } |
... | ... | @@ -528,7 +499,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
528 | 499 | } |
529 | 500 | |
530 | 501 | function exportWidget ($event, widget) { |
531 | - resetWidgetClick(); | |
532 | 502 | if ($event) { |
533 | 503 | $event.stopPropagation(); |
534 | 504 | } |
... | ... | @@ -538,7 +508,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
538 | 508 | } |
539 | 509 | |
540 | 510 | function removeWidget($event, widget) { |
541 | - resetWidgetClick(); | |
542 | 511 | if ($event) { |
543 | 512 | $event.stopPropagation(); |
544 | 513 | } |
... | ... | @@ -548,27 +517,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t |
548 | 517 | } |
549 | 518 | |
550 | 519 | function highlightWidget(widget, delay) { |
551 | - highlightedMode = true; | |
552 | - highlightedWidget = widget; | |
553 | - if (vm.gridster) { | |
554 | - var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; | |
555 | - if (item) { | |
556 | - var height = $(item).outerHeight(true); | |
557 | - var rectHeight = gridsterParent.height(); | |
558 | - var offset = (rectHeight - height) / 2; | |
559 | - var scrollTop = item.offsetTop; | |
560 | - if (offset > 0) { | |
561 | - scrollTop -= offset; | |
562 | - } | |
563 | - gridsterParent.animate({ | |
564 | - scrollTop: scrollTop | |
565 | - }, delay); | |
566 | - } | |
520 | + if (!highlightedMode || highlightedWidget != widget) { | |
521 | + highlightedMode = true; | |
522 | + highlightedWidget = widget; | |
523 | + scrollToWidget(widget, delay); | |
567 | 524 | } |
568 | 525 | } |
569 | 526 | |
570 | 527 | function selectWidget(widget, delay) { |
571 | - selectedWidget = widget; | |
528 | + if (selectedWidget != widget) { | |
529 | + selectedWidget = widget; | |
530 | + scrollToWidget(widget, delay); | |
531 | + } | |
532 | + } | |
533 | + | |
534 | + function scrollToWidget(widget, delay) { | |
572 | 535 | if (vm.gridster) { |
573 | 536 | var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; |
574 | 537 | if (item) { | ... | ... |
... | ... | @@ -37,9 +37,7 @@ |
37 | 37 | 'tb-not-highlighted': vm.isNotHighlighted(widget), |
38 | 38 | 'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}" |
39 | 39 | tb-mousedown="vm.widgetMouseDown($event, widget)" |
40 | - tb-mousemove="vm.widgetMouseMove($event, widget)" | |
41 | - tb-mouseup="vm.widgetMouseUp($event, widget)" | |
42 | - ng-click="" | |
40 | + ng-click="vm.widgetClicked($event, widget)" | |
43 | 41 | tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)" |
44 | 42 | ng-style="{cursor: 'pointer', |
45 | 43 | color: vm.widgetColor(widget), |
... | ... | @@ -49,7 +47,7 @@ |
49 | 47 | <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span> |
50 | 48 | <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> |
51 | 49 | </div> |
52 | - <div class="tb-widget-actions" layout="row" layout-align="start center"> | |
50 | + <div class="tb-widget-actions" layout="row" layout-align="start center" tb-mousedown="$event.stopPropagation()"> | |
53 | 51 | <md-button id="expand-button" |
54 | 52 | ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)" |
55 | 53 | aria-label="{{ 'fullscreen.fullscreen' | translate }}" | ... | ... |
... | ... | @@ -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(); | ... | ... |
... | ... | @@ -26,6 +26,7 @@ import gridTemplate from './grid.tpl.html'; |
26 | 26 | |
27 | 27 | export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav]) |
28 | 28 | .directive('tbGrid', Grid) |
29 | + .controller('ItemCardController', ItemCardController) | |
29 | 30 | .directive('tbGridCardContent', GridCardContent) |
30 | 31 | .filter('range', RangeFilter) |
31 | 32 | .name; |
... | ... | @@ -44,14 +45,52 @@ function RangeFilter() { |
44 | 45 | } |
45 | 46 | |
46 | 47 | /*@ngInject*/ |
47 | -function GridCardContent($compile) { | |
48 | +function ItemCardController() { | |
49 | + | |
50 | + var vm = this; //eslint-disable-line | |
51 | + | |
52 | +} | |
53 | + | |
54 | +/*@ngInject*/ | |
55 | +function GridCardContent($compile, $controller) { | |
48 | 56 | var linker = function(scope, element) { |
57 | + | |
58 | + var controllerInstance = null; | |
59 | + | |
49 | 60 | scope.$watch('itemTemplate', |
50 | - function(value) { | |
51 | - element.html(value); | |
52 | - $compile(element.contents())(scope); | |
61 | + function() { | |
62 | + initContent(); | |
63 | + } | |
64 | + ); | |
65 | + scope.$watch('itemController', | |
66 | + function() { | |
67 | + initContent(); | |
53 | 68 | } |
54 | 69 | ); |
70 | + scope.$watch('parentCtl', | |
71 | + function() { | |
72 | + controllerInstance.parentCtl = scope.parentCtl; | |
73 | + } | |
74 | + ); | |
75 | + scope.$watch('item', | |
76 | + function() { | |
77 | + controllerInstance.item = scope.item; | |
78 | + } | |
79 | + ); | |
80 | + | |
81 | + function initContent() { | |
82 | + if (scope.itemTemplate && scope.itemController && !controllerInstance) { | |
83 | + element.html(scope.itemTemplate); | |
84 | + var locals = {}; | |
85 | + angular.extend(locals, {$scope: scope, $element: element}); | |
86 | + var controller = $controller(scope.itemController, locals, true, 'vm'); | |
87 | + controller.instance = controller(); | |
88 | + controllerInstance = controller.instance; | |
89 | + controllerInstance.item = scope.item; | |
90 | + controllerInstance.parentCtl = scope.parentCtl; | |
91 | + $compile(element.contents())(scope); | |
92 | + } | |
93 | + } | |
55 | 94 | }; |
56 | 95 | |
57 | 96 | return { |
... | ... | @@ -61,6 +100,7 @@ function GridCardContent($compile) { |
61 | 100 | parentCtl: "=parentCtl", |
62 | 101 | gridCtl: "=gridCtl", |
63 | 102 | itemTemplate: "=itemTemplate", |
103 | + itemController: "=itemController", | |
64 | 104 | item: "=item" |
65 | 105 | } |
66 | 106 | }; |
... | ... | @@ -171,26 +211,31 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra |
171 | 211 | vm.items.pending = true; |
172 | 212 | promise.then( |
173 | 213 | function success(items) { |
174 | - vm.items.data = vm.items.data.concat(items.data); | |
175 | - var startIndex = vm.items.data.length - items.data.length; | |
176 | - var endIndex = vm.items.data.length; | |
177 | - for (var i = startIndex; i < endIndex; i++) { | |
178 | - var item = vm.items.data[i]; | |
179 | - item.index = i; | |
180 | - var row = Math.floor(i / vm.columns); | |
181 | - var itemRow = vm.items.rowData[row]; | |
182 | - if (!itemRow) { | |
183 | - itemRow = []; | |
184 | - vm.items.rowData.push(itemRow); | |
214 | + if (vm.items.reloadPending) { | |
215 | + vm.items.pending = false; | |
216 | + reload(); | |
217 | + } else { | |
218 | + vm.items.data = vm.items.data.concat(items.data); | |
219 | + var startIndex = vm.items.data.length - items.data.length; | |
220 | + var endIndex = vm.items.data.length; | |
221 | + for (var i = startIndex; i < endIndex; i++) { | |
222 | + var item = vm.items.data[i]; | |
223 | + item.index = i; | |
224 | + var row = Math.floor(i / vm.columns); | |
225 | + var itemRow = vm.items.rowData[row]; | |
226 | + if (!itemRow) { | |
227 | + itemRow = []; | |
228 | + vm.items.rowData.push(itemRow); | |
229 | + } | |
230 | + itemRow.push(item); | |
185 | 231 | } |
186 | - itemRow.push(item); | |
187 | - } | |
188 | - vm.items.nextPageLink = items.nextPageLink; | |
189 | - vm.items.hasNext = items.hasNext; | |
190 | - if (vm.items.hasNext) { | |
191 | - vm.items.nextPageLink.limit = pageSize; | |
232 | + vm.items.nextPageLink = items.nextPageLink; | |
233 | + vm.items.hasNext = items.hasNext; | |
234 | + if (vm.items.hasNext) { | |
235 | + vm.items.nextPageLink.limit = pageSize; | |
236 | + } | |
237 | + vm.items.pending = false; | |
192 | 238 | } |
193 | - vm.items.pending = false; | |
194 | 239 | }, |
195 | 240 | function fail() { |
196 | 241 | vm.items.hasNext = false; |
... | ... | @@ -291,6 +336,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra |
291 | 336 | } else if (vm.config.itemCardTemplateUrl) { |
292 | 337 | vm.itemCardTemplate = $templateCache.get(vm.config.itemCardTemplateUrl); |
293 | 338 | } |
339 | + if (vm.config.itemCardController) { | |
340 | + vm.itemCardController = vm.config.itemCardController; | |
341 | + } else { | |
342 | + vm.itemCardController = 'ItemCardController'; | |
343 | + } | |
294 | 344 | |
295 | 345 | vm.parentCtl = vm.config.parentCtl || vm; |
296 | 346 | |
... | ... | @@ -380,26 +430,35 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra |
380 | 430 | } |
381 | 431 | |
382 | 432 | $scope.$on('searchTextUpdated', function () { |
383 | - vm.items = { | |
384 | - data: [], | |
385 | - rowData: [], | |
386 | - nextPageLink: { | |
387 | - limit: pageSize, | |
388 | - textSearch: $scope.searchConfig.searchText | |
389 | - }, | |
390 | - selections: {}, | |
391 | - selectedCount: 0, | |
392 | - hasNext: true, | |
393 | - pending: false | |
394 | - }; | |
395 | - vm.detailsConfig.isDetailsOpen = false; | |
396 | - vm.itemRows.getItemAtIndex(pageSize); | |
433 | + reload(); | |
397 | 434 | }); |
398 | 435 | |
399 | 436 | vm.onGridInited(vm); |
400 | 437 | |
401 | 438 | vm.itemRows.getItemAtIndex(pageSize); |
402 | 439 | |
440 | + function reload() { | |
441 | + if (vm.items && vm.items.pending) { | |
442 | + vm.items.reloadPending = true; | |
443 | + } else { | |
444 | + vm.items = { | |
445 | + data: [], | |
446 | + rowData: [], | |
447 | + nextPageLink: { | |
448 | + limit: pageSize, | |
449 | + textSearch: $scope.searchConfig.searchText | |
450 | + }, | |
451 | + selections: {}, | |
452 | + selectedCount: 0, | |
453 | + hasNext: true, | |
454 | + pending: false | |
455 | + }; | |
456 | + vm.detailsConfig.isDetailsOpen = false; | |
457 | + vm.items.reloadPending = false; | |
458 | + vm.itemRows.getItemAtIndex(pageSize); | |
459 | + } | |
460 | + } | |
461 | + | |
403 | 462 | function refreshList() { |
404 | 463 | $state.go($state.current, vm.refreshParamsFunc(), {reload: true}); |
405 | 464 | } | ... | ... |
... | ... | @@ -43,13 +43,13 @@ |
43 | 43 | </md-card-title> |
44 | 44 | </section> |
45 | 45 | <md-card-content flex> |
46 | - <tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content> | |
46 | + <tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content> | |
47 | 47 | </md-card-content> |
48 | 48 | <md-card-actions layout="row" layout-align="end end"> |
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); | ... | ... |
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 | +/* eslint-disable import/no-unresolved, import/default */ | |
18 | + | |
19 | +import socialsharePanelTemplate from './socialshare-panel.tpl.html'; | |
20 | + | |
21 | +/* eslint-enable import/no-unresolved, import/default */ | |
22 | + | |
23 | + | |
24 | +export default angular.module('thingsboard.directives.socialsharePanel', []) | |
25 | + .directive('tbSocialSharePanel', SocialsharePanel) | |
26 | + .name; | |
27 | + | |
28 | +/*@ngInject*/ | |
29 | +function SocialsharePanel() { | |
30 | + return { | |
31 | + restrict: "E", | |
32 | + scope: true, | |
33 | + bindToController: { | |
34 | + shareTitle: '@', | |
35 | + shareText: '@', | |
36 | + shareLink: '@', | |
37 | + shareHashTags: '@' | |
38 | + }, | |
39 | + controller: SocialsharePanelController, | |
40 | + controllerAs: 'vm', | |
41 | + templateUrl: socialsharePanelTemplate | |
42 | + }; | |
43 | +} | |
44 | + | |
45 | +/*@ngInject*/ | |
46 | +function SocialsharePanelController(utils) { | |
47 | + | |
48 | + let vm = this; | |
49 | + | |
50 | + vm.isShareLinkLocal = function() { | |
51 | + if (vm.shareLink && vm.shareLink.length > 0) { | |
52 | + return utils.isLocalUrl(vm.shareLink); | |
53 | + } else { | |
54 | + return true; | |
55 | + } | |
56 | + } | |
57 | + | |
58 | +} | |
\ No newline at end of file | ... | ... |
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 | + | |
19 | +<div layout="row" ng-show="!vm.isShareLinkLocal()"> | |
20 | + <md-button class="md-icon-button md-raised md-primary" | |
21 | + socialshare | |
22 | + socialshare-provider="facebook" | |
23 | + socialshare-title="{{ vm.shareTitle }}" | |
24 | + socialshare-text="{{ vm.shareText }}" | |
25 | + socialshare-url="{{ vm.shareLink }}"> | |
26 | + <ng-md-icon icon="facebook" aria-label="Facebook"></ng-md-icon> | |
27 | + <md-tooltip md-direction="top"> | |
28 | + {{ 'action.share-via' | translate:{provider:'Facebook'} }} | |
29 | + </md-tooltip> | |
30 | + </md-button> | |
31 | + <md-button class="md-icon-button md-raised md-primary" | |
32 | + socialshare | |
33 | + socialshare-provider="twitter" | |
34 | + socialshare-text="{{ vm.shareTitle }}" | |
35 | + socialshare-hashtags="{{ vm.shareHashTags }}" | |
36 | + socialshare-url="{{ vm.shareLink }}"> | |
37 | + <ng-md-icon icon="twitter" aria-label="Twitter"></ng-md-icon> | |
38 | + <md-tooltip md-direction="top"> | |
39 | + {{ 'action.share-via' | translate:{provider:'Twitter'} }} | |
40 | + </md-tooltip> | |
41 | + </md-button> | |
42 | + <md-button class="md-icon-button md-raised md-primary" | |
43 | + socialshare | |
44 | + socialshare-provider="linkedin" | |
45 | + socialshare-text="{{ vm.shareTitle }}" | |
46 | + socialshare-url="{{ vm.shareLink }}"> | |
47 | + <ng-md-icon icon="linkedin" aria-label="Linkedin"></ng-md-icon> | |
48 | + <md-tooltip md-direction="top"> | |
49 | + {{ 'action.share-via' | translate:{provider:'Linkedin'} }} | |
50 | + </md-tooltip> | |
51 | + </md-button> | |
52 | + <md-button class="md-icon-button md-raised md-primary" | |
53 | + socialshare | |
54 | + socialshare-provider="reddit" | |
55 | + socialshare-text="{{ vm.shareTitle }}" | |
56 | + socialshare-url="{{ vm.shareLink }}"> | |
57 | + <md-icon md-svg-icon="mdi:reddit" aria-label="Reddit"></md-icon> | |
58 | + <md-tooltip md-direction="top"> | |
59 | + {{ 'action.share-via' | translate:{provider:'Reddit'} }} | |
60 | + </md-tooltip> | |
61 | + </md-button> | |
62 | +</div> | |
\ No newline at end of file | ... | ... |
... | ... | @@ -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 }} | ... | ... |