diff --git a/.gitignore b/.gitignore index 6f6dd6136c..9039e8ec4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.toDelete output/** *.class *~ diff --git a/README.md b/README.md index 4639932208..f6a514c3d2 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,16 @@ Thingsboard is an open-source IoT platform for data collection, processing, visu Thingsboard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs). -## Sample Dashboards +## IoT use cases -[**Smart energy monitoring**](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github) -[![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) +[**Smart energy**](https://thingsboard.io/smart-energy/) +[![Smart energy monitoring demo](https://cloud.githubusercontent.com/assets/8308069/24495682/aebd45d0-153e-11e7-8de4-7360ed5b41ae.gif "Smart energy")](https://thingsboard.io/smart-energy/) -[**Silos monitoring**](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github) -[![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) +[**Smart farming**](https://thingsboard.io/smart-farming/) +[![Smart farming](https://cloud.githubusercontent.com/assets/8308069/24496824/10dc1144-1542-11e7-8aa1-5d3a281d5a1a.gif "Smart farming")](https://thingsboard.io/smart-farming/) -[**Smart bus tracking**](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github) -[![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) +[**Fleet tracking**](https://thingsboard.io/fleet-tracking/) +[![Fleet tracking](https://cloud.githubusercontent.com/assets/8308069/24497169/3a1a61e0-1543-11e7-8d55-3c8a13f35634.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/) ## Getting Started diff --git a/application/.gitignore b/application/.gitignore index 08eb0a07a6..b5246c6bdd 100644 --- a/application/.gitignore +++ b/application/.gitignore @@ -1 +1,2 @@ -!bin/ \ No newline at end of file +!bin/ +/bin/ diff --git a/application/pom.xml b/application/pom.xml index 9fbaebd644..f3fd1d04e4 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index d4c42d82b7..0c73470d5c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -51,13 +51,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody; import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg; import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -205,25 +199,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) { refreshAttributes(msg); - Set keys = msg.getDeletedKeys(); if (attributeSubscriptions.size() > 0) { ToDeviceMsg notification = null; if (msg.isDeleted()) { - List sharedKeys = keys.stream() + List sharedKeys = msg.getDeletedKeys().stream() .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) .collect(Collectors.toList()); notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys)); } else { - List attributes = keys.stream() - .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope())) - .map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey())) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - if (attributes.size() > 0) { - notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes)); - } else { - logger.debug("[{}] No public server side attributes changed!", deviceId); + if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) { + List attributes = new ArrayList<>(msg.getValues()); + if (attributes.size() > 0) { + notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes)); + } else { + logger.debug("[{}] No public server side attributes changed!", deviceId); + } } } if (notification != null) { diff --git a/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java b/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java new file mode 100644 index 0000000000..62b4ec266f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by yyh on 2017/5/2. + * CORS configuration + */ +@Configuration +@ConfigurationProperties(prefix = "spring.mvc.cors") +public class MvcCorsProperties { + + private Map mappings = new HashMap<>(); + + public MvcCorsProperties() { + } + + public Map getMappings() { + return mappings; + } + + public void setMappings(Map mappings) { + this.mappings = mappings; + } +} diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index a7918e05b7..cdca099c3a 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -18,7 +18,9 @@ package org.thingsboard.server.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -34,11 +36,15 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.*; import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; +import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; import java.util.ArrayList; import java.util.Arrays; @@ -56,6 +62,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt public static final String WEBJARS_ENTRY_POINT = "/webjars/**"; public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**"; public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; + public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; @@ -87,10 +94,18 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt return filter; } + @Bean + protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception { + RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper); + filter.setAuthenticationManager(this.authenticationManager); + return filter; + } + @Bean protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { List pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); - 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)); + pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, + PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); @@ -135,6 +150,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @Override protected void configure(HttpSecurity http) throws Exception { http.headers().cacheControl().disable().frameOptions().disable() + .and() + .cors() .and() .csrf().disable() .exceptionHandling() @@ -146,6 +163,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point + .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .and() @@ -156,8 +174,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler) .and() .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); } + + + @Bean + @ConditionalOnMissingBean(CorsFilter.class) + public CorsFilter corsFilter(@Autowired MvcCorsProperties mvcCorsProperties) { + if (mvcCorsProperties.getMappings().size() == 0) { + return new CorsFilter(new UrlBasedCorsConfigurationSource()); + } else { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.setCorsConfigurations(mvcCorsProperties.getMappings()); + return new CorsFilter(source); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 5ef52c236d..d06f2bed9d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -36,6 +36,7 @@ import org.thingsboard.server.exception.ThingsboardException; import org.thingsboard.server.service.mail.MailService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtToken; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -167,7 +168,8 @@ public class AuthController extends BaseController { String encodedPassword = passwordEncoder.encode(password); UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword); User user = userService.findUserById(credentials.getUserId()); - SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); String baseUrl = constructBaseUrl(request); String loginUrl = String.format("%s/login", baseUrl); String email = user.getEmail(); @@ -201,7 +203,8 @@ public class AuthController extends BaseController { userCredentials.setResetToken(null); userCredentials = userService.saveUserCredentials(userCredentials); User user = userService.findUserById(userCredentials.getUserId()); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal); String baseUrl = constructBaseUrl(request); String loginUrl = String.format("%s/login", baseUrl); String email = user.getEmail(); diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 4d7c7cd49d..51bc4527bd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -42,6 +45,28 @@ public class CustomerController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET) + @ResponseBody + public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId); + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode infoObject = objectMapper.createObjectNode(); + infoObject.put("title", customer.getTitle()); + boolean isPublic = false; + if (customer.getAdditionalInfo() != null && customer.getAdditionalInfo().has("isPublic")) { + isPublic = customer.getAdditionalInfo().get("isPublic").asBoolean(); + } + infoObject.put("isPublic", isPublic); + return infoObject; + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer", method = RequestMethod.POST) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index d72f02548e..3812610d20 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; @@ -116,6 +117,21 @@ public class DashboardController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST) + @ResponseBody + public Dashboard assignDashboardToPublicCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException { + checkParameter("dashboardId", strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); + return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId())); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index b08a9640a7..bebab8b880 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; @@ -117,6 +118,21 @@ public class DeviceController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST) + @ResponseBody + public Device assignDeviceToPublicCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException { + checkParameter("deviceId", strDeviceId); + try { + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + Device device = checkDeviceId(deviceId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId()); + return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId())); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 5ba84bf112..811f39f5d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -16,32 +16,40 @@ package org.thingsboard.server.service.security.auth.jwt; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.InsufficientAuthenticationException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; +import java.util.UUID; + @Component public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { private final JwtTokenFactory tokenFactory; private final UserService userService; + private final CustomerService customerService; @Autowired - public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) { + public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) { this.userService = userService; + this.customerService = customerService; this.tokenFactory = tokenFactory; } @@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide Assert.notNull(authentication, "No authentication data provided"); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); + UserPrincipal principal = unsafeUser.getUserPrincipal(); + SecurityUser securityUser; + if (principal.getType() == UserPrincipal.Type.USER_NAME) { + securityUser = authenticateByUserId(unsafeUser.getId()); + } else { + securityUser = authenticateByPublicId(principal.getValue()); + } + return new RefreshAuthenticationToken(securityUser); + } - User user = userService.findUserById(unsafeUser.getId()); + private SecurityUser authenticateByUserId(UserId userId) { + User user = userService.findUserById(userId); if (user == null) { throw new UsernameNotFoundException("User not found by refresh token"); } @@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - return new RefreshAuthenticationToken(securityUser); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + + return securityUser; + } + + private SecurityUser authenticateByPublicId(String publicId) { + CustomerId customerId; + try { + customerId = new CustomerId(UUID.fromString(publicId)); + } catch (Exception e) { + throw new BadCredentialsException("Refresh token is not valid"); + } + Customer publicCustomer = customerService.findCustomerById(customerId); + if (publicCustomer == null) { + throw new UsernameNotFoundException("Public entity not found by refresh token"); + } + boolean isPublic = false; + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) { + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean(); + } + if (!isPublic) { + throw new BadCredentialsException("Refresh token is not valid"); + } + User user = new User(new UserId(UUIDBased.EMPTY)); + user.setTenantId(publicCustomer.getTenantId()); + user.setCustomerId(publicCustomer.getId()); + user.setEmail(publicId); + user.setAuthority(Authority.CUSTOMER_USER); + user.setFirstName("Public"); + user.setLastName("Public"); + + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId); + + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal); + + return securityUser; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java new file mode 100644 index 0000000000..54ef093804 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.rest; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PublicLoginRequest { + + private String publicId; + + @JsonCreator + public PublicLoginRequest(@JsonProperty("publicId") String publicId) { + this.publicId = publicId; + } + + public String getPublicId() { + return publicId; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 686bb465c4..af10674294 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; + +import java.util.UUID; @Component public class RestAuthenticationProvider implements AuthenticationProvider { private final BCryptPasswordEncoder encoder; private final UserService userService; + private final CustomerService customerService; @Autowired - public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) { + public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) { this.userService = userService; + this.customerService = customerService; this.encoder = encoder; } @@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider { public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.notNull(authentication, "No authentication data provided"); - String username = (String) authentication.getPrincipal(); - String password = (String) authentication.getCredentials(); + Object principal = authentication.getPrincipal(); + if (!(principal instanceof UserPrincipal)) { + throw new BadCredentialsException("Authentication Failed. Bad user principal."); + } + UserPrincipal userPrincipal = (UserPrincipal) principal; + if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { + String username = userPrincipal.getValue(); + String password = (String) authentication.getCredentials(); + return authenticateByUsernameAndPassword(userPrincipal, username, password); + } else { + String publicId = userPrincipal.getValue(); + return authenticateByPublicId(userPrincipal, publicId); + } + } + + private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) { User user = userService.findUserByEmail(username); if (user == null) { throw new UsernameNotFoundException("User not found: " + username); @@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); + } + + private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { + CustomerId customerId; + try { + customerId = new CustomerId(UUID.fromString(publicId)); + } catch (Exception e) { + throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); + } + Customer publicCustomer = customerService.findCustomerById(customerId); + if (publicCustomer == null) { + throw new UsernameNotFoundException("Public entity not found: " + publicId); + } + boolean isPublic = false; + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) { + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean(); + } + if (!isPublic) { + throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); + } + User user = new User(new UserId(UUIDBased.EMPTY)); + user.setTenantId(publicCustomer.getTenantId()); + user.setCustomerId(publicCustomer.getId()); + user.setEmail(publicId); + user.setAuthority(Authority.CUSTOMER_USER); + user.setFirstName("Public"); + user.setLastName("Public"); + + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal); return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java index d191905056..c32b1f221e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java @@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; +import org.thingsboard.server.service.security.model.UserPrincipal; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF throw new AuthenticationServiceException("Username or Password not provided"); } - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword()); return this.getAuthenticationManager().authenticate(token); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java new file mode 100644 index 0000000000..3a3b7cb2f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; +import org.thingsboard.server.service.security.model.UserPrincipal; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { + private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class); + + private final AuthenticationSuccessHandler successHandler; + private final AuthenticationFailureHandler failureHandler; + + private final ObjectMapper objectMapper; + + public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, + AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { + super(defaultProcessUrl); + this.successHandler = successHandler; + this.failureHandler = failureHandler; + this.objectMapper = mapper; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + if (!HttpMethod.POST.name().equals(request.getMethod())) { + if(logger.isDebugEnabled()) { + logger.debug("Authentication method not supported. Request method: " + request.getMethod()); + } + throw new AuthMethodNotSupportedException("Authentication method not supported"); + } + + PublicLoginRequest loginRequest; + try { + loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class); + } catch (Exception e) { + throw new AuthenticationServiceException("Invalid public login request payload"); + } + + if (StringUtils.isBlank(loginRequest.getPublicId())) { + throw new AuthenticationServiceException("Public Id is not provided"); + } + + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, ""); + + return this.getAuthenticationManager().authenticate(token); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authResult) throws IOException, ServletException { + successHandler.onAuthenticationSuccess(request, response, authResult); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + SecurityContextHolder.clearContext(); + failureHandler.onAuthenticationFailure(request, response, failed); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index 2da3a97366..0839695261 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -30,6 +30,7 @@ public class SecurityUser extends User { private Collection authorities; private boolean enabled; + private UserPrincipal userPrincipal; public SecurityUser() { super(); @@ -39,9 +40,10 @@ public class SecurityUser extends User { super(id); } - public SecurityUser(User user, boolean enabled) { + public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) { super(user); this.enabled = enabled; + this.userPrincipal = userPrincipal; } public Collection getAuthorities() { @@ -57,8 +59,16 @@ public class SecurityUser extends User { return enabled; } + public UserPrincipal getUserPrincipal() { + return userPrincipal; + } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public void setUserPrincipal(UserPrincipal userPrincipal) { + this.userPrincipal = userPrincipal; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java new file mode 100644 index 0000000000..6f23783c0b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.service.security.model; + +public class UserPrincipal { + + private final Type type; + private final String value; + + public UserPrincipal(Type type, String value) { + this.type = type; + this.value = value; + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + public enum Type { + USER_NAME, + PUBLIC_ID + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 8ade253412..20de6d8e73 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import java.util.Arrays; import java.util.List; @@ -43,6 +44,7 @@ public class JwtTokenFactory { private static final String FIRST_NAME = "firstName"; private static final String LAST_NAME = "lastName"; private static final String ENABLED = "enabled"; + private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; @@ -63,12 +65,15 @@ public class JwtTokenFactory { if (securityUser.getAuthority() == null) throw new IllegalArgumentException("User doesn't have any privileges"); - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); + UserPrincipal principal = securityUser.getUserPrincipal(); + String subject = principal.getValue(); + Claims claims = Jwts.claims().setSubject(subject); claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList())); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(FIRST_NAME, securityUser.getFirstName()); claims.put(LAST_NAME, securityUser.getLastName()); claims.put(ENABLED, securityUser.isEnabled()); + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); if (securityUser.getTenantId() != null) { claims.put(TENANT_ID, securityUser.getTenantId().getId().toString()); } @@ -104,6 +109,9 @@ public class JwtTokenFactory { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); + securityUser.setUserPrincipal(principal); String tenantId = claims.get(TENANT_ID, String.class); if (tenantId != null) { securityUser.setTenantId(new TenantId(UUID.fromString(tenantId))); @@ -123,9 +131,11 @@ public class JwtTokenFactory { DateTime currentTime = new DateTime(); - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); + UserPrincipal principal = securityUser.getUserPrincipal(); + Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name())); claims.put(USER_ID, securityUser.getId().getId().toString()); + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); String token = Jwts.builder() .setClaims(claims) @@ -150,8 +160,10 @@ public class JwtTokenFactory { if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) { throw new IllegalArgumentException("Invalid Refresh Token scope"); } + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); - securityUser.setEmail(subject); + securityUser.setUserPrincipal(principal); return securityUser; } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 778406aadf..e64e9cd95c 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -188,3 +188,25 @@ cache: updates: # Enable/disable updates checking. enabled: "${UPDATES_ENABLED:true}" + + # spring CORS configuration +spring.mvc.cors: + mappings: + # Intercept path + "/api/auth/**": + #Comma-separated list of origins to allow. '*' allows all origins. When not set,CORS support is disabled. + allowed-origins: "*" + #Comma-separated list of methods to allow. '*' allows all methods. + allowed-methods: "POST,GET,OPTIONS" + #Comma-separated list of headers to allow in a request. '*' allows all headers. + allowed-headers: "*" + #How long, in seconds, the response from a pre-flight request can be cached by clients. + max-age: "1800" + #Set whether credentials are supported. When not set, credentials are not supported. + allow-credentials: "true" + "/api/v1/**": + allowed-origins: "*" + allowed-methods: "*" + allowed-headers: "*" + max-age: "1800" + allow-credentials: "true" diff --git a/application/src/main/scripts/windows/install.bat b/application/src/main/scripts/windows/install.bat index 562661435b..d124e9a358 100644 --- a/application/src/main/scripts/windows/install.bat +++ b/application/src/main/scripts/windows/install.bat @@ -2,9 +2,6 @@ setlocal ENABLEEXTENSIONS -IF %PROCESSOR_ARCHITECTURE%==AMD64 GOTO CHECK_JAVA_64 -IF %PROCESSOR_ARCHITECTURE%==x86 GOTO CHECK_JAVA_32 - @ECHO Detecting Java version installed. :CHECK_JAVA_64 @ECHO Detecting if it is 64 bit machine @@ -36,7 +33,6 @@ if defined ValueName ( ) IF NOT "%JRE_PATH2%" == "" GOTO JAVA_INSTALLED -IF "%JRE_PATH2%" == "" GOTO JAVA_NOT_INSTALLED :CHECK_JAVA_32 @ECHO Detecting if it is 32 bit machine diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index 896c549d08..b1076e0413 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java @@ -288,7 +288,7 @@ public class CustomerControllerTest extends AbstractControllerTest { for (int i=0;i<143;i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title1+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); @@ -299,7 +299,7 @@ public class CustomerControllerTest extends AbstractControllerTest { for (int i=0;i<175;i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title2+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index ca4abfeab4..5d72076ee6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -149,7 +149,7 @@ public class TenantControllerTest extends AbstractControllerTest { List tenantsTitle1 = new ArrayList<>(); for (int i=0;i<134;i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title1+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); @@ -159,7 +159,7 @@ public class TenantControllerTest extends AbstractControllerTest { List tenantsTitle2 = new ArrayList<>(); for (int i=0;i<127;i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); String title = title2+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); diff --git a/common/data/pom.xml b/common/data/pom.xml index 537a30619f..aca4c9c0d0 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 3f3f0a98d5..01d3b45b32 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index 246ee7bad7..9c7b51e1c8 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/common/transport/pom.xml b/common/transport/pom.xml index d0eb1a5e72..4a2bb52a2f 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 2e8afb5ff7..146b8c197e 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java index 6d7247b3a6..5639044127 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java @@ -16,12 +16,14 @@ package org.thingsboard.server.dao.customer; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.model.CustomerEntity; +import org.thingsboard.server.dao.model.DeviceEntity; /** * The Interface CustomerDao. @@ -44,5 +46,14 @@ public interface CustomerDao extends Dao { * @return the list of customer objects */ List findCustomersByTenantId(UUID tenantId, TextPageLink pageLink); + + /** + * Find customers by tenantId and customer title. + * + * @param tenantId the tenantId + * @param title the customer title + * @return the optional customer object + */ + Optional findCustomersByTenantIdAndTitle(UUID tenantId, String title); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java index 7b53836274..25c111642a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java @@ -16,11 +16,18 @@ package org.thingsboard.server.dao.customer; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY; + import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.UUID; +import com.datastax.driver.core.querybuilder.Select; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; @@ -60,4 +67,13 @@ public class CustomerDaoImpl extends AbstractSearchTextDao imple return customerEntities; } + @Override + public Optional findCustomersByTenantIdAndTitle(UUID tenantId, String title) { + Select select = select().from(CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME); + Select.Where query = select.where(); + query.and(eq(CUSTOMER_TENANT_ID_PROPERTY, tenantId)); + query.and(eq(CUSTOMER_TITLE_PROPERTY, title)); + return Optional.ofNullable(findOneByStatement(query)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index 70321a20b0..01f4f99995 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -28,6 +28,8 @@ public interface CustomerService { public Customer saveCustomer(Customer customer); public void deleteCustomer(CustomerId customerId); + + public Customer findOrCreatePublicCustomer(TenantId tenantId); public TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 18ead42d5a..a80c6e84b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -18,8 +18,12 @@ package org.thingsboard.server.dao.customer; import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.getData; +import java.io.IOException; import java.util.List; +import java.util.Optional; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.Customer; @@ -46,6 +50,8 @@ import org.thingsboard.server.dao.service.Validator; @Slf4j public class CustomerServiceImpl implements CustomerService { + private static final String PUBLIC_CUSTOMER_TITLE = "Public"; + @Autowired private CustomerDao customerDao; @@ -80,7 +86,7 @@ public class CustomerServiceImpl implements CustomerService { @Override public void deleteCustomer(CustomerId customerId) { log.trace("Executing deleteCustomer [{}]", customerId); - Validator.validateId(customerId, "Incorrect tenantId " + customerId); + Validator.validateId(customerId, "Incorrect customerId " + customerId); Customer customer = findCustomerById(customerId); if (customer == null) { throw new IncorrectParameterException("Unable to delete non-existent customer."); @@ -91,6 +97,27 @@ public class CustomerServiceImpl implements CustomerService { customerDao.removeById(customerId.getId()); } + @Override + public Customer findOrCreatePublicCustomer(TenantId tenantId) { + log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId); + Validator.validateId(tenantId, "Incorrect customerId " + tenantId); + Optional publicCustomerEntity = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE); + if (publicCustomerEntity.isPresent()) { + return getData(publicCustomerEntity.get()); + } else { + Customer publicCustomer = new Customer(); + publicCustomer.setTenantId(tenantId); + publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE); + try { + publicCustomer.setAdditionalInfo(new ObjectMapper().readValue("{ \"isPublic\": true }", JsonNode.class)); + } catch (IOException e) { + throw new IncorrectParameterException("Unable to create public customer.", e); + } + CustomerEntity customerEntity = customerDao.save(publicCustomer); + return getData(customerEntity); + } + } + @Override public TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) { log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService { private DataValidator customerValidator = new DataValidator() { + + @Override + protected void validateCreate(Customer customer) { + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent( + c -> { + throw new DataValidationException("Customer with such title already exists!"); + } + ); + } + + @Override + protected void validateUpdate(Customer customer) { + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent( + c -> { + if (!c.getId().equals(customer.getUuidId())) { + throw new DataValidationException("Customer with such title already exists!"); + } + } + ); + } + @Override protected void validateDataImpl(Customer customer) { if (StringUtils.isEmpty(customer.getTitle())) { throw new DataValidationException("Customer title should be specified!"); } + if (customer.getTitle().equals(PUBLIC_CUSTOMER_TITLE)) { + throw new DataValidationException("'Public' title for customer is system reserved!"); + } if (!StringUtils.isEmpty(customer.getEmail())) { validateEmail(customer.getEmail()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index d3ed5d1dd1..9c9c66b376 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -111,6 +111,7 @@ public class ModelConstants { public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text"; + public static final String CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME = "customer_by_tenant_and_title"; /** * Cassandra device constants. diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql index febd1e94ef..6e4543026b 100644 --- a/dao/src/main/resources/schema.cql +++ b/dao/src/main/resources/schema.cql @@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer ( PRIMARY KEY (id, tenant_id) ); +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_title AS + SELECT * + from thingsboard.customer + WHERE tenant_id IS NOT NULL AND title IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, title, id ) + WITH CLUSTERING ORDER BY ( title ASC, id DESC ); + CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS SELECT * from thingsboard.customer diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql index cd00b1d97b..fc7c3155d9 100644 --- a/dao/src/main/resources/system-data.cql +++ b/dao/src/main/resources/system-data.cql @@ -80,7 +80,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel', INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table', -'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Timestamp{{ h.dataKey.label }}
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
\n
\n \n \n
\n
","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}"}', +'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"\n","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}"}', 'Timeseries table' ); INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java index fd0b20f1ad..723be798b0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java @@ -34,11 +34,11 @@ import org.junit.Test; import com.datastax.driver.core.utils.UUIDs; public class CustomerServiceImplTest extends AbstractServiceTest { - + private IdComparator idComparator = new IdComparator<>(); - + private TenantId tenantId; - + @Before public void before() { Tenant tenant = new Tenant(); @@ -59,23 +59,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest { customer.setTenantId(tenantId); customer.setTitle("My customer"); Customer savedCustomer = customerService.saveCustomer(customer); - + Assert.assertNotNull(savedCustomer); Assert.assertNotNull(savedCustomer.getId()); Assert.assertTrue(savedCustomer.getCreatedTime() > 0); Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId()); Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle()); - - + + savedCustomer.setTitle("My new customer"); - + customerService.saveCustomer(savedCustomer); Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle()); - + customerService.deleteCustomer(savedCustomer.getId()); } - + @Test public void testFindCustomerById() { Customer customer = new Customer(); @@ -87,21 +87,21 @@ public class CustomerServiceImplTest extends AbstractServiceTest { Assert.assertEquals(savedCustomer, foundCustomer); customerService.deleteCustomer(savedCustomer.getId()); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithEmptyTitle() { Customer customer = new Customer(); customer.setTenantId(tenantId); customerService.saveCustomer(customer); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithEmptyTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); customerService.saveCustomer(customer); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithInvalidTenant() { Customer customer = new Customer(); @@ -109,7 +109,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest { customer.setTenantId(new TenantId(UUIDs.timeBased())); customerService.saveCustomer(customer); } - + @Test(expected = DataValidationException.class) public void testSaveCustomerWithInvalidEmail() { Customer customer = new Customer(); @@ -118,7 +118,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest { customer.setEmail("invalid@mail"); customerService.saveCustomer(customer); } - + @Test public void testDeleteCustomer() { Customer customer = new Customer(); @@ -129,23 +129,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest { Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId()); Assert.assertNull(foundCustomer); } - + @Test public void testFindCustomersByTenantId() { Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); tenant = tenantService.saveTenant(tenant); - + TenantId tenantId = tenant.getId(); - + List customers = new ArrayList<>(); - for (int i=0;i<135;i++) { + for (int i = 0; i < 135; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - customer.setTitle("Customer"+i); + customer.setTitle("Customer" + i); customers.add(customerService.saveCustomer(customer)); } - + List loadedCustomers = new ArrayList<>(); TextPageLink pageLink = new TextPageLink(23); TextPageData pageData = null; @@ -156,47 +156,47 @@ public class CustomerServiceImplTest extends AbstractServiceTest { pageLink = pageData.getNextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(customers, idComparator); Collections.sort(loadedCustomers, idComparator); - + Assert.assertEquals(customers, loadedCustomers); - + customerService.deleteCustomersByTenantId(tenantId); pageLink = new TextPageLink(33); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); - + tenantService.deleteTenant(tenantId); } - + @Test public void testFindCustomersByTenantIdAndTitle() { String title1 = "Customer title 1"; List customersTitle1 = new ArrayList<>(); - for (int i=0;i<143;i++) { + for (int i = 0; i < 143; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title1+suffix; + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); + String title = title1 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); customersTitle1.add(customerService.saveCustomer(customer)); } String title2 = "Customer title 2"; List customersTitle2 = new ArrayList<>(); - for (int i=0;i<175;i++) { + for (int i = 0; i < 175; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title2+suffix; + String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10)); + String title = title2 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); customersTitle2.add(customerService.saveCustomer(customer)); } - + List loadedCustomersTitle1 = new ArrayList<>(); TextPageLink pageLink = new TextPageLink(15, title1); TextPageData pageData = null; @@ -207,12 +207,12 @@ public class CustomerServiceImplTest extends AbstractServiceTest { pageLink = pageData.getNextPageLink(); } } while (pageData.hasNext()); - + Collections.sort(customersTitle1, idComparator); Collections.sort(loadedCustomersTitle1, idComparator); - + Assert.assertEquals(customersTitle1, loadedCustomersTitle1); - + List loadedCustomersTitle2 = new ArrayList<>(); pageLink = new TextPageLink(4, title2); do { @@ -225,22 +225,22 @@ public class CustomerServiceImplTest extends AbstractServiceTest { Collections.sort(customersTitle2, idComparator); Collections.sort(loadedCustomersTitle2, idComparator); - + Assert.assertEquals(customersTitle2, loadedCustomersTitle2); for (Customer customer : loadedCustomersTitle1) { customerService.deleteCustomer(customer.getId()); } - + pageLink = new TextPageLink(4, title1); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); - + for (Customer customer : loadedCustomersTitle2) { customerService.deleteCustomer(customer.getId()); } - + pageLink = new TextPageLink(4, title2); pageData = customerService.findCustomersByTenantId(tenantId, pageLink); Assert.assertFalse(pageData.hasNext()); diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml index 71e19538c9..b99d274cf0 100644 --- a/extensions-api/pom.xml +++ b/extensions-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml index 4664b150a2..9ccc087af2 100644 --- a/extensions-core/pom.xml +++ b/extensions-core/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java index ea7a185b20..8cb1bf364e 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.extensions.core.plugin.telemetry; -import com.sun.javafx.collections.MappingChange; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.DataConstants; @@ -292,7 +291,6 @@ public class SubscriptionManager { } else { log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress); subscriptionIterator.remove(); - //TODO: onUpdate state of subscription by WsSessionId and other maps. } } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java index c7cb47c2a1..633f620e97 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java @@ -139,7 +139,12 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { public void onSuccess(PluginContext ctx, List data) { Map> result = new LinkedHashMap<>(); for (TsKvEntry entry : data) { - result.put(entry.getKey(), data.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList())); + List vList = result.get(entry.getKey()); + if (vList == null) { + vList = new ArrayList<>(); + result.put(entry.getKey(), vList); + } + vList.add(new TsData(entry.getTs(), entry.getValueAsString())); } msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK)); } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java index 4e7793e0d3..ac168d085b 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java @@ -30,7 +30,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg; import org.thingsboard.server.extensions.api.rules.RuleException; import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; /** @@ -51,7 +51,7 @@ public class TimePlugin extends AbstractPlugin implemen String reply; if (!StringUtils.isEmpty(format)) { - reply = "\"" + formatter.format(LocalDateTime.now()) + "\""; + reply = "\"" + formatter.format(ZonedDateTime.now()) + "\""; } else { reply = Long.toString(System.currentTimeMillis()); } diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml index 7e54bfb48d..9ec8238505 100644 --- a/extensions/extension-kafka/pom.xml +++ b/extensions/extension-kafka/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT extensions org.thingsboard.extensions diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml index 355d22d10e..93503e265a 100644 --- a/extensions/extension-rabbitmq/pom.xml +++ b/extensions/extension-rabbitmq/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT extensions org.thingsboard.extensions diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml index 40a446e9b0..9646e36595 100644 --- a/extensions/extension-rest-api-call/pom.xml +++ b/extensions/extension-rest-api-call/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT extensions org.thingsboard.extensions diff --git a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java index 797ebf5cb7..a07c80d74e 100644 --- a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java +++ b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java @@ -55,6 +55,13 @@ public class RestApiCallPlugin extends AbstractPlugin { + log.debug("Adding header to request object. Key = {}, Value = {}", h.getKey(), h.getValue()); + this.headers.add(h.getKey(), h.getValue()); + }); + } + init(); } diff --git a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java index 2b20e9bec1..cfd23b8c20 100644 --- a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java +++ b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java @@ -16,6 +16,9 @@ package org.thingsboard.server.extensions.rest.plugin; import lombok.Data; +import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties; + +import java.util.List; @Data public class RestApiCallPluginConfiguration { @@ -27,4 +30,6 @@ public class RestApiCallPluginConfiguration { private String userName; private String password; + + private List headers; } diff --git a/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json b/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json index e0e4d1814d..06f8559cf0 100644 --- a/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json +++ b/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json @@ -30,6 +30,24 @@ "password": { "title": "Password", "type": "string" + }, + "headers": { + "title": "Request Headers", + "type": "array", + "items": { + "title": "Request Header", + "type": "object", + "properties": { + "key": { + "title": "Key", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + } } }, "required": [ @@ -62,6 +80,7 @@ { "key": "password", "type": "password" - } + }, + "headers" ] } \ No newline at end of file diff --git a/extensions/pom.xml b/extensions/pom.xml index 44830b1afc..63b34cf7eb 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/pom.xml b/pom.xml index b85f6929d6..6324712dfd 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 1.2.2 + 1.2.3-SNAPSHOT pom Thingsboard diff --git a/tools/pom.xml b/tools/pom.xml index 94b0066d68..96235e2cbe 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard @@ -54,40 +54,4 @@ - - - - org.apache.maven.plugins - maven-shade-plugin - - - package - - shade - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - org.thingsboard.client.tools.MqttStressTestTool - - - - - - - - - diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java index f742e6131e..d635dbcb3e 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java +++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java @@ -27,6 +27,7 @@ import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -76,6 +77,12 @@ public class RestClient implements ClientHttpRequestInterceptor { return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); } + + public Device assignDevice(CustomerId customerId, DeviceId deviceId) { + return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class, + customerId.toString(), deviceId.toString()).getBody(); + } + public DeviceCredentials getCredentials(DeviceId id) { return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody(); } diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 426337d50d..ef3953ded2 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 017cbad5e2..16e5626157 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index fe3f734059..15c640c0a2 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 76cdf1ac5d..36868c5906 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -247,7 +247,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private MqttMessage createUnSubAckMessage(int msgId) { MqttFixedHeader mqttFixedHeader = - new MqttFixedHeader(SUBACK, false, AT_LEAST_ONCE, false, 0); + new MqttFixedHeader(UNSUBACK, false, AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); } diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java index 9444cdd1c0..af109f652b 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java @@ -40,6 +40,8 @@ import java.security.cert.CertificateException; */ public class MqttTransportServerInitializer extends ChannelInitializer { + private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024; + private final SessionMsgProcessor processor; private final DeviceService deviceService; private final DeviceAuthService authService; @@ -63,7 +65,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/ui/package.json b/ui/package.json index 37152a58a5..ddcc96e4f7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard", "private": true, - "version": "1.2.2", + "version": "1.2.3", "description": "Thingsboard UI", "licenses": [ { @@ -33,6 +33,7 @@ "angular-messages": "1.5.8", "angular-route": "1.5.8", "angular-sanitize": "1.5.8", + "angular-socialshare": "^2.3.8", "angular-storage": "0.0.15", "angular-touch": "1.5.8", "angular-translate": "2.13.1", @@ -49,6 +50,7 @@ "clipboard": "^1.5.15", "compass-sass-mixins": "^0.12.7", "flot": "git://github.com/flot/flot.git#0.9-work", + "flot-curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", "font-awesome": "^4.6.3", "javascript-detect-element-resize": "^0.5.3", "jquery": "^3.1.0", diff --git a/ui/pom.xml b/ui/pom.xml index e9e8f10566..1ffaa6721f 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 1.2.2 + 1.2.3-SNAPSHOT thingsboard org.thingsboard diff --git a/ui/src/app/api/customer.service.js b/ui/src/app/api/customer.service.js index 5524e91c04..b36b44afdf 100644 --- a/ui/src/app/api/customer.service.js +++ b/ui/src/app/api/customer.service.js @@ -18,11 +18,14 @@ export default angular.module('thingsboard.api.customer', []) .name; /*@ngInject*/ -function CustomerService($http, $q) { +function CustomerService($http, $q, types) { var service = { getCustomers: getCustomers, getCustomer: getCustomer, + getShortCustomerInfo: getShortCustomerInfo, + applyAssignedCustomersInfo: applyAssignedCustomersInfo, + applyAssignedCustomerInfo: applyAssignedCustomerInfo, deleteCustomer: deleteCustomer, saveCustomer: saveCustomer } @@ -60,6 +63,88 @@ function CustomerService($http, $q) { return deferred.promise; } + function getShortCustomerInfo(customerId) { + var deferred = $q.defer(); + var url = '/api/customer/' + customerId + '/shortInfo'; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail(response) { + deferred.reject(response.data); + }); + return deferred.promise; + } + + function applyAssignedCustomersInfo(items) { + var deferred = $q.defer(); + var assignedCustomersMap = {}; + function loadNextCustomerInfoOrComplete(i) { + i++; + if (i < items.length) { + loadNextCustomerInfo(i); + } else { + deferred.resolve(items); + } + } + + function loadNextCustomerInfo(i) { + var item = items[i]; + item.assignedCustomer = {}; + if (item.customerId && item.customerId.id != types.id.nullUid) { + item.assignedCustomer.id = item.customerId.id; + var assignedCustomer = assignedCustomersMap[item.customerId.id]; + if (assignedCustomer){ + item.assignedCustomer = assignedCustomer; + loadNextCustomerInfoOrComplete(i); + } else { + getShortCustomerInfo(item.customerId.id).then( + function success(info) { + assignedCustomer = { + id: item.customerId.id, + title: info.title, + isPublic: info.isPublic + }; + assignedCustomersMap[assignedCustomer.id] = assignedCustomer; + item.assignedCustomer = assignedCustomer; + loadNextCustomerInfoOrComplete(i); + }, + function fail() { + loadNextCustomerInfoOrComplete(i); + } + ); + } + } else { + loadNextCustomerInfoOrComplete(i); + } + } + if (items.length > 0) { + loadNextCustomerInfo(0); + } else { + deferred.resolve(items); + } + return deferred.promise; + } + + function applyAssignedCustomerInfo(items, customerId) { + var deferred = $q.defer(); + getShortCustomerInfo(customerId).then( + function success(info) { + var assignedCustomer = { + id: customerId, + title: info.title, + isPublic: info.isPublic + } + items.forEach(function(item) { + item.assignedCustomer = assignedCustomer; + }); + deferred.resolve(items); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + function saveCustomer(customer) { var deferred = $q.defer(); var url = '/api/customer'; diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js index be450ed477..b2a7897916 100644 --- a/ui/src/app/api/dashboard.service.js +++ b/ui/src/app/api/dashboard.service.js @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', []) .factory('dashboardService', DashboardService).name; /*@ngInject*/ -function DashboardService($http, $q) { +function DashboardService($http, $q, $location, customerService) { var service = { assignDashboardToCustomer: assignDashboardToCustomer, @@ -27,7 +27,9 @@ function DashboardService($http, $q) { getTenantDashboards: getTenantDashboards, deleteDashboard: deleteDashboard, saveDashboard: saveDashboard, - unassignDashboardFromCustomer: unassignDashboardFromCustomer + unassignDashboardFromCustomer: unassignDashboardFromCustomer, + makeDashboardPublic: makeDashboardPublic, + getPublicDashboardLink: getPublicDashboardLink } return service; @@ -45,7 +47,15 @@ function DashboardService($http, $q) { url += '&textOffset=' + pageLink.textOffset; } $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + customerService.applyAssignedCustomersInfo(response.data.data).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); }, function fail() { deferred.reject(); }); @@ -65,7 +75,15 @@ function DashboardService($http, $q) { url += '&textOffset=' + pageLink.textOffset; } $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); }, function fail() { deferred.reject(); }); @@ -92,8 +110,8 @@ function DashboardService($http, $q) { var url = '/api/dashboard/' + dashboardId; $http.get(url, null).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -103,8 +121,8 @@ function DashboardService($http, $q) { var url = '/api/dashboard'; $http.post(url, dashboard).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -114,8 +132,8 @@ function DashboardService($http, $q) { var url = '/api/dashboard/' + dashboardId; $http.delete(url).then(function success() { deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -123,10 +141,10 @@ function DashboardService($http, $q) { function assignDashboardToCustomer(customerId, dashboardId) { var deferred = $q.defer(); var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; - $http.post(url, null).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + $http.post(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -134,12 +152,33 @@ function DashboardService($http, $q) { function unassignDashboardFromCustomer(dashboardId) { var deferred = $q.defer(); var url = '/api/customer/dashboard/' + dashboardId; - $http.delete(url).then(function success() { - deferred.resolve(); - }, function fail(response) { - deferred.reject(response.data); + $http.delete(url).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function makeDashboardPublic(dashboardId) { + var deferred = $q.defer(); + var url = '/api/customer/public/dashboard/' + dashboardId; + $http.post(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); }); return deferred.promise; } + function getPublicDashboardLink(dashboard) { + var url = $location.protocol() + '://' + $location.host(); + var port = $location.port(); + if (port != 80 && port != 443) { + url += ":" + port; + } + url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.customerId.id; + return url; + } + } diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js index 6041cbd946..f87d547dbf 100644 --- a/ui/src/app/api/datasource.service.js +++ b/ui/src/app/api/datasource.service.js @@ -58,10 +58,10 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t var datasourceSubscription = { datasourceType: datasource.type, dataKeys: subscriptionDataKeys, - type: listener.widget.type + type: listener.subscriptionType }; - if (listener.widget.type === types.widgetType.timeseries.value) { + if (listener.subscriptionType === types.widgetType.timeseries.value) { datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow); } if (datasourceSubscription.datasourceType === types.datasourceType.device) { diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js index 2c4a100ec4..ea197a1bc5 100644 --- a/ui/src/app/api/device.service.js +++ b/ui/src/app/api/device.service.js @@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes]) .name; /*@ngInject*/ -function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) { +function DeviceService($http, $q, $filter, userService, customerService, telemetryWebsocketService, types) { var deviceAttributesSubscriptionMap = {}; @@ -33,6 +33,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic getDevices: getDevices, processDeviceAliases: processDeviceAliases, checkDeviceAlias: checkDeviceAlias, + fetchAliasDeviceByNameFilter: fetchAliasDeviceByNameFilter, getDeviceCredentials: getDeviceCredentials, getDeviceKeys: getDeviceKeys, getDeviceTimeseriesValues: getDeviceTimeseriesValues, @@ -40,6 +41,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic saveDevice: saveDevice, saveDeviceCredentials: saveDeviceCredentials, unassignDeviceFromCustomer: unassignDeviceFromCustomer, + makeDevicePublic: makeDevicePublic, getDeviceAttributes: getDeviceAttributes, subscribeForDeviceAttributes: subscribeForDeviceAttributes, unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes, @@ -51,7 +53,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic return service; - function getTenantDevices(pageLink) { + function getTenantDevices(pageLink, applyCustomersInfo, config) { var deferred = $q.defer(); var url = '/api/tenant/devices?limit=' + pageLink.limit; if (angular.isDefined(pageLink.textSearch)) { @@ -63,15 +65,27 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic if (angular.isDefined(pageLink.textOffset)) { url += '&textOffset=' + pageLink.textOffset; } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + $http.get(url, config).then(function success(response) { + if (applyCustomersInfo) { + customerService.applyAssignedCustomersInfo(response.data.data).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(response.data); + } }, function fail() { deferred.reject(); }); return deferred.promise; } - function getCustomerDevices(customerId, pageLink) { + function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) { var deferred = $q.defer(); var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit; if (angular.isDefined(pageLink.textSearch)) { @@ -83,18 +97,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic if (angular.isDefined(pageLink.textOffset)) { url += '&textOffset=' + pageLink.textOffset; } - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + $http.get(url, config).then(function success(response) { + if (applyCustomersInfo) { + customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(response.data); + } }, function fail() { deferred.reject(); }); + return deferred.promise; } - function getDevice(deviceId, ignoreErrors) { + function getDevice(deviceId, ignoreErrors, config) { var deferred = $q.defer(); var url = '/api/device/' + deviceId; - $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) { + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.get(url, config).then(function success(response) { deferred.resolve(response.data); }, function fail(response) { deferred.reject(response.data); @@ -102,7 +133,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic return deferred.promise; } - function getDevices(deviceIds) { + function getDevices(deviceIds, config) { var deferred = $q.defer(); var ids = ''; for (var i=0;i -1) { + this.registrations.splice(index, 1); + } + } + } + + initRpc() { + + if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) { + this.targetDeviceAliasId = this.targetDeviceAliasIds[0]; + if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) { + this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId; + } + var subscription = this; + var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () { + var deviceId = null; + if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) { + deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId; + } + if (!angular.equals(deviceId, subscription.targetDeviceId)) { + subscription.targetDeviceId = deviceId; + if (subscription.targetDeviceId) { + subscription.rpcEnabled = true; + } else { + subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false; + } + subscription.callbacks.rpcStateChanged(subscription); + } + }); + this.registrations.push(registration); + } else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) { + this.targetDeviceId = this.targetDeviceIds[0]; + } + + if (this.targetDeviceId) { + this.rpcEnabled = true; + } else { + this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false; + } + this.callbacks.rpcStateChanged(this); + } + + clearRpcError() { + this.rpcRejection = null; + this.rpcErrorText = null; + this.callbacks.onRpcErrorCleared(this); + } + + sendOneWayCommand(method, params, timeout) { + return this.sendCommand(true, method, params, timeout); + } + + sendTwoWayCommand(method, params, timeout) { + return this.sendCommand(false, method, params, timeout); + } + + sendCommand(oneWayElseTwoWay, method, params, timeout) { + if (!this.rpcEnabled) { + return this.ctx.$q.reject(); + } + + if (this.rpcRejection && this.rpcRejection.status !== 408) { + this.rpcRejection = null; + this.rpcErrorText = null; + this.callbacks.onRpcErrorCleared(this); + } + + var subscription = this; + + var requestBody = { + method: method, + params: params + }; + + if (timeout && timeout > 0) { + requestBody.timeout = timeout; + } + + var deferred = this.ctx.$q.defer(); + this.executingRpcRequest = true; + this.callbacks.rpcStateChanged(this); + if (this.ctx.$scope.widgetEditMode) { + this.ctx.$timeout(function() { + subscription.executingRpcRequest = false; + subscription.callbacks.rpcStateChanged(subscription); + if (oneWayElseTwoWay) { + deferred.resolve(); + } else { + deferred.resolve(requestBody); + } + }, 500); + } else { + this.executingPromises.push(deferred.promise); + var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand; + targetSendFunction(this.targetDeviceId, requestBody).then( + function success(responseBody) { + subscription.rpcRejection = null; + subscription.rpcErrorText = null; + var index = subscription.executingPromises.indexOf(deferred.promise); + if (index >= 0) { + subscription.executingPromises.splice( index, 1 ); + } + subscription.executingRpcRequest = subscription.executingPromises.length > 0; + subscription.callbacks.onRpcSuccess(subscription); + deferred.resolve(responseBody); + }, + function fail(rejection) { + var index = subscription.executingPromises.indexOf(deferred.promise); + if (index >= 0) { + subscription.executingPromises.splice( index, 1 ); + } + subscription.executingRpcRequest = subscription.executingPromises.length > 0; + subscription.callbacks.rpcStateChanged(subscription); + if (!subscription.executingRpcRequest || rejection.status === 408) { + subscription.rpcRejection = rejection; + if (rejection.status === 408) { + subscription.rpcErrorText = 'Device is offline.'; + } else { + subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + if (rejection.data && rejection.data.length > 0) { + subscription.rpcErrorText += '
'; + subscription.rpcErrorText += rejection.data; + } + } + subscription.callbacks.onRpcFailed(subscription); + } + deferred.reject(rejection); + } + ); + } + return deferred.promise; + } + + updateDataVisibility(index) { + var hidden = this.legendData.data[index].hidden; + if (hidden) { + this.hiddenData[index].data = this.data[index].data; + this.data[index].data = []; + } else { + this.data[index].data = this.hiddenData[index].data; + this.hiddenData[index].data = []; + } + this.onDataUpdated(); + } + + onDataUpdated(apply) { + if (this.cafs['dataUpdated']) { + this.cafs['dataUpdated'](); + this.cafs['dataUpdated'] = null; + } + var subscription = this; + this.cafs['dataUpdated'] = this.ctx.tbRaf(function() { + try { + subscription.callbacks.onDataUpdated(subscription, apply); + } catch (e) { + subscription.callbacks.onDataUpdateError(subscription, e); + } + }); + if (apply) { + this.ctx.$scope.$digest(); + } + } + + updateTimewindowConfig(newTimewindow) { + this.timeWindowConfig = newTimewindow; + } + + onResetTimewindow() { + if (this.useDashboardTimewindow) { + this.ctx.dashboardTimewindowApi.onResetTimewindow(); + } else { + if (this.originalTimewindow) { + this.stopWatchingTimewindow(); + this.timeWindowConfig = angular.copy(this.originalTimewindow); + this.originalTimewindow = null; + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); + this.unsubscribe(); + this.subscribe(); + this.startWatchingTimewindow(); + } + } + } + + onUpdateTimewindow(startTimeMs, endTimeMs) { + if (this.useDashboardTimewindow) { + this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); + } else { + this.stopWatchingTimewindow(); + if (!this.originalTimewindow) { + this.originalTimewindow = angular.copy(this.timeWindowConfig); + } + this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs); + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); + this.unsubscribe(); + this.subscribe(); + this.startWatchingTimewindow(); + } + } + + notifyDataLoading() { + this.loadingData = true; + this.callbacks.dataLoading(this); + } + + notifyDataLoaded() { + this.loadingData = false; + this.callbacks.dataLoading(this); + } + + updateTimewindow() { + this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; + if (this.subscriptionTimewindow.realtimeWindowMs) { + this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff; + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; + } else if (this.subscriptionTimewindow.fixedWindow) { + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; + } + } + + updateRealtimeSubscription(subscriptionTimewindow) { + if (subscriptionTimewindow) { + this.subscriptionTimewindow = subscriptionTimewindow; + } else { + this.subscriptionTimewindow = + this.ctx.timeService.createSubscriptionTimewindow( + this.timeWindowConfig, + this.timeWindow.stDiff); + } + this.updateTimewindow(); + return this.subscriptionTimewindow; + } + + dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { + this.notifyDataLoaded(); + var update = true; + var currentData; + if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) { + currentData = this.hiddenData[datasourceIndex + dataKeyIndex]; + } else { + currentData = this.data[datasourceIndex + dataKeyIndex]; + } + if (this.type === this.ctx.types.widgetType.latest.value) { + var prevData = currentData.data; + if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { + var prevValue = prevData[0][1]; + if (prevValue === sourceData.data[0][1]) { + update = false; + } + } + } + if (update) { + if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { + this.updateTimewindow(); + } + currentData.data = sourceData.data; + if (this.caulculateLegendData) { + this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); + } + this.onDataUpdated(apply); + } + } + + updateLegend(dataIndex, data, apply) { + var legendKeyData = this.legendData.data[dataIndex]; + if (this.legendConfig.showMin) { + legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units); + } + if (this.legendConfig.showMax) { + legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units); + } + if (this.legendConfig.showAvg) { + legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units); + } + if (this.legendConfig.showTotal) { + legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units); + } + this.callbacks.legendDataUpdated(this, apply !== false); + } + + subscribe() { + if (this.type === this.ctx.types.widgetType.rpc.value) { + return; + } + this.notifyDataLoading(); + if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) { + this.updateRealtimeSubscription(); + if (this.subscriptionTimewindow.fixedWindow) { + this.onDataUpdated(); + } + } + var index = 0; + for (var i = 0; i < this.datasources.length; i++) { + var datasource = this.datasources[i]; + if (angular.isFunction(datasource)) + continue; + var deviceId = null; + if (datasource.type === this.ctx.types.datasourceType.device) { + var aliasName = null; + var deviceName = null; + if (datasource.deviceId) { + deviceId = datasource.deviceId; + datasource.name = datasource.deviceName; + aliasName = datasource.deviceName; + deviceName = datasource.deviceName; + } else if (datasource.deviceAliasId && this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId]) { + deviceId = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId; + datasource.name = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; + aliasName = this.ctx.aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; + deviceName = ''; + var devicesInfo = this.ctx.aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId]; + for (var d = 0; d < devicesInfo.length; d++) { + if (devicesInfo[d].id === deviceId) { + deviceName = devicesInfo[d].name; + break; + } + } + } + } else { + datasource.name = datasource.name || this.ctx.types.datasourceType.function; + } + for (var dk = 0; dk < datasource.dataKeys.length; dk++) { + updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, deviceName, aliasName); + } + + var subscription = this; + + var listener = { + subscriptionType: this.type, + subscriptionTimewindow: this.subscriptionTimewindow, + datasource: datasource, + deviceId: deviceId, + dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) { + subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply); + }, + updateRealtimeSubscription: function () { + this.subscriptionTimewindow = subscription.updateRealtimeSubscription(); + return this.subscriptionTimewindow; + }, + setRealtimeSubscription: function (subscriptionTimewindow) { + subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow)); + }, + datasourceIndex: index + }; + + for (var a = 0; a < datasource.dataKeys.length; a++) { + this.data[index + a].data = []; + } + + index += datasource.dataKeys.length; + + this.datasourceListeners.push(listener); + this.ctx.datasourceService.subscribeToDatasource(listener); + } + } + + unsubscribe() { + if (this.type !== this.ctx.types.widgetType.rpc.value) { + for (var i = 0; i < this.datasourceListeners.length; i++) { + var listener = this.datasourceListeners[i]; + this.ctx.datasourceService.unsubscribeFromDatasource(listener); + } + this.datasourceListeners = []; + } + } + + checkSubscriptions() { + var subscriptionsChanged = false; + for (var i = 0; i < this.datasourceListeners.length; i++) { + var listener = this.datasourceListeners[i]; + var deviceId = null; + var aliasName = null; + if (listener.datasource.type === this.ctx.types.datasourceType.device) { + if (listener.datasource.deviceAliasId && + this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) { + deviceId = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId; + aliasName = this.ctx.aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias; + } + if (!angular.equals(deviceId, listener.deviceId) || + !angular.equals(aliasName, listener.datasource.name)) { + subscriptionsChanged = true; + break; + } + } + } + if (subscriptionsChanged) { + this.unsubscribe(); + this.subscribe(); + } + } + + destroy() { + this.unsubscribe(); + for (var cafId in this.cafs) { + if (this.cafs[cafId]) { + this.cafs[cafId](); + this.cafs[cafId] = null; + } + } + this.registrations.forEach(function (registration) { + registration(); + }); + this.registrations = []; + } + +} + +const varsRegex = /\$\{([^\}]*)\}/g; + +function updateDataKeyLabel(dataKey, dsName, deviceName, aliasName) { + var pattern = dataKey.pattern; + var label = dataKey.pattern; + var match = varsRegex.exec(pattern); + while (match !== null) { + var variable = match[0]; + var variableName = match[1]; + if (variableName === 'dsName') { + label = label.split(variable).join(dsName); + } else if (variableName === 'deviceName') { + label = label.split(variable).join(deviceName); + } else if (variableName === 'aliasName') { + label = label.split(variable).join(aliasName); + } + match = varsRegex.exec(pattern); + } + dataKey.label = label; +} + +function calculateMin(data) { + if (data.length > 0) { + var result = Number(data[0][1]); + for (var i=1;i 0) { + var result = Number(data[0][1]); + for (var i=1;i 0) { + return calculateTotal(data)/data.length; + } else { + return null; + } +} + +function calculateTotal(data) { + if (data.length > 0) { + var result = 0; + for (var i = 0; i < data.length; i++) { + result += Number(data[i][1]); + } + return result; + } else { + return null; + } +} diff --git a/ui/src/app/api/telemetry-websocket.service.js b/ui/src/app/api/telemetry-websocket.service.js index e03aafa087..e69c26493d 100644 --- a/ui/src/app/api/telemetry-websocket.service.js +++ b/ui/src/app/api/telemetry-websocket.service.js @@ -45,15 +45,21 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty socketCloseTimer, reconnectTimer; + var port = location.port; if (location.protocol === "https:") { + if (!port) { + port = "443"; + } telemetryUri = "wss:"; } else { + if (!port) { + port = "80"; + } telemetryUri = "ws:"; } - telemetryUri += "//" + location.hostname + ":" + location.port; + telemetryUri += "//" + location.hostname + ":" + port; telemetryUri += "/api/ws/plugins/telemetry"; - var service = { subscribe: subscribe, unsubscribe: unsubscribe diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index ad401df47f..4dc41f7d99 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -22,9 +22,10 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin, .name; /*@ngInject*/ -function UserService($http, $q, $rootScope, adminService, dashboardService, toast, store, jwtHelper, $translate, $state) { +function UserService($http, $q, $rootScope, adminService, dashboardService, loginService, toast, store, jwtHelper, $translate, $state, $location) { var currentUser = null, currentUserDetails = null, + lastPublicDashboardId = null, allowedDashboardIds = [], userLoaded = false; @@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas var service = { deleteUser: deleteUser, getAuthority: getAuthority, + isPublic: isPublic, + getPublicId: getPublicId, + parsePublicId: parsePublicId, isAuthenticated: isAuthenticated, getCurrentUser: getCurrentUser, getCustomerUsers: getCustomerUsers, @@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas updateAuthorizationHeader: updateAuthorizationHeader, gotoDefaultPlace: gotoDefaultPlace, forceDefaultPlace: forceDefaultPlace, - logout: logout + updateLastPublicDashboardId: updateLastPublicDashboardId, + logout: logout, + reloadUser: reloadUser } - loadUser(true).then(function success() { - notifyUserLoaded(); - }, function fail() { - notifyUserLoaded(); - }); + reloadUser(); return service; - function updateAndValidateToken(token, prefix) { + function reloadUser() { + userLoaded = false; + loadUser(true).then(function success() { + notifyUserLoaded(); + }, function fail() { + notifyUserLoaded(); + }); + } + + function updateAndValidateToken(token, prefix, notify) { var valid = false; var tokenData = jwtHelper.decodeToken(token); var issuedAt = tokenData.iat; @@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas valid = true; } } - if (!valid) { + if (!valid && notify) { $rootScope.$broadcast('unauthenticated'); } } @@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) { currentUser = null; currentUserDetails = null; + lastPublicDashboardId = null; allowedDashboardIds = []; if (!jwtToken) { clearTokenData(); @@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas $rootScope.$broadcast('unauthenticated', doLogout); } } else { - updateAndValidateToken(jwtToken, 'jwt_token'); - updateAndValidateToken(refreshToken, 'refresh_token'); + updateAndValidateToken(jwtToken, 'jwt_token', true); + updateAndValidateToken(refreshToken, 'refresh_token', true); if (notify) { loadUser(false).then(function success() { $rootScope.$broadcast('authenticated'); @@ -213,13 +225,58 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas } } + function isPublic() { + if (currentUser) { + return currentUser.isPublic; + } else { + return false; + } + } + + function getPublicId() { + if (isPublic()) { + return currentUser.sub; + } else { + return null; + } + } + + function parsePublicId() { + var token = getJwtToken(); + if (token) { + var tokenData = jwtHelper.decodeToken(token); + if (tokenData && tokenData.isPublic) { + return tokenData.sub; + } + } + return null; + } + function isUserLoaded() { return userLoaded; } function loadUser(doTokenRefresh) { + var deferred = $q.defer(); - if (!currentUser) { + + function fetchAllowedDashboardIds() { + var pageLink = {limit: 100}; + dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then( + function success(result) { + var dashboards = result.data; + for (var d=0;d -1) { + if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) { + if (to.name === 'home.profile') { + if (userHasProfile()) { return false; } else { return true; } + } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) { + return false; + } else { + return true; } } } @@ -395,11 +459,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas if (currentUser && isAuthenticated()) { var place = 'home.links'; if (currentUser.authority === 'CUSTOMER_USER') { - if (currentUserDetails && - currentUserDetails.additionalInfo && - currentUserDetails.additionalInfo.defaultDashboardId) { + if (userHasDefaultDashboard()) { place = 'home.dashboards.dashboard'; params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId}; + } else if (isPublic()) { + place = 'home.dashboards.dashboard'; + params = {dashboardId: lastPublicDashboardId}; } } else if (currentUser.authority === 'SYS_ADMIN') { adminService.checkUpdates().then( @@ -416,4 +481,27 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas } } + function userHasDefaultDashboard() { + return currentUserDetails && + currentUserDetails.additionalInfo && + currentUserDetails.additionalInfo.defaultDashboardId; + } + + function userForceFullscreen() { + return (currentUser && currentUser.isPublic) || + (currentUserDetails.additionalInfo && + currentUserDetails.additionalInfo.defaultDashboardFullscreen && + currentUserDetails.additionalInfo.defaultDashboardFullscreen === true); + } + + function userHasProfile() { + return currentUser && !currentUser.isPublic; + } + + function updateLastPublicDashboardId(dashboardId) { + if (isPublic()) { + lastPublicDashboardId = dashboardId; + } + } + } diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index fb8565c0fa..21c38e254c 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -17,7 +17,8 @@ import $ from 'jquery'; import moment from 'moment'; import tinycolor from 'tinycolor2'; -import thinsboardLedLight from '../components/led-light.directive'; +import thingsboardLedLight from '../components/led-light.directive'; +import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; import TbFlot from '../widget/lib/flot-widget'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; @@ -31,7 +32,8 @@ import cssjs from '../../vendor/css.js/css'; import thingsboardTypes from '../common/types.constant'; import thingsboardUtils from '../common/utils.service'; -export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thinsboardLedLight, thingsboardTypes, thingsboardUtils]) +export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, + thingsboardTypes, thingsboardUtils]) .factory('widgetService', WidgetService) .name; @@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ ' }\n\n' + + ' self.useCustomDatasources = function() {\n\n' + + + ' }\n\n' + + ' self.onResize = function() {\n\n' + ' }\n\n' + @@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); } + if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) { + result.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); + } else { + result.useCustomDatasources = false; + } return result; } catch (e) { utils.processWidgetException(e); @@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ if (widgetType.dataKeySettingsSchema) { widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; } + widgetInfo.useCustomDatasources = widgetType.useCustomDatasources; putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); deferred.resolve(widgetInfo); diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js index 9b2ad251a7..659f53e15a 100644 --- a/ui/src/app/app.config.js +++ b/ui/src/app/app.config.js @@ -16,6 +16,7 @@ import injectTapEventPlugin from 'react-tap-event-plugin'; import UrlHandler from './url.handler'; import addLocaleKorean from './locale/locale.constant-ko'; +import addLocaleChinese from './locale/locale.constant-zh'; /* eslint-disable import/no-unresolved, import/default */ @@ -50,11 +51,15 @@ export default function AppConfig($provide, $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); addLocaleKorean(locales); + addLocaleChinese(locales); var $window = angular.injector(['ng']).get('$window'); var lang = $window.navigator.language || $window.navigator.userLanguage; if (lang === 'ko') { $translateProvider.useSanitizeValueStrategy(null); $translateProvider.preferredLanguage('ko_KR'); + } else if (lang === 'zh') { + $translateProvider.useSanitizeValueStrategy(null); + $translateProvider.preferredLanguage('zh_CN'); } for (var langKey in locales) { diff --git a/ui/src/app/app.js b/ui/src/app/app.js index e3b36e3c6b..290e9cdf1c 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -19,6 +19,7 @@ import angular from 'angular'; import ngMaterial from 'angular-material'; import ngMdIcons from 'angular-material-icons'; import ngCookies from 'angular-cookies'; +import angularSocialshare from 'angular-socialshare'; import 'angular-translate'; import 'angular-translate-loader-static-files'; import 'angular-translate-storage-local'; @@ -82,6 +83,7 @@ angular.module('thingsboard', [ ngMaterial, ngMdIcons, ngCookies, + angularSocialshare, 'pascalprecht.translate', 'mdColorPicker', mdPickers, diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js index 02fe8fd8fb..75b16fd1c8 100644 --- a/ui/src/app/app.run.js +++ b/ui/src/app/app.run.js @@ -55,8 +55,39 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, }); $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { + + function waitForUserLoaded() { + if ($rootScope.userLoadedHandle) { + $rootScope.userLoadedHandle(); + } + $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () { + $rootScope.userLoadedHandle(); + $state.go(to.name, params); + }); + } + + function reloadUserFromPublicId() { + userService.setUserFromJwtToken(null, null, false); + waitForUserLoaded(); + userService.reloadUser(); + } + + var locationSearch = $location.search(); + var publicId = locationSearch.publicId; + if (userService.isUserLoaded() === true) { if (userService.isAuthenticated()) { + if (userService.isPublic()) { + if (userService.parsePublicId() !== publicId) { + evt.preventDefault(); + if (publicId && publicId.length > 0) { + reloadUserFromPublicId(); + } else { + userService.logout(); + } + return; + } + } if (userService.forceDefaultPlace(to, params)) { evt.preventDefault(); gotoDefaultPlace(params); @@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, } } } else { - if (to.module === 'private') { + if (publicId && publicId.length > 0) { + evt.preventDefault(); + reloadUserFromPublicId(); + } else if (to.module === 'private') { evt.preventDefault(); if (to.url === '/home' || to.url === '/') { $state.go('login', params); @@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, } } else { evt.preventDefault(); - if ($rootScope.userLoadedHandle) { - $rootScope.userLoadedHandle(); - } - $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () { - $rootScope.userLoadedHandle(); - $state.go(to.name, params); - }); + waitForUserLoaded(); } }) $rootScope.pageTitle = 'Thingsboard'; - $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to) { + $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) { + if (userService.isPublic() && to.name === 'home.dashboards.dashboard') { + $location.search('publicId', userService.getPublicId()); + userService.updateLastPublicDashboardId(params.dashboardId); + } if (angular.isDefined(to.data.pageTitle)) { $translate(to.data.pageTitle).then(function (translation) { $rootScope.pageTitle = 'Thingsboard | ' + translation; diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index 3fc5202b51..6e05bbbf6c 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) .name; /*@ngInject*/ -function Utils($mdColorPalette, $rootScope, $window, types) { +function Utils($mdColorPalette, $rootScope, $window, $q, deviceService, types) { var predefinedFunctions = {}, predefinedFunctionsList = [], @@ -104,7 +104,10 @@ function Utils($mdColorPalette, $rootScope, $window, types) { parseException: parseException, processWidgetException: processWidgetException, isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, - filterSearchTextEntities: filterSearchTextEntities + filterSearchTextEntities: filterSearchTextEntities, + guid: guid, + createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo, + isLocalUrl: isLocalUrl } return service; @@ -276,4 +279,165 @@ function Utils($mdColorPalette, $rootScope, $window, types) { deferred.resolve(response); } + function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + } + + function genNextColor(datasources) { + var index = 0; + if (datasources) { + for (var i = 0; i < datasources.length; i++) { + var datasource = datasources[i]; + index += datasource.dataKeys.length; + } + } + return getMaterialColor(index); + } + + /*var defaultDataKey = { + name: 'f(x)', + type: types.dataKeyType.function, + label: 'Sin', + color: getMaterialColor(0), + funcBody: getPredefinedFunctionBody('Sin'), + settings: {}, + _hash: Math.random() + }; + + var defaultDatasource = { + type: types.datasourceType.function, + name: types.datasourceType.function, + dataKeys: [angular.copy(defaultDataKey)] + };*/ + + function createKey(keyInfo, type, datasources) { + var dataKey = { + name: keyInfo.name, + type: type, + label: keyInfo.label || keyInfo.name, + color: genNextColor(datasources), + funcBody: keyInfo.funcBody, + settings: {}, + _hash: Math.random() + } + return dataKey; + } + + function createDatasourceKeys(keyInfos, type, datasource, datasources) { + for (var i=0;i 0) { + for (var i = 0; i < devices.length; i++) { + var device = devices[i]; + createDatasourceFromSubscription(subscriptionInfo, datasources, device); + } + } + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + }, + function fail() { + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + } + ) + } else { + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + } + } else if (subscriptionInfo.type === types.datasourceType.function) { + createDatasourceFromSubscription(subscriptionInfo, datasources); + index++; + processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred); + } + } else { + deferred.resolve(datasources); + } + } + + function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) { + var deferred = $q.defer(); + var datasources = []; + processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred); + return deferred.promise; + } + + function isLocalUrl(url) { + var parser = document.createElement('a'); //eslint-disable-line + parser.href = url; + var host = parser.hostname; + if (host === "localhost" || host === "127.0.0.1") { + return true; + } else { + return false; + } + } + } diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index 101ff51563..131616be0c 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -87,8 +87,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t var highlightedMode = false; var highlightedWidget = null; var selectedWidget = null; - var mouseDownWidget = -1; - var widgetMouseMoved = false; var gridsterParent = $('#gridster-parent', $element); var gridsterElement = angular.element($('#gridster-child', gridsterParent)); @@ -152,9 +150,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.resetHighlight = resetHighlight; vm.onWidgetFullscreenChanged = onWidgetFullscreenChanged; + vm.widgetMouseDown = widgetMouseDown; - vm.widgetMouseMove = widgetMouseMove; - vm.widgetMouseUp = widgetMouseUp; + vm.widgetClicked = widgetClicked; vm.widgetSizeX = widgetSizeX; vm.widgetSizeY = widgetSizeY; @@ -184,23 +182,23 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.dashboardTimewindowApi = { onResetTimewindow: function() { - if (vm.originalDashboardTimewindow) { - vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); - vm.originalDashboardTimewindow = null; - } + $timeout(function() { + if (vm.originalDashboardTimewindow) { + vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); + vm.originalDashboardTimewindow = null; + } + }, 0); }, onUpdateTimewindow: function(startTimeMs, endTimeMs) { if (!vm.originalDashboardTimewindow) { vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow); } - vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs); + $timeout(function() { + vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs); + }, 0); } }; - //$element[0].onmousemove=function(){ - // widgetMouseMove(); - // } - //TODO: widgets visibility /*gridsterParent.scroll(function () { updateVisibleRect(); @@ -350,7 +348,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t } function loadDashboard() { - resetWidgetClick(); $timeout(function () { if (vm.loadWidgets) { var promise = vm.loadWidgets(); @@ -434,42 +431,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t return gridsterElement && gridsterElement[0] === gridster.$element[0]; } - function resetWidgetClick () { - mouseDownWidget = -1; - widgetMouseMoved = false; - } - function onWidgetFullscreenChanged(expanded, widget) { vm.isWidgetExpanded = expanded; $scope.$broadcast('onWidgetFullscreenChanged', vm.isWidgetExpanded, widget); } function widgetMouseDown ($event, widget) { - mouseDownWidget = widget; - widgetMouseMoved = false; if (vm.onWidgetMouseDown) { vm.onWidgetMouseDown({event: $event, widget: widget}); } } - function widgetMouseMove () { - if (mouseDownWidget) { - widgetMouseMoved = true; - } - } - - function widgetMouseUp ($event, widget) { - $timeout(function () { - if (!widgetMouseMoved && mouseDownWidget) { - if (widget === mouseDownWidget) { - widgetClicked($event, widget); - } - } - mouseDownWidget = null; - widgetMouseMoved = false; - }, 0); - } - function widgetClicked ($event, widget) { if ($event) { $event.stopPropagation(); @@ -518,7 +490,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t } function editWidget ($event, widget) { - resetWidgetClick(); if ($event) { $event.stopPropagation(); } @@ -528,7 +499,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t } function exportWidget ($event, widget) { - resetWidgetClick(); if ($event) { $event.stopPropagation(); } @@ -538,7 +508,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t } function removeWidget($event, widget) { - resetWidgetClick(); if ($event) { $event.stopPropagation(); } @@ -548,27 +517,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t } function highlightWidget(widget, delay) { - highlightedMode = true; - highlightedWidget = widget; - if (vm.gridster) { - var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; - if (item) { - var height = $(item).outerHeight(true); - var rectHeight = gridsterParent.height(); - var offset = (rectHeight - height) / 2; - var scrollTop = item.offsetTop; - if (offset > 0) { - scrollTop -= offset; - } - gridsterParent.animate({ - scrollTop: scrollTop - }, delay); - } + if (!highlightedMode || highlightedWidget != widget) { + highlightedMode = true; + highlightedWidget = widget; + scrollToWidget(widget, delay); } } function selectWidget(widget, delay) { - selectedWidget = widget; + if (selectedWidget != widget) { + selectedWidget = widget; + scrollToWidget(widget, delay); + } + } + + function scrollToWidget(widget, delay) { if (vm.gridster) { var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; if (item) { diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html index 8c42278784..3d11e46531 100644 --- a/ui/src/app/components/dashboard.tpl.html +++ b/ui/src/app/components/dashboard.tpl.html @@ -37,9 +37,7 @@ 'tb-not-highlighted': vm.isNotHighlighted(widget), 'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}" tb-mousedown="vm.widgetMouseDown($event, widget)" - tb-mousemove="vm.widgetMouseMove($event, widget)" - tb-mouseup="vm.widgetMouseUp($event, widget)" - ng-click="" + ng-click="vm.widgetClicked($event, widget)" tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)" ng-style="{cursor: 'pointer', color: vm.widgetColor(widget), @@ -49,7 +47,7 @@ {{widget.config.title}} -
+
- + - {{ action.details() }} + {{ action.details( rowItem[n] ) }} @@ -62,7 +62,7 @@
0) { + return utils.isLocalUrl(vm.shareLink); + } else { + return true; + } + } + +} \ No newline at end of file diff --git a/ui/src/app/components/socialshare-panel.tpl.html b/ui/src/app/components/socialshare-panel.tpl.html new file mode 100644 index 0000000000..7b9560de36 --- /dev/null +++ b/ui/src/app/components/socialshare-panel.tpl.html @@ -0,0 +1,62 @@ + + +
+ + + + {{ 'action.share-via' | translate:{provider:'Facebook'} }} + + + + + + {{ 'action.share-via' | translate:{provider:'Twitter'} }} + + + + + + {{ 'action.share-via' | translate:{provider:'Linkedin'} }} + + + + + + {{ 'action.share-via' | translate:{provider:'Reddit'} }} + + +
\ No newline at end of file diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js index 61a5d14558..bcffa721cc 100644 --- a/ui/src/app/components/widget-config.directive.js +++ b/ui/src/app/components/widget-config.directive.js @@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.forceExpandDatasources = false; } + if (angular.isUndefined(scope.isDataEnabled)) { + scope.isDataEnabled = true; + } + scope.currentSettingsSchema = {}; scope.currentSettings = angular.copy(scope.emptySettingsSchema); @@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ? ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value; scope.legendConfig = ngModelCtrl.$viewValue.legendConfig; - if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) { + if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value + && scope.isDataEnabled) { if (scope.datasources) { scope.datasources.splice(0, scope.datasources.length); } else { @@ -119,7 +124,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]}); } } - } else if (scope.widgetType === types.widgetType.rpc.value) { + } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) { var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0]; if (scope.deviceAliases[aliasId]) { @@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti if (ngModelCtrl.$viewValue) { var value = ngModelCtrl.$viewValue; var valid; - if (scope.widgetType === types.widgetType.rpc.value) { + if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0; ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); - } else if (scope.widgetType !== types.widgetType.static.value) { + } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { valid = value && value.datasources && value.datasources.length > 0; ngModelCtrl.$setValidity('datasources', valid); } @@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti scope.$watch('datasources', function () { if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value - && scope.widgetType !== types.widgetType.static.value) { + && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; if (value.datasources) { value.datasources.splice(0, value.datasources.length); @@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti }, true); scope.$watch('targetDeviceAlias.value', function () { - if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value) { + if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) { var value = ngModelCtrl.$viewValue; if (scope.targetDeviceAlias.value) { value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id]; @@ -359,6 +364,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti require: "^ngModel", scope: { forceExpandDatasources: '=?', + isDataEnabled: '=?', widgetType: '=', widgetSettingsSchema: '=', datakeySettingsSchema: '=', diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html index 66b440fe09..e7c03e3f25 100644 --- a/ui/src/app/components/widget-config.tpl.html +++ b/ui/src/app/components/widget-config.tpl.html @@ -31,7 +31,7 @@
+ ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled"> {{ 'widget-config.datasources' | translate }} @@ -96,7 +96,7 @@ + ng-show="widgetType === types.widgetType.rpc.value && isDataEnabled"> {{ 'widget-config.target-device' | translate }} diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index 6307a87e1a..7c7caa9e2b 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -15,6 +15,7 @@ */ import $ from 'jquery'; import 'javascript-detect-element-resize/detect-element-resize'; +import Subscription from '../api/subscription'; /* eslint-disable angular/angularelement */ @@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q $scope.rpcErrorText = null; $scope.rpcEnabled = false; $scope.executingRpcRequest = false; - $scope.executingPromises = []; var gridsterItemInited = false; - var datasourceListeners = []; - var targetDeviceAliasId = null; - var targetDeviceId = null; - var originalTimewindow = null; - var subscriptionTimewindow = null; var cafs = {}; - var varsRegex = /\$\{([^\}]*)\}/g; - /* * data = array of datasourceData * datasourceData = { @@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q * dataKey, { name, config } * data = array of [time, value] * } - * - * */ var widgetContext = { @@ -71,22 +62,70 @@ export default function WidgetController($scope, $timeout, $window, $element, $q settings: widget.config.settings, units: widget.config.units || '', decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2, - datasources: angular.copy(widget.config.datasources), - data: [], - hiddenData: [], - timeWindow: { - stDiff: stDiff - }, + subscriptions: {}, + defaultSubscription: null, timewindowFunctions: { - onUpdateTimewindow: onUpdateTimewindow, - onResetTimewindow: onResetTimewindow + onUpdateTimewindow: function(startTimeMs, endTimeMs) { + if (widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs); + } + }, + onResetTimewindow: function() { + if (widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.onResetTimewindow(); + } + } + }, + subscriptionApi: { + createSubscription: function(options, subscribe) { + return createSubscription(options, subscribe); + }, + + + // type: "timeseries" or "latest" or "rpc" + /* devicesSubscriptionInfo = [ + { + deviceId: "" + deviceName: "" + timeseries: [{ name: "", label: "" }, ..] + attributes: [{ name: "", label: "" }, ..] + } + .. + ]*/ + + // options = { + // timeWindowConfig, + // useDashboardTimewindow, + // legendConfig, + // decimals, + // units, + // callbacks [ onDataUpdated(subscription, apply) ] + // } + // + + createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) { + return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe); + }, + removeSubscription: function(id) { + var subscription = widgetContext.subscriptions[id]; + if (subscription) { + subscription.destroy(); + delete widgetContext.subscriptions[id]; + } + } }, controlApi: { sendOneWayCommand: function(method, params, timeout) { - return sendCommand(true, method, params, timeout); + if (widgetContext.defaultSubscription) { + return widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout); + } + return null; }, sendTwoWayCommand: function(method, params, timeout) { - return sendCommand(false, method, params, timeout); + if (widgetContext.defaultSubscription) { + return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout); + } + return null; } }, utils: { @@ -94,7 +133,27 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } }; + var subscriptionContext = { + $scope: $scope, + $q: $q, + $filter: $filter, + $timeout: $timeout, + tbRaf: tbRaf, + timeService: timeService, + deviceService: deviceService, + datasourceService: datasourceService, + utils: utils, + widgetUtils: widgetContext.utils, + dashboardTimewindowApi: dashboardTimewindowApi, + types: types, + stDiff: stDiff, + aliasesInfo: aliasesInfo + }; + var widgetTypeInstance; + + vm.useCustomDatasources = false; + try { widgetTypeInstance = new widgetType(widgetContext); } catch (e) { @@ -119,19 +178,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q if (!widgetTypeInstance.onDestroy) { widgetTypeInstance.onDestroy = function() {}; } - - //var bounds = {top: 0, left: 0, bottom: 0, right: 0}; - //TODO: widgets visibility - /*var visible = false;*/ - - $scope.clearRpcError = function() { - $scope.rpcRejection = null; - $scope.rpcErrorText = null; + if (widgetTypeInstance.useCustomDatasources) { + vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); } - vm.gridsterItemInitialized = gridsterItemInitialized; - //TODO: widgets visibility + + //var bounds = {top: 0, left: 0, bottom: 0, right: 0}; + /*var visible = false;*/ /*vm.visibleRectChanged = visibleRectChanged; function visibleRectChanged(newVisibleRect) { @@ -139,23 +193,216 @@ export default function WidgetController($scope, $timeout, $window, $element, $q updateVisibility(); }*/ + $scope.clearRpcError = function() { + if (widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.clearRpcError(); + } + } + + vm.gridsterItemInitialized = gridsterItemInitialized; + initialize(); - function handleWidgetException(e) { - $log.error(e); - $scope.widgetErrorData = utils.processWidgetException(e); + + /* + options = { + type, + targetDeviceAliasIds, // RPC + targetDeviceIds, // RPC + datasources, + timeWindowConfig, + useDashboardTimewindow, + legendConfig, + decimals, + units, + callbacks + } + */ + + function createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe) { + var deferred = $q.defer(); + options.type = type; + + if (useDefaultComponents) { + defaultComponentsOptions(options); + } else { + if (!options.timeWindowConfig) { + options.useDashboardTimewindow = true; + } + } + + utils.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then( + function (datasources) { + options.datasources = datasources; + var subscription = createSubscription(options, subscribe); + if (useDefaultComponents) { + defaultSubscriptionOptions(subscription, options); + } + deferred.resolve(subscription); + } + ); + return deferred.promise; + } + + function createSubscription(options, subscribe) { + options.dashboardTimewindow = dashboardTimewindow; + var subscription = + new Subscription(subscriptionContext, options); + widgetContext.subscriptions[subscription.id] = subscription; + if (subscribe) { + subscription.subscribe(); + } + return subscription; } - function notifyDataLoaded() { - if ($scope.loadingData === true) { + function defaultComponentsOptions(options) { + options.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow) + ? widget.config.useDashboardTimewindow : true; + + options.timeWindowConfig = options.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow; + options.legendConfig = null; + + if ($scope.displayLegend) { + options.legendConfig = $scope.legendConfig; + } + options.decimals = widgetContext.decimals; + options.units = widgetContext.units; + + options.callbacks = { + onDataUpdated: function() { + widgetTypeInstance.onDataUpdated(); + }, + onDataUpdateError: function(subscription, e) { + handleWidgetException(e); + }, + dataLoading: function(subscription) { + if ($scope.loadingData !== subscription.loadingData) { + $scope.loadingData = subscription.loadingData; + } + }, + legendDataUpdated: function(subscription, apply) { + if (apply) { + $scope.$digest(); + } + }, + timeWindowUpdated: function(subscription, timeWindowConfig) { + widget.config.timewindow = timeWindowConfig; + $scope.$apply(); + } + } + } + + function defaultSubscriptionOptions(subscription, options) { + if (!options.useDashboardTimewindow) { + $scope.$watch(function () { + return widget.config.timewindow; + }, function (newTimewindow, prevTimewindow) { + if (!angular.equals(newTimewindow, prevTimewindow)) { + subscription.updateTimewindowConfig(widget.config.timewindow); + } + }); + } + if ($scope.displayLegend) { + $scope.legendData = subscription.legendData; + } + } + + function createDefaultSubscription() { + var subscription; + var options; + if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { + options = { + type: widget.type, + datasources: angular.copy(widget.config.datasources) + }; + defaultComponentsOptions(options); + + subscription = createSubscription(options); + + defaultSubscriptionOptions(subscription, options); + + // backward compatibility + + widgetContext.datasources = subscription.datasources; + widgetContext.data = subscription.data; + widgetContext.hiddenData = subscription.hiddenData; + widgetContext.timeWindow = subscription.timeWindow; + + } else if (widget.type === types.widgetType.rpc.value) { + $scope.loadingData = false; + options = { + type: widget.type, + targetDeviceAliasIds: widget.config.targetDeviceAliasIds + } + options.callbacks = { + rpcStateChanged: function(subscription) { + $scope.rpcEnabled = subscription.rpcEnabled; + $scope.executingRpcRequest = subscription.executingRpcRequest; + }, + onRpcSuccess: function(subscription) { + $scope.executingRpcRequest = subscription.executingRpcRequest; + }, + onRpcFailed: function(subscription) { + $scope.executingRpcRequest = subscription.executingRpcRequest; + $scope.rpcErrorText = subscription.rpcErrorText; + $scope.rpcRejection = subscription.rpcRejection; + }, + onRpcErrorCleared: function() { + $scope.rpcErrorText = null; + $scope.rpcRejection = null; + } + } + subscription = createSubscription(options); + } else if (widget.type === types.widgetType.static.value) { $scope.loadingData = false; } + if (subscription) { + widgetContext.defaultSubscription = subscription; + } } - function notifyDataLoading() { - if ($scope.loadingData === false) { - $scope.loadingData = true; + + function initialize() { + + if (!vm.useCustomDatasources) { + createDefaultSubscription(); + } else { + $scope.loadingData = false; } + + $scope.$on('toggleDashboardEditMode', function (event, isEdit) { + onEditModeChanged(isEdit); + }); + + addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef + + $scope.$watch(function () { + return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder; + }, function () { + //updateBounds(); + $scope.$emit("widgetPositionChanged", widget); + }); + + $scope.$on('gridster-item-resized', function (event, item) { + if (!widgetContext.isMobile) { + widget.sizeX = item.sizeX; + widget.sizeY = item.sizeY; + } + }); + + $scope.$on('mobileModeChanged', function (event, newIsMobile) { + onMobileModeChanged(newIsMobile); + }); + + $scope.$on("$destroy", function () { + removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef + onDestroy(); + }); + } + + function handleWidgetException(e) { + $log.error(e); + $scope.widgetErrorData = utils.processWidgetException(e); } function onInit() { @@ -166,42 +413,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } catch (e) { handleWidgetException(e); } - if (widgetContext.dataUpdatePending) { - widgetContext.dataUpdatePending = false; - onDataUpdated(); + if (!vm.useCustomDatasources && widgetContext.defaultSubscription) { + widgetContext.defaultSubscription.subscribe(); } } } - function updateTimewindow() { - widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000; - if (subscriptionTimewindow.realtimeWindowMs) { - widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff; - widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs; - } else if (subscriptionTimewindow.fixedWindow) { - widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs; - widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs; - } - } - - function onDataUpdated() { - if (widgetContext.inited) { - if (cafs['dataUpdate']) { - cafs['dataUpdate'](); - cafs['dataUpdate'] = null; - } - cafs['dataUpdate'] = tbRaf(function() { - try { - widgetTypeInstance.onDataUpdated(); - } catch (e) { - handleWidgetException(e); - } - }); - } else { - widgetContext.dataUpdatePending = true; - } - } - function checkSize() { var width = widgetContext.$containerParent.width(); var height = widgetContext.$containerParent.height(); @@ -289,11 +506,35 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } } + function isNumeric(val) { + return (val - parseFloat( val ) + 1) >= 0; + } + + function formatValue(value, dec, units) { + if (angular.isDefined(value) && + value !== null && isNumeric(value)) { + var formatted = value; + if (angular.isDefined(dec)) { + formatted = formatted.toFixed(dec); + } + formatted = (formatted * 1).toString(); + if (angular.isDefined(units) && units.length > 0) { + formatted += ' ' + units; + } + return formatted; + } else { + return ''; + } + } + function onDestroy() { - unsubscribe(); + for (var id in widgetContext.subscriptions) { + var subscription = widgetContext.subscriptions[id]; + subscription.destroy(); + } + widgetContext.subscriptions = []; if (widgetContext.inited) { widgetContext.inited = false; - widgetContext.dataUpdatePending = false; for (var cafId in cafs) { if (cafs[cafId]) { cafs[cafId](); @@ -308,243 +549,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } } - function onRestart() { - onDestroy(); - onInit(); - } - -/* scope.legendData = { - keys: [], - data: [] - - key: { - label: '', - color: '' - dataIndex: 0 - } - data: { - min: null, - max: null, - avg: null, - total: null - } - };*/ - - - function initialize() { - - $scope.caulculateLegendData = $scope.displayLegend && - widget.type === types.widgetType.timeseries.value && - ($scope.legendConfig.showMin === true || - $scope.legendConfig.showMax === true || - $scope.legendConfig.showAvg === true || - $scope.legendConfig.showTotal === true); - - if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { - for (var i = 0; i < widgetContext.datasources.length; i++) { - var datasource = widgetContext.datasources[i]; - for (var a = 0; a < datasource.dataKeys.length; a++) { - var dataKey = datasource.dataKeys[a]; - dataKey.pattern = angular.copy(dataKey.label); - var datasourceData = { - datasource: datasource, - dataKey: dataKey, - data: [] - }; - widgetContext.data.push(datasourceData); - widgetContext.hiddenData.push({data: []}); - if ($scope.displayLegend) { - var legendKey = { - dataKey: dataKey, - dataIndex: Number(i) + Number(a) - }; - $scope.legendData.keys.push(legendKey); - var legendKeyData = { - min: null, - max: null, - avg: null, - total: null, - hidden: false - }; - $scope.legendData.data.push(legendKeyData); - } - } - } - if ($scope.displayLegend) { - $scope.legendData.keys = $filter('orderBy')($scope.legendData.keys, '+label'); - $scope.$on('legendDataHiddenChanged', function (event, index) { - event.stopPropagation(); - var hidden = $scope.legendData.data[index].hidden; - if (hidden) { - widgetContext.hiddenData[index].data = widgetContext.data[index].data; - widgetContext.data[index].data = []; - } else { - widgetContext.data[index].data = widgetContext.hiddenData[index].data; - widgetContext.hiddenData[index].data = []; - } - onDataUpdated(); - }); - } - } else if (widget.type === types.widgetType.rpc.value) { - if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { - targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; - if (aliasesInfo.deviceAliases[targetDeviceAliasId]) { - targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId; - } - } - if (targetDeviceId) { - $scope.rpcEnabled = true; - } else { - $scope.rpcEnabled = $scope.widgetEditMode ? true : false; - } - } - - $scope.$on('toggleDashboardEditMode', function (event, isEdit) { - onEditModeChanged(isEdit); - }); - - addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef - - $scope.$watch(function () { - return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder; - }, function () { - //updateBounds(); - $scope.$emit("widgetPositionChanged", widget); - }); - - $scope.$on('gridster-item-resized', function (event, item) { - if (!widgetContext.isMobile) { - widget.sizeX = item.sizeX; - widget.sizeY = item.sizeY; - } - }); - - $scope.$on('mobileModeChanged', function (event, newIsMobile) { - onMobileModeChanged(newIsMobile); - }); - - $scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) { - aliasesInfo = newAliasesInfo; - if (widget.type === types.widgetType.rpc.value) { - if (targetDeviceAliasId) { - var deviceId = null; - if (aliasesInfo.deviceAliases[targetDeviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId; - } - if (!angular.equals(deviceId, targetDeviceId)) { - targetDeviceId = deviceId; - if (targetDeviceId) { - $scope.rpcEnabled = true; - } else { - $scope.rpcEnabled = $scope.widgetEditMode ? true : false; - } - onRestart(); - } - } - } else { - checkSubscriptions(); - } - }); - - $scope.$on("$destroy", function () { - removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef - onDestroy(); - }); - - if (widget.type === types.widgetType.timeseries.value) { - widgetContext.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow) - ? widget.config.useDashboardTimewindow : true; - if (widgetContext.useDashboardTimewindow) { - $scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) { - if (!angular.equals(dashboardTimewindow, newDashboardTimewindow)) { - dashboardTimewindow = newDashboardTimewindow; - unsubscribe(); - subscribe(); - } - }); - } else { - $scope.$watch(function () { - return widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow; - }, function (newTimewindow, prevTimewindow) { - if (!angular.equals(newTimewindow, prevTimewindow)) { - unsubscribe(); - subscribe(); - } - }); - } - } - subscribe(); - } - - function sendCommand(oneWayElseTwoWay, method, params, timeout) { - if (!$scope.rpcEnabled) { - return $q.reject(); - } - - if ($scope.rpcRejection && $scope.rpcRejection.status !== 408) { - $scope.rpcRejection = null; - $scope.rpcErrorText = null; - } - - var requestBody = { - method: method, - params: params - }; - - if (timeout && timeout > 0) { - requestBody.timeout = timeout; - } - - var deferred = $q.defer(); - $scope.executingRpcRequest = true; - if ($scope.widgetEditMode) { - $timeout(function() { - $scope.executingRpcRequest = false; - if (oneWayElseTwoWay) { - deferred.resolve(); - } else { - deferred.resolve(requestBody); - } - }, 500); - } else { - $scope.executingPromises.push(deferred.promise); - var targetSendFunction = oneWayElseTwoWay ? deviceService.sendOneWayRpcCommand : deviceService.sendTwoWayRpcCommand; - targetSendFunction(targetDeviceId, requestBody).then( - function success(responseBody) { - $scope.rpcRejection = null; - $scope.rpcErrorText = null; - var index = $scope.executingPromises.indexOf(deferred.promise); - if (index >= 0) { - $scope.executingPromises.splice( index, 1 ); - } - $scope.executingRpcRequest = $scope.executingPromises.length > 0; - deferred.resolve(responseBody); - }, - function fail(rejection) { - var index = $scope.executingPromises.indexOf(deferred.promise); - if (index >= 0) { - $scope.executingPromises.splice( index, 1 ); - } - $scope.executingRpcRequest = $scope.executingPromises.length > 0; - if (!$scope.executingRpcRequest || rejection.status === 408) { - $scope.rpcRejection = rejection; - if (rejection.status === 408) { - $scope.rpcErrorText = 'Device is offline.'; - } else { - $scope.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; - if (rejection.data && rejection.data.length > 0) { - $scope.rpcErrorText += '
'; - $scope.rpcErrorText += rejection.data; - } - } - } - deferred.reject(rejection); - } - ); - } - return deferred.promise; - } - //TODO: widgets visibility /*function updateVisibility(forceRedraw) { if (visibleRect) { @@ -583,285 +587,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q onRedraw(); }*/ - function onResetTimewindow() { - if (widgetContext.useDashboardTimewindow) { - dashboardTimewindowApi.onResetTimewindow(); - } else { - if (originalTimewindow) { - widget.config.timewindow = angular.copy(originalTimewindow); - originalTimewindow = null; - } - } - } - - function onUpdateTimewindow(startTimeMs, endTimeMs) { - if (widgetContext.useDashboardTimewindow) { - dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); - } else { - if (!originalTimewindow) { - originalTimewindow = angular.copy(widget.config.timewindow); - } - widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs); - } - } - - function dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { - notifyDataLoaded(); - var update = true; - var currentData; - if ($scope.displayLegend && $scope.legendData.data[datasourceIndex + dataKeyIndex].hidden) { - currentData = widgetContext.hiddenData[datasourceIndex + dataKeyIndex]; - } else { - currentData = widgetContext.data[datasourceIndex + dataKeyIndex]; - } - if (widget.type === types.widgetType.latest.value) { - var prevData = currentData.data; - if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { - var prevValue = prevData[0][1]; - if (prevValue === sourceData.data[0][1]) { - update = false; - } - } - } - if (update) { - if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) { - updateTimewindow(); - } - currentData.data = sourceData.data; - onDataUpdated(); - if ($scope.caulculateLegendData) { - updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); - } - } - if (apply) { - $scope.$digest(); - } - } - - function updateLegend(dataIndex, data, apply) { - var legendKeyData = $scope.legendData.data[dataIndex]; - if ($scope.legendConfig.showMin) { - legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showMax) { - legendKeyData.max = formatValue(calculateMax(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showAvg) { - legendKeyData.avg = formatValue(calculateAvg(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showTotal) { - legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units); - } - $scope.$broadcast('legendDataUpdated', apply !== false); - } - - function isNumeric(val) { - return (val - parseFloat( val ) + 1) >= 0; - } - - function formatValue(value, dec, units) { - if (angular.isDefined(value) && - value !== null && isNumeric(value)) { - var formatted = value; - if (angular.isDefined(dec)) { - formatted = formatted.toFixed(dec); - } - formatted = (formatted * 1).toString(); - if (angular.isDefined(units) && units.length > 0) { - formatted += ' ' + units; - } - return formatted; - } else { - return ''; - } - } - - function calculateMin(data) { - if (data.length > 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - return calculateTotal(data)/data.length; - } else { - return null; - } - } - - function calculateTotal(data) { - if (data.length > 0) { - var result = 0; - for (var i = 0; i < data.length; i++) { - result += Number(data[i][1]); - } - return result; - } else { - return null; - } - } - - function checkSubscriptions() { - if (widget.type !== types.widgetType.rpc.value) { - var subscriptionsChanged = false; - for (var i = 0; i < datasourceListeners.length; i++) { - var listener = datasourceListeners[i]; - var deviceId = null; - var aliasName = null; - if (listener.datasource.type === types.datasourceType.device) { - if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId; - aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias; - } - if (!angular.equals(deviceId, listener.deviceId) || - !angular.equals(aliasName, listener.datasource.name)) { - subscriptionsChanged = true; - break; - } - } - } - if (subscriptionsChanged) { - unsubscribe(); - subscribe(); - } - } - } - - function unsubscribe() { - if (widget.type !== types.widgetType.rpc.value) { - for (var i = 0; i < datasourceListeners.length; i++) { - var listener = datasourceListeners[i]; - datasourceService.unsubscribeFromDatasource(listener); - } - datasourceListeners = []; - } - } - - function updateRealtimeSubscription(_subscriptionTimewindow) { - if (_subscriptionTimewindow) { - subscriptionTimewindow = _subscriptionTimewindow; - } else { - subscriptionTimewindow = - timeService.createSubscriptionTimewindow( - widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow, - widgetContext.timeWindow.stDiff); - } - updateTimewindow(); - return subscriptionTimewindow; - } - - function hasTimewindow() { - if (widgetContext.useDashboardTimewindow) { - return angular.isDefined(dashboardTimewindow); - } else { - return angular.isDefined(widget.config.timewindow); - } - } - - function updateDataKeyLabel(dataKey, deviceName, aliasName) { - var pattern = dataKey.pattern; - var label = dataKey.pattern; - var match = varsRegex.exec(pattern); - while (match !== null) { - var variable = match[0]; - var variableName = match[1]; - if (variableName === 'deviceName') { - label = label.split(variable).join(deviceName); - } else if (variableName === 'aliasName') { - label = label.split(variable).join(aliasName); - } - match = varsRegex.exec(pattern); - } - dataKey.label = label; - } - - function subscribe() { - if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { - notifyDataLoading(); - if (widget.type === types.widgetType.timeseries.value && - hasTimewindow()) { - updateRealtimeSubscription(); - if (subscriptionTimewindow.fixedWindow) { - onDataUpdated(); - } - } - var index = 0; - for (var i = 0; i < widgetContext.datasources.length; i++) { - var datasource = widgetContext.datasources[i]; - if (angular.isFunction(datasource)) - continue; - var deviceId = null; - if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) { - if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId; - datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - var deviceName = ''; - var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId]; - for (var d=0;d -
{{ item | contactShort }}
\ No newline at end of file +
{{ item | contactShort }}
\ No newline at end of file diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html index 62ef6b3f35..a2165712e6 100644 --- a/ui/src/app/customer/customer-fieldset.tpl.html +++ b/ui/src/app/customer/customer-fieldset.tpl.html @@ -15,13 +15,13 @@ limitations under the License. --> -{{ 'customer.manage-users' | translate }} +{{ 'customer.manage-users' | translate }} {{ 'customer.manage-devices' | translate }} {{ 'customer.manage-dashboards' | translate }} -{{ 'customer.delete' | translate }} +{{ 'customer.delete' | translate }} -
+
diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js index 57386604f3..4182588555 100644 --- a/ui/src/app/customer/customer.controller.js +++ b/ui/src/app/customer/customer.controller.js @@ -30,14 +30,23 @@ export default function CustomerController(customerService, $state, $stateParams }, name: function() { return $translate.instant('user.users') }, details: function() { return $translate.instant('customer.manage-customer-users') }, - icon: "account_circle" + icon: "account_circle", + isEnabled: function(customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + } }, { onAction: function ($event, item) { openCustomerDevices($event, item); }, name: function() { return $translate.instant('device.devices') }, - details: function() { return $translate.instant('customer.manage-customer-devices') }, + details: function(customer) { + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { + return $translate.instant('customer.manage-public-devices') + } else { + return $translate.instant('customer.manage-customer-devices') + } + }, icon: "devices_other" }, { @@ -45,7 +54,13 @@ export default function CustomerController(customerService, $state, $stateParams openCustomerDashboards($event, item); }, name: function() { return $translate.instant('dashboard.dashboards') }, - details: function() { return $translate.instant('customer.manage-customer-dashboards') }, + details: function(customer) { + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { + return $translate.instant('customer.manage-public-dashboards') + } else { + return $translate.instant('customer.manage-customer-dashboards') + } + }, icon: "dashboard" }, { @@ -54,7 +69,10 @@ export default function CustomerController(customerService, $state, $stateParams }, name: function() { return $translate.instant('action.delete') }, details: function() { return $translate.instant('customer.delete') }, - icon: "delete" + icon: "delete", + isEnabled: function(customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + } } ]; @@ -86,7 +104,19 @@ export default function CustomerController(customerService, $state, $stateParams addItemText: function() { return $translate.instant('customer.add-customer-text') }, noItemsText: function() { return $translate.instant('customer.no-customers-text') }, - itemDetailsText: function() { return $translate.instant('customer.customer-details') } + itemDetailsText: function(customer) { + if (customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic)) { + return $translate.instant('customer.customer-details') + } else { + return ''; + } + }, + isSelectionEnabled: function (customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + }, + isDetailsReadOnly: function (customer) { + return customer && customer.additionalInfo && customer.additionalInfo.isPublic; + } }; if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js index f23711ae5e..4a4e09ca0b 100644 --- a/ui/src/app/customer/customer.directive.js +++ b/ui/src/app/customer/customer.directive.js @@ -24,7 +24,21 @@ export default function CustomerDirective($compile, $templateCache) { var linker = function (scope, element) { var template = $templateCache.get(customerFieldsetTemplate); element.html(template); + + scope.isPublic = false; + + scope.$watch('customer', function(newVal) { + if (newVal) { + if (scope.customer.additionalInfo) { + scope.isPublic = scope.customer.additionalInfo.isPublic; + } else { + scope.isPublic = false; + } + } + }); + $compile(element.contents())(scope); + } return { restrict: "E", diff --git a/ui/src/app/dashboard/dashboard-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html index 9cb8a4bd81..636786758e 100644 --- a/ui/src/app/dashboard/dashboard-card.tpl.html +++ b/ui/src/app/dashboard/dashboard-card.tpl.html @@ -15,6 +15,6 @@ limitations under the License. --> -
dashboard.assignedToCustomer
+
{{'dashboard.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
+
{{'dashboard.public' | translate}}
+ diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html index 6325e17229..954d881588 100644 --- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html +++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html @@ -15,25 +15,50 @@ limitations under the License. --> +{{ 'dashboard.export' | translate }} +{{ 'dashboard.make-public' | translate }} {{ 'dashboard.assign-to-customer' | translate }} -{{ 'dashboard.unassign-from-customer' | translate }} -{{ 'dashboard.export' | translate }} + class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }} {{ 'dashboard.delete' | translate }} - + ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'"> +
+ + +
+ + + + + + + + {{ 'dashboard.copy-public-link' | translate }} + + +
+
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js index aac6da3dd0..06c64eb757 100644 --- a/ui/src/app/dashboard/dashboard-settings.controller.js +++ b/ui/src/app/dashboard/dashboard-settings.controller.js @@ -31,6 +31,18 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti vm.gridSettings.showTitle = true; } + if (angular.isUndefined(vm.gridSettings.showDevicesSelect)) { + vm.gridSettings.showDevicesSelect = true; + } + + if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) { + vm.gridSettings.showDashboardTimewindow = true; + } + + if (angular.isUndefined(vm.gridSettings.showDashboardExport)) { + vm.gridSettings.showDashboardExport = true; + } + vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)'; vm.gridSettings.columns = vm.gridSettings.columns || 24; diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html index 2ed25d3edf..ec6f28b369 100644 --- a/ui/src/app/dashboard/dashboard-settings.tpl.html +++ b/ui/src/app/dashboard/dashboard-settings.tpl.html @@ -48,6 +48,17 @@ md-color-history="false" > +
+ {{ 'dashboard.display-device-selection' | translate }} + + {{ 'dashboard.display-dashboard-timewindow' | translate }} + + {{ 'dashboard.display-dashboard-export' | translate }} + +
- + - {{ 'dashboard.export' | translate }} file_download - + - @@ -304,6 +309,6 @@ diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js index b006d95981..134baffc59 100644 --- a/ui/src/app/dashboard/dashboards.controller.js +++ b/ui/src/app/dashboard/dashboards.controller.js @@ -19,11 +19,56 @@ import addDashboardTemplate from './add-dashboard.tpl.html'; import dashboardCard from './dashboard-card.tpl.html'; import assignToCustomerTemplate from './assign-to-customer.tpl.html'; import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html'; +import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function DashboardsController(userService, dashboardService, customerService, importExport, types, $scope, $controller, +export function MakeDashboardPublicDialogController($mdDialog, $translate, toast, dashboardService, dashboard) { + + var vm = this; + + vm.dashboard = dashboard; + vm.publicLink = dashboardService.getPublicDashboardLink(dashboard); + + vm.onPublicLinkCopied = onPublicLinkCopied; + vm.close = close; + + function onPublicLinkCopied(){ + toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element('#make-dialog-public-content'), 'bottom left'); + } + + function close() { + $mdDialog.hide(); + } + +} + +/*@ngInject*/ +export function DashboardCardController(types) { + + var vm = this; + + vm.types = types; + + vm.isAssignedToCustomer = function() { + if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' && + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { + return true; + } + return false; + } + + vm.isPublic = function() { + if (vm.item && vm.item.assignedCustomer && vm.parentCtl.dashboardsScope === 'tenant' && vm.item.assignedCustomer.isPublic) { + return true; + } + return false; + } +} + +/*@ngInject*/ +export function DashboardsController(userService, dashboardService, customerService, importExport, types, $state, $stateParams, $mdDialog, $document, $q, $translate) { var customerId = $stateParams.customerId; @@ -58,6 +103,7 @@ export default function DashboardsController(userService, dashboardService, cust clickItemFunc: openDashboard, getItemTitleFunc: getDashboardTitle, + itemCardController: 'DashboardCardController', itemCardTemplateUrl: dashboardCard, parentCtl: vm, @@ -90,6 +136,7 @@ export default function DashboardsController(userService, dashboardService, cust vm.dashboardsScope = $state.$current.data.dashboardsType; vm.assignToCustomer = assignToCustomer; + vm.makePublic = makePublic; vm.unassignFromCustomer = unassignFromCustomer; vm.exportDashboard = exportDashboard; @@ -107,6 +154,17 @@ export default function DashboardsController(userService, dashboardService, cust customerId = user.customerId; } + if (customerId) { + vm.customerDashboardsTitle = $translate.instant('customer.dashboards'); + customerService.getShortCustomerInfo(customerId).then( + function success(info) { + if (info.isPublic) { + vm.customerDashboardsTitle = $translate.instant('customer.public-dashboards'); + } + } + ); + } + if (vm.dashboardsScope === 'tenant') { fetchDashboardsFunction = function (pageLink) { return dashboardService.getTenantDashboards(pageLink); @@ -126,8 +184,21 @@ export default function DashboardsController(userService, dashboardService, cust name: function() { $translate.instant('action.export') }, details: function() { return $translate.instant('dashboard.export') }, icon: "file_download" - }, - { + }); + + dashboardActionsList.push({ + onAction: function ($event, item) { + makePublic($event, item); + }, + name: function() { return $translate.instant('action.share') }, + details: function() { return $translate.instant('dashboard.make-public') }, + icon: "share", + isEnabled: function(dashboard) { + return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); + } + }); + + dashboardActionsList.push({ onAction: function ($event, item) { assignToCustomer($event, [ item.id.id ]); }, @@ -137,19 +208,29 @@ export default function DashboardsController(userService, dashboardService, cust isEnabled: function(dashboard) { return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid); } - }, - { + }); + dashboardActionsList.push({ onAction: function ($event, item) { - unassignFromCustomer($event, item); + unassignFromCustomer($event, item, false); }, name: function() { return $translate.instant('action.unassign') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') }, icon: "assignment_return", isEnabled: function(dashboard) { - return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid; + return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && !dashboard.assignedCustomer.isPublic; } - } - ); + }); + dashboardActionsList.push({ + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('dashboard.make-private') }, + icon: "reply", + isEnabled: function(dashboard) { + return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && dashboard.assignedCustomer.isPublic; + } + }); dashboardActionsList.push( { @@ -233,11 +314,27 @@ export default function DashboardsController(userService, dashboardService, cust dashboardActionsList.push( { onAction: function ($event, item) { - unassignFromCustomer($event, item); + unassignFromCustomer($event, item, false); }, name: function() { return $translate.instant('action.unassign') }, details: function() { return $translate.instant('dashboard.unassign-from-customer') }, - icon: "assignment_return" + icon: "assignment_return", + isEnabled: function(dashboard) { + return dashboard && !dashboard.assignedCustomer.isPublic; + } + } + ); + dashboardActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('dashboard.make-private') }, + icon: "reply", + isEnabled: function(dashboard) { + return dashboard && dashboard.assignedCustomer.isPublic; + } } ); @@ -307,7 +404,28 @@ export default function DashboardsController(userService, dashboardService, cust } function saveDashboard(dashboard) { - return dashboardService.saveDashboard(dashboard); + var deferred = $q.defer(); + dashboardService.saveDashboard(dashboard).then( + function success(savedDashboard) { + var dashboards = [ savedDashboard ]; + customerService.applyAssignedCustomersInfo(dashboards).then( + function success(items) { + if (items && items.length == 1) { + deferred.resolve(items[0]); + } else { + deferred.reject(); + } + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; } function assignToCustomer($event, dashboardIds) { @@ -389,15 +507,27 @@ export default function DashboardsController(userService, dashboardService, cust assignToCustomer($event, dashboardIds); } - function unassignFromCustomer($event, dashboard) { + function unassignFromCustomer($event, dashboard, isPublic) { if ($event) { $event.stopPropagation(); } + var title; + var content; + var label; + if (isPublic) { + title = $translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title}); + content = $translate.instant('dashboard.make-private-dashboard-text'); + label = $translate.instant('dashboard.make-private-dashboard'); + } else { + title = $translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}); + content = $translate.instant('dashboard.unassign-dashboard-text'); + label = $translate.instant('dashboard.unassign-dashboard'); + } var confirm = $mdDialog.confirm() .targetEvent($event) - .title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title})) - .htmlContent($translate.instant('dashboard.unassign-dashboard-text')) - .ariaLabel($translate.instant('dashboard.unassign-dashboard')) + .title(title) + .htmlContent(content) + .ariaLabel(label) .cancel($translate.instant('action.no')) .ok($translate.instant('action.yes')); $mdDialog.show(confirm).then(function () { @@ -407,6 +537,25 @@ export default function DashboardsController(userService, dashboardService, cust }); } + function makePublic($event, dashboard) { + if ($event) { + $event.stopPropagation(); + } + dashboardService.makeDashboardPublic(dashboard.id.id).then(function success(dashboard) { + $mdDialog.show({ + controller: 'MakeDashboardPublicDialogController', + controllerAs: 'vm', + templateUrl: makeDashboardPublicDialogTemplate, + locals: {dashboard: dashboard}, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: $event + }).then(function () { + vm.grid.refreshList(); + }); + }); + } + function exportDashboard($event, dashboard) { $event.stopPropagation(); importExport.exportDashboard(dashboard.id.id); diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html index bae57b91d0..dde2f864b7 100644 --- a/ui/src/app/dashboard/dashboards.tpl.html +++ b/ui/src/app/dashboard/dashboards.tpl.html @@ -24,7 +24,8 @@ dashboard-scope="vm.dashboardsScope" the-form="vm.grid.detailsForm" on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" - on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)" + on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)" + on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)" on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)" on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js index 1c2e461a96..ced39f57d6 100644 --- a/ui/src/app/dashboard/edit-widget.directive.js +++ b/ui/src/app/dashboard/edit-widget.directive.js @@ -37,6 +37,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ scope.widgetConfig = scope.widget.config; var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; + scope.isDataEnabled = !widgetInfo.useCustomDatasources; if (!settingsSchema || settingsSchema === '') { scope.settingsSchema = {}; } else { diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html index 7aa63395f9..1aaf861d0e 100644 --- a/ui/src/app/dashboard/edit-widget.tpl.html +++ b/ui/src/app/dashboard/edit-widget.tpl.html @@ -18,6 +18,7 @@
+ +
+ +
+

+ + + + +
+
+ +
+ + +
+
{{ vm.publicLink }}
+ + + + {{ 'dashboard.copy-public-link' | translate }} + + +
+
dashboard.public-dashboard-notice
+ + +
+
+
+ + + {{ 'action.ok' | + translate }} + + +
+
diff --git a/ui/src/app/device/add-devices-to-customer.controller.js b/ui/src/app/device/add-devices-to-customer.controller.js index 9fd0cec6fd..c54daab3a3 100644 --- a/ui/src/app/device/add-devices-to-customer.controller.js +++ b/ui/src/app/device/add-devices-to-customer.controller.js @@ -52,7 +52,7 @@ export default function AddDevicesToCustomerController(deviceService, $mdDialog, fetchMoreItems_: function () { if (vm.devices.hasNext && !vm.devices.pending) { vm.devices.pending = true; - deviceService.getTenantDevices(vm.devices.nextPageLink).then( + deviceService.getTenantDevices(vm.devices.nextPageLink, false).then( function success(devices) { vm.devices.data = vm.devices.data.concat(devices.data); vm.devices.nextPageLink = devices.nextPageLink; diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js index 560a4dc10f..701fd37754 100644 --- a/ui/src/app/device/attribute/attribute-table.directive.js +++ b/ui/src/app/device/attribute/attribute-table.directive.js @@ -72,7 +72,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS scope.$watch("deviceId", function(newVal, prevVal) { if (newVal && !angular.equals(newVal, prevVal)) { scope.resetFilter(); - scope.getDeviceAttributes(); + scope.getDeviceAttributes(false, true); } }); @@ -81,7 +81,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS scope.mode = 'default'; scope.query.search = null; scope.selectedAttributes = []; - scope.getDeviceAttributes(); + scope.getDeviceAttributes(false, true); } }); @@ -117,15 +117,25 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS } } - scope.getDeviceAttributes = function(forceUpdate) { + scope.onReorder = function() { + scope.getDeviceAttributes(false, false); + } + + scope.onPaginate = function() { + scope.getDeviceAttributes(false, false); + } + + scope.getDeviceAttributes = function(forceUpdate, reset) { if (scope.attributesDeferred) { scope.attributesDeferred.resolve(); } if (scope.deviceId && scope.attributeScope) { - scope.attributes = { - count: 0, - data: [] - }; + if (reset) { + scope.attributes = { + count: 0, + data: [] + }; + } scope.checkSubscription(); scope.attributesDeferred = deviceService.getDeviceAttributes(scope.deviceId, scope.attributeScope.value, scope.query, function(attributes, update, apply) { diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html index 915fbea861..b6099c4ffa 100644 --- a/ui/src/app/device/attribute/attribute-table.tpl.html +++ b/ui/src/app/device/attribute/attribute-table.tpl.html @@ -126,7 +126,7 @@ - + @@ -147,7 +147,7 @@ + md-on-paginate="onPaginate" md-page-select>
Last update time Key
+ + + + + + + + + + + +
Timestamp{{ h.dataKey.label }}
+
+
+ + + + \ No newline at end of file diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index 9f89aba308..ab9b0d7d73 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -236,6 +236,32 @@ div { } } +pre.tb-highlight { + background-color: #f7f7f7; + display: block; + margin: 20px 0; + padding: 15px; + overflow-x: auto; + code { + padding: 0; + color: #303030; + font-family: monospace; + display: inline-block; + box-sizing: border-box; + vertical-align: bottom; + font-size: 16px; + font-weight: bold; + } +} + +.tb-notice { + background-color: #f7f7f7; + padding: 15px; + border: 1px solid #ccc; + font-size: 16px; +} + + /*********************** * Flow ***********************/ diff --git a/ui/src/svg/logo_title_white.svg b/ui/src/svg/logo_title_white.svg index fff36813c2..3e6d570b47 100644 --- a/ui/src/svg/logo_title_white.svg +++ b/ui/src/svg/logo_title_white.svg @@ -1,40 +1,35 @@ - + - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/ui/webpack.config.dev.js b/ui/webpack.config.dev.js index c669ec5b22..fa019d3292 100644 --- a/ui/webpack.config.dev.js +++ b/ui/webpack.config.dev.js @@ -60,6 +60,7 @@ module.exports = { allChunks: true, }), new webpack.DefinePlugin({ + THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version), '__DEVTOOLS__': false, 'process.env': { NODE_ENV: JSON.stringify('development'), diff --git a/ui/webpack.config.prod.js b/ui/webpack.config.prod.js index 09374fb6a5..a746488850 100644 --- a/ui/webpack.config.prod.js +++ b/ui/webpack.config.prod.js @@ -58,6 +58,7 @@ module.exports = { allChunks: true, }), new webpack.DefinePlugin({ + THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version), '__DEVTOOLS__': false, 'process.env': { NODE_ENV: JSON.stringify('production'),