Commit 71c4e0a48c3618644a17afaec3486c5ed30e5a9b

Authored by Andrew Shvayka
Committed by GitHub
2 parents 71308c3d 6a51eda9

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.

  1 +*.toDelete
1 2 output/**
2 3 *.class
3 4 *~
... ...
... ... @@ -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   -[![Smart energy monitoring demo](https://cloud.githubusercontent.com/assets/8308069/23790111/62c8a61e-0586-11e7-84eb-51febc54ec09.png "Smart energy monitoring demo")](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github)
  15 +[**Smart energy**](https://thingsboard.io/smart-energy/)
  16 +[![Smart energy monitoring demo](https://cloud.githubusercontent.com/assets/8308069/24495682/aebd45d0-153e-11e7-8de4-7360ed5b41ae.gif "Smart energy")](https://thingsboard.io/smart-energy/)
17 17
18   -[**Silos monitoring**](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github)
19   -[![Silos monitoring demo](https://cloud.githubusercontent.com/assets/8308069/23996135/00214844-0a55-11e7-9623-d1e3be0702ca.png "Silos monitoring demo")](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github)
  18 +[**Smart farming**](https://thingsboard.io/smart-farming/)
  19 +[![Smart farming](https://cloud.githubusercontent.com/assets/8308069/24496824/10dc1144-1542-11e7-8aa1-5d3a281d5a1a.gif "Smart farming")](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   -[![Smart bus tracking demo](https://cloud.githubusercontent.com/assets/8308069/23790110/62c6ecde-0586-11e7-8249-19eafd5bf8cc.png "Smart bus tracking demo")](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github)
  21 +[**Fleet tracking**](https://thingsboard.io/fleet-tracking/)
  22 +[![Fleet tracking](https://cloud.githubusercontent.com/assets/8308069/24497169/3a1a61e0-1543-11e7-8d55-3c8a13f35634.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/)
23 23
24 24 ## Getting Started
25 25
... ...
1   -!bin/
\ No newline at end of file
  1 +!bin/
  2 +/bin/
... ...
... ... @@ -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>
... ...
... ... @@ -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
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.rest;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonCreator;
  19 +import com.fasterxml.jackson.annotation.JsonProperty;
  20 +
  21 +public class PublicLoginRequest {
  22 +
  23 + private String publicId;
  24 +
  25 + @JsonCreator
  26 + public PublicLoginRequest(@JsonProperty("publicId") String publicId) {
  27 + this.publicId = publicId;
  28 + }
  29 +
  30 + public String getPublicId() {
  31 + return publicId;
  32 + }
  33 +
  34 +}
... ...
... ... @@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
23 23 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
24 24 import org.springframework.stereotype.Component;
25 25 import org.springframework.util.Assert;
  26 +import org.thingsboard.server.common.data.Customer;
26 27 import org.thingsboard.server.common.data.User;
  28 +import org.thingsboard.server.common.data.id.CustomerId;
  29 +import org.thingsboard.server.common.data.id.UUIDBased;
  30 +import org.thingsboard.server.common.data.id.UserId;
  31 +import org.thingsboard.server.common.data.security.Authority;
27 32 import org.thingsboard.server.common.data.security.UserCredentials;
  33 +import org.thingsboard.server.dao.customer.CustomerService;
28 34 import org.thingsboard.server.dao.user.UserService;
29 35 import org.thingsboard.server.service.security.model.SecurityUser;
  36 +import org.thingsboard.server.service.security.model.UserPrincipal;
  37 +
  38 +import java.util.UUID;
30 39
31 40 @Component
32 41 public class RestAuthenticationProvider implements AuthenticationProvider {
33 42
34 43 private final BCryptPasswordEncoder encoder;
35 44 private final UserService userService;
  45 + private final CustomerService customerService;
36 46
37 47 @Autowired
38   - public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) {
  48 + public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) {
39 49 this.userService = userService;
  50 + this.customerService = customerService;
40 51 this.encoder = encoder;
41 52 }
42 53
... ... @@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
44 55 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
45 56 Assert.notNull(authentication, "No authentication data provided");
46 57
47   - String username = (String) authentication.getPrincipal();
48   - String password = (String) authentication.getCredentials();
  58 + Object principal = authentication.getPrincipal();
  59 + if (!(principal instanceof UserPrincipal)) {
  60 + throw new BadCredentialsException("Authentication Failed. Bad user principal.");
  61 + }
49 62
  63 + UserPrincipal userPrincipal = (UserPrincipal) principal;
  64 + if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
  65 + String username = userPrincipal.getValue();
  66 + String password = (String) authentication.getCredentials();
  67 + return authenticateByUsernameAndPassword(userPrincipal, username, password);
  68 + } else {
  69 + String publicId = userPrincipal.getValue();
  70 + return authenticateByPublicId(userPrincipal, publicId);
  71 + }
  72 + }
  73 +
  74 + private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) {
50 75 User user = userService.findUserByEmail(username);
51 76 if (user == null) {
52 77 throw new UsernameNotFoundException("User not found: " + username);
... ... @@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
67 92
68 93 if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
69 94
70   - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled());
  95 + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
  96 +
  97 + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
  98 + }
  99 +
  100 + private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
  101 + CustomerId customerId;
  102 + try {
  103 + customerId = new CustomerId(UUID.fromString(publicId));
  104 + } catch (Exception e) {
  105 + throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
  106 + }
  107 + Customer publicCustomer = customerService.findCustomerById(customerId);
  108 + if (publicCustomer == null) {
  109 + throw new UsernameNotFoundException("Public entity not found: " + publicId);
  110 + }
  111 + boolean isPublic = false;
  112 + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) {
  113 + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean();
  114 + }
  115 + if (!isPublic) {
  116 + throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
  117 + }
  118 + User user = new User(new UserId(UUIDBased.EMPTY));
  119 + user.setTenantId(publicCustomer.getTenantId());
  120 + user.setCustomerId(publicCustomer.getId());
  121 + user.setEmail(publicId);
  122 + user.setAuthority(Authority.CUSTOMER_USER);
  123 + user.setFirstName("Public");
  124 + user.setLastName("Public");
  125 +
  126 + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
71 127
72 128 return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
73 129 }
... ...
... ... @@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
29 29 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
30 30 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
31 31 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
32 33
33 34 import javax.servlet.FilterChain;
34 35 import javax.servlet.ServletException;
... ... @@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
73 74 throw new AuthenticationServiceException("Username or Password not provided");
74 75 }
75 76
76   - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
  77 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername());
  78 +
  79 + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword());
77 80
78 81 return this.getAuthenticationManager().authenticate(token);
79 82 }
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.rest;
  17 +
  18 +import com.fasterxml.jackson.databind.ObjectMapper;
  19 +import org.apache.commons.lang3.StringUtils;
  20 +import org.slf4j.Logger;
  21 +import org.slf4j.LoggerFactory;
  22 +import org.springframework.http.HttpMethod;
  23 +import org.springframework.security.authentication.AuthenticationServiceException;
  24 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  25 +import org.springframework.security.core.Authentication;
  26 +import org.springframework.security.core.AuthenticationException;
  27 +import org.springframework.security.core.context.SecurityContextHolder;
  28 +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
  29 +import org.springframework.security.web.authentication.AuthenticationFailureHandler;
  30 +import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
  31 +import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
  33 +
  34 +import javax.servlet.FilterChain;
  35 +import javax.servlet.ServletException;
  36 +import javax.servlet.http.HttpServletRequest;
  37 +import javax.servlet.http.HttpServletResponse;
  38 +import java.io.IOException;
  39 +
  40 +public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
  41 + private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class);
  42 +
  43 + private final AuthenticationSuccessHandler successHandler;
  44 + private final AuthenticationFailureHandler failureHandler;
  45 +
  46 + private final ObjectMapper objectMapper;
  47 +
  48 + public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
  49 + AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
  50 + super(defaultProcessUrl);
  51 + this.successHandler = successHandler;
  52 + this.failureHandler = failureHandler;
  53 + this.objectMapper = mapper;
  54 + }
  55 +
  56 + @Override
  57 + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
  58 + throws AuthenticationException, IOException, ServletException {
  59 + if (!HttpMethod.POST.name().equals(request.getMethod())) {
  60 + if(logger.isDebugEnabled()) {
  61 + logger.debug("Authentication method not supported. Request method: " + request.getMethod());
  62 + }
  63 + throw new AuthMethodNotSupportedException("Authentication method not supported");
  64 + }
  65 +
  66 + PublicLoginRequest loginRequest;
  67 + try {
  68 + loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class);
  69 + } catch (Exception e) {
  70 + throw new AuthenticationServiceException("Invalid public login request payload");
  71 + }
  72 +
  73 + if (StringUtils.isBlank(loginRequest.getPublicId())) {
  74 + throw new AuthenticationServiceException("Public Id is not provided");
  75 + }
  76 +
  77 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId());
  78 +
  79 + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "");
  80 +
  81 + return this.getAuthenticationManager().authenticate(token);
  82 + }
  83 +
  84 + @Override
  85 + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
  86 + Authentication authResult) throws IOException, ServletException {
  87 + successHandler.onAuthenticationSuccess(request, response, authResult);
  88 + }
  89 +
  90 + @Override
  91 + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
  92 + AuthenticationException failed) throws IOException, ServletException {
  93 + SecurityContextHolder.clearContext();
  94 + failureHandler.onAuthenticationFailure(request, response, failed);
  95 + }
  96 +}
... ...
... ... @@ -30,6 +30,7 @@ public class SecurityUser extends User {
30 30
31 31 private Collection<GrantedAuthority> authorities;
32 32 private boolean enabled;
  33 + private UserPrincipal userPrincipal;
33 34
34 35 public SecurityUser() {
35 36 super();
... ... @@ -39,9 +40,10 @@ public class SecurityUser extends User {
39 40 super(id);
40 41 }
41 42
42   - public SecurityUser(User user, boolean enabled) {
  43 + public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) {
43 44 super(user);
44 45 this.enabled = enabled;
  46 + this.userPrincipal = userPrincipal;
45 47 }
46 48
47 49 public Collection<? extends GrantedAuthority> getAuthorities() {
... ... @@ -57,8 +59,16 @@ public class SecurityUser extends User {
57 59 return enabled;
58 60 }
59 61
  62 + public UserPrincipal getUserPrincipal() {
  63 + return userPrincipal;
  64 + }
  65 +
60 66 public void setEnabled(boolean enabled) {
61 67 this.enabled = enabled;
62 68 }
63 69
  70 + public void setUserPrincipal(UserPrincipal userPrincipal) {
  71 + this.userPrincipal = userPrincipal;
  72 + }
  73 +
64 74 }
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package org.thingsboard.server.service.security.model;
  18 +
  19 +public class UserPrincipal {
  20 +
  21 + private final Type type;
  22 + private final String value;
  23 +
  24 + public UserPrincipal(Type type, String value) {
  25 + this.type = type;
  26 + this.value = value;
  27 + }
  28 +
  29 + public Type getType() {
  30 + return type;
  31 + }
  32 +
  33 + public String getValue() {
  34 + return value;
  35 + }
  36 +
  37 + public enum Type {
  38 + USER_NAME,
  39 + PUBLIC_ID
  40 + }
  41 +
  42 +}
... ...
... ... @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId;
29 29 import org.thingsboard.server.common.data.security.Authority;
30 30 import org.thingsboard.server.config.JwtSettings;
31 31 import org.thingsboard.server.service.security.model.SecurityUser;
  32 +import org.thingsboard.server.service.security.model.UserPrincipal;
32 33
33 34 import java.util.Arrays;
34 35 import java.util.List;
... ... @@ -43,6 +44,7 @@ public class JwtTokenFactory {
43 44 private static final String FIRST_NAME = "firstName";
44 45 private static final String LAST_NAME = "lastName";
45 46 private static final String ENABLED = "enabled";
  47 + private static final String IS_PUBLIC = "isPublic";
46 48 private static final String TENANT_ID = "tenantId";
47 49 private static final String CUSTOMER_ID = "customerId";
48 50
... ... @@ -63,12 +65,15 @@ public class JwtTokenFactory {
63 65 if (securityUser.getAuthority() == null)
64 66 throw new IllegalArgumentException("User doesn't have any privileges");
65 67
66   - Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
  68 + UserPrincipal principal = securityUser.getUserPrincipal();
  69 + String subject = principal.getValue();
  70 + Claims claims = Jwts.claims().setSubject(subject);
67 71 claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
68 72 claims.put(USER_ID, securityUser.getId().getId().toString());
69 73 claims.put(FIRST_NAME, securityUser.getFirstName());
70 74 claims.put(LAST_NAME, securityUser.getLastName());
71 75 claims.put(ENABLED, securityUser.isEnabled());
  76 + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
72 77 if (securityUser.getTenantId() != null) {
73 78 claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
74 79 }
... ... @@ -104,6 +109,9 @@ public class JwtTokenFactory {
104 109 securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
105 110 securityUser.setLastName(claims.get(LAST_NAME, String.class));
106 111 securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
  112 + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
  113 + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
  114 + securityUser.setUserPrincipal(principal);
107 115 String tenantId = claims.get(TENANT_ID, String.class);
108 116 if (tenantId != null) {
109 117 securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
... ... @@ -123,9 +131,11 @@ public class JwtTokenFactory {
123 131
124 132 DateTime currentTime = new DateTime();
125 133
126   - Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
  134 + UserPrincipal principal = securityUser.getUserPrincipal();
  135 + Claims claims = Jwts.claims().setSubject(principal.getValue());
127 136 claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name()));
128 137 claims.put(USER_ID, securityUser.getId().getId().toString());
  138 + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
129 139
130 140 String token = Jwts.builder()
131 141 .setClaims(claims)
... ... @@ -150,8 +160,10 @@ public class JwtTokenFactory {
150 160 if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
151 161 throw new IllegalArgumentException("Invalid Refresh Token scope");
152 162 }
  163 + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
  164 + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
153 165 SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
154   - securityUser.setEmail(subject);
  166 + securityUser.setUserPrincipal(principal);
155 167 return securityUser;
156 168 }
157 169
... ...
... ... @@ -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>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</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>thingsboard</artifactId>
25 25 </parent>
26 26 <groupId>org.thingsboard</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());
... ...
... ... @@ -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>
... ...
... ... @@ -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>
... ...
... ... @@ -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>
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <groupId>org.thingsboard</groupId>
22 22 <artifactId>thingsboard</artifactId>
23   - <version>1.2.2</version>
  23 + <version>1.2.3-SNAPSHOT</version>
24 24 <packaging>pom</packaging>
25 25
26 26 <name>Thingsboard</name>
... ...
... ... @@ -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))));
... ...
... ... @@ -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>
... ...
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",
... ...
... ... @@ -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>
... ...
... ... @@ -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;
... ...
  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 }}
... ...