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..6a9e449c04 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -39,6 +39,7 @@ import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvi 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 +57,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 +89,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); @@ -146,6 +156,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,6 +167,7 @@ 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); 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 96458cf273..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.*; @@ -43,14 +46,22 @@ public class CustomerController extends BaseController { } @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/customer/{customerId}/title", method = RequestMethod.GET, produces = "application/text") + @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET) @ResponseBody - public String getCustomerTitleById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException { + public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); Customer customer = checkCustomerId(customerId); - return customer.getTitle(); + 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); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index d72f02548e..3812610d20 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; @@ -116,6 +117,21 @@ public class DashboardController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST) + @ResponseBody + public Dashboard assignDashboardToPublicCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException { + checkParameter("dashboardId", strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); + return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId())); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index b08a9640a7..bebab8b880 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; @@ -117,6 +118,21 @@ public class DeviceController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST) + @ResponseBody + public Device assignDeviceToPublicCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException { + checkParameter("deviceId", strDeviceId); + try { + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + Device device = checkDeviceId(deviceId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId()); + return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId())); + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 5ba84bf112..811f39f5d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -16,32 +16,40 @@ package org.thingsboard.server.service.security.auth.jwt; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.InsufficientAuthenticationException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; +import java.util.UUID; + @Component public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { private final JwtTokenFactory tokenFactory; private final UserService userService; + private final CustomerService customerService; @Autowired - public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) { + public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) { this.userService = userService; + this.customerService = customerService; this.tokenFactory = tokenFactory; } @@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide Assert.notNull(authentication, "No authentication data provided"); RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); + UserPrincipal principal = unsafeUser.getUserPrincipal(); + SecurityUser securityUser; + if (principal.getType() == UserPrincipal.Type.USER_NAME) { + securityUser = authenticateByUserId(unsafeUser.getId()); + } else { + securityUser = authenticateByPublicId(principal.getValue()); + } + return new RefreshAuthenticationToken(securityUser); + } - User user = userService.findUserById(unsafeUser.getId()); + private SecurityUser authenticateByUserId(UserId userId) { + User user = userService.findUserById(userId); if (user == null) { throw new UsernameNotFoundException("User not found by refresh token"); } @@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - return new RefreshAuthenticationToken(securityUser); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + + return securityUser; + } + + private SecurityUser authenticateByPublicId(String publicId) { + CustomerId customerId; + try { + customerId = new CustomerId(UUID.fromString(publicId)); + } catch (Exception e) { + throw new BadCredentialsException("Refresh token is not valid"); + } + Customer publicCustomer = customerService.findCustomerById(customerId); + if (publicCustomer == null) { + throw new UsernameNotFoundException("Public entity not found by refresh token"); + } + boolean isPublic = false; + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) { + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean(); + } + if (!isPublic) { + throw new BadCredentialsException("Refresh token is not valid"); + } + User user = new User(new UserId(UUIDBased.EMPTY)); + user.setTenantId(publicCustomer.getTenantId()); + user.setCustomerId(publicCustomer.getId()); + user.setEmail(publicId); + user.setAuthority(Authority.CUSTOMER_USER); + user.setFirstName("Public"); + user.setLastName("Public"); + + UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId); + + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal); + + return securityUser; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java new file mode 100644 index 0000000000..54ef093804 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.rest; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PublicLoginRequest { + + private String publicId; + + @JsonCreator + public PublicLoginRequest(@JsonProperty("publicId") String publicId) { + this.publicId = publicId; + } + + public String getPublicId() { + return publicId; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 686bb465c4..af10674294 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; + +import java.util.UUID; @Component public class RestAuthenticationProvider implements AuthenticationProvider { private final BCryptPasswordEncoder encoder; private final UserService userService; + private final CustomerService customerService; @Autowired - public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) { + public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) { this.userService = userService; + this.customerService = customerService; this.encoder = encoder; } @@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider { public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.notNull(authentication, "No authentication data provided"); - String username = (String) authentication.getPrincipal(); - String password = (String) authentication.getCredentials(); + Object principal = authentication.getPrincipal(); + if (!(principal instanceof UserPrincipal)) { + throw new BadCredentialsException("Authentication Failed. Bad user principal."); + } + UserPrincipal userPrincipal = (UserPrincipal) principal; + if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { + String username = userPrincipal.getValue(); + String password = (String) authentication.getCredentials(); + return authenticateByUsernameAndPassword(userPrincipal, username, password); + } else { + String publicId = userPrincipal.getValue(); + return authenticateByPublicId(userPrincipal, publicId); + } + } + + private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) { User user = userService.findUserByEmail(username); if (user == null) { throw new UsernameNotFoundException("User not found: " + username); @@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled()); + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); + } + + private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) { + CustomerId customerId; + try { + customerId = new CustomerId(UUID.fromString(publicId)); + } catch (Exception e) { + throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); + } + Customer publicCustomer = customerService.findCustomerById(customerId); + if (publicCustomer == null) { + throw new UsernameNotFoundException("Public entity not found: " + publicId); + } + boolean isPublic = false; + if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) { + isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean(); + } + if (!isPublic) { + throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); + } + User user = new User(new UserId(UUIDBased.EMPTY)); + user.setTenantId(publicCustomer.getTenantId()); + user.setCustomerId(publicCustomer.getId()); + user.setEmail(publicId); + user.setAuthority(Authority.CUSTOMER_USER); + user.setFirstName("Public"); + user.setLastName("Public"); + + SecurityUser securityUser = new SecurityUser(user, true, userPrincipal); return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java index d191905056..c32b1f221e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java @@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; +import org.thingsboard.server.service.security.model.UserPrincipal; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF throw new AuthenticationServiceException("Username or Password not provided"); } - UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword()); return this.getAuthenticationManager().authenticate(token); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java new file mode 100644 index 0000000000..3a3b7cb2f1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; +import org.thingsboard.server.service.security.model.UserPrincipal; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter { + private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class); + + private final AuthenticationSuccessHandler successHandler; + private final AuthenticationFailureHandler failureHandler; + + private final ObjectMapper objectMapper; + + public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler, + AuthenticationFailureHandler failureHandler, ObjectMapper mapper) { + super(defaultProcessUrl); + this.successHandler = successHandler; + this.failureHandler = failureHandler; + this.objectMapper = mapper; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + if (!HttpMethod.POST.name().equals(request.getMethod())) { + if(logger.isDebugEnabled()) { + logger.debug("Authentication method not supported. Request method: " + request.getMethod()); + } + throw new AuthMethodNotSupportedException("Authentication method not supported"); + } + + PublicLoginRequest loginRequest; + try { + loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class); + } catch (Exception e) { + throw new AuthenticationServiceException("Invalid public login request payload"); + } + + if (StringUtils.isBlank(loginRequest.getPublicId())) { + throw new AuthenticationServiceException("Public Id is not provided"); + } + + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId()); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, ""); + + return this.getAuthenticationManager().authenticate(token); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authResult) throws IOException, ServletException { + successHandler.onAuthenticationSuccess(request, response, authResult); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + SecurityContextHolder.clearContext(); + failureHandler.onAuthenticationFailure(request, response, failed); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java index 2da3a97366..0839695261 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java @@ -30,6 +30,7 @@ public class SecurityUser extends User { private Collection authorities; private boolean enabled; + private UserPrincipal userPrincipal; public SecurityUser() { super(); @@ -39,9 +40,10 @@ public class SecurityUser extends User { super(id); } - public SecurityUser(User user, boolean enabled) { + public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) { super(user); this.enabled = enabled; + this.userPrincipal = userPrincipal; } public Collection getAuthorities() { @@ -57,8 +59,16 @@ public class SecurityUser extends User { return enabled; } + public UserPrincipal getUserPrincipal() { + return userPrincipal; + } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public void setUserPrincipal(UserPrincipal userPrincipal) { + this.userPrincipal = userPrincipal; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java new file mode 100644 index 0000000000..6f23783c0b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.service.security.model; + +public class UserPrincipal { + + private final Type type; + private final String value; + + public UserPrincipal(Type type, String value) { + this.type = type; + this.value = value; + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + public enum Type { + USER_NAME, + PUBLIC_ID + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 8ade253412..20de6d8e73 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.config.JwtSettings; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import java.util.Arrays; import java.util.List; @@ -43,6 +44,7 @@ public class JwtTokenFactory { private static final String FIRST_NAME = "firstName"; private static final String LAST_NAME = "lastName"; private static final String ENABLED = "enabled"; + private static final String IS_PUBLIC = "isPublic"; private static final String TENANT_ID = "tenantId"; private static final String CUSTOMER_ID = "customerId"; @@ -63,12 +65,15 @@ public class JwtTokenFactory { if (securityUser.getAuthority() == null) throw new IllegalArgumentException("User doesn't have any privileges"); - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); + UserPrincipal principal = securityUser.getUserPrincipal(); + String subject = principal.getValue(); + Claims claims = Jwts.claims().setSubject(subject); claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList())); claims.put(USER_ID, securityUser.getId().getId().toString()); claims.put(FIRST_NAME, securityUser.getFirstName()); claims.put(LAST_NAME, securityUser.getLastName()); claims.put(ENABLED, securityUser.isEnabled()); + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); if (securityUser.getTenantId() != null) { claims.put(TENANT_ID, securityUser.getTenantId().getId().toString()); } @@ -104,6 +109,9 @@ public class JwtTokenFactory { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); + securityUser.setUserPrincipal(principal); String tenantId = claims.get(TENANT_ID, String.class); if (tenantId != null) { securityUser.setTenantId(new TenantId(UUID.fromString(tenantId))); @@ -123,9 +131,11 @@ public class JwtTokenFactory { DateTime currentTime = new DateTime(); - Claims claims = Jwts.claims().setSubject(securityUser.getEmail()); + UserPrincipal principal = securityUser.getUserPrincipal(); + Claims claims = Jwts.claims().setSubject(principal.getValue()); claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name())); claims.put(USER_ID, securityUser.getId().getId().toString()); + claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID); String token = Jwts.builder() .setClaims(claims) @@ -150,8 +160,10 @@ public class JwtTokenFactory { if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) { throw new IllegalArgumentException("Invalid Refresh Token scope"); } + boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); - securityUser.setEmail(subject); + securityUser.setUserPrincipal(principal); return securityUser; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java index 6d7247b3a6..5639044127 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java @@ -16,12 +16,14 @@ package org.thingsboard.server.dao.customer; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.model.CustomerEntity; +import org.thingsboard.server.dao.model.DeviceEntity; /** * The Interface CustomerDao. @@ -44,5 +46,14 @@ public interface CustomerDao extends Dao { * @return the list of customer objects */ List findCustomersByTenantId(UUID tenantId, TextPageLink pageLink); + + /** + * Find customers by tenantId and customer title. + * + * @param tenantId the tenantId + * @param title the customer title + * @return the optional customer object + */ + Optional findCustomersByTenantIdAndTitle(UUID tenantId, String title); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java index 7b53836274..25c111642a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDaoImpl.java @@ -16,11 +16,18 @@ package org.thingsboard.server.dao.customer; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY; + import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.UUID; +import com.datastax.driver.core.querybuilder.Select; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; @@ -60,4 +67,13 @@ public class CustomerDaoImpl extends AbstractSearchTextDao imple return customerEntities; } + @Override + public Optional findCustomersByTenantIdAndTitle(UUID tenantId, String title) { + Select select = select().from(CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME); + Select.Where query = select.where(); + query.and(eq(CUSTOMER_TENANT_ID_PROPERTY, tenantId)); + query.and(eq(CUSTOMER_TITLE_PROPERTY, title)); + return Optional.ofNullable(findOneByStatement(query)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index 70321a20b0..01f4f99995 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -28,6 +28,8 @@ public interface CustomerService { public Customer saveCustomer(Customer customer); public void deleteCustomer(CustomerId customerId); + + public Customer findOrCreatePublicCustomer(TenantId tenantId); public TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 18ead42d5a..a80c6e84b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -18,8 +18,12 @@ package org.thingsboard.server.dao.customer; import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.getData; +import java.io.IOException; import java.util.List; +import java.util.Optional; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.Customer; @@ -46,6 +50,8 @@ import org.thingsboard.server.dao.service.Validator; @Slf4j public class CustomerServiceImpl implements CustomerService { + private static final String PUBLIC_CUSTOMER_TITLE = "Public"; + @Autowired private CustomerDao customerDao; @@ -80,7 +86,7 @@ public class CustomerServiceImpl implements CustomerService { @Override public void deleteCustomer(CustomerId customerId) { log.trace("Executing deleteCustomer [{}]", customerId); - Validator.validateId(customerId, "Incorrect tenantId " + customerId); + Validator.validateId(customerId, "Incorrect customerId " + customerId); Customer customer = findCustomerById(customerId); if (customer == null) { throw new IncorrectParameterException("Unable to delete non-existent customer."); @@ -91,6 +97,27 @@ public class CustomerServiceImpl implements CustomerService { customerDao.removeById(customerId.getId()); } + @Override + public Customer findOrCreatePublicCustomer(TenantId tenantId) { + log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId); + Validator.validateId(tenantId, "Incorrect customerId " + tenantId); + Optional publicCustomerEntity = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE); + if (publicCustomerEntity.isPresent()) { + return getData(publicCustomerEntity.get()); + } else { + Customer publicCustomer = new Customer(); + publicCustomer.setTenantId(tenantId); + publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE); + try { + publicCustomer.setAdditionalInfo(new ObjectMapper().readValue("{ \"isPublic\": true }", JsonNode.class)); + } catch (IOException e) { + throw new IncorrectParameterException("Unable to create public customer.", e); + } + CustomerEntity customerEntity = customerDao.save(publicCustomer); + return getData(customerEntity); + } + } + @Override public TextPageData findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) { log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); @@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService { private DataValidator customerValidator = new DataValidator() { + + @Override + protected void validateCreate(Customer customer) { + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent( + c -> { + throw new DataValidationException("Customer with such title already exists!"); + } + ); + } + + @Override + protected void validateUpdate(Customer customer) { + customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent( + c -> { + if (!c.getId().equals(customer.getUuidId())) { + throw new DataValidationException("Customer with such title already exists!"); + } + } + ); + } + @Override protected void validateDataImpl(Customer customer) { if (StringUtils.isEmpty(customer.getTitle())) { throw new DataValidationException("Customer title should be specified!"); } + if (customer.getTitle().equals(PUBLIC_CUSTOMER_TITLE)) { + throw new DataValidationException("'Public' title for customer is system reserved!"); + } if (!StringUtils.isEmpty(customer.getEmail())) { validateEmail(customer.getEmail()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index d3ed5d1dd1..9c9c66b376 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -111,6 +111,7 @@ public class ModelConstants { public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text"; + public static final String CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME = "customer_by_tenant_and_title"; /** * Cassandra device constants. diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql index febd1e94ef..6e4543026b 100644 --- a/dao/src/main/resources/schema.cql +++ b/dao/src/main/resources/schema.cql @@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer ( PRIMARY KEY (id, tenant_id) ); +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_title AS + SELECT * + from thingsboard.customer + WHERE tenant_id IS NOT NULL AND title IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, title, id ) + WITH CLUSTERING ORDER BY ( title ASC, id DESC ); + CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS SELECT * from thingsboard.customer diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql index cd00b1d97b..fc7c3155d9 100644 --- a/dao/src/main/resources/system-data.cql +++ b/dao/src/main/resources/system-data.cql @@ -80,7 +80,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel', INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table', -'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Timestamp{{ h.dataKey.label }}
0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n
\n
\n \n \n
\n
","templateCss":"table.md-table thead.md-head>tr.md-row {\n height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n height: 38px;\n}\n\n.md-table-pagination>* {\n height: 46px;\n}\n","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n \n self.ctx.filter = scope.$injector.get(\"$filter\");\n\n scope.sources = [];\n scope.sourceIndex = 0;\n scope.showTimestamp = self.ctx.settings.showTimestamp !== false;\n var origColor = self.ctx.widgetConfig.color || ''rgba(0, 0, 0, 0.87)'';\n var defaultColor = tinycolor(origColor);\n var mdDark = defaultColor.setAlpha(0.87).toRgbString();\n var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();\n var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();\n var mdDarkIcon = mdDarkSecondary;\n var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();\n \n var cssString = ''table.md-table th.md-column {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column md-icon.md-sort-icon {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell.md-placeholder {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''.md-table-pagination .buttons md-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination md-select:not([disabled]):focus .md-select-value {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}'';\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = ''ts-table-'' + hashCode(cssString);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, cssString);\n self.ctx.$container.addClass(namespace);\n \n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n var keyOffset = 0;\n for (var ds = 0; ds < self.ctx.datasources.length; ds++) {\n var source = {};\n var datasource = self.ctx.datasources[ds];\n source.keyStartIndex = keyOffset;\n keyOffset += datasource.dataKeys.length;\n source.keyEndIndex = keyOffset;\n source.datasource = datasource;\n source.data = [];\n source.rawData = [];\n source.query = {\n limit: 5,\n page: 1,\n order: ''-0''\n }\n source.ts = {\n header: [],\n count: 0,\n data: [],\n stylesInfo: [],\n contentsInfo: [],\n rowDataTemplate: {}\n }\n source.ts.rowDataTemplate[''Timestamp''] = null;\n for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n var dataKey = datasource.dataKeys[a];\n var keySettings = dataKey.settings;\n source.ts.header.push({\n index: a+1,\n dataKey: dataKey\n });\n source.ts.rowDataTemplate[dataKey.label] = null;\n\n var cellStyleFunction = null;\n var useCellStyleFunction = false;\n \n if (keySettings.useCellStyleFunction === true) {\n if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n try {\n cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n useCellStyleFunction = true;\n } catch (e) {\n cellStyleFunction = null;\n useCellStyleFunction = false;\n }\n }\n }\n\n source.ts.stylesInfo.push({\n useCellStyleFunction: useCellStyleFunction,\n cellStyleFunction: cellStyleFunction\n });\n \n var cellContentFunction = null;\n var useCellContentFunction = false;\n \n if (keySettings.useCellContentFunction === true) {\n if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n try {\n cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n useCellContentFunction = true;\n } catch (e) {\n cellContentFunction = null;\n useCellContentFunction = false;\n }\n }\n }\n \n source.ts.contentsInfo.push({\n useCellContentFunction: useCellContentFunction,\n cellContentFunction: cellContentFunction\n });\n \n }\n scope.sources.push(source);\n }\n\n scope.onPaginate = function(source) {\n updatePage(source);\n }\n \n scope.onReorder = function(source) {\n reorder(source);\n updatePage(source);\n }\n \n scope.cellStyle = function(source, index, value) {\n var style = {};\n if (index > 0) {\n var styleInfo = source.ts.stylesInfo[index-1];\n if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n try {\n style = styleInfo.cellStyleFunction(value);\n } catch (e) {\n style = {};\n }\n }\n }\n return style;\n }\n\n scope.cellContent = function(source, index, row, value) {\n if (index === 0) {\n return self.ctx.filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n } else {\n var strContent = '''';\n if (angular.isDefined(value)) {\n strContent = ''''+value;\n }\n var content = strContent;\n var contentInfo = source.ts.contentsInfo[index-1];\n if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n try {\n var rowData = source.ts.rowDataTemplate;\n rowData[''Timestamp''] = row[0];\n for (var h=0; h < source.ts.header.length; h++) {\n var headerInfo = source.ts.header[h];\n rowData[headerInfo.dataKey.name] = row[headerInfo.index];\n }\n content = contentInfo.cellContentFunction(value, rowData, self.ctx.filter);\n } catch (e) {\n content = strContent;\n }\n } \n return content;\n }\n }\n \n scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n if (newIndex != oldIndex) {\n updateSourceData(scope.sources[scope.sourceIndex]);\n } \n });\n}\n\nself.onDataUpdated = function() {\n var scope = self.ctx.$scope;\n for (var s=0; s < scope.sources.length; s++) {\n var source = scope.sources[s];\n source.rawData = self.ctx.data.slice(source.keyStartIndex, source.keyEndIndex);\n }\n updateSourceData(scope.sources[scope.sourceIndex]);\n scope.$digest();\n}\n\nself.onDestroy = function() {\n}\n\nfunction updatePage(source) {\n var startIndex = source.query.limit * (source.query.page - 1);\n source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n source.data = self.ctx.filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n var rowsMap = {};\n for (var d = 0; d < data.length; d++) {\n var columnData = data[d].data;\n for (var i = 0; i < columnData.length; i++) {\n var cellData = columnData[i];\n var timestamp = cellData[0];\n var row = rowsMap[timestamp];\n if (!row) {\n row = [];\n row[0] = timestamp;\n for (var c = 0; c < data.length; c++) {\n row[c+1] = undefined;\n }\n rowsMap[timestamp] = row;\n }\n row[d+1] = cellData[1];\n }\n }\n var rows = [];\n for (var t in rowsMap) {\n rows.push(rowsMap[t]);\n }\n return rows;\n}\n\nfunction updateSourceData(source) {\n source.data = convertData(source.rawData);\n source.ts.count = source.data.length;\n reorder(source);\n updatePage(source);\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}', +'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"\n","templateCss":"","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get(''utils'').guid();\n\n scope.config = {\n settings: self.ctx.settings,\n widgetConfig: self.ctx.widgetConfig\n }\n\n scope.datasources = self.ctx.datasources;\n scope.data = self.ctx.data;\n scope.tableId = \"table-\"+id;\n \n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.data = self.ctx.data;\n self.ctx.$scope.$broadcast(''timeseries-table-data-updated'', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}', 'Timeseries table' ); INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) diff --git a/ui/src/app/api/customer.service.js b/ui/src/app/api/customer.service.js index 133021bc40..b36b44afdf 100644 --- a/ui/src/app/api/customer.service.js +++ b/ui/src/app/api/customer.service.js @@ -18,12 +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, - getCustomerTitle: getCustomerTitle, + getShortCustomerInfo: getShortCustomerInfo, + applyAssignedCustomersInfo: applyAssignedCustomersInfo, + applyAssignedCustomerInfo: applyAssignedCustomerInfo, deleteCustomer: deleteCustomer, saveCustomer: saveCustomer } @@ -61,9 +63,9 @@ function CustomerService($http, $q) { return deferred.promise; } - function getCustomerTitle(customerId) { + function getShortCustomerInfo(customerId) { var deferred = $q.defer(); - var url = '/api/customer/' + customerId + '/title'; + var url = '/api/customer/' + customerId + '/shortInfo'; $http.get(url, null).then(function success(response) { deferred.resolve(response.data); }, function fail(response) { @@ -72,6 +74,77 @@ function CustomerService($http, $q) { 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 b369e90b57..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, config) { + function getTenantDevices(pageLink, applyCustomersInfo, config) { var deferred = $q.defer(); var url = '/api/tenant/devices?limit=' + pageLink.limit; if (angular.isDefined(pageLink.textSearch)) { @@ -64,14 +66,26 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic url += '&textOffset=' + pageLink.textOffset; } $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); + 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 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(this, apply); + } catch (e) { + subscription.callbacks.onDataUpdateError(this, 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.timeWindowConfig = angular.copy(this.originalTimewindow); + this.originalTimewindow = null; + this.callbacks.timeWindowUpdated(this, this.timeWindowConfig); + } + } + } + + onUpdateTimewindow(startTimeMs, endTimeMs) { + if (this.useDashboardTimewindow) { + this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); + } else { + 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); + } + } + + 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/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.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..64fb6d8919 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,9 @@ function Utils($mdColorPalette, $rootScope, $window, types) { parseException: parseException, processWidgetException: processWidgetException, isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, - filterSearchTextEntities: filterSearchTextEntities + filterSearchTextEntities: filterSearchTextEntities, + guid: guid, + createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo } return service; @@ -276,4 +278,153 @@ 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; + } + } diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index 7c7a5526dc..131616be0c 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -182,12 +182,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t vm.dashboardTimewindowApi = { onResetTimewindow: function() { - if (vm.originalDashboardTimewindow) { - $timeout(function() { + $timeout(function() { + if (vm.originalDashboardTimewindow) { vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow); vm.originalDashboardTimewindow = null; - }, 0); - } + } + }, 0); }, onUpdateTimewindow: function(startTimeMs, endTimeMs) { if (!vm.originalDashboardTimewindow) { diff --git a/ui/src/app/components/device-filter.directive.js b/ui/src/app/components/device-filter.directive.js index ecf12cd542..0233c95b02 100644 --- a/ui/src/app/components/device-filter.directive.js +++ b/ui/src/app/components/device-filter.directive.js @@ -41,7 +41,7 @@ function DeviceFilter($compile, $templateCache, $q, deviceService) { var deferred = $q.defer(); - deviceService.getTenantDevices(pageLink).then(function success(result) { + deviceService.getTenantDevices(pageLink, false).then(function success(result) { deferred.resolve(result.data); }, function fail() { deferred.reject(); diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html index 1cf0085576..c29c2e5b1a 100644 --- a/ui/src/app/components/grid.tpl.html +++ b/ui/src/app/components/grid.tpl.html @@ -49,7 +49,7 @@ - {{ action.details() }} + {{ action.details( rowItem[n] ) }} @@ -62,7 +62,7 @@ 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 bf500b1746..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,244 +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) { - var dataIndex = 0; - 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: dataIndex++ - }; - $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) { @@ -584,285 +587,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q onRedraw(); }*/ - function onResetTimewindow() { - if (widgetContext.useDashboardTimewindow) { - dashboardTimewindowApi.onResetTimewindow(); - } else { - if (originalTimewindow) { - widget.config.timewindow = angular.copy(originalTimewindow); - originalTimewindow = null; - } - } - } - - function onUpdateTimewindow(startTimeMs, endTimeMs) { - if (widgetContext.useDashboardTimewindow) { - dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs); - } else { - if (!originalTimewindow) { - originalTimewindow = angular.copy(widget.config.timewindow); - } - widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs); - } - } - - function dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) { - notifyDataLoaded(); - var update = true; - var currentData; - if ($scope.displayLegend && $scope.legendData.data[datasourceIndex + dataKeyIndex].hidden) { - currentData = widgetContext.hiddenData[datasourceIndex + dataKeyIndex]; - } else { - currentData = widgetContext.data[datasourceIndex + dataKeyIndex]; - } - if (widget.type === types.widgetType.latest.value) { - var prevData = currentData.data; - if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { - var prevValue = prevData[0][1]; - if (prevValue === sourceData.data[0][1]) { - update = false; - } - } - } - if (update) { - if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) { - updateTimewindow(); - } - currentData.data = sourceData.data; - onDataUpdated(); - if ($scope.caulculateLegendData) { - updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply); - } - } - if (apply) { - $scope.$digest(); - } - } - - function updateLegend(dataIndex, data, apply) { - var legendKeyData = $scope.legendData.data[dataIndex]; - if ($scope.legendConfig.showMin) { - legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showMax) { - legendKeyData.max = formatValue(calculateMax(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showAvg) { - legendKeyData.avg = formatValue(calculateAvg(data), widgetContext.decimals, widgetContext.units); - } - if ($scope.legendConfig.showTotal) { - legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units); - } - $scope.$broadcast('legendDataUpdated', apply !== false); - } - - function isNumeric(val) { - return (val - parseFloat( val ) + 1) >= 0; - } - - function formatValue(value, dec, units) { - if (angular.isDefined(value) && - value !== null && isNumeric(value)) { - var formatted = value; - if (angular.isDefined(dec)) { - formatted = formatted.toFixed(dec); - } - formatted = (formatted * 1).toString(); - if (angular.isDefined(units) && units.length > 0) { - formatted += ' ' + units; - } - return formatted; - } else { - return ''; - } - } - - function calculateMin(data) { - if (data.length > 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - var result = Number(data[0][1]); - for (var i=1;i 0) { - return calculateTotal(data)/data.length; - } else { - return null; - } - } - - function calculateTotal(data) { - if (data.length > 0) { - var result = 0; - for (var i = 0; i < data.length; i++) { - result += Number(data[i][1]); - } - return result; - } else { - return null; - } - } - - function checkSubscriptions() { - if (widget.type !== types.widgetType.rpc.value) { - var subscriptionsChanged = false; - for (var i = 0; i < datasourceListeners.length; i++) { - var listener = datasourceListeners[i]; - var deviceId = null; - var aliasName = null; - if (listener.datasource.type === types.datasourceType.device) { - if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId; - aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias; - } - if (!angular.equals(deviceId, listener.deviceId) || - !angular.equals(aliasName, listener.datasource.name)) { - subscriptionsChanged = true; - break; - } - } - } - if (subscriptionsChanged) { - unsubscribe(); - subscribe(); - } - } - } - - function unsubscribe() { - if (widget.type !== types.widgetType.rpc.value) { - for (var i = 0; i < datasourceListeners.length; i++) { - var listener = datasourceListeners[i]; - datasourceService.unsubscribeFromDatasource(listener); - } - datasourceListeners = []; - } - } - - function updateRealtimeSubscription(_subscriptionTimewindow) { - if (_subscriptionTimewindow) { - subscriptionTimewindow = _subscriptionTimewindow; - } else { - subscriptionTimewindow = - timeService.createSubscriptionTimewindow( - widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow, - widgetContext.timeWindow.stDiff); - } - updateTimewindow(); - return subscriptionTimewindow; - } - - function hasTimewindow() { - if (widgetContext.useDashboardTimewindow) { - return angular.isDefined(dashboardTimewindow); - } else { - return angular.isDefined(widget.config.timewindow); - } - } - - function updateDataKeyLabel(dataKey, deviceName, aliasName) { - var pattern = dataKey.pattern; - var label = dataKey.pattern; - var match = varsRegex.exec(pattern); - while (match !== null) { - var variable = match[0]; - var variableName = match[1]; - if (variableName === 'deviceName') { - label = label.split(variable).join(deviceName); - } else if (variableName === 'aliasName') { - label = label.split(variable).join(aliasName); - } - match = varsRegex.exec(pattern); - } - dataKey.label = label; - } - - function subscribe() { - if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) { - notifyDataLoading(); - if (widget.type === types.widgetType.timeseries.value && - hasTimewindow()) { - updateRealtimeSubscription(); - if (subscriptionTimewindow.fixedWindow) { - onDataUpdated(); - } - } - var index = 0; - for (var i = 0; i < widgetContext.datasources.length; i++) { - var datasource = widgetContext.datasources[i]; - if (angular.isFunction(datasource)) - continue; - var deviceId = null; - if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) { - if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) { - deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId; - datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias; - var deviceName = ''; - var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId]; - for (var d=0;d -
{{ item | contactShort }}
\ No newline at end of file +
{{ item | contactShort }}
\ No newline at end of file diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html index 62ef6b3f35..a2165712e6 100644 --- a/ui/src/app/customer/customer-fieldset.tpl.html +++ b/ui/src/app/customer/customer-fieldset.tpl.html @@ -15,13 +15,13 @@ limitations under the License. --> -{{ 'customer.manage-users' | translate }} +{{ 'customer.manage-users' | translate }} {{ 'customer.manage-devices' | translate }} {{ 'customer.manage-dashboards' | translate }} -{{ 'customer.delete' | translate }} +{{ 'customer.delete' | translate }} -
+
diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js index 57386604f3..4182588555 100644 --- a/ui/src/app/customer/customer.controller.js +++ b/ui/src/app/customer/customer.controller.js @@ -30,14 +30,23 @@ export default function CustomerController(customerService, $state, $stateParams }, name: function() { return $translate.instant('user.users') }, details: function() { return $translate.instant('customer.manage-customer-users') }, - icon: "account_circle" + icon: "account_circle", + isEnabled: function(customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + } }, { onAction: function ($event, item) { openCustomerDevices($event, item); }, name: function() { return $translate.instant('device.devices') }, - details: function() { return $translate.instant('customer.manage-customer-devices') }, + details: function(customer) { + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { + return $translate.instant('customer.manage-public-devices') + } else { + return $translate.instant('customer.manage-customer-devices') + } + }, icon: "devices_other" }, { @@ -45,7 +54,13 @@ export default function CustomerController(customerService, $state, $stateParams openCustomerDashboards($event, item); }, name: function() { return $translate.instant('dashboard.dashboards') }, - details: function() { return $translate.instant('customer.manage-customer-dashboards') }, + details: function(customer) { + if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) { + return $translate.instant('customer.manage-public-dashboards') + } else { + return $translate.instant('customer.manage-customer-dashboards') + } + }, icon: "dashboard" }, { @@ -54,7 +69,10 @@ export default function CustomerController(customerService, $state, $stateParams }, name: function() { return $translate.instant('action.delete') }, details: function() { return $translate.instant('customer.delete') }, - icon: "delete" + icon: "delete", + isEnabled: function(customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + } } ]; @@ -86,7 +104,19 @@ export default function CustomerController(customerService, $state, $stateParams addItemText: function() { return $translate.instant('customer.add-customer-text') }, noItemsText: function() { return $translate.instant('customer.no-customers-text') }, - itemDetailsText: function() { return $translate.instant('customer.customer-details') } + itemDetailsText: function(customer) { + if (customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic)) { + return $translate.instant('customer.customer-details') + } else { + return ''; + } + }, + isSelectionEnabled: function (customer) { + return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic); + }, + isDetailsReadOnly: function (customer) { + return customer && customer.additionalInfo && customer.additionalInfo.isPublic; + } }; if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js index f23711ae5e..4a4e09ca0b 100644 --- a/ui/src/app/customer/customer.directive.js +++ b/ui/src/app/customer/customer.directive.js @@ -24,7 +24,21 @@ export default function CustomerDirective($compile, $templateCache) { var linker = function (scope, element) { var template = $templateCache.get(customerFieldsetTemplate); element.html(template); + + scope.isPublic = false; + + scope.$watch('customer', function(newVal) { + if (newVal) { + if (scope.customer.additionalInfo) { + scope.isPublic = scope.customer.additionalInfo.isPublic; + } else { + scope.isPublic = false; + } + } + }); + $compile(element.contents())(scope); + } return { restrict: "E", diff --git a/ui/src/app/dashboard/dashboard-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html index 9f6cd00b7a..636786758e 100644 --- a/ui/src/app/dashboard/dashboard-card.tpl.html +++ b/ui/src/app/dashboard/dashboard-card.tpl.html @@ -15,5 +15,6 @@ limitations under the License. --> -
{{'dashboard.assignedToCustomer' | translate}} '{{vm.customerTitle}}'
+
{{'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..1d11fb2c60 100644 --- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html +++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html @@ -15,25 +15,42 @@ limitations under the License. --> +{{ 'dashboard.export' | translate }} +{{ 'dashboard.make-public' | translate }} {{ 'dashboard.assign-to-customer' | translate }} -{{ 'dashboard.unassign-from-customer' | translate }} -{{ 'dashboard.export' | translate }} + class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }} {{ 'dashboard.delete' | translate }} - + ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'"> +
+ + + + + + + + {{ 'dashboard.copy-public-link' | translate }} + + +
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index fbb3e37844..d100522ff0 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -86,6 +86,7 @@ export default function DashboardController(types, widgetService, userService, vm.exportDashboard = exportDashboard; vm.exportWidget = exportWidget; vm.importWidget = importWidget; + vm.isPublicUser = isPublicUser; vm.isTenantAdmin = isTenantAdmin; vm.isSystemAdmin = isSystemAdmin; vm.loadDashboard = loadDashboard; @@ -273,6 +274,10 @@ export default function DashboardController(types, widgetService, userService, vm.dashboardInitComplete = true; } + function isPublicUser() { + return vm.user.isPublic === true; + } + function isTenantAdmin() { return vm.user.authority === 'TENANT_ADMIN'; } @@ -617,22 +622,8 @@ export default function DashboardController(types, widgetService, userService, sizeY: widgetTypeInfo.sizeY, config: config }; - $mdDialog.show({ - controller: 'AddWidgetController', - controllerAs: 'vm', - templateUrl: addWidgetTemplate, - locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo}, - parent: angular.element($document[0].body), - fullscreen: true, - skipHide: true, - targetEvent: event, - onComplete: function () { - var w = angular.element($window); - w.triggerHandler('resize'); - } - }).then(function (result) { - var widget = result.widget; - vm.aliasesInfo = result.aliasesInfo; + + function addWidget(widget) { var columns = 24; if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) { columns = vm.dashboard.configuration.gridSettings.columns; @@ -643,9 +634,37 @@ export default function DashboardController(types, widgetService, userService, widget.sizeY *= ratio; } vm.widgets.push(widget); - }, function (rejection) { - vm.aliasesInfo = rejection.aliasesInfo; - }); + } + + if (widgetTypeInfo.useCustomDatasources) { + addWidget(newWidget); + } else { + $mdDialog.show({ + controller: 'AddWidgetController', + controllerAs: 'vm', + templateUrl: addWidgetTemplate, + locals: { + dashboard: vm.dashboard, + aliasesInfo: vm.aliasesInfo, + widget: newWidget, + widgetInfo: widgetTypeInfo + }, + parent: angular.element($document[0].body), + fullscreen: true, + skipHide: true, + targetEvent: event, + onComplete: function () { + var w = angular.element($window); + w.triggerHandler('resize'); + } + }).then(function (result) { + var widget = result.widget; + vm.aliasesInfo = result.aliasesInfo; + addWidget(widget); + }, function (rejection) { + vm.aliasesInfo = rejection.aliasesInfo; + }); + } } ); } diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js index 2e9f55aa19..b2f3117a04 100644 --- a/ui/src/app/dashboard/dashboard.directive.js +++ b/ui/src/app/dashboard/dashboard.directive.js @@ -20,30 +20,44 @@ import dashboardFieldsetTemplate from './dashboard-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function DashboardDirective($compile, $templateCache, types, customerService) { +export default function DashboardDirective($compile, $templateCache, $translate, types, toast, customerService, dashboardService) { var linker = function (scope, element) { var template = $templateCache.get(dashboardFieldsetTemplate); element.html(template); scope.isAssignedToCustomer = false; + scope.isPublic = false; scope.assignedCustomer = null; + scope.publicLink = null; scope.$watch('dashboard', function(newVal) { if (newVal) { if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) { scope.isAssignedToCustomer = true; - customerService.getCustomer(scope.dashboard.customerId.id).then( + customerService.getShortCustomerInfo(scope.dashboard.customerId.id).then( function success(customer) { scope.assignedCustomer = customer; + scope.isPublic = customer.isPublic; + if (scope.isPublic) { + scope.publicLink = dashboardService.getPublicDashboardLink(scope.dashboard); + } else { + scope.publicLink = null; + } } ); } else { scope.isAssignedToCustomer = false; + scope.isPublic = false; + scope.publicLink = null; scope.assignedCustomer = null; } } }); + scope.onPublicLinkCopied = function() { + toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + $compile(element.contents())(scope); } return { @@ -55,6 +69,7 @@ export default function DashboardDirective($compile, $templateCache, types, cust dashboardScope: '=', theForm: '=', onAssignToCustomer: '&', + onMakePublic: '&', onUnassignFromCustomer: '&', onExportDashboard: '&', onDeleteDashboard: '&' diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js index 2c400ea97d..e9fe1f24ea 100644 --- a/ui/src/app/dashboard/dashboard.routes.js +++ b/ui/src/app/dashboard/dashboard.routes.js @@ -62,7 +62,7 @@ export default function DashboardRoutes($stateProvider) { pageTitle: 'customer.dashboards' }, ncyBreadcrumb: { - label: '{"icon": "dashboard", "label": "customer.dashboards"}' + label: '{"icon": "dashboard", "label": "{{ vm.customerDashboardsTitle }}", "translate": "false"}' } }) .state('home.dashboards.dashboard', { diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index ad9e52b572..7e314b828b 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -47,7 +47,7 @@ aria-label="{{ 'fullscreen.fullscreen' | translate }}" class="md-icon-button"> - + diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js index 3ef96a7198..d0fa95860c 100644 --- a/ui/src/app/dashboard/dashboards.controller.js +++ b/ui/src/app/dashboard/dashboards.controller.js @@ -19,11 +19,33 @@ 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 function DashboardCardController($scope, types, customerService) { +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; @@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) { vm.isAssignedToCustomer = function() { if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' && - vm.item.customerId.id != vm.types.id.nullUid) { + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { return true; } return false; } - $scope.$watch('vm.item', - function() { - if (vm.isAssignedToCustomer()) { - customerService.getCustomerTitle(vm.item.customerId.id).then( - function success(title) { - vm.customerTitle = title; - } - ); - } + 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, $scope, $controller, +export function DashboardsController(userService, dashboardService, customerService, importExport, types, $state, $stateParams, $mdDialog, $document, $q, $translate) { var customerId = $stateParams.customerId; @@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ vm.dashboardsScope = $state.$current.data.dashboardsType; vm.assignToCustomer = assignToCustomer; + vm.makePublic = makePublic; vm.unassignFromCustomer = unassignFromCustomer; vm.exportDashboard = exportDashboard; @@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ 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); @@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ 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 ]); }, @@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ 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.unshare') }, + 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( { @@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ 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.unshare') }, + details: function() { return $translate.instant('dashboard.make-private') }, + icon: "reply", + isEnabled: function(dashboard) { + return dashboard && dashboard.assignedCustomer.isPublic; + } } ); @@ -418,15 +486,27 @@ export function DashboardsController(userService, dashboardService, customerServ 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 () { @@ -436,6 +516,25 @@ export function DashboardsController(userService, dashboardService, customerServ }); } + 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/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html index 1c667f33b5..bb84af7182 100644 --- a/ui/src/app/device/device-card.tpl.html +++ b/ui/src/app/device/device-card.tpl.html @@ -15,4 +15,5 @@ limitations under the License. --> -
{{'device.assignedToCustomer' | translate}} '{{vm.customerTitle}}'
+
{{'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.device-public' | translate }} +
diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js index 4de12bb3a4..ed9ff6ff8c 100644 --- a/ui/src/app/device/device.controller.js +++ b/ui/src/app/device/device.controller.js @@ -24,7 +24,7 @@ import deviceCredentialsTemplate from './device-credentials.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export function DeviceCardController($scope, types, customerService) { +export function DeviceCardController(types) { var vm = this; @@ -32,28 +32,23 @@ export function DeviceCardController($scope, types, customerService) { vm.isAssignedToCustomer = function() { if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === 'tenant' && - vm.item.customerId.id != vm.types.id.nullUid) { + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { return true; } return false; } - $scope.$watch('vm.item', - function() { - if (vm.isAssignedToCustomer()) { - customerService.getCustomerTitle(vm.item.customerId.id).then( - function success(title) { - vm.customerTitle = title; - } - ); - } + 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, $scope, $controller, $state, $stateParams, $document, $mdDialog, $q, $translate, types) { +export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) { var customerId = $stateParams.customerId; @@ -107,6 +102,7 @@ export function DeviceController(userService, deviceService, customerService, $s vm.devicesScope = $state.$current.data.devicesType; vm.assignToCustomer = assignToCustomer; + vm.makePublic = makePublic; vm.unassignFromCustomer = unassignFromCustomer; vm.manageCredentials = manageCredentials; @@ -123,10 +119,20 @@ export function DeviceController(userService, deviceService, customerService, $s 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); @@ -135,6 +141,18 @@ export function DeviceController(userService, deviceService, customerService, $s 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) { @@ -152,17 +170,29 @@ export function DeviceController(userService, deviceService, customerService, $s 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.unshare') }, + 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) { @@ -213,7 +243,7 @@ export function DeviceController(userService, deviceService, customerService, $s } 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); @@ -226,13 +256,30 @@ export function DeviceController(userService, deviceService, customerService, $s 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.unshare') }, + details: function() { return $translate.instant('device.make-private') }, + icon: "reply", + isEnabled: function(device) { + return device && device.assignedCustomer.isPublic; + } + } + ); + deviceActionsList.push( { onAction: function ($event, item) { @@ -365,7 +412,7 @@ export function DeviceController(userService, deviceService, customerService, $s $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, @@ -404,15 +451,27 @@ export function DeviceController(userService, deviceService, customerService, $s 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 () { @@ -441,6 +500,24 @@ export function DeviceController(userService, deviceService, customerService, $s }); } + 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/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 3de229e9aa..37a2b93b48 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", + "unshare": "Unshare", "apply": "Apply", "apply-changes": "Apply changes", "edit-mode": "Edit mode", @@ -154,11 +156,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 +197,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 +227,12 @@ 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", "select-dashboard": "Select dashboard", "no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.", "dashboard-required": "Dashboard is required.", @@ -267,7 +281,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 +341,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 +357,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 +393,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" diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 4cc2192bea..be61de7f93 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -34,39 +34,6 @@ export default class TbFlot { 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; - } - - if (this.chartType === 'line' && settings.smoothLines && !series.points.show) { - series.curvedLines = { - apply: true - } - } - - 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 = $("
"); @@ -183,7 +150,6 @@ export default class TbFlot { }; var options = { - colors: colors, title: null, subtitle: null, shadowSize: settings.shadowSize || 4, @@ -290,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: { @@ -340,55 +302,112 @@ 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.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(); + } } } } } 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() { @@ -708,17 +727,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); + } } } } @@ -732,8 +753,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; @@ -794,33 +815,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 () { @@ -829,38 +850,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; } } @@ -952,10 +973,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; } } @@ -963,9 +984,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) { @@ -992,12 +1013,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; } @@ -1007,7 +1028,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..caac144415 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, @@ -219,8 +219,17 @@ export default class TbGoogleMap { 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 +275,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..c39a1421bd 100644 --- a/ui/src/app/widget/lib/map-widget.js +++ b/ui/src/app/widget/lib/map-widget.js @@ -19,11 +19,60 @@ import tinycolor from 'tinycolor2'; import TbGoogleMap from './google-map'; import TbOpenStreetMap from './openstreet-map'; +function procesTooltipPattern(tbMap, pattern, datasources) { + 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 +95,113 @@ 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, + label: datasource.name, + labelColor: this.ctx.widgetConfig.color || '#000000', + color: "#FE7569", + useColorFunction: false, + colorFunction: null, + markerImage: null, + markerImageSize: 34, + useMarkerImage: false, + useMarkerImageFunction: false, + markerImageFunction: null, + markerImages: [], + tooltipPattern: "Latitude: ${#"+latKeyIndex+":7}
Longitude: ${#"+lngKeyIndex+":7}" + }; + + locationsSettings.tooltipReplaceInfo = procesTooltipPattern(this, locationsSettings.tooltipPattern, this.subscription.datasources); + + 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); } - 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 = []; } @@ -99,7 +212,7 @@ export default class TbMapWidget { lngKeyName: "lng", showLabel: true, label: "", - labelColor: ctx.widgetConfig.color || '#000000', + labelColor: this.ctx.widgetConfig.color || '#000000', color: "#FE7569", useColorFunction: false, colorFunction: null, @@ -112,7 +225,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 +236,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 +245,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 +254,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 +270,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 +330,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 +357,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 +372,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 +405,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 +431,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 +450,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 +517,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 +535,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); } } } diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js index aacb505c06..77332d5677 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({ @@ -111,9 +111,17 @@ export default class TbOpenStreetMap { 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 +153,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 ***********************/