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) -[](https://demo.thingsboard.io/demo?dashboardId=e8e409c0-f2b5-11e6-a6ee-bb0136cc33d0&source=github) +[**Smart energy**](https://thingsboard.io/smart-energy/) +[](https://thingsboard.io/smart-energy/) -[**Silos monitoring**](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github) -[](https://demo.thingsboard.io/demo?dashboardId=1f9828d0-058e-11e7-87f7-bb0136cc33d0&source=github) +[**Smart farming**](https://thingsboard.io/smart-farming/) +[](https://thingsboard.io/smart-farming/) -[**Smart bus tracking**](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github) -[](https://demo.thingsboard.io/demo?dashboardId=3d0bf910-ee09-11e6-b619-bb0136cc33d0&source=github) +[**Fleet tracking**](https://thingsboard.io/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 extends GrantedAuthority> 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 Timestamp\n {{ h.dataKey.label }}\n \n \n \n \n 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 \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 }} - + customer.title 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.assignedToCustomer + + + + + + dashboard.public-link + + + + + + {{ 'dashboard.copy-public-link' | translate }} + + + + dashboard.title 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.columns-count - + - {{ 'dashboard.export' | translate }} file_download - + - @@ -304,6 +309,6 @@ - Powered by Thingsboard + Powered by Thingsboard v.{{ vm.thingsboardVersion }} 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 @@ - + Last update time Key @@ -147,7 +147,7 @@ + md-on-paginate="onPaginate" md-page-select> -device.assignedToCustomer +{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}' +{{'device.public' | translate}} diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html index 99cedbdada..fd6c9d1f72 100644 --- a/ui/src/app/device/device-fieldset.tpl.html +++ b/ui/src/app/device/device-fieldset.tpl.html @@ -15,12 +15,15 @@ limitations under the License. --> +{{ 'device.make-public' | translate }} {{ 'device.assign-to-customer' | translate }} -{{ 'device.unassign-from-customer' | translate }} + class="md-raised md-primary">{{ isPublic ? 'device.make-private' : 'device.unassign-from-customer' | translate }} {{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }} @@ -47,10 +50,14 @@ + ng-show="!isEdit && isAssignedToCustomer && !isPublic && deviceScope === 'tenant'"> device.assignedToCustomer + + {{ 'device.device-public' | translate }} + device.name diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js index d111df8acb..55b70832e2 100644 --- a/ui/src/app/device/device.controller.js +++ b/ui/src/app/device/device.controller.js @@ -24,7 +24,31 @@ import deviceCredentialsTemplate from './device-credentials.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function DeviceController(userService, deviceService, customerService, $scope, $controller, $state, $stateParams, $document, $mdDialog, $q, $translate, types) { +export function DeviceCardController(types) { + + var vm = this; + + vm.types = types; + + vm.isAssignedToCustomer = function() { + if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === '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.devicesScope === 'tenant' && vm.item.assignedCustomer.isPublic) { + return true; + } + return false; + } +} + + +/*@ngInject*/ +export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) { var customerId = $stateParams.customerId; @@ -47,6 +71,7 @@ export default function DeviceController(userService, deviceService, customerSer getItemTitleFunc: getDeviceTitle, + itemCardController: 'DeviceCardController', itemCardTemplateUrl: deviceCard, parentCtl: vm, @@ -77,6 +102,7 @@ export default function DeviceController(userService, deviceService, customerSer vm.devicesScope = $state.$current.data.devicesType; vm.assignToCustomer = assignToCustomer; + vm.makePublic = makePublic; vm.unassignFromCustomer = unassignFromCustomer; vm.manageCredentials = manageCredentials; @@ -93,10 +119,20 @@ export default function DeviceController(userService, deviceService, customerSer vm.devicesScope = 'customer_user'; customerId = user.customerId; } + if (customerId) { + vm.customerDevicesTitle = $translate.instant('customer.devices'); + customerService.getShortCustomerInfo(customerId).then( + function success(info) { + if (info.isPublic) { + vm.customerDevicesTitle = $translate.instant('customer.public-devices'); + } + } + ); + } if (vm.devicesScope === 'tenant') { fetchDevicesFunction = function (pageLink) { - return deviceService.getTenantDevices(pageLink); + return deviceService.getTenantDevices(pageLink, true); }; deleteDeviceFunction = function (deviceId) { return deviceService.deleteDevice(deviceId); @@ -105,6 +141,18 @@ export default function DeviceController(userService, deviceService, customerSer return {"topIndex": vm.topIndex}; }; + deviceActionsList.push({ + onAction: function ($event, item) { + makePublic($event, item); + }, + name: function() { return $translate.instant('action.share') }, + details: function() { return $translate.instant('device.make-public') }, + icon: "share", + isEnabled: function(device) { + return device && (!device.customerId || device.customerId.id === types.id.nullUid); + } + }); + deviceActionsList.push( { onAction: function ($event, item) { @@ -122,17 +170,29 @@ export default function DeviceController(userService, deviceService, customerSer deviceActionsList.push( { onAction: function ($event, item) { - unassignFromCustomer($event, item); + unassignFromCustomer($event, item, false); }, name: function() { return $translate.instant('action.unassign') }, details: function() { return $translate.instant('device.unassign-from-customer') }, icon: "assignment_return", isEnabled: function(device) { - return device && device.customerId && device.customerId.id !== types.id.nullUid; + return device && device.customerId && device.customerId.id !== types.id.nullUid && !device.assignedCustomer.isPublic; } } ); + deviceActionsList.push({ + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('device.make-private') }, + icon: "reply", + isEnabled: function(device) { + return device && device.customerId && device.customerId.id !== types.id.nullUid && device.assignedCustomer.isPublic; + } + }); + deviceActionsList.push( { onAction: function ($event, item) { @@ -183,7 +243,7 @@ export default function DeviceController(userService, deviceService, customerSer } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') { fetchDevicesFunction = function (pageLink) { - return deviceService.getCustomerDevices(customerId, pageLink); + return deviceService.getCustomerDevices(customerId, pageLink, true); }; deleteDeviceFunction = function (deviceId) { return deviceService.unassignDeviceFromCustomer(deviceId); @@ -196,13 +256,30 @@ export default function DeviceController(userService, deviceService, customerSer deviceActionsList.push( { onAction: function ($event, item) { - unassignFromCustomer($event, item); + unassignFromCustomer($event, item, false); }, name: function() { return $translate.instant('action.unassign') }, details: function() { return $translate.instant('device.unassign-from-customer') }, - icon: "assignment_return" + icon: "assignment_return", + isEnabled: function(device) { + return device && !device.assignedCustomer.isPublic; + } } ); + deviceActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('device.make-private') }, + icon: "reply", + isEnabled: function(device) { + return device && device.assignedCustomer.isPublic; + } + } + ); + deviceActionsList.push( { onAction: function ($event, item) { @@ -287,8 +364,29 @@ export default function DeviceController(userService, deviceService, customerSer return device ? device.name : ''; } - function saveDevice (device) { - return deviceService.saveDevice(device); + function saveDevice(device) { + var deferred = $q.defer(); + deviceService.saveDevice(device).then( + function success(savedDevice) { + var devices = [ savedDevice ]; + customerService.applyAssignedCustomersInfo(devices).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 isCustomerUser() { @@ -335,7 +433,7 @@ export default function DeviceController(userService, deviceService, customerSer $event.stopPropagation(); } var pageSize = 10; - deviceService.getTenantDevices({limit: pageSize, textSearch: ''}).then( + deviceService.getTenantDevices({limit: pageSize, textSearch: ''}, false).then( function success(_devices) { var devices = { pageSize: pageSize, @@ -374,15 +472,27 @@ export default function DeviceController(userService, deviceService, customerSer assignToCustomer($event, deviceIds); } - function unassignFromCustomer($event, device) { + function unassignFromCustomer($event, device, isPublic) { if ($event) { $event.stopPropagation(); } + var title; + var content; + var label; + if (isPublic) { + title = $translate.instant('device.make-private-device-title', {deviceName: device.name}); + content = $translate.instant('device.make-private-device-text'); + label = $translate.instant('device.make-private'); + } else { + title = $translate.instant('device.unassign-device-title', {deviceName: device.name}); + content = $translate.instant('device.unassign-device-text'); + label = $translate.instant('device.unassign-device'); + } var confirm = $mdDialog.confirm() .targetEvent($event) - .title($translate.instant('device.unassign-device-title', {deviceName: device.name})) - .htmlContent($translate.instant('device.unassign-device-text')) - .ariaLabel($translate.instant('device.unassign-device')) + .title(title) + .htmlContent(content) + .ariaLabel(label) .cancel($translate.instant('action.no')) .ok($translate.instant('action.yes')); $mdDialog.show(confirm).then(function () { @@ -411,6 +521,24 @@ export default function DeviceController(userService, deviceService, customerSer }); } + function makePublic($event, device) { + if ($event) { + $event.stopPropagation(); + } + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('device.make-public-device-title', {deviceName: device.name})) + .htmlContent($translate.instant('device.make-public-device-text')) + .ariaLabel($translate.instant('device.make-public')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + deviceService.makeDevicePublic(device.id.id).then(function success() { + vm.grid.refreshList(); + }); + }); + } + function manageCredentials($event, device) { if ($event) { $event.stopPropagation(); diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js index 9c927aebae..eb50a155c6 100644 --- a/ui/src/app/device/device.directive.js +++ b/ui/src/app/device/device.directive.js @@ -26,6 +26,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl element.html(template); scope.isAssignedToCustomer = false; + scope.isPublic = false; scope.assignedCustomer = null; scope.deviceCredentials = null; @@ -41,13 +42,15 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl } if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) { scope.isAssignedToCustomer = true; - customerService.getCustomer(scope.device.customerId.id).then( + customerService.getShortCustomerInfo(scope.device.customerId.id).then( function success(customer) { scope.assignedCustomer = customer; + scope.isPublic = customer.isPublic; } ); } else { scope.isAssignedToCustomer = false; + scope.isPublic = false; scope.assignedCustomer = null; } } @@ -72,6 +75,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl deviceScope: '=', theForm: '=', onAssignToCustomer: '&', + onMakePublic: '&', onUnassignFromCustomer: '&', onManageCredentials: '&', onDeleteDevice: '&' diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js index d0293b416f..c1f5b278dc 100644 --- a/ui/src/app/device/device.routes.js +++ b/ui/src/app/device/device.routes.js @@ -61,7 +61,7 @@ export default function DeviceRoutes($stateProvider) { pageTitle: 'customer.devices' }, ncyBreadcrumb: { - label: '{"icon": "devices_other", "label": "customer.devices"}' + label: '{"icon": "devices_other", "label": "{{ vm.customerDevicesTitle }}", "translate": "false"}' } }); diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index 52c05f275f..90909e31aa 100644 --- a/ui/src/app/device/devices.tpl.html +++ b/ui/src/app/device/devices.tpl.html @@ -27,7 +27,8 @@ device-scope="vm.devicesScope" 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-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)" on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"> diff --git a/ui/src/app/device/index.js b/ui/src/app/device/index.js index 611e664837..b84497aa21 100644 --- a/ui/src/app/device/index.js +++ b/ui/src/app/device/index.js @@ -21,7 +21,7 @@ import thingsboardApiDevice from '../api/device.service'; import thingsboardApiCustomer from '../api/customer.service'; import DeviceRoutes from './device.routes'; -import DeviceController from './device.controller'; +import {DeviceController, DeviceCardController} from './device.controller'; import AssignDeviceToCustomerController from './assign-to-customer.controller'; import AddDevicesToCustomerController from './add-devices-to-customer.controller'; import ManageDeviceCredentialsController from './device-credentials.controller'; @@ -40,6 +40,7 @@ export default angular.module('thingsboard.device', [ ]) .config(DeviceRoutes) .controller('DeviceController', DeviceController) + .controller('DeviceCardController', DeviceCardController) .controller('AssignDeviceToCustomerController', AssignDeviceToCustomerController) .controller('AddDevicesToCustomerController', AddDevicesToCustomerController) .controller('ManageDeviceCredentialsController', ManageDeviceCredentialsController) diff --git a/ui/src/app/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js index 8ba6c04bbe..86cd676940 100644 --- a/ui/src/app/global-interceptor.service.js +++ b/ui/src/app/global-interceptor.service.js @@ -111,11 +111,12 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { function request(config) { var rejected = false; if (config.url.startsWith('/api/')) { - $rootScope.loading = !isInternalUrlPrefix(config.url); + var isLoading = !isInternalUrlPrefix(config.url); + updateLoadingState(config, isLoading); if (isTokenBasedAuthEntryPoint(config.url)) { if (!getUserService().updateAuthorizationHeader(config.headers) && !getUserService().refreshTokenPending()) { - $rootScope.loading = false; + updateLoadingState(config, false); rejected = true; getUserService().clearJwtToken(false); return $q.reject({ data: {message: getTranslate().instant('access.unauthorized')}, status: 401, config: config}); @@ -131,21 +132,21 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { function requestError(rejection) { if (rejection.config.url.startsWith('/api/')) { - $rootScope.loading = false; + updateLoadingState(rejection.config, false); } return $q.reject(rejection); } function response(response) { if (response.config.url.startsWith('/api/')) { - $rootScope.loading = false; + updateLoadingState(response.config, false); } return response; } function responseError(rejection) { if (rejection.config.url.startsWith('/api/')) { - $rootScope.loading = false; + updateLoadingState(rejection.config, false); } var unhandled = false; var ignoreErrors = rejection.config.ignoreErrors; @@ -184,4 +185,10 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { } return $q.reject(rejection); } + + function updateLoadingState(config, isLoading) { + if (!config || angular.isUndefined(config.ignoreLoading) || !config.ignoreLoading) { + $rootScope.loading = isLoading; + } + } } diff --git a/ui/src/app/layout/home.controller.js b/ui/src/app/layout/home.controller.js index 290cacf9a1..4979501085 100644 --- a/ui/src/app/layout/home.controller.js +++ b/ui/src/app/layout/home.controller.js @@ -26,7 +26,7 @@ import logoSvg from '../../svg/logo_title_white.svg'; /*@ngInject*/ export default function HomeController(loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state, - $log, $mdMedia, $animate, $timeout) { + $window, $log, $mdMedia, $animate, $timeout) { var siteSideNav = $('.tb-site-sidenav', $element); @@ -48,6 +48,7 @@ export default function HomeController(loginService, userService, deviceService, vm.displaySearchMode = displaySearchMode; vm.openSidenav = openSidenav; + vm.goBack = goBack; vm.searchTextUpdated = searchTextUpdated; vm.sidenavClicked = sidenavClicked; vm.toggleFullscreen = toggleFullscreen; @@ -104,6 +105,10 @@ export default function HomeController(loginService, userService, deviceService, vm.isShowSidenav = true; } + function goBack() { + $window.history.back(); + } + function closeSidenav() { vm.isShowSidenav = false; } diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html index 31c8e7fad9..bfb37eb3cd 100644 --- a/ui/src/app/layout/home.tpl.html +++ b/ui/src/app/layout/home.tpl.html @@ -45,6 +45,10 @@ class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}"> menu + + arrow_back + arrow_back diff --git a/ui/src/app/locale/locale.constant-ko.js b/ui/src/app/locale/locale.constant-ko.js index bdc938863d..ec194f70ad 100644 --- a/ui/src/app/locale/locale.constant-ko.js +++ b/ui/src/app/locale/locale.constant-ko.js @@ -775,7 +775,8 @@ export default function addLocaleKorean(locales) { "language": { "language": "언어", "en_US": "영어", - "ko_KR": "한글" + "ko_KR": "한글", + "zh_CN": "중국어" } }; angular.extend(locales, {'ko_KR': ko_KR}); diff --git a/ui/src/app/locale/locale.constant-zh.js b/ui/src/app/locale/locale.constant-zh.js new file mode 100644 index 0000000000..0ded94a793 --- /dev/null +++ b/ui/src/app/locale/locale.constant-zh.js @@ -0,0 +1,818 @@ +/* + * 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. + */ + +export default function addLocaleChinese(locales) { + var zh_CN = { + "access" : { + "unauthorized" : "未授权", + "unauthorized-access" : "未授权访问", + "unauthorized-access-text" : "您需要登陆才能访问这个资源!", + "access-forbidden" : "禁止访问", + "access-forbidden-text" : "您没有访问此位置的权限如果您仍希望访问此位置,请尝试使用其他用户登录。", + "refresh-token-expired" : "会话已过期", + "refresh-token-failed" : "无法刷新会话" + }, + "action" : { + "activate" : "激活", + "suspend" : "暂停", + "save" : "保存", + "saveAs" : "另存为", + "cancel" : "取消", + "ok" : "确定", + "delete" : "删除", + "add" : "添加", + "yes" : "是", + "no" : "否", + "update" : "更新", + "remove" : "移除", + "search" : "查询", + "assign" : "分配", + "unassign" : "取消分配", + "share" : "分享", + "make-private" : "私有", + "apply" : "应用", + "apply-changes" : "应用更改", + "edit-mode" : "编辑模式", + "enter-edit-mode" : "进入编辑模式", + "decline-changes" : "拒绝变更", + "close" : "关闭", + "back" : "后退", + "run" : "运行", + "sign-in" : "登录!", + "edit" : "编辑", + "view" : "查看", + "create" : "创建", + "drag" : "拖拽", + "refresh" : "刷新", + "undo" : "撤销", + "copy" : "复制", + "paste" : "粘贴", + "import" : "导入", + "export" : "导出", + "share-via" : "通过 {{provider}}分享" + }, + "aggregation" : { + "aggregation" : "聚合", + "function" : "数据聚合功能", + "limit" : "最大值", + "group-interval" : "分组间隔", + "min" : "最少值", + "max" : "最大值", + "avg" : "平均值", + "sum" : "求和", + "count" : "计数", + "none" : "空" + }, + "admin" : { + "general" : "常规", + "general-settings" : "常规设置", + "outgoing-mail" : "发送邮件", + "outgoing-mail-settings" : "发送邮件设置", + "system-settings" : "系统设置", + "test-mail-sent" : "测试邮件发送成功!", + "base-url" : "基本URL", + "base-url-required" : "基本URL是必须的。", + "mail-from" : "邮件来自", + "mail-from-required" : "邮件发件人是必须的。", + "smtp-protocol" : "SMTP协议", + "smtp-host" : "SMTP主机", + "smtp-host-required" : "SMTP主机是必须的。", + "smtp-port" : "SMTP端口", + "smtp-port-required" : "您必须提供一个smtp端口。", + "smtp-port-invalid" : "这看起来不是有效的smtp端口。", + "timeout-msec" : "超时(ms)", + "timeout-required" : "超时是必须的。", + "timeout-invalid" : "这看起来不像有效的超时值。", + "enable-tls" : "启用TLS", + "send-test-mail" : "发送测试邮件" + }, + "attribute" : { + "attributes" : "属性", + "latest-telemetry" : "最新遥测", + "attributes-scope" : "设备属性范围", + "scope-latest-telemetry" : "最新遥测", + "scope-client" : "客户端属性", + "scope-server" : "服务端属性", + "scope-shared" : "共享属性", + "add" : "添加属性", + "key" : "键", + "key-required" : "属性键是必需的。", + "value" : "值", + "value-required" : "属性值是必需的。", + "delete-attributes-title" : "您确定要删除 { count, select, 1 {1 attribute} other {# attributes} }吗?", + "delete-attributes-text" : "注意,确认后所有选中的属性都会被删除。", + "delete-attributes" : "删除属性", + "enter-attribute-value" : "输入属性值", + "show-on-widget" : "在小部件上显示", + "widget-mode" : "小部件模式", + "next-widget" : "下一个小部件", + "prev-widget" : "上一个小部件", + "add-to-dashboard" : "添加到仪表板", + "add-widget-to-dashboard" : "将小部件添加到仪表板", + "selected-attributes" : "{ count, select, 1 {1 attribute} other {# attributes} } 被选中", + "selected-telemetry" : "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } 被选中" + }, + "confirm-on-exit" : { + "message" : "您有未保存的更改。确定要离开此页吗?", + "html-message" : "您有未保存的更改。 确定要离开此页面吗?", + "title" : "未保存的更改" + }, + "contact" : { + "country" : "国家", + "city" : "城市", + "state" : "州", + "postal-code" : "邮政编码", + "postal-code-invalid" : "只允许数字。", + "address" : "地址", + "address2" : "地址2", + "phone" : "手机", + "email" : "邮箱", + "no-address" : "无地址" + }, + "common" : { + "username" : "用户名", + "password" : "密码", + "enter-username" : "输入用户名", + "enter-password" : "输入密码", + "enter-search" : "输入检索条件" + }, + "customer" : { + "customers" : "客户", + "management" : "客户管理", + "dashboard" : "客户仪表板", + "dashboards" : "客户仪表板", + "devices" : "客户设备", + "public-dashboards" : "公共仪表板", + "public-devices" : "公共设备", + "add" : "添加客户", + "delete" : "删除客户", + "manage-customer-users" : "管理客户用户", + "manage-customer-devices" : "管理客户设备", + "manage-customer-dashboards" : "管理客户仪表板", + "manage-public-devices" : "管理公共设备", + "manage-public-dashboards" : "管理公共仪表板", + "add-customer-text" : "添加新客户", + "no-customers-text" : "没有找到客户", + "customer-details" : "客户详情", + "delete-customer-title" : "您确定要删除客户'{{customerTitle}}'吗?", + "delete-customer-text" : "小心!确认后,客户及其所有相关数据将不可恢复。", + "delete-customers-title" : "您确定要删除 { count, select, 1 {1 customer} other {# customers} }?吗", + "delete-customers-action-title" : "删除 { count, select, 1 {1 customer} other {# customers} }", + "delete-customers-text" : "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。", + "manage-users" : "管理用户", + "manage-devices" : "管理设备", + "manage-dashboards" : "管理仪表板", + "title" : "标题", + "title-required" : "需要标题。", + "description" : "描述" + }, + "datetime" : { + "date-from" : "日期从", + "time-from" : "时间从", + "date-to" : "日期到", + "time-to" : "时间到" + }, + "dashboard" : { + "dashboard" : "仪表板", + "dashboards" : "仪表板库", + "management" : "仪表板管理", + "view-dashboards" : "查看仪表板", + "add" : "添加仪表板", + "assign-dashboard-to-customer" : "将仪表板分配给客户", + "assign-dashboard-to-customer-text" : "请选择要分配给客户的仪表板", + "assign-to-customer-text" : "请选择客户分配仪表板", + "assign-to-customer" : "分配给客户", + "unassign-from-customer" : "取消分配客户", + "make-public" : "使仪表板公有", + "make-private" : "使仪表板私有", + "no-dashboards-text" : "没有找到仪表板", + "no-widgets" : "没有配置小部件", + "add-widget" : "添加新的小部件", + "title" : "标题", + "select-widget-title" : "选择小部件", + "select-widget-subtitle" : "可用的小部件类型列表", + "delete" : "删除仪表板", + "title-required" : "需要标题。", + "description" : "描述", + "details" : "详情", + "dashboard-details" : "仪表板详情", + "add-dashboard-text" : "添加新的仪表板", + "assign-dashboards" : "分配仪表板", + "assign-new-dashboard" : "分配新的仪表板", + "assign-dashboards-text" : "分配 { count, select, 1 {1 dashboard} other {# dashboards} } 给客户", + "delete-dashboards" : "删除仪表板", + "unassign-dashboards" : "取消分配仪表板", + "unassign-dashboards-action-title" : "取消分配 { count, select, 1 {1 dashboard} other {# dashboards} } from customer", + "delete-dashboard-title" : "您确定要删除仪表板 '{{dashboardTitle}}'吗?", + "delete-dashboard-text" : "小心!确认后仪表板及其所有相关数据将不可恢复。", + "delete-dashboards-title" : "你确定你要删除 { count, select, 1 {1 dashboard} other {# dashboards} }吗?", + "delete-dashboards-action-title" : "删除 { count, select, 1 {1 dashboard} other {# dashboards} }", + "delete-dashboards-text" : "小心!确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。", + "unassign-dashboard-title" : "您确定要取消分配仪表板 '{{dashboardTitle}}'吗?", + "unassign-dashboard-text" : "确认后,面板将被取消分配,客户将无法访问。", + "unassign-dashboard" : "取消分配仪表板", + "unassign-dashboards-title" : "您确定要取消分配仪表板 { count, select, 1 {1 dashboard} other {# dashboards} } 吗?", + "unassign-dashboards-text" : "确认后,所有选定的仪表板将被取消分配,客户将无法访问。", + "public-dashboard-title" : "仪表板现已公布", + "public-dashboard-text" : "你的仪表板 {{dashboardTitle}} 已被公开,可通过如下 链接访问:", + "public-dashboard-notice" : "提示: 不要忘记将相关设备公开以访问其数据。", + "make-private-dashboard-title" : "您确定要使仪表板 '{{dashboardTitle}}' 私有吗?", + "make-private-dashboard-text" : "确认后,仪表板将被私有,不能被其他人访问。", + "make-private-dashboard" : "仪表板私有", + "socialshare-text" : "'{{dashboardTitle}}' 由ThingsBoard提供支持", + "socialshare-title" : "'{{dashboardTitle}}' 由ThingsBoard提供支持", + "select-dashboard" : "选择仪表板", + "no-dashboards-matching" : "找不到符合 '{{dashboard}}' 的仪表板。", + "dashboard-required" : "仪表板是必需的。", + "select-existing" : "选择现有仪表板", + "create-new" : "创建新的仪表板", + "new-dashboard-title" : "新仪表板标题", + "open-dashboard" : "打开仪表板", + "set-background" : "设置背景", + "background-color" : "背景颜色", + "background-image" : "背景图片", + "background-size-mode" : "背景大小模式", + "no-image" : "无图像选择", + "drop-image" : "拖拽图像或单击以选择要上传的文件。", + "settings" : "设置", + "columns-count" : "列数", + "columns-count-required" : "需要列数。", + "min-columns-count-message" : "只允许最少10列", + "max-columns-count-message" : "只允许最多1000列", + "widgets-margins" : "部件间边距", + "horizontal-margin" : "水平边距", + "horizontal-margin-required" : "需要水平边距值。", + "min-horizontal-margin-message" : "只允许0作为最小水平边距值。", + "max-horizontal-margin-message" : "只允许50作为最大水平边距值。", + "vertical-margin" : "垂直边距", + "vertical-margin-required" : "需要垂直边距值。", + "min-vertical-margin-message" : "只允许0作为最小垂直边距值。", + "max-vertical-margin-message" : "只允许50作为最大垂直边距值。", + "display-title" : "显示仪表板标题", + "title-color" : "标题颜色", + "display-device-selection" : "显示设备选择", + "display-dashboard-timewindow" : "显示时间窗口", + "display-dashboard-export" : "显示导出", + "import" : "导入仪表板", + "export" : "导出仪表板", + "export-failed-error" : "无法导出仪表板: {{error}}", + "create-new-dashboard" : "创建新的仪表板", + "dashboard-file" : "仪表板文件", + "invalid-dashboard-file-error" : "无法导入仪表板: 仪表板数据结构无效。", + "dashboard-import-missing-aliases-title" : "配置导入仪表板使用的别名", + "create-new-widget" : "创建新小部件", + "import-widget" : "导入小部件", + "widget-file" : "小部件文件", + "invalid-widget-file-error" : "无法导入窗口小部件: 窗口小部件数据结构无效。", + "widget-import-missing-aliases-title" : "配置导入的窗口小部件使用的别名", + "open-toolbar" : "打开仪表板工具栏", + "close-toolbar" : "关闭工具栏", + "configuration-error" : "配置错误", + "alias-resolution-error-title" : "仪表板别名配置错误", + "invalid-aliases-config" : "无法找到与某些别名过滤器匹配的任何设备。" + + "请联系您的管理员以解决此问题。", + "select-devices" : "选择设备", + "assignedToCustomer" : "分配给客户", + "public" : "公共", + "public-link" : "公共链接", + "copy-public-link" : "复制公共链接", + "public-link-copied-message" : "仪表板的公共链接已被复制到剪贴板" + }, + "datakey" : { + "settings": "设置", + "advanced": "高级", + "label": "标签", + "color": "颜色", + "data-generation-func": "数据生成功能", + "use-data-post-processing-func": "使用数据后处理功能", + "configuration": "数据键配置", + "timeseries": "时间序列", + "attributes": "属性", + "timeseries-required": "需要设备时间序列。", + "timeseries-or-attributes-required": "设备时间/属性是必需的。", + "function-types": "函数类型", + "function-types-required": "需要函数类型。" + }, + "datasource" : { + "type": "数据源类型", + "add-datasource-prompt": "请添加数据源" + }, + "details" : { + "edit-mode": "编辑模式", + "toggle-edit-mode": "切换编辑模式" + }, + "device" : { + "device": "设备", + "device-required": "设备是必需的", + "devices": "设备", + "management": "设备管理", + "view-devices": "查看设备", + "device-alias": "设备别名", + "aliases": "设备别名", + "no-alias-matching" : "'{{alias}}' 没有找到。", + "no-aliases-found" : "找不到别名。", + "no-key-matching" : "'{{key}}' 没有找到。", + "no-keys-found" : "找不到密钥。", + "create-new-alias": "创建一个新的!", + "create-new-key": "创建一个新的!", + "duplicate-alias-error" : "找到重复别名 '{{alias}}'。 设备别名必须是唯一的。", + "configure-alias" : "配置 '{{alias}}' 别名", + "no-devices-matching" : "找不到与 '{{device}}' 匹配的设备。", + "alias" : "别名", + "alias-required" : "需要设备别名。", + "remove-alias": "删除设备别名", + "add-alias": "添加设备别名", + "name-starts-with" : "名称前缀", + "device-list" : "设备列表", + "use-device-name-filter" : "使用过滤器", + "device-list-empty" : "没有被选中的设备", + "device-name-filter-required" : "设备名称过滤器是必需得。", + "device-name-filter-no-device-matched" : "找不到以'{{device}}' 开头的设备。", + "add" : "添加设备", + "assign-to-customer": "分配给客户", + "assign-device-to-customer": "将设备分配给客户", + "assign-device-to-customer-text": "请选择要分配给客户的设备", + "make-public" : "公有", + "make-private" : "私有", + "no-devices-text": "找不到设备", + "assign-to-customer-text": "请选择客户分配设备", + "device-details": "设备详细信息", + "add-device-text": "添加新设备", + "credentials": "凭据", + "manage-credentials": "管理凭据", + "delete": "删除设备", + "assign-devices": "分配设备", + "assign-devices-text": "将{count,select,1 {1 device} other {# devices}}分配给客户", + "delete-devices": "删除设备", + "unassign-from-customer": "取消分配客户", + "unassign-devices": "取消分配设备", + "unassign-devices-action-title": "从客户处取消分配{count,select,1 {1 device} other {# devices}}", + "assign-new-device": "分配新设备", + "make-public-device-title" : "您确定要将设备 '{{deviceName}}' 设为公开吗?", + "make-public-device-text" : "确认后,设备及其所有数据将被公开并可被其他人访问。", + "make-private-device-title" : "您确定要将设备 '{{deviceName}}' 设为私有吗?", + "make-private-device-text" : "确认后,设备及其所有数据将被私有化,不被其他人访问。", + "view-credentials": "查看凭据", + "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", + "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", + "delete-devices-title": "您确定要删除{count,select,1 {1 device} other {# devices}} 吗?", + "delete-devices-action-title": "删除 {count,select,1 {1 device} other {# devices}}", + "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", + "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", + "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", + "unassign-device": "取消分配设备", + "unassign-devices-title": "您确定要取消分配{count,select,1 {1 device} other {# devices}} 吗?", + "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", + "device-credentials": "设备凭据", + "credentials-type": "凭据类型", + "access-token": "访问令牌", + "access-token-required": "需要访问令牌", + "access-token-invalid": "访问令牌长度必须为1到20个字符。", + "rsa-key": "RSA公钥", + "rsa-key-required": "需要RSA公钥", + "secret": "密钥", + "secret-required": "密钥是必需的", + "name": "名称", + "name-required": "名称是必需的。", + "description": "说明", + "events": "事件", + "details": "详细信息", + "copyId": "复制设备ID", + "copyAccessToken": "复制访问令牌", + "idCopiedMessage": "设备ID已复制到剪贴板", + "accessTokenCopiedMessage": "设备访问令牌已复制到剪贴板", + "assignedToCustomer": "分配给客户", + "unable-delete-device-alias-title": "无法删除设备别名", + "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用: {{widgetsList}}", + "is-gateway": "是网关", + "public" : "公共", + "device-public" : "设备是公共的" + }, + "dialog" : { + "close" : "关闭对话框" + }, + "error" : { + "unable-to-connect": "无法连接到服务器!请检查您的互联网连接。", + "unhandled-error-code": "未处理的错误代码: {{errorCode}}", + "unknown-error": "未知错误" + }, + "event" : { + "event-type": "事件类型", + "type-alarm": "报警", + "type-error": "错误", + "type-lc-event": "生命周期事件", + "type-stats": "类型统计", + "no-events-prompt": "找不到事件", + "error": "错误", + "alarm": "报警", + "event-time": "事件时间", + "server": "服务器", + "body": "整体", + "method": "方法", + "event": "事件", + "status": "状态", + "success": "成功", + "failed": "失败", + "messages-processed": "消息处理", + "errors-occurred": "错误发生" + }, + "fullscreen" : { + "expand": "展开到全屏", + "exit": "退出全屏", + "toggle": "切换全屏模式", + "fullscreen": "全屏" + }, + "function" : { + "function" : "函数" + }, + "grid" : { + "delete-item-title": "您确定要删除此项吗?", + "delete-item-text": "注意,确认后此项及其所有相关数据将变得不可恢复。", + "delete-items-title" : "你确定你要删除 { count, select, 1 {1 item} other {# items} }吗?", + "delete-items-action-title" : "删除 { count, select, 1 {1 item} other {# items} }", + "delete-items-text": "注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。", + "add-item-text": "添加新项目", + "no-items-text": "没有找到项目", + "item-details": "项目详细信息", + "delete-item": "删除项目", + "delete-items": "删除项目", + "scroll-to-top": "滚动到顶部" + }, + "help" : { + "goto-help-page" : "转到帮助页面" + }, + "home" : { + "home": "首页", + "profile": "属性", + "logout": "注销", + "menu": "菜单", + "avatar": "头像", + "open-user-menu": "打开用户菜单" + }, + "import" : { + "no-file" : "没有选择文件", + "drop-file" : "拖动一个JSON文件或者单击以选择要上传的文件。" + }, + "item" : { + "selected" : "选择" + }, + "js-func" : { + "no-return-error": "函数必须返回值!", + "return-type-mismatch": "函数必须返回 '{{type}}' 类型的值!" + }, + "legend" : { + "position" : "图例位置", + "show-max" : "显示最大值", + "show-min" : "显示最小值", + "show-avg" : "显示平均值", + "show-total" : "显示总数", + "settings" : "图例设置", + "min" : "最小值", + "max" : "最大值", + "avg" : "平均值", + "total" : "总数" + }, + "login" : { + "login": "登录", + "request-password-reset": "请求密码重置", + "reset-password": "重置密码", + "create-password": "创建密码", + "passwords-mismatch-error": "输入的密码必须相同!", + "password-again": "再次输入密码", + "sign-in": "登录 ", + "username": "用户名(电子邮件)", + "remember-me": "记住我", + "forgot-password": "忘记密码?", + "password-reset": "密码重置", + "new-password": "新密码", + "new-password-again": "再次输入新密码", + "password-link-sent-message": "密码重置链接已成功发送!", + "email": "电子邮件" + }, + "plugin" : { + "plugins" : "插件", + "delete" : "删除插件", + "activate" : "激活插件", + "suspend" : "暂停插件", + "active" : "激活", + "suspended" : "暂停", + "name" : "名称", + "name-required" : "名称是必填项。", + "description" : "描述", + "add" : "添加插件", + "delete-plugin-title" : "你确定要删除插件 '{{pluginName}}' 吗?", + "delete-plugin-text" : "小心!确认后,插件和所有相关数据将不可恢复。", + "delete-plugins-title" : "你确定你要删除 { count, select, 1 {1 plugin} other {# plugins} } 吗?", + "delete-plugins-action-title" : "删除 { count, select, 1 {1 plugin} other {# plugins} }", + "delete-plugins-text" : "小心!确认后,所有选定的插件将被删除,所有相关数据将不可恢复。", + "add-plugin-text" : "添加新的插件", + "no-plugins-text" : "没有找到插件", + "plugin-details" : "插件详细信息", + "api-token" : "API令牌", + "api-token-required" : "API令牌是必需的。", + "type" : "插件类型", + "type-required" : "插件类型是必需的。", + "configuration" : "插件配置", + "system" : "系统", + "select-plugin" : "选择插件", + "plugin" : "插件", + "no-plugins-matching" : "没有找到匹配'{{plugin}}'的插件。", + "plugin-required" : "插件是必需的。", + "plugin-require-match" : "请选择一个现有的插件。", + "events" : "事件", + "details" : "详情", + "import" : "导入插件", + "export" : "导出插件", + "export-failed-error" : "无法导出插件:{{error}}", + "create-new-plugin" : "创建新的插件", + "plugin-file" : "插件文件", + "invalid-plugin-file-error" : "无法导入插件:插件数据结构无效。" + }, + "position" : { + "top" : "顶部", + "bottom" : "底部", + "left" : "左侧", + "right" : "右侧" + }, + "profile" : { + "profile": "属性", + "change-password": "更改密码", + "current-password": "当前密码" + }, + "rule" : { + "rules" : "规则", + "delete" : "删除规则", + "activate" : "激活规则", + "suspend" : "暂停规则", + "active" : "激活", + "suspended" : "暂停", + "name" : "名称", + "name-required" : "名称是必填项。", + "description" : "描述", + "add" : "添加规则", + "delete-rule-title" : "您确定要删除规则'{{ruleName}}'吗?", + "delete-rule-text" : "小心!确认后,规则和所有相关数据将不可恢复。", + "delete-rules-title" : "你确定要删除 {count, select, 1 {1 rule} other {# rules}} 吗?", + "delete-rules-action-title" : "删除 { count, select, 1 {1 rule} other {# rules} }", + "delete-rules-text" : "小心!确认后,所有选定的规则将被删除,所有相关数据将不可恢复。", + "add-rule-text" : "添加新规则", + "no-rules-text" : "没有找到规则", + "rule-details" : "规则详情", + "filters" : "过滤器", + "filter" : "过滤器", + "add-filter-prompt" : "请添加过滤器", + "remove-filter" : "删除过滤器", + "add-filter" : "添加过滤器", + "filter-name" : "过滤器名称", + "filter-type" : "过滤器类型", + "edit-filter" : "编辑过滤器", + "view-filter" : "查看过滤器", + "component-name" : "名称", + "component-name-required" : "名称是必填项。", + "component-type" : "类型", + "component-type-required" : "类型是必填项。", + "processor" : "处理器", + "no-processor-configured" : "未配置处理器", + "create-processor" : "创建处理器", + "processor-name" : "处理器名称", + "processor-type" : "处理器类型", + "plugin-action" : "插件动作", + "action-name" : "动作名称", + "action-type" : "动作类型", + "create-action-prompt" : "请创建动作", + "create-action" : "创建动作", + "details" : "详情", + "events" : "事件", + "system" : "系统", + "import" : "导入规则", + "export" : "导出规则", + "export-failed-error" : "无法导出规则:{{error}}", + "create-new-rule" : "创建新规则", + "rule-file" : "规则文件", + "invalid-rule-file-error" : "无法导入规则:规则数据结构无效。" + }, + "rule-plugin" : { + "management" : "规则和插件管理" + }, + "tenant" : { + "tenants" : "租户", + "management" : "租户管理", + "add" : "添加租户", + "admins" : "管理员", + "manage-tenant-admins" : "管理租户管理员", + "delete" : "删除租户", + "add-tenant-text" : "添加新租户", + "no-tenants-text" : "没有找到租户", + "tenant-details" : "租客详情", + "delete-tenant-title" : "您确定要删除租户'{{tenantTitle}}'吗?", + "delete-tenant-text" : "小心!确认后,租户和所有相关数据将不可恢复。", + "delete-tenants-title" : "您确定要删除 {count,select,1 {1 tenant} other {# tenants}} 吗?", + "delete-tenants-action-title" : "删除 { count, select, 1 {1 tenant} other {# tenants} }", + "delete-tenants-text" : "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", + "title" : "标题", + "title-required" : "标题是必填项。", + "description" : "描述" + }, + "timeinterval" : { + "seconds-interval" : "{ seconds, select, 1 {1 second} other {# seconds} }", + "minutes-interval" : "{ minutes, select, 1 {1 minute} other {# minutes} }", + "hours-interval" : "{ hours, select, 1 {1 hour} other {# hours} }", + "days-interval" : "{ days, select, 1 {1 day} other {# days} }", + "days" : "天", + "hours" : "时", + "minutes" : "分", + "seconds" : "秒", + "advanced" : "高级" + }, + "timewindow" : { + "days" : "{ days, select, 1 { day } other {# days } }", + "hours" : "{ hours, select, 0 { hour } 1 {1 hour } other {# hours } }", + "minutes" : "{ minutes, select, 0 { minute } 1 {1 minute } other {# minutes } }", + "seconds" : "{ seconds, select, 0 { second } 1 {1 second } other {# seconds } }", + "realtime" : "实时", + "history" : "历史", + "last-prefix" : "最后", + "period" : "从 {{ startTime }} 到 {{ endTime }}", + "edit" : "编辑时间窗口", + "date-range" : "日期范围", + "last" : "最后", + "time-period" : "时间段" + }, + "user" : { + "users" : "用户", + "customer-users" : "客户用户", + "tenant-admins" : "租户管理员", + "sys-admin" : "系统管理员", + "tenant-admin" : "租户管理员", + "customer" : "客户", + "anonymous" : "匿名", + "add" : "添加用户", + "delete" : "删除用户", + "add-user-text" : "添加新用户", + "no-users-text" : "找不到用户", + "user-details" : "用户详细信息", + "delete-user-title" : "您确定要删除用户 '{{userEmail}}' 吗?", + "delete-user-text" : "小心!确认后,用户和所有相关数据将不可恢复。", + "delete-users-title" : "你确定你要删除 { count, select, 1 {1 user} other {# users} } 吗?", + "delete-users-action-title" : "删除 { count, select, 1 {1 user} other {# users} }", + "delete-users-text" : "小心!确认后,所有选定的用户将被删除,所有相关数据将不可恢复。", + "activation-email-sent-message" : "激活电子邮件已成功发送!", + "resend-activation" : "重新发送激活", + "email" : "电子邮件", + "email-required" : "电子邮件是必需的。", + "first-name" : "名字", + "last-name" : "姓", + "description" : "描述", + "default-dashboard" : "默认面板", + "always-fullscreen" : "始终全屏" + }, + "value" : { + "type" : "值类型", + "string" : "字符串", + "string-value" : "字符串值", + "integer" : "数字", + "integer-value" : "数字值", + "invalid-integer-value" : "整数值无效", + "double" : "双精度小数", + "double-value" : "双精度小数值", + "boolean" : "布尔", + "boolean-value" : "布尔值", + "false" : "假", + "true" : "真" + }, + "widget" : { + "widget-library" : "小部件库", + "widget-bundle" : "小部件包", + "select-widgets-bundle" : "选择小部件包", + "management" : "小部件管理", + "editor" : "小部件编辑器", + "widget-type-not-found" : "加载小部件配置时出现问题。 可能关联的\n 小部件类型已删除。", + "widget-type-load-error" : "由于以下错误,小工具未加载:", + "remove" : "删除小部件", + "edit" : "编辑小部件", + "remove-widget-title" : "您确定要删除小部件 '{{widgetTitle}}' 吗?", + "remove-widget-text" : "确认后,窗口小部件和所有相关数据将不可恢复。", + "timeseries" : "时间序列", + "latest-values" : "最新值", + "rpc" : "控件小部件", + "static" : "静态小部件", + "select-widget-type" : "选择窗口小部件类型", + "missing-widget-title-error" : "小部件标题必须指定!", + "widget-saved" : "小部件已保存", + "unable-to-save-widget-error" : "无法保存窗口小部件! 小部件有错误!", + "save" : "保存小部件", + "saveAs" : "将小部件另存为", + "save-widget-type-as" : "将小部件类型另存为", + "save-widget-type-as-text" : "请输入新的小部件标题和/或选择目标小部件包", + "toggle-fullscreen" : "切换全屏", + "run" : "运行小部件", + "title" : "小部件标题", + "title-required" : "需要小部件标题。", + "type" : "小部件类型", + "resources" : "资源", + "resource-url" : "JavaScript/CSS URL", + "remove-resource" : "删除资源", + "add-resource" : "添加资源", + "html" : "HTML", + "tidy" : "整理", + "css" : "CSS", + "settings-schema" : "设置模式", + "datakey-settings-schema" : "数据键设置模式", + "javascript" : "Javascript", + "remove-widget-type-title" : "您确定要删除小部件类型 '{{widgetName}}'吗?", + "remove-widget-type-text" : "确认后,窗口小部件类型和所有相关数据将不可恢复。", + "remove-widget-type" : "删除小部件类型", + "add-widget-type" : "添加新的小部件类型", + "widget-type-load-failed-error" : "无法加载小部件类型!", + "widget-template-load-failed-error" : "无法加载小部件模板!", + "add" : "添加小部件", + "undo" : "撤消小部件更改", + "export" : "导出小部件" + }, + "widgets-bundle" : { + "current" : "当前包", + "widgets-bundles" : "小部件包", + "add" : "添加小部件包", + "delete" : "删除小部件包", + "title" : "标题", + "title-required" : "标题是必填项。", + "add-widgets-bundle-text" : "添加新的小部件包", + "no-widgets-bundles-text" : "找不到小部件包", + "empty" : "小部件包是空的", + "details" : "详情", + "widgets-bundle-details" : "小部件包详细信息", + "delete-widgets-bundle-title" : "您确定要删除小部件包 '{{widgetsBundleTitle}}'吗?", + "delete-widgets-bundle-text" : "小心!确认后,小部件包和所有相关数据将不可恢复。", + "delete-widgets-bundles-title" : "你确定你要删除 { count, select, 1 {1 widgets bundle} other {# widgets bundles} } 吗?", + "delete-widgets-bundles-action-title" : "删除 { count, select, 1 {1 widgets bundle} other {# widgets bundles} }", + "delete-widgets-bundles-text" : "小心!确认后,所有选定的小部件包将被删除,所有相关数据将不可恢复。", + "no-widgets-bundles-matching" : "没有找到与 '{{widgetsBundle}}' 匹配的小部件包。", + "widgets-bundle-required" : "需要小部件包。", + "system" : "系统", + "import" : "导入小部件包", + "export" : "导出小部件包", + "export-failed-error" : "无法导出小部件包: {{error}}", + "create-new-widgets-bundle" : "创建新的小部件包", + "widgets-bundle-file" : "小部件包文件", + "invalid-widgets-bundle-file-error" : "无法导入小部件包:无效的小部件包数据结构。" + }, + "widget-config" : { + "data" : "数据", + "settings" : "设置", + "advanced" : "高级", + "title" : "标题", + "general-settings" : "常规设置", + "display-title" : "显示标题", + "drop-shadow" : "阴影", + "enable-fullscreen" : "启用全屏", + "background-color" : "背景颜色", + "text-color" : "文字颜色", + "padding" : "填充", + "title-style" : "标题风格", + "mobile-mode-settings" : "移动模式设置", + "order" : "顺序", + "height" : "高度", + "units" : "特殊符号展示值", + "decimals" : "浮点数后的位数", + "timewindow" : "时间窗口", + "use-dashboard-timewindow" : "使用仪表板的时间窗口", + "display-legend" : "显示图例", + "datasources" : "数据源", + "datasource-type" : "类型", + "datasource-parameters" : "参数", + "remove-datasource" : "移除数据源", + "add-datasource" : "添加数据源", + "target-device" : "目标设备" + }, + "widget-type" : { + "import" : "导入小部件类型", + "export" : "导出小部件类型", + "export-failed-error" : "无法导出小部件类型: {{error}}", + "create-new-widget-type" : "创建新的小部件类型", + "widget-type-file" : "小部件类型文件", + "invalid-widget-type-file-error" : "无法导入小部件类型:无效的小部件类型数据结构。" + }, + "language" : { + "language" : "语言", + "en_US" : "英语", + "ko_KR" : "韩语", + "zh_CN" : "汉语" + } + }; + angular.extend(locales, { + 'zh_CN' : zh_CN + }); +} \ No newline at end of file diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 3de229e9aa..c7cef449e0 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -43,6 +43,8 @@ export default angular.module('thingsboard.locale', []) "search": "Search", "assign": "Assign", "unassign": "Unassign", + "share": "Share", + "make-private": "Make private", "apply": "Apply", "apply-changes": "Apply changes", "edit-mode": "Edit mode", @@ -61,7 +63,8 @@ export default angular.module('thingsboard.locale', []) "copy": "Copy", "paste": "Paste", "import": "Import", - "export": "Export" + "export": "Export", + "share-via": "Share via {{provider}}" }, "aggregation": { "aggregation": "Aggregation", @@ -154,11 +157,15 @@ export default angular.module('thingsboard.locale', []) "dashboard": "Customer Dashboard", "dashboards": "Customer Dashboards", "devices": "Customer Devices", + "public-dashboards": "Public Dashboards", + "public-devices": "Public Devices", "add": "Add Customer", "delete": "Delete customer", "manage-customer-users": "Manage customer users", "manage-customer-devices": "Manage customer devices", "manage-customer-dashboards": "Manage customer dashboards", + "manage-public-devices": "Manage public devices", + "manage-public-dashboards": "Manage public dashboards", "add-customer-text": "Add new customer", "no-customers-text": "No customers found", "customer-details": "Customer details", @@ -191,6 +198,8 @@ export default angular.module('thingsboard.locale', []) "assign-to-customer-text": "Please select the customer to assign the dashboard(s)", "assign-to-customer": "Assign to customer", "unassign-from-customer": "Unassign from customer", + "make-public": "Make dashboard public", + "make-private": "Make dashboard private", "no-dashboards-text": "No dashboards found", "no-widgets": "No widgets configured", "add-widget": "Add new widget", @@ -219,6 +228,14 @@ export default angular.module('thingsboard.locale', []) "unassign-dashboard": "Unassign dashboard", "unassign-dashboards-title": "Are you sure you want to unassign { count, select, 1 {1 dashboard} other {# dashboards} }?", "unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.", + "public-dashboard-title": "Dashboard is now public", + "public-dashboard-text": "Your dashboard {{dashboardTitle}} is now public and accessible via next public link:", + "public-dashboard-notice": "Note: Do not forget to make related devices public in order to access their data.", + "make-private-dashboard-title": "Are you sure you want to make the dashboard '{{dashboardTitle}}' private?", + "make-private-dashboard-text": "After the confirmation the dashboard will be made private and won't be accessible by others.", + "make-private-dashboard": "Make dashboard private", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", "select-dashboard": "Select dashboard", "no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.", "dashboard-required": "Dashboard is required.", @@ -248,6 +265,9 @@ export default angular.module('thingsboard.locale', []) "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", "display-title": "Display dashboard title", "title-color": "Title color", + "display-device-selection": "Display device selection", + "display-dashboard-timewindow": "Display timewindow", + "display-dashboard-export": "Display export", "import": "Import dashboard", "export": "Export dashboard", "export-failed-error": "Unable to export dashboard: {{error}}", @@ -267,7 +287,11 @@ export default angular.module('thingsboard.locale', []) "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter." + "Please contact your administrator in order to resolve this issue.", "select-devices": "Select devices", - "assignedToCustomer": "Assigned to customer" + "assignedToCustomer": "Assigned to customer", + "public": "Public", + "public-link": "Public link", + "copy-public-link": "Copy public link", + "public-link-copied-message": "Dashboard public link has been copied to clipboard" }, "datakey": { "settings": "Settings", @@ -323,6 +347,8 @@ export default angular.module('thingsboard.locale', []) "assign-to-customer": "Assign to customer", "assign-device-to-customer": "Assign Device(s) To Customer", "assign-device-to-customer-text": "Please select the devices to assign to the customer", + "make-public": "Make device public", + "make-private": "Make device private", "no-devices-text": "No devices found", "assign-to-customer-text": "Please select the customer to assign the device(s)", "device-details": "Device details", @@ -337,6 +363,10 @@ export default angular.module('thingsboard.locale', []) "unassign-devices": "Unassign devices", "unassign-devices-action-title": "Unassign { count, select, 1 {1 device} other {# devices} } from customer", "assign-new-device": "Assign new device", + "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?", + "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.", + "make-private-device-title": "Are you sure you want to make the device '{{deviceName}}' private?", + "make-private-device-text": "After the confirmation the device and all its data will be made private and won't be accessible by others.", "view-credentials": "View credentials", "delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?", "delete-device-text": "Be careful, after the confirmation the device and all related data will become unrecoverable.", @@ -369,7 +399,9 @@ export default angular.module('thingsboard.locale', []) "assignedToCustomer": "Assigned to customer", "unable-delete-device-alias-title": "Unable to delete device alias", "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):{{widgetsList}}", - "is-gateway": "Is gateway" + "is-gateway": "Is gateway", + "public": "Public", + "device-public": "Device is public" }, "dialog": { "close": "Close dialog" @@ -778,7 +810,8 @@ export default angular.module('thingsboard.locale', []) "language": { "language": "Language", "en_US": "English", - "ko_KR": "Korean" + "ko_KR": "Korean", + "zh_CN": "Chinese" } } } diff --git a/ui/src/app/profile/profile.controller.js b/ui/src/app/profile/profile.controller.js index 19a16dadde..0e0b5ac0ba 100644 --- a/ui/src/app/profile/profile.controller.js +++ b/ui/src/app/profile/profile.controller.js @@ -27,7 +27,11 @@ export default function ProfileController(userService, $scope, $document, $mdDia vm.save = save; vm.changePassword = changePassword; - vm.languageList = {en_US: {value: "en_US", name: "language.en_US"}, ko_KR: {value : "ko_KR", name: "language.ko_KR"}}; + vm.languageList = { + en_US: {value : "en_US", name: "language.en_US"}, + ko_KR: {value : "ko_KR", name: "language.ko_KR"}, + zh_CN: {value : "zh_CN", name: "language.zh_CN"} + }; loadProfile(); diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 446881de8b..af70de184b 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -24,6 +24,7 @@ import 'flot/src/plugins/jquery.flot.selection'; import 'flot/src/plugins/jquery.flot.pie'; import 'flot/src/plugins/jquery.flot.crosshair'; import 'flot/src/plugins/jquery.flot.stack'; +import 'flot.curvedlines/curvedLines'; /* eslint-disable angular/angularelement */ export default class TbFlot { @@ -31,34 +32,8 @@ export default class TbFlot { this.ctx = ctx; this.chartType = chartType || 'line'; + var settings = ctx.settings; - var colors = []; - for (var i = 0; i < ctx.data.length; i++) { - var series = ctx.data[i]; - colors.push(series.dataKey.color); - var keySettings = series.dataKey.settings; - - series.lines = { - fill: keySettings.fillLines === true, - show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true - }; - - series.points = { - show: false, - radius: 8 - }; - if (keySettings.showPoints === true) { - series.points.show = true; - series.points.lineWidth = 5; - series.points.radius = 3; - } - - var lineColor = tinycolor(series.dataKey.color); - lineColor.setAlpha(.75); - - series.highlightColor = lineColor.toRgbString(); - - } ctx.tooltip = $('#flot-series-tooltip'); if (ctx.tooltip.length === 0) { ctx.tooltip = $(""); @@ -160,7 +135,6 @@ export default class TbFlot { }; } - var settings = ctx.settings; ctx.trackDecimals = angular.isDefined(settings.decimals) ? settings.decimals : ctx.decimals; @@ -176,7 +150,6 @@ export default class TbFlot { }; var options = { - colors: colors, title: null, subtitle: null, shadowSize: settings.shadowSize || 4, @@ -268,6 +241,13 @@ export default class TbFlot { stack: settings.stack === true } + if (this.chartType === 'line' && settings.smoothLines) { + options.series.curvedLines = { + active: true, + monotonicFit: true + } + } + if (this.chartType === 'bar') { options.series.lines = { show: false, @@ -276,14 +256,10 @@ export default class TbFlot { } options.series.bars ={ show: true, - barWidth: ctx.timeWindow.interval * 0.6, lineWidth: 0, fill: 0.9 } } - - options.xaxis.min = ctx.timeWindow.minTime; - options.xaxis.max = ctx.timeWindow.maxTime; } else if (this.chartType === 'pie') { options.series = { pie: { @@ -326,55 +302,121 @@ export default class TbFlot { this.options = options; + if (this.ctx.defaultSubscription) { + this.init(this.ctx.$container, this.ctx.defaultSubscription); + } + } + + init($element, subscription) { + this.subscription = subscription; + this.$element = $element; + var colors = []; + for (var i = 0; i < this.subscription.data.length; i++) { + var series = this.subscription.data[i]; + colors.push(series.dataKey.color); + var keySettings = series.dataKey.settings; + + series.lines = { + fill: keySettings.fillLines === true, + show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true + }; + + series.points = { + show: false, + radius: 8 + }; + if (keySettings.showPoints === true) { + series.points.show = true; + series.points.lineWidth = 5; + series.points.radius = 3; + } + + if (this.chartType === 'line' && this.ctx.settings.smoothLines && !series.points.show) { + series.curvedLines = { + apply: true + } + } + + var lineColor = tinycolor(series.dataKey.color); + lineColor.setAlpha(.75); + + series.highlightColor = lineColor.toRgbString(); + + } + this.options.colors = colors; + if (this.chartType === 'line' || this.chartType === 'bar') { + if (this.chartType === 'bar') { + this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + } + this.options.xaxis.min = this.subscription.timeWindow.minTime; + this.options.xaxis.max = this.subscription.timeWindow.maxTime; + } + this.checkMouseEvents(); + if (this.ctx.plot) { + this.ctx.plot.destroy(); + } if (this.chartType === 'pie' && this.ctx.animatedPie) { this.ctx.pieDataAnimationDuration = 250; - this.ctx.pieData = angular.copy(this.ctx.data); + this.pieData = angular.copy(this.subscription.data); this.ctx.pieRenderedData = []; this.ctx.pieTargetData = []; - for (i = 0; i < this.ctx.data.length; i++) { - this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0]) - ? this.ctx.data[i].data[0][1] : 0; + for (i = 0; i < this.subscription.data.length; i++) { + this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) + ? this.subscription.data[i].data[0][1] : 0; } this.pieDataRendered(); - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options); + this.ctx.plot = $.plot(this.$element, this.pieData, this.options); } else { - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); + this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options); } } update() { - if (!this.isMouseInteraction && this.ctx.plot) { - if (this.chartType === 'line' || this.chartType === 'bar') { - this.options.xaxis.min = this.ctx.timeWindow.minTime; - this.options.xaxis.max = this.ctx.timeWindow.maxTime; - this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime; - this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime; - if (this.chartType === 'bar') { - this.options.series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; - this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6; - } - this.ctx.plot.setData(this.ctx.data); - this.ctx.plot.setupGrid(); - this.ctx.plot.draw(); - } else if (this.chartType === 'pie') { - if (this.ctx.animatedPie) { - this.nextPieDataAnimation(true); - } else { - this.ctx.plot.setData(this.ctx.data); + if (this.updateTimeoutHandle) { + this.ctx.$scope.$timeout.cancel(this.updateTimeoutHandle); + this.updateTimeoutHandle = null; + } + if (this.subscription) { + if (!this.isMouseInteraction && this.ctx.plot) { + if (this.chartType === 'line' || this.chartType === 'bar') { + this.options.xaxis.min = this.subscription.timeWindow.minTime; + this.options.xaxis.max = this.subscription.timeWindow.maxTime; + this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime; + this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime; + if (this.chartType === 'bar') { + this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + this.ctx.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6; + } + this.ctx.plot.setData(this.subscription.data); + this.ctx.plot.setupGrid(); this.ctx.plot.draw(); + } else if (this.chartType === 'pie') { + if (this.ctx.animatedPie) { + this.nextPieDataAnimation(true); + } else { + this.ctx.plot.setData(this.subscription.data); + this.ctx.plot.draw(); + } } + } else if (this.isMouseInteraction && this.ctx.plot){ + var tbFlot = this; + this.updateTimeoutHandle = this.ctx.$scope.$timeout(function() { + tbFlot.update(); + }, 30, false); } } } resize() { - this.ctx.plot.resize(); - if (this.chartType !== 'pie') { - this.ctx.plot.setupGrid(); + if (this.ctx.plot) { + this.ctx.plot.resize(); + if (this.chartType !== 'pie') { + this.ctx.plot.setupGrid(); + } + this.ctx.plot.draw(); } - this.ctx.plot.draw(); } static get pieSettingsSchema() { @@ -473,6 +515,11 @@ export default class TbFlot { "type": "boolean", "default": false }, + "smoothLines": { + "title": "Display smooth (curved) lines", + "type": "boolean", + "default": false + }, "shadowSize": { "title": "Shadow size", "type": "number", @@ -591,6 +638,7 @@ export default class TbFlot { }, "form": [ "stack", + "smoothLines", "shadowSize", { "key": "fontColor", @@ -688,17 +736,19 @@ export default class TbFlot { var enabled = !this.ctx.isMobile && !this.ctx.isEdit; if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) { this.mouseEventsEnabled = enabled; - if (enabled) { - this.enableMouseEvents(); - } else { - this.disableMouseEvents(); - } - if (this.ctx.plot) { - this.ctx.plot.destroy(); - if (this.chartType === 'pie' && this.ctx.animatedPie) { - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options); + if (this.$element) { + if (enabled) { + this.enableMouseEvents(); } else { - this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options); + this.disableMouseEvents(); + } + if (this.ctx.plot) { + this.ctx.plot.destroy(); + if (this.chartType === 'pie' && this.ctx.animatedPie) { + this.ctx.plot = $.plot(this.$element, this.pieData, this.options); + } else { + this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options); + } } } } @@ -712,8 +762,8 @@ export default class TbFlot { } enableMouseEvents() { - this.ctx.$container.css('pointer-events',''); - this.ctx.$container.addClass('mouse-events'); + this.$element.css('pointer-events',''); + this.$element.addClass('mouse-events'); this.options.selection = { mode : 'x' }; var tbFlot = this; @@ -774,33 +824,33 @@ export default class TbFlot { tbFlot.ctx.plot.unhighlight(); } }; - this.ctx.$container.bind('plothover', this.flotHoverHandler); + this.$element.bind('plothover', this.flotHoverHandler); } if (!this.flotSelectHandler) { this.flotSelectHandler = function (event, ranges) { tbFlot.ctx.plot.clearSelection(); - tbFlot.ctx.timewindowFunctions.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); + tbFlot.subscription.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to); }; - this.ctx.$container.bind('plotselected', this.flotSelectHandler); + this.$element.bind('plotselected', this.flotSelectHandler); } if (!this.dblclickHandler) { this.dblclickHandler = function () { - tbFlot.ctx.timewindowFunctions.onResetTimewindow(); + tbFlot.subscription.onResetTimewindow(); }; - this.ctx.$container.bind('dblclick', this.dblclickHandler); + this.$element.bind('dblclick', this.dblclickHandler); } if (!this.mousedownHandler) { this.mousedownHandler = function () { tbFlot.isMouseInteraction = true; }; - this.ctx.$container.bind('mousedown', this.mousedownHandler); + this.$element.bind('mousedown', this.mousedownHandler); } if (!this.mouseupHandler) { this.mouseupHandler = function () { tbFlot.isMouseInteraction = false; }; - this.ctx.$container.bind('mouseup', this.mouseupHandler); + this.$element.bind('mouseup', this.mouseupHandler); } if (!this.mouseleaveHandler) { this.mouseleaveHandler = function () { @@ -809,38 +859,38 @@ export default class TbFlot { tbFlot.ctx.plot.unhighlight(); tbFlot.isMouseInteraction = false; }; - this.ctx.$container.bind('mouseleave', this.mouseleaveHandler); + this.$element.bind('mouseleave', this.mouseleaveHandler); } } disableMouseEvents() { - this.ctx.$container.css('pointer-events','none'); - this.ctx.$container.removeClass('mouse-events'); + this.$element.css('pointer-events','none'); + this.$element.removeClass('mouse-events'); this.options.selection = { mode : null }; if (this.flotHoverHandler) { - this.ctx.$container.unbind('plothover', this.flotHoverHandler); + this.$element.unbind('plothover', this.flotHoverHandler); this.flotHoverHandler = null; } if (this.flotSelectHandler) { - this.ctx.$container.unbind('plotselected', this.flotSelectHandler); + this.$element.unbind('plotselected', this.flotSelectHandler); this.flotSelectHandler = null; } if (this.dblclickHandler) { - this.ctx.$container.unbind('dblclick', this.dblclickHandler); + this.$element.unbind('dblclick', this.dblclickHandler); this.dblclickHandler = null; } if (this.mousedownHandler) { - this.ctx.$container.unbind('mousedown', this.mousedownHandler); + this.$element.unbind('mousedown', this.mousedownHandler); this.mousedownHandler = null; } if (this.mouseupHandler) { - this.ctx.$container.unbind('mouseup', this.mouseupHandler); + this.$element.unbind('mouseup', this.mouseupHandler); this.mouseupHandler = null; } if (this.mouseleaveHandler) { - this.ctx.$container.unbind('mouseleave', this.mouseleaveHandler); + this.$element.unbind('mouseleave', this.mouseleaveHandler); this.mouseleaveHandler = null; } } @@ -910,7 +960,7 @@ export default class TbFlot { value = series.data[hoverIndex][1]; } - if (series.stack) { + if (series.stack || (series.curvedLines && series.curvedLines.apply)) { hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); } results.seriesHover.push({ @@ -932,10 +982,10 @@ export default class TbFlot { for (var i = 0; i < this.ctx.pieTargetData.length; i++) { var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0; this.ctx.pieRenderedData[i] = value; - if (!this.ctx.pieData[i].data[0]) { - this.ctx.pieData[i].data[0] = [0,0]; + if (!this.pieData[i].data[0]) { + this.pieData[i].data[0] = [0,0]; } - this.ctx.pieData[i].data[0][1] = value; + this.pieData[i].data[0][1] = value; } } @@ -943,9 +993,9 @@ export default class TbFlot { if (start) { this.finishPieDataAnimation(); this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now(); - for (var i = 0; i < this.ctx.data.length; i++) { - this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0]) - ? this.ctx.data[i].data[0][1] : 0; + for (var i = 0; i < this.subscription.data.length; i++) { + this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0]) + ? this.subscription.data[i].data[0][1] : 0; } } if (this.ctx.pieAnimationCaf) { @@ -972,12 +1022,12 @@ export default class TbFlot { var prevValue = this.ctx.pieRenderedData[i]; var targetValue = this.ctx.pieTargetData[i]; var value = prevValue + (targetValue - prevValue) * progress; - if (!this.ctx.pieData[i].data[0]) { - this.ctx.pieData[i].data[0] = [0,0]; + if (!this.pieData[i].data[0]) { + this.pieData[i].data[0] = [0,0]; } - this.ctx.pieData[i].data[0][1] = value; + this.pieData[i].data[0][1] = value; } - this.ctx.plot.setData(this.ctx.pieData); + this.ctx.plot.setData(this.pieData); this.ctx.plot.draw(); this.ctx.pieAnimationLastTime = time; } @@ -987,7 +1037,7 @@ export default class TbFlot { finishPieDataAnimation() { this.pieDataRendered(); - this.ctx.plot.setData(this.ctx.pieData); + this.ctx.plot.setData(this.pieData); this.ctx.plot.draw(); } } diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js index 9722839541..0e104ebfa9 100644 --- a/ui/src/app/widget/lib/google-map.js +++ b/ui/src/app/widget/lib/google-map.js @@ -182,7 +182,7 @@ export default class TbGoogleMap { /* eslint-enable no-undef */ /* eslint-disable no-undef */ - createMarker(location, settings) { + createMarker(location, settings, onClickListener) { var height = 34; var pinColor = settings.color.substr(1); var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + pinColor, @@ -217,10 +217,21 @@ export default class TbGoogleMap { this.updateMarkerImage(marker, settings, settings.markerImage, settings.markerImageSize || 34); } - this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + if (settings.displayTooltip) { + this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + } + + if (onClickListener) { + marker.addListener('click', onClickListener); + } return marker; } + + removeMarker(marker) { + marker.setMap(null); + } + /* eslint-enable no-undef */ /* eslint-disable no-undef */ @@ -266,6 +277,10 @@ export default class TbGoogleMap { } /* eslint-enable no-undef */ + removePolyline(polyline) { + polyline.setMap(null); + } + /* eslint-disable no-undef */ fitBounds(bounds) { if (this.dontFitMapBounds && this.defaultZoomLevel) { diff --git a/ui/src/app/widget/lib/map-widget.js b/ui/src/app/widget/lib/map-widget.js index be7b118f8c..88ef1ca848 100644 --- a/ui/src/app/widget/lib/map-widget.js +++ b/ui/src/app/widget/lib/map-widget.js @@ -19,11 +19,62 @@ import tinycolor from 'tinycolor2'; import TbGoogleMap from './google-map'; import TbOpenStreetMap from './openstreet-map'; +function procesTooltipPattern(tbMap, pattern, datasources, dsIndex) { + var match = tbMap.varsRegex.exec(pattern); + var replaceInfo = {}; + replaceInfo.variables = []; + while (match !== null) { + var variableInfo = {}; + variableInfo.dataKeyIndex = -1; + var variable = match[0]; + var label = match[1]; + var valDec = 2; + var splitVals = label.split(':'); + if (splitVals.length > 1) { + label = splitVals[0]; + valDec = parseFloat(splitVals[1]); + } + variableInfo.variable = variable; + variableInfo.valDec = valDec; + + if (label.startsWith('#')) { + var keyIndexStr = label.substring(1); + var n = Math.floor(Number(keyIndexStr)); + if (String(n) === keyIndexStr && n >= 0) { + variableInfo.dataKeyIndex = n; + } + } + if (variableInfo.dataKeyIndex === -1) { + var offset = 0; + for (var i=0;i 0 && settings.defaultZoomLevel < 21) { this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel); @@ -43,52 +97,116 @@ export default class TbMapWidget { this.dontFitMapBounds = settings.fitMapBounds === false; - function procesTooltipPattern(pattern) { - var match = tbMap.varsRegex.exec(pattern); - var replaceInfo = {}; - replaceInfo.variables = []; - while (match !== null) { - var variableInfo = {}; - variableInfo.dataKeyIndex = -1; - var variable = match[0]; - var label = match[1]; - var valDec = 2; - var splitVals = label.split(':'); - if (splitVals.length > 1) { - label = splitVals[0]; - valDec = parseFloat(splitVals[1]); + if (!useDynamicLocations) { + this.subscription = this.ctx.defaultSubscription; + this.configureLocationsFromSettings(); + } + + var minZoomLevel = this.drawRoutes ? 18 : 15; + + var initCallback = function() { + tbMap.update(); + tbMap.resize(); + }; + + if (mapProvider === 'google-map') { + this.map = new TbGoogleMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType); + } else if (mapProvider === 'openstreet-map') { + this.map = new TbOpenStreetMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel); + } + + } + + setCallbacks(callbacks) { + Object.assign(this.callbacks, callbacks); + } + + clearLocations() { + if (this.locations) { + var tbMap = this; + this.locations.forEach(function(location) { + if (location.marker) { + tbMap.map.removeMarker(location.marker); } - variableInfo.variable = variable; - variableInfo.valDec = valDec; - - if (label.startsWith('#')) { - var keyIndexStr = label.substring(1); - var n = Math.floor(Number(keyIndexStr)); - if (String(n) === keyIndexStr && n >= 0) { - variableInfo.dataKeyIndex = n; - } + if (location.polyline) { + tbMap.map.removePolyline(location.polyline); } - if (variableInfo.dataKeyIndex === -1) { - var offset = 0; - for (var i=0;i -1 && lngKeyIndex > -1) { + var locationsSettings = { + latKeyName: localLatKeyName, + lngKeyName: localLngKeyName, + showLabel: subscriptionLocationSettings.showLabel !== false, + displayTooltip: subscriptionLocationSettings.displayTooltip !== false, + label: datasource.name, + labelColor: subscriptionLocationSettings.labelColor || this.ctx.widgetConfig.color || '#000000', + color: "#FE7569", + useColorFunction: false, + colorFunction: null, + markerImage: null, + markerImageSize: 34, + useMarkerImage: false, + useMarkerImageFunction: false, + markerImageFunction: null, + markerImages: [], + tooltipPattern: subscriptionLocationSettings.tooltipPattern || "Latitude: ${latitude:7}Longitude: ${longitude:7}" + }; + + locationsSettings.tooltipReplaceInfo = procesTooltipPattern(this, locationsSettings.tooltipPattern, this.subscription.datasources, i); + + locationsSettings.useColorFunction = subscriptionLocationSettings.useColorFunction === true; + if (angular.isDefined(subscriptionLocationSettings.colorFunction) && subscriptionLocationSettings.colorFunction.length > 0) { + try { + locationsSettings.colorFunction = new Function('data, dsData, dsIndex', subscriptionLocationSettings.colorFunction); + } catch (e) { + locationsSettings.colorFunction = null; } - offset += datasource.dataKeys.length; } + + this.locationsSettings.push(locationsSettings); + latKeyIndex = -1; + lngKeyIndex = -1; } - replaceInfo.variables.push(variableInfo); - match = tbMap.varsRegex.exec(pattern); + index++; } - return replaceInfo; } + } - var configuredLocationsSettings = drawRoutes ? settings.routesSettings : settings.markersSettings; + configureLocationsFromSettings() { + var configuredLocationsSettings = this.drawRoutes ? this.ctx.settings.routesSettings : this.ctx.settings.markersSettings; if (!configuredLocationsSettings) { configuredLocationsSettings = []; } @@ -98,8 +216,9 @@ export default class TbMapWidget { latKeyName: "lat", lngKeyName: "lng", showLabel: true, + displayTooltip: true, label: "", - labelColor: ctx.widgetConfig.color || '#000000', + labelColor: this.ctx.widgetConfig.color || '#000000', color: "#FE7569", useColorFunction: false, colorFunction: null, @@ -112,7 +231,7 @@ export default class TbMapWidget { tooltipPattern: "Latitude: ${lat:7}Longitude: ${lng:7}" }; - if (drawRoutes) { + if (this.drawRoutes) { this.locationsSettings[i].strokeWeight = 2; this.locationsSettings[i].strokeOpacity = 1.0; } @@ -123,7 +242,7 @@ export default class TbMapWidget { this.locationsSettings[i].tooltipPattern = configuredLocationsSettings[i].tooltipPattern || "Latitude: ${"+this.locationsSettings[i].latKeyName+":7}Longitude: ${"+this.locationsSettings[i].lngKeyName+":7}"; - this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this.locationsSettings[i].tooltipPattern); + this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this, this.locationsSettings[i].tooltipPattern, this.subscription.datasources); this.locationsSettings[i].showLabel = configuredLocationsSettings[i].showLabel !== false; this.locationsSettings[i].label = configuredLocationsSettings[i].label || this.locationsSettings[i].label; @@ -132,7 +251,7 @@ export default class TbMapWidget { this.locationsSettings[i].useColorFunction = configuredLocationsSettings[i].useColorFunction === true; if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) { try { - this.locationsSettings[i].colorFunction = new Function('data', configuredLocationsSettings[i].colorFunction); + this.locationsSettings[i].colorFunction = new Function('data, dsData, dsIndex', configuredLocationsSettings[i].colorFunction); } catch (e) { this.locationsSettings[i].colorFunction = null; } @@ -141,7 +260,7 @@ export default class TbMapWidget { this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true; if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) { try { - this.locationsSettings[i].markerImageFunction = new Function('data, images', configuredLocationsSettings[i].markerImageFunction); + this.locationsSettings[i].markerImageFunction = new Function('data, images, dsData, dsIndex', configuredLocationsSettings[i].markerImageFunction); } catch (e) { this.locationsSettings[i].markerImageFunction = null; } @@ -157,26 +276,12 @@ export default class TbMapWidget { this.locationsSettings[i].markerImageSize = configuredLocationsSettings[i].markerImageSize || 34; } - if (drawRoutes) { + if (this.drawRoutes) { this.locationsSettings[i].strokeWeight = configuredLocationsSettings[i].strokeWeight || this.locationsSettings[i].strokeWeight; this.locationsSettings[i].strokeOpacity = angular.isDefined(configuredLocationsSettings[i].strokeOpacity) ? configuredLocationsSettings[i].strokeOpacity : this.locationsSettings[i].strokeOpacity; } } } - - var minZoomLevel = this.drawRoutes ? 18 : 15; - - var initCallback = function() { - tbMap.update(); - tbMap.resize(); - }; - - if (mapProvider === 'google-map') { - this.map = new TbGoogleMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType); - } else if (mapProvider === 'openstreet-map') { - this.map = new TbOpenStreetMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel); - } - } update() { @@ -231,22 +336,22 @@ export default class TbMapWidget { return true; } - function calculateLocationColor(settings, dataMap) { - if (settings.useColorFunction && settings.colorFunction) { + function calculateLocationColor(location, dataMap) { + if (location.settings.useColorFunction && location.settings.colorFunction) { var color = '#FE7569'; try { - color = settings.colorFunction(dataMap); + color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex); } catch (e) { color = '#FE7569'; } return tinycolor(color).toHexString(); } else { - return settings.color; + return location.settings.color; } } function updateLocationColor(location, dataMap) { - var color = calculateLocationColor(location.settings, dataMap); + var color = calculateLocationColor(location, dataMap); if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) { if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) { tbMap.map.updateMarkerColor(location.marker, color); @@ -258,11 +363,11 @@ export default class TbMapWidget { } } - function calculateLocationMarkerImage(settings, dataMap) { - if (settings.useMarkerImageFunction && settings.markerImageFunction) { + function calculateLocationMarkerImage(location, dataMap) { + if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) { var image = null; try { - image = settings.markerImageFunction(dataMap, settings.markerImages); + image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex); } catch (e) { image = null; } @@ -273,7 +378,7 @@ export default class TbMapWidget { } function updateLocationMarkerImage(location, dataMap) { - var image = calculateLocationMarkerImage(location.settings, dataMap); + var image = calculateLocationMarkerImage(location, dataMap); if (image != null && (!location.settings.calculatedImage || !angular.equals(location.settings.calculatedImage, image))) { tbMap.map.updateMarkerImage(location.marker, location.settings, image.url, image.size); location.settings.calculatedImage = image; @@ -306,7 +411,11 @@ export default class TbMapWidget { if (latLngs.length > 0) { var markerLocation = latLngs[latLngs.length-1]; if (!location.marker) { - location.marker = tbMap.map.createMarker(markerLocation, location.settings); + location.marker = tbMap.map.createMarker(markerLocation, location.settings, + function() { + tbMap.callbacks.onLocationClick(location); + } + ); } else { tbMap.map.setMarkerPosition(location.marker, markerLocation); } @@ -328,7 +437,9 @@ export default class TbMapWidget { lng = lngData[lngData.length-1][1]; latLng = tbMap.map.createLatLng(lat, lng); if (!location.marker) { - location.marker = tbMap.map.createMarker(latLng, location.settings); + location.marker = tbMap.map.createMarker(latLng, location.settings, function() { + tbMap.callbacks.onLocationClick(location); + }); tbMap.markers.push(location.marker); locationChanged = true; } else { @@ -345,8 +456,12 @@ export default class TbMapWidget { return locationChanged; } - function toLabelValueMap(data) { + function toLabelValueMap(data, datasources) { var dataMap = {}; + var dsDataMap = []; + for (var d=0;d -1 && lngIndex > -1) { + var ds = data[latIndex].datasource; + var dsIndex = datasources.indexOf(ds); var location = { latIndex: latIndex, lngIndex: lngIndex, + dsIndex: dsIndex, settings: locationSettings }; tbMap.locations.push(location); @@ -394,10 +523,10 @@ export default class TbMapWidget { tbMap.map.fitBounds(bounds); } - function updateLocations(data) { + function updateLocations(data, datasources) { var locationsChanged = false; var bounds = tbMap.map.createBounds(); - var dataMap = toLabelValueMap(data); + var dataMap = toLabelValueMap(data, datasources); for (var p = 0; p < tbMap.locations.length; p++) { var location = tbMap.locations[p]; locationsChanged |= updateLocation(location, data, dataMap); @@ -412,36 +541,36 @@ export default class TbMapWidget { } } - if (this.map && this.map.inited()) { - if (this.ctx.data) { + if (this.map && this.map.inited() && this.subscription) { + if (this.subscription.data) { if (!this.locations) { - loadLocations(this.ctx.data); + loadLocations(this.subscription.data, this.subscription.datasources); } else { - updateLocations(this.ctx.data); + updateLocations(this.subscription.data, this.subscription.datasources); } - } - var tooltips = this.map.getTooltips(); - for (var t=0; t < tooltips.length; t++) { - var tooltip = tooltips[t]; - var text = tooltip.pattern; - var replaceInfo = tooltip.replaceInfo; - for (var v = 0; v < replaceInfo.variables.length; v++) { - var variableInfo = replaceInfo.variables[v]; - var txtVal = ''; - if (variableInfo.dataKeyIndex > -1) { - var varData = this.ctx.data[variableInfo.dataKeyIndex].data; - if (varData.length > 0) { - var val = varData[varData.length - 1][1]; - if (isNumber(val)) { - txtVal = padValue(val, variableInfo.valDec, 0); - } else { - txtVal = val; + var tooltips = this.map.getTooltips(); + for (var t=0; t < tooltips.length; t++) { + var tooltip = tooltips[t]; + var text = tooltip.pattern; + var replaceInfo = tooltip.replaceInfo; + for (var v = 0; v < replaceInfo.variables.length; v++) { + var variableInfo = replaceInfo.variables[v]; + var txtVal = ''; + if (variableInfo.dataKeyIndex > -1 && this.subscription.data[variableInfo.dataKeyIndex]) { + var varData = this.subscription.data[variableInfo.dataKeyIndex].data; + if (varData.length > 0) { + var val = varData[varData.length - 1][1]; + if (isNumber(val)) { + txtVal = padValue(val, variableInfo.valDec, 0); + } else { + txtVal = val; + } } } + text = text.split(variableInfo.variable).join(txtVal); } - text = text.split(variableInfo.variable).join(txtVal); + tooltip.popup.setContent(text); } - tooltip.popup.setContent(text); } } } @@ -449,7 +578,7 @@ export default class TbMapWidget { resize() { if (this.map && this.map.inited()) { this.map.invalidateSize(); - if (this.locations && this.locations.size > 0) { + if (this.locations && this.locations.length > 0) { var bounds = this.map.createBounds(); for (var m = 0; m < this.markers.length; m++) { this.map.extendBoundsWithMarker(bounds, this.markers[m]); diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js index aacb505c06..65f7c7f06f 100644 --- a/ui/src/app/widget/lib/openstreet-map.js +++ b/ui/src/app/widget/lib/openstreet-map.js @@ -85,7 +85,7 @@ export default class TbOpenStreetMap { testImage.src = image; } - createMarker(location, settings) { + createMarker(location, settings, onClickListener) { var height = 34; var pinColor = settings.color.substr(1); var icon = L.icon({ @@ -109,11 +109,21 @@ export default class TbOpenStreetMap { this.updateMarkerImage(marker, settings, settings.markerImage, settings.markerImageSize || 34); } - this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + if (settings.displayTooltip) { + this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo); + } + + if (onClickListener) { + marker.on('click', onClickListener); + } return marker; } + removeMarker(marker) { + this.map.removeLayer(marker); + } + createTooltip(marker, pattern, replaceInfo) { var popup = L.popup(); popup.setContent(''); @@ -145,6 +155,10 @@ export default class TbOpenStreetMap { return polyline; } + removePolyline(polyline) { + this.map.removeLayer(polyline); + } + fitBounds(bounds) { if (bounds.isValid()) { if (this.dontFitMapBounds && this.defaultZoomLevel) { diff --git a/ui/src/app/widget/lib/timeseries-table-widget.js b/ui/src/app/widget/lib/timeseries-table-widget.js new file mode 100644 index 0000000000..c55c519879 --- /dev/null +++ b/ui/src/app/widget/lib/timeseries-table-widget.js @@ -0,0 +1,325 @@ +/* + * 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. + */ + +import './timeseries-table-widget.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import timeseriesTableWidgetTemplate from './timeseries-table-widget.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import tinycolor from 'tinycolor2'; +import cssjs from '../../../vendor/css.js/css'; + +export default angular.module('thingsboard.widgets.timeseriesTableWidget', []) + .directive('tbTimeseriesTableWidget', TimeseriesTableWidget) + .name; + +/*@ngInject*/ +function TimeseriesTableWidget() { + return { + restrict: "E", + scope: true, + bindToController: { + tableId: '=', + config: '=', + datasources: '=', + data: '=' + }, + controller: TimeseriesTableWidgetController, + controllerAs: 'vm', + templateUrl: timeseriesTableWidgetTemplate + }; +} + +/*@ngInject*/ +function TimeseriesTableWidgetController($element, $scope, $filter) { + var vm = this; + + vm.sources = []; + vm.sourceIndex = 0; + + $scope.$watch('vm.config', function() { + if (vm.config) { + vm.settings = vm.config.settings; + vm.widgetConfig = vm.config.widgetConfig; + initialize(); + } + }); + + function initialize() { + vm.showTimestamp = vm.settings.showTimestamp !== false; + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; + var defaultColor = tinycolor(origColor); + var mdDark = defaultColor.setAlpha(0.87).toRgbString(); + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); + //var mdDarkIcon = mdDarkSecondary; + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); + + var cssString = 'table.md-table th.md-column {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ + 'color: ' + mdDark + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell {\n'+ + 'color: ' + mdDark + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + 'table.md-table td.md-cell.md-placeholder {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + '.md-table-pagination .buttons md-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}'; + + var cssParser = new cssjs(); + cssParser.testMode = false; + var namespace = 'ts-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $element.addClass(namespace); + + function hashCode(str) { + var hash = 0; + var i, char; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i++) { + char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return hash; + } + } + + $scope.$watch('vm.datasources', function() { + updateDatasources(); + }); + + $scope.$on('timeseries-table-data-updated', function(event, tableId) { + if (vm.tableId == tableId) { + dataUpdated(); + } + }); + + function dataUpdated() { + for (var s=0; s < vm.sources.length; s++) { + var source = vm.sources[s]; + source.rawData = vm.data.slice(source.keyStartIndex, source.keyEndIndex); + } + updateSourceData(vm.sources[vm.sourceIndex]); + $scope.$digest(); + } + + vm.onPaginate = function(source) { + updatePage(source); + } + + vm.onReorder = function(source) { + reorder(source); + updatePage(source); + } + + vm.cellStyle = function(source, index, value) { + var style = {}; + if (index > 0) { + var styleInfo = source.ts.stylesInfo[index-1]; + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } + } + return style; + } + + vm.cellContent = function(source, index, row, value) { + if (index === 0) { + return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss'); + } else { + var strContent = ''; + if (angular.isDefined(value)) { + strContent = ''+value; + } + var content = strContent; + var contentInfo = source.ts.contentsInfo[index-1]; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + var rowData = source.ts.rowDataTemplate; + rowData['Timestamp'] = row[0]; + for (var h=0; h < source.ts.header.length; h++) { + var headerInfo = source.ts.header[h]; + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; + } + content = contentInfo.cellContentFunction(value, rowData, $filter); + } catch (e) { + content = strContent; + } + } + return content; + } + } + + $scope.$watch('vm.sourceIndex', function(newIndex, oldIndex) { + if (newIndex != oldIndex) { + updateSourceData(vm.sources[vm.sourceIndex]); + } + }); + + function updateDatasources() { + vm.sources = []; + vm.sourceIndex = 0; + var keyOffset = 0; + if (vm.datasources) { + for (var ds = 0; ds < vm.datasources.length; ds++) { + var source = {}; + var datasource = vm.datasources[ds]; + source.keyStartIndex = keyOffset; + keyOffset += datasource.dataKeys.length; + source.keyEndIndex = keyOffset; + source.datasource = datasource; + source.data = []; + source.rawData = []; + source.query = { + limit: 5, + page: 1, + order: '-0' + } + source.ts = { + header: [], + count: 0, + data: [], + stylesInfo: [], + contentsInfo: [], + rowDataTemplate: {} + } + source.ts.rowDataTemplate['Timestamp'] = null; + for (var a = 0; a < datasource.dataKeys.length; a++ ) { + var dataKey = datasource.dataKeys[a]; + var keySettings = dataKey.settings; + source.ts.header.push({ + index: a+1, + dataKey: dataKey + }); + source.ts.rowDataTemplate[dataKey.label] = null; + + var cellStyleFunction = null; + var useCellStyleFunction = false; + + if (keySettings.useCellStyleFunction === true) { + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { + try { + cellStyleFunction = new Function('value', keySettings.cellStyleFunction); + useCellStyleFunction = true; + } catch (e) { + cellStyleFunction = null; + useCellStyleFunction = false; + } + } + } + + source.ts.stylesInfo.push({ + useCellStyleFunction: useCellStyleFunction, + cellStyleFunction: cellStyleFunction + }); + + var cellContentFunction = null; + var useCellContentFunction = false; + + if (keySettings.useCellContentFunction === true) { + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { + try { + cellContentFunction = new Function('value, rowData, filter', keySettings.cellContentFunction); + useCellContentFunction = true; + } catch (e) { + cellContentFunction = null; + useCellContentFunction = false; + } + } + } + + source.ts.contentsInfo.push({ + useCellContentFunction: useCellContentFunction, + cellContentFunction: cellContentFunction + }); + + } + vm.sources.push(source); + } + } + } + + function updatePage(source) { + var startIndex = source.query.limit * (source.query.page - 1); + source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit); + } + + function reorder(source) { + source.data = $filter('orderBy')(source.data, source.query.order); + } + + function convertData(data) { + var rowsMap = {}; + for (var d = 0; d < data.length; d++) { + var columnData = data[d].data; + for (var i = 0; i < columnData.length; i++) { + var cellData = columnData[i]; + var timestamp = cellData[0]; + var row = rowsMap[timestamp]; + if (!row) { + row = []; + row[0] = timestamp; + for (var c = 0; c < data.length; c++) { + row[c+1] = undefined; + } + rowsMap[timestamp] = row; + } + row[d+1] = cellData[1]; + } + } + var rows = []; + for (var t in rowsMap) { + rows.push(rowsMap[t]); + } + return rows; + } + + function updateSourceData(source) { + source.data = convertData(source.rawData); + source.ts.count = source.data.length; + reorder(source); + updatePage(source); + } + +} \ No newline at end of file diff --git a/ui/src/app/widget/lib/timeseries-table-widget.scss b/ui/src/app/widget/lib/timeseries-table-widget.scss new file mode 100644 index 0000000000..99a06537e9 --- /dev/null +++ b/ui/src/app/widget/lib/timeseries-table-widget.scss @@ -0,0 +1,29 @@ +/** + * 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. + */ + +tb-timeseries-table-widget { + table.md-table thead.md-head>tr.md-row { + height: 40px; + } + + table.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row { + height: 38px; + } + + .md-table-pagination>* { + height: 46px; + } +} diff --git a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html new file mode 100644 index 0000000000..2b6d72c10a --- /dev/null +++ b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html @@ -0,0 +1,43 @@ + + + + + + + + + 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'),
{{ vm.publicLink }}