thingsboard-aplcache
Changes
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java 14(+13 -1)
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java 69(+61 -8)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java 34(+34 -0)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java 64(+60 -4)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java 5(+4 -1)
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java 96(+96 -0)
application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java 12(+11 -1)
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java 18(+15 -3)
application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java 42(+42 -0)
dao/src/main/resources/schema.cql 7(+7 -0)
ui/src/app/api/customer.service.js 81(+77 -4)
ui/src/app/api/dashboard.service.js 75(+57 -18)
ui/src/app/api/datasource.service.js 4(+2 -2)
ui/src/app/api/device.service.js 104(+73 -31)
ui/src/app/api/login.service.js 14(+14 -0)
ui/src/app/api/subscription.js 647(+647 -0)
ui/src/app/api/user.service.js 166(+127 -39)
ui/src/app/api/widget.service.js 16(+14 -2)
ui/src/app/app.run.js 50(+41 -9)
ui/src/app/common/utils.service.js 155(+153 -2)
ui/src/app/components/grid.tpl.html 4(+2 -2)
ui/src/app/components/widget.controller.js 902(+313 -589)
ui/src/app/customer/customer.controller.js 40(+35 -5)
ui/src/app/customer/customer.directive.js 14(+14 -0)
ui/src/app/dashboard/dashboard.controller.js 57(+38 -19)
ui/src/app/dashboard/dashboard.directive.js 19(+17 -2)
ui/src/app/dashboard/dashboards.controller.js 153(+126 -27)
ui/src/app/dashboard/index.js 3(+2 -1)
ui/src/app/device/device.controller.js 125(+101 -24)
ui/src/app/device/device.routes.js 2(+1 -1)
ui/src/app/device/device-fieldset.tpl.html 13(+10 -3)
ui/src/app/device/devices.tpl.html 3(+2 -1)
ui/src/app/locale/locale.constant.js 30(+28 -2)
ui/src/app/widget/lib/flot-widget.js 231(+126 -105)
ui/src/app/widget/lib/google-map.js 15(+14 -1)
ui/src/app/widget/lib/map-widget.js 325(+224 -101)
ui/src/app/widget/lib/openstreet-map.js 14(+13 -1)
ui/src/app/widget/lib/timeseries-table-widget.js 325(+325 -0)
ui/src/scss/main.scss 26(+26 -0)
Details
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 a7918e0..6a9e449 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/**";
@@ -88,9 +90,17 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
}
@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<String> 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 5ef52c2..d06f2be 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 96458cf..51bc452 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 d72f025..3812610 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;
@@ -117,6 +118,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
public TextPageData<DashboardInfo> getTenantDashboards(
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 b08a964..bebab8b 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 5ba84bf..811f39f 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 0000000..54ef093
--- /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 686bb46..af10674 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 d191905..c32b1f2 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 0000000..3a3b7cb
--- /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 2da3a97..0839695 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<GrantedAuthority> authorities;
private boolean enabled;
+ private UserPrincipal userPrincipal;
public SecurityUser() {
super();
@@ -39,9 +40,10 @@ public class SecurityUser extends User {
super(id);
}
- public SecurityUser(User user, boolean enabled) {
+ public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) {
super(user);
this.enabled = enabled;
+ this.userPrincipal = userPrincipal;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
@@ -57,8 +59,16 @@ public class SecurityUser extends User {
return enabled;
}
+ public UserPrincipal getUserPrincipal() {
+ return userPrincipal;
+ }
+
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
+ public void setUserPrincipal(UserPrincipal userPrincipal) {
+ this.userPrincipal = userPrincipal;
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
index 8ade253..20de6d8 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
import java.util.Arrays;
import java.util.List;
@@ -43,6 +44,7 @@ public class JwtTokenFactory {
private static final String FIRST_NAME = "firstName";
private static final String LAST_NAME = "lastName";
private static final String ENABLED = "enabled";
+ private static final String IS_PUBLIC = "isPublic";
private static final String TENANT_ID = "tenantId";
private static final String CUSTOMER_ID = "customerId";
@@ -63,12 +65,15 @@ public class JwtTokenFactory {
if (securityUser.getAuthority() == null)
throw new IllegalArgumentException("User doesn't have any privileges");
- Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
+ UserPrincipal principal = securityUser.getUserPrincipal();
+ String subject = principal.getValue();
+ Claims claims = Jwts.claims().setSubject(subject);
claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(FIRST_NAME, securityUser.getFirstName());
claims.put(LAST_NAME, securityUser.getLastName());
claims.put(ENABLED, securityUser.isEnabled());
+ claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
if (securityUser.getTenantId() != null) {
claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
}
@@ -104,6 +109,9 @@ public class JwtTokenFactory {
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
securityUser.setLastName(claims.get(LAST_NAME, String.class));
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
+ boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
+ UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
+ securityUser.setUserPrincipal(principal);
String tenantId = claims.get(TENANT_ID, String.class);
if (tenantId != null) {
securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
@@ -123,9 +131,11 @@ public class JwtTokenFactory {
DateTime currentTime = new DateTime();
- Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
+ UserPrincipal principal = securityUser.getUserPrincipal();
+ Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name()));
claims.put(USER_ID, securityUser.getId().getId().toString());
+ claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
String token = Jwts.builder()
.setClaims(claims)
@@ -150,8 +160,10 @@ public class JwtTokenFactory {
if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
throw new IllegalArgumentException("Invalid Refresh Token scope");
}
+ boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
+ UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
- securityUser.setEmail(subject);
+ securityUser.setUserPrincipal(principal);
return securityUser;
}
diff --git a/application/src/main/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 0000000..6f23783
--- /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/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
index 6d7247b..5639044 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<CustomerEntity> {
* @return the list of customer objects
*/
List<CustomerEntity> 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<CustomerEntity> 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 7b53836..25c1116 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<CustomerEntity> imple
return customerEntities;
}
+ @Override
+ public Optional<CustomerEntity> 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 70321a2..01f4f99 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<Customer> 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 18ead42..a80c6e8 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.");
@@ -92,6 +98,27 @@ public class CustomerServiceImpl implements CustomerService {
}
@Override
+ public Customer findOrCreatePublicCustomer(TenantId tenantId) {
+ log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
+ Validator.validateId(tenantId, "Incorrect customerId " + tenantId);
+ Optional<CustomerEntity> 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<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) {
log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
@@ -110,11 +137,35 @@ public class CustomerServiceImpl implements CustomerService {
private DataValidator<Customer> customerValidator =
new DataValidator<Customer>() {
+
+ @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 d3ed5d1..9c9c66b 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.
dao/src/main/resources/schema.cql 7(+7 -0)
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
index febd1e9..6e45430 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 cd00b1d..fc7c315 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":"<md-tabs md-selected=\"sourceIndex\" ng-class=\"{''tb-headless'': sources.length === 1}\"\n id=\"tabs\" md-border-bottom flex class=\"tb-absolute-fill\">\n <md-tab ng-repeat=\"source in sources\" label=\"{{ source.datasource.name }}\">\n <md-table-container>\n <table md-table>\n <thead md-head md-order=\"source.query.order\" md-on-reorder=\"onReorder(source)\">\n <tr md-row>\n <th ng-show=\"showTimestamp\" md-column md-order-by=\"0\"><span>Timestamp</span></th>\n <th md-column md-order-by=\"{{ h.index }}\" ng-repeat=\"h in source.ts.header\"><span>{{ h.dataKey.label }}</span></th>\n </tr>\n </thead>\n <tbody md-body>\n <tr md-row ng-repeat=\"row in source.ts.data\">\n <td ng-show=\"$index > 0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n </td>\n </tr> \n </tbody> \n </table>\n </md-table-container>\n <md-table-pagination md-limit=\"source.query.limit\" md-limit-options=\"[5, 10, 15]\"\n md-page=\"source.query.page\" md-total=\"{{source.ts.count}}\"\n md-on-paginate=\"onPaginate(source)\" md-page-select>\n </md-table-pagination>\n </md-tab>\n</md-tabs>","templateCss":"table.md-table thead.md-head>tr.md-row {\n height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n height: 38px;\n}\n\n.md-table-pagination>* {\n height: 46px;\n}\n","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n \n self.ctx.filter = scope.$injector.get(\"$filter\");\n\n scope.sources = [];\n scope.sourceIndex = 0;\n scope.showTimestamp = self.ctx.settings.showTimestamp !== false;\n var origColor = self.ctx.widgetConfig.color || ''rgba(0, 0, 0, 0.87)'';\n var defaultColor = tinycolor(origColor);\n var mdDark = defaultColor.setAlpha(0.87).toRgbString();\n var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();\n var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();\n var mdDarkIcon = mdDarkSecondary;\n var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();\n \n var cssString = ''table.md-table th.md-column {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column md-icon.md-sort-icon {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell {\\n''+\n ''color: '' + mdDark + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell.md-placeholder {\\n''+\n ''color: '' + mdDarkDisabled + '';\\n''+\n ''}\\n''+\n ''table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n ''}\\n''+\n ''.md-table-pagination .buttons md-icon {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}\\n''+\n ''.md-table-pagination md-select:not([disabled]):focus .md-select-value {\\n''+\n ''color: '' + mdDarkSecondary + '';\\n''+\n ''}'';\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = ''ts-table-'' + hashCode(cssString);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, cssString);\n self.ctx.$container.addClass(namespace);\n \n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n var keyOffset = 0;\n for (var ds = 0; ds < self.ctx.datasources.length; ds++) {\n var source = {};\n var datasource = self.ctx.datasources[ds];\n source.keyStartIndex = keyOffset;\n keyOffset += datasource.dataKeys.length;\n source.keyEndIndex = keyOffset;\n source.datasource = datasource;\n source.data = [];\n source.rawData = [];\n source.query = {\n limit: 5,\n page: 1,\n order: ''-0''\n }\n source.ts = {\n header: [],\n count: 0,\n data: [],\n stylesInfo: [],\n contentsInfo: [],\n rowDataTemplate: {}\n }\n source.ts.rowDataTemplate[''Timestamp''] = null;\n for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n var dataKey = datasource.dataKeys[a];\n var keySettings = dataKey.settings;\n source.ts.header.push({\n index: a+1,\n dataKey: dataKey\n });\n source.ts.rowDataTemplate[dataKey.label] = null;\n\n var cellStyleFunction = null;\n var useCellStyleFunction = false;\n \n if (keySettings.useCellStyleFunction === true) {\n if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n try {\n cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n useCellStyleFunction = true;\n } catch (e) {\n cellStyleFunction = null;\n useCellStyleFunction = false;\n }\n }\n }\n\n source.ts.stylesInfo.push({\n useCellStyleFunction: useCellStyleFunction,\n cellStyleFunction: cellStyleFunction\n });\n \n var cellContentFunction = null;\n var useCellContentFunction = false;\n \n if (keySettings.useCellContentFunction === true) {\n if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n try {\n cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n useCellContentFunction = true;\n } catch (e) {\n cellContentFunction = null;\n useCellContentFunction = false;\n }\n }\n }\n \n source.ts.contentsInfo.push({\n useCellContentFunction: useCellContentFunction,\n cellContentFunction: cellContentFunction\n });\n \n }\n scope.sources.push(source);\n }\n\n scope.onPaginate = function(source) {\n updatePage(source);\n }\n \n scope.onReorder = function(source) {\n reorder(source);\n updatePage(source);\n }\n \n scope.cellStyle = function(source, index, value) {\n var style = {};\n if (index > 0) {\n var styleInfo = source.ts.stylesInfo[index-1];\n if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n try {\n style = styleInfo.cellStyleFunction(value);\n } catch (e) {\n style = {};\n }\n }\n }\n return style;\n }\n\n scope.cellContent = function(source, index, row, value) {\n if (index === 0) {\n return self.ctx.filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n } else {\n var strContent = '''';\n if (angular.isDefined(value)) {\n strContent = ''''+value;\n }\n var content = strContent;\n var contentInfo = source.ts.contentsInfo[index-1];\n if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n try {\n var rowData = source.ts.rowDataTemplate;\n rowData[''Timestamp''] = row[0];\n for (var h=0; h < source.ts.header.length; h++) {\n var headerInfo = source.ts.header[h];\n rowData[headerInfo.dataKey.name] = row[headerInfo.index];\n }\n content = contentInfo.cellContentFunction(value, rowData, self.ctx.filter);\n } catch (e) {\n content = strContent;\n }\n } \n return content;\n }\n }\n \n scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n if (newIndex != oldIndex) {\n updateSourceData(scope.sources[scope.sourceIndex]);\n } \n });\n}\n\nself.onDataUpdated = function() {\n var scope = self.ctx.$scope;\n for (var s=0; s < scope.sources.length; s++) {\n var source = scope.sources[s];\n source.rawData = self.ctx.data.slice(source.keyStartIndex, source.keyEndIndex);\n }\n updateSourceData(scope.sources[scope.sourceIndex]);\n scope.$digest();\n}\n\nself.onDestroy = function() {\n}\n\nfunction updatePage(source) {\n var startIndex = source.query.limit * (source.query.page - 1);\n source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n source.data = self.ctx.filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n var rowsMap = {};\n for (var d = 0; d < data.length; d++) {\n var columnData = data[d].data;\n for (var i = 0; i < columnData.length; i++) {\n var cellData = columnData[i];\n var timestamp = cellData[0];\n var row = rowsMap[timestamp];\n if (!row) {\n row = [];\n row[0] = timestamp;\n for (var c = 0; c < data.length; c++) {\n row[c+1] = undefined;\n }\n rowsMap[timestamp] = row;\n }\n row[d+1] = cellData[1];\n }\n }\n var rows = [];\n for (var t in rowsMap) {\n rows.push(rowsMap[t]);\n }\n return rows;\n}\n\nfunction updateSourceData(source) {\n source.data = convertData(source.rawData);\n source.ts.count = source.data.length;\n reorder(source);\n updatePage(source);\n}\n","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}',
+'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<tb-timeseries-table-widget \n config=\"config\"\n table-id=\"tableId\"\n datasources=\"datasources\"\n data=\"data\">\n</tb-timeseries-table-widget>","templateCss":"","controllerScript":"self.onInit = function() {\n \n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get(''utils'').guid();\n\n scope.config = {\n settings: self.ctx.settings,\n widgetConfig: self.ctx.widgetConfig\n }\n\n scope.datasources = self.ctx.datasources;\n scope.data = self.ctx.data;\n scope.tableId = \"table-\"+id;\n \n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.data = self.ctx.data;\n self.ctx.$scope.$broadcast(''timeseries-table-data-updated'', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}","settingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}","dataKeySettingsSchema":"{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: ''20px'',\\n color: ''#ffffff'',\\n background: color.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor(''blue'');\\n backgroundColor.setAlpha(value/100);\\n var color = ''blue'';\\n if (value > 50) {\\n color = ''white'';\\n }\\n \\n return {\\n paddingLeft: ''20px'',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: ''18px''\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}',
'Timeseries table' );
INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
ui/src/app/api/customer.service.js 81(+77 -4)
diff --git a/ui/src/app/api/customer.service.js b/ui/src/app/api/customer.service.js
index 133021b..b36b44a 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';
ui/src/app/api/dashboard.service.js 75(+57 -18)
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
index be450ed..b2a7897 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;
+ }
+
}
ui/src/app/api/datasource.service.js 4(+2 -2)
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
index 6041cbd..f87d547 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) {
ui/src/app/api/device.service.js 104(+73 -31)
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index b369e90..ea197a1 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<deviceIds.length;i++) {
@@ -112,7 +143,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
ids += deviceIds[i];
}
var url = '/api/devices?deviceIds=' + ids;
- $http.get(url, null).then(function success(response) {
+ $http.get(url, config).then(function success(response) {
var devices = response.data;
devices.sort(function (device1, device2) {
var id1 = device1.id.id;
@@ -128,16 +159,16 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
return deferred.promise;
}
- function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
+ function fetchAliasDeviceByNameFilter(deviceNameFilter, limit, applyCustomersInfo, config) {
var deferred = $q.defer();
var user = userService.getCurrentUser();
var promise;
var pageLink = {limit: limit, textSearch: deviceNameFilter};
if (user.authority === 'CUSTOMER_USER') {
var customerId = user.customerId;
- promise = getCustomerDevices(customerId, pageLink);
+ promise = getCustomerDevices(customerId, pageLink, applyCustomersInfo, config);
} else {
- promise = getTenantDevices(pageLink);
+ promise = getTenantDevices(pageLink, applyCustomersInfo, config);
}
promise.then(
function success(result) {
@@ -194,7 +225,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var deviceFilter = deviceAlias.deviceFilter;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
- fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
+ fetchAliasDeviceByNameFilter(deviceNameFilter, 100, false).then(
function(devices) {
if (devices && devices != null) {
var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
@@ -276,7 +307,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var promise;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
- promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
+ promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1, false);
} else {
var deviceList = deviceFilter.deviceList;
promise = getDevices(deviceList);
@@ -301,8 +332,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device';
$http.post(url, device).then(function success(response) {
deferred.resolve(response.data);
- }, function fail(response) {
- deferred.reject(response.data);
+ }, function fail() {
+ deferred.reject();
});
return deferred.promise;
}
@@ -312,8 +343,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/' + deviceId;
$http.delete(url).then(function success() {
deferred.resolve();
- }, function fail(response) {
- deferred.reject(response.data);
+ }, function fail() {
+ deferred.reject();
});
return deferred.promise;
}
@@ -323,8 +354,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/' + deviceId + '/credentials';
$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;
}
@@ -334,8 +365,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
var url = '/api/device/credentials';
$http.post(url, deviceCredentials).then(function success(response) {
deferred.resolve(response.data);
- }, function fail(response) {
- deferred.reject(response.data);
+ }, function fail() {
+ deferred.reject();
});
return deferred.promise;
}
@@ -343,10 +374,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
function assignDeviceToCustomer(customerId, deviceId) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/device/' + deviceId;
- $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;
}
@@ -354,10 +385,21 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
function unassignDeviceFromCustomer(deviceId) {
var deferred = $q.defer();
var url = '/api/customer/device/' + deviceId;
- $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 makeDevicePublic(deviceId) {
+ var deferred = $q.defer();
+ var url = '/api/customer/public/device/' + deviceId;
+ $http.post(url, null).then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
});
return deferred.promise;
}
ui/src/app/api/login.service.js 14(+14 -0)
diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js
index 5213229..272e4df 100644
--- a/ui/src/app/api/login.service.js
+++ b/ui/src/app/api/login.service.js
@@ -25,6 +25,7 @@ function LoginService($http, $q) {
changePassword: changePassword,
hasUser: hasUser,
login: login,
+ publicLogin: publicLogin,
resetPassword: resetPassword,
sendResetPasswordLink: sendResetPasswordLink,
}
@@ -49,6 +50,19 @@ function LoginService($http, $q) {
return deferred.promise;
}
+ function publicLogin(publicId) {
+ var deferred = $q.defer();
+ var pubilcLoginRequest = {
+ publicId: publicId
+ };
+ $http.post('/api/auth/login/public', pubilcLoginRequest).then(function success(response) {
+ deferred.resolve(response);
+ }, function fail(response) {
+ deferred.reject(response);
+ });
+ return deferred.promise;
+ }
+
function sendResetPasswordLink(email) {
var deferred = $q.defer();
var url = '/api/noauth/resetPasswordByEmail?email=' + email;
ui/src/app/api/subscription.js 647(+647 -0)
diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
new file mode 100644
index 0000000..d63b02a
--- /dev/null
+++ b/ui/src/app/api/subscription.js
@@ -0,0 +1,647 @@
+/*
+ * 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.
+ */
+
+/*
+ options = {
+ type,
+ targetDeviceAliasIds, // RPC
+ targetDeviceIds, // RPC
+ datasources,
+ timeWindowConfig,
+ useDashboardTimewindow,
+ legendConfig,
+ decimals,
+ units,
+ callbacks
+ }
+ */
+
+export default class Subscription {
+ constructor(subscriptionContext, options) {
+
+ this.ctx = subscriptionContext;
+ this.type = options.type;
+ this.callbacks = options.callbacks;
+ this.id = this.ctx.utils.guid();
+ this.cafs = {};
+ this.registrations = [];
+
+ if (this.type === this.ctx.types.widgetType.rpc.value) {
+ this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || function(){};
+ this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || function(){};
+ this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || function(){};
+ this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || function(){};
+
+ this.targetDeviceAliasIds = options.targetDeviceAliasIds;
+ this.targetDeviceIds = options.targetDeviceIds;
+
+ this.targetDeviceAliasId = null;
+ this.targetDeviceId = null;
+
+ this.rpcRejection = null;
+ this.rpcErrorText = null;
+ this.rpcEnabled = false;
+ this.executingRpcRequest = false;
+ this.executingPromises = [];
+ this.initRpc();
+ } else {
+ this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
+ this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
+ this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
+ this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){};
+ this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
+
+ this.datasources = options.datasources;
+ this.datasourceListeners = [];
+ this.data = [];
+ this.hiddenData = [];
+ this.originalTimewindow = null;
+ this.timeWindow = {
+ stDiff: this.ctx.stDiff
+ }
+ this.useDashboardTimewindow = options.useDashboardTimewindow;
+
+ if (this.useDashboardTimewindow) {
+ this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
+ } else {
+ this.timeWindowConfig = angular.copy(options.timeWindowConfig);
+ }
+
+ this.subscriptionTimewindow = null;
+
+ this.units = options.units || '';
+ this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2;
+
+ this.loadingData = false;
+
+ if (options.legendConfig) {
+ this.legendConfig = options.legendConfig;
+ this.legendData = {
+ keys: [],
+ data: []
+ };
+ this.displayLegend = true;
+ } else {
+ this.displayLegend = false;
+ }
+ this.caulculateLegendData = this.displayLegend &&
+ this.type === this.ctx.types.widgetType.timeseries.value &&
+ (this.legendConfig.showMin === true ||
+ this.legendConfig.showMax === true ||
+ this.legendConfig.showAvg === true ||
+ this.legendConfig.showTotal === true);
+ this.initDataSubscription();
+ }
+ }
+
+ initDataSubscription() {
+ var dataIndex = 0;
+ for (var i = 0; i < this.datasources.length; i++) {
+ var datasource = this.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: []
+ };
+ this.data.push(datasourceData);
+ this.hiddenData.push({data: []});
+ if (this.displayLegend) {
+ var legendKey = {
+ dataKey: dataKey,
+ dataIndex: dataIndex++
+ };
+ this.legendData.keys.push(legendKey);
+ var legendKeyData = {
+ min: null,
+ max: null,
+ avg: null,
+ total: null,
+ hidden: false
+ };
+ this.legendData.data.push(legendKeyData);
+ }
+ }
+ }
+
+ var subscription = this;
+ var registration;
+
+ if (this.displayLegend) {
+ this.legendData.keys = this.ctx.$filter('orderBy')(this.legendData.keys, '+label');
+ registration = this.ctx.$scope.$watch(
+ function() {
+ return subscription.legendData.data;
+ },
+ function (newValue, oldValue) {
+ for(var i = 0; i < newValue.length; i++) {
+ if(newValue[i].hidden != oldValue[i].hidden) {
+ subscription.updateDataVisibility(i);
+ }
+ }
+ }, true);
+ this.registrations.push(registration);
+ }
+
+ if (this.type === this.ctx.types.widgetType.timeseries.value) {
+ if (this.useDashboardTimewindow) {
+ registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
+ if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
+ subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
+ subscription.unsubscribe();
+ subscription.subscribe();
+ }
+ });
+ this.registrations.push(registration);
+ } else {
+ registration = this.ctx.$scope.$watch(function () {
+ return subscription.timeWindowConfig;
+ }, function (newTimewindow, prevTimewindow) {
+ if (!angular.equals(newTimewindow, prevTimewindow)) {
+ subscription.unsubscribe();
+ subscription.subscribe();
+ }
+ });
+ this.registrations.push(registration);
+ }
+ }
+
+ registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
+ subscription.checkSubscriptions();
+ });
+
+ this.registrations.push(registration);
+ }
+
+ initRpc() {
+
+ if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
+ this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
+ if (this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId]) {
+ this.targetDeviceId = this.ctx.aliasesInfo.deviceAliases[this.targetDeviceAliasId].deviceId;
+ }
+ var subscription = this;
+ var registration = this.ctx.$scope.$on('deviceAliasListChanged', function () {
+ var deviceId = null;
+ if (subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId]) {
+ deviceId = subscription.ctx.aliasesInfo.deviceAliases[subscription.targetDeviceAliasId].deviceId;
+ }
+ if (!angular.equals(deviceId, subscription.targetDeviceId)) {
+ subscription.targetDeviceId = deviceId;
+ if (subscription.targetDeviceId) {
+ subscription.rpcEnabled = true;
+ } else {
+ subscription.rpcEnabled = subscription.ctx.$scope.widgetEditMode ? true : false;
+ }
+ subscription.callbacks.rpcStateChanged(subscription);
+ }
+ });
+ this.registrations.push(registration);
+ } else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
+ this.targetDeviceId = this.targetDeviceIds[0];
+ }
+
+ if (this.targetDeviceId) {
+ this.rpcEnabled = true;
+ } else {
+ this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
+ }
+ this.callbacks.rpcStateChanged(this);
+ }
+
+ clearRpcError() {
+ this.rpcRejection = null;
+ this.rpcErrorText = null;
+ this.callbacks.onRpcErrorCleared(this);
+ }
+
+ sendOneWayCommand(method, params, timeout) {
+ return this.sendCommand(true, method, params, timeout);
+ }
+
+ sendTwoWayCommand(method, params, timeout) {
+ return this.sendCommand(false, method, params, timeout);
+ }
+
+ sendCommand(oneWayElseTwoWay, method, params, timeout) {
+ if (!this.rpcEnabled) {
+ return this.ctx.$q.reject();
+ }
+
+ if (this.rpcRejection && this.rpcRejection.status !== 408) {
+ this.rpcRejection = null;
+ this.rpcErrorText = null;
+ this.callbacks.onRpcErrorCleared(this);
+ }
+
+ var subscription = this;
+
+ var requestBody = {
+ method: method,
+ params: params
+ };
+
+ if (timeout && timeout > 0) {
+ requestBody.timeout = timeout;
+ }
+
+ var deferred = this.ctx.$q.defer();
+ this.executingRpcRequest = true;
+ this.callbacks.rpcStateChanged(this);
+ if (this.ctx.$scope.widgetEditMode) {
+ this.ctx.$timeout(function() {
+ subscription.executingRpcRequest = false;
+ subscription.callbacks.rpcStateChanged(subscription);
+ if (oneWayElseTwoWay) {
+ deferred.resolve();
+ } else {
+ deferred.resolve(requestBody);
+ }
+ }, 500);
+ } else {
+ this.executingPromises.push(deferred.promise);
+ var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand;
+ targetSendFunction(this.targetDeviceId, requestBody).then(
+ function success(responseBody) {
+ subscription.rpcRejection = null;
+ subscription.rpcErrorText = null;
+ var index = subscription.executingPromises.indexOf(deferred.promise);
+ if (index >= 0) {
+ subscription.executingPromises.splice( index, 1 );
+ }
+ subscription.executingRpcRequest = subscription.executingPromises.length > 0;
+ subscription.callbacks.onRpcSuccess(subscription);
+ deferred.resolve(responseBody);
+ },
+ function fail(rejection) {
+ var index = subscription.executingPromises.indexOf(deferred.promise);
+ if (index >= 0) {
+ subscription.executingPromises.splice( index, 1 );
+ }
+ subscription.executingRpcRequest = subscription.executingPromises.length > 0;
+ subscription.callbacks.rpcStateChanged(subscription);
+ if (!subscription.executingRpcRequest || rejection.status === 408) {
+ subscription.rpcRejection = rejection;
+ if (rejection.status === 408) {
+ subscription.rpcErrorText = 'Device is offline.';
+ } else {
+ subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
+ if (rejection.data && rejection.data.length > 0) {
+ subscription.rpcErrorText += '</br>';
+ 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<data.length;i++) {
+ result = Math.min(result, Number(data[i][1]));
+ }
+ return result;
+ } else {
+ return null;
+ }
+}
+
+function calculateMax(data) {
+ if (data.length > 0) {
+ var result = Number(data[0][1]);
+ for (var i=1;i<data.length;i++) {
+ result = Math.max(result, Number(data[i][1]));
+ }
+ return result;
+ } else {
+ return null;
+ }
+}
+
+function calculateAvg(data) {
+ if (data.length > 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;
+ }
+}
ui/src/app/api/user.service.js 166(+127 -39)
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index ad401df..4dc41f7 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<dashboards.length;d++) {
+ allowedDashboardIds.push(dashboards[d].id.id);
+ }
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ }
+
+ function procceedJwtTokenValidate() {
validateJwtToken(doTokenRefresh).then(function success() {
var jwtToken = store.get('jwt_token');
currentUser = jwtHelper.decodeToken(jwtToken);
@@ -228,29 +285,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
} else if (currentUser) {
currentUser.authority = "ANONYMOUS";
}
- if (currentUser.userId) {
+ if (currentUser.isPublic) {
+ $rootScope.forceFullscreen = true;
+ fetchAllowedDashboardIds();
+ } else if (currentUser.userId) {
getUser(currentUser.userId).then(
function success(user) {
currentUserDetails = user;
$rootScope.forceFullscreen = false;
- if (currentUserDetails.additionalInfo &&
- currentUserDetails.additionalInfo.defaultDashboardFullscreen) {
- $rootScope.forceFullscreen = currentUserDetails.additionalInfo.defaultDashboardFullscreen === true;
+ if (userForceFullscreen()) {
+ $rootScope.forceFullscreen = true;
}
if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
- var pageLink = {limit: 100};
- dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
- function success(result) {
- var dashboards = result.data;
- for (var d=0;d<dashboards.length;d++) {
- allowedDashboardIds.push(dashboards[d].id.id);
- }
- deferred.resolve();
- },
- function fail() {
- deferred.reject();
- }
- );
+ fetchAllowedDashboardIds();
} else {
deferred.resolve();
}
@@ -265,6 +312,23 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
}, function fail() {
deferred.reject();
});
+ }
+
+ if (!currentUser) {
+ var locationSearch = $location.search();
+ if (locationSearch.publicId) {
+ loginService.publicLogin(locationSearch.publicId).then(function success(response) {
+ var token = response.data.token;
+ var refreshToken = response.data.refreshToken;
+ updateAndValidateToken(token, 'jwt_token', false);
+ updateAndValidateToken(refreshToken, 'refresh_token', false);
+ procceedJwtTokenValidate();
+ }, function fail() {
+ deferred.reject();
+ });
+ } else {
+ procceedJwtTokenValidate();
+ }
} else {
deferred.resolve();
}
@@ -373,17 +437,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) {
if (currentUser.authority === 'CUSTOMER_USER') {
- if (currentUserDetails &&
- currentUserDetails.additionalInfo &&
- currentUserDetails.additionalInfo.defaultDashboardId) {
- if ($rootScope.forceFullscreen) {
- if (to.name === 'home.profile') {
- return false;
- } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -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;
+ }
+ }
+
}
ui/src/app/api/widget.service.js 16(+14 -2)
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index fb8565c..21c38e2 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);
ui/src/app/app.run.js 50(+41 -9)
diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js
index 02fe8fd..75b16fd 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;
ui/src/app/common/utils.service.js 155(+153 -2)
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index 3fc5202..64fb6d8 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<keyInfos.length;i++) {
+ var keyInfo = keyInfos[i];
+ var dataKey = createKey(keyInfo, type, datasources);
+ datasource.dataKeys.push(dataKey);
+ }
+ }
+
+ function createDatasourceFromSubscription(subscriptionInfo, datasources, device) {
+ var datasource;
+ if (subscriptionInfo.type === types.datasourceType.device) {
+ datasource = {
+ type: subscriptionInfo.type,
+ deviceName: device.name,
+ deviceId: device.id.id,
+ dataKeys: []
+ }
+ } else if (subscriptionInfo.type === types.datasourceType.function) {
+ datasource = {
+ type: subscriptionInfo.type,
+ name: subscriptionInfo.name || types.datasourceType.function,
+ dataKeys: []
+ }
+ }
+ datasources.push(datasource);
+ if (subscriptionInfo.timeseries) {
+ createDatasourceKeys(subscriptionInfo.timeseries, types.dataKeyType.timeseries, datasource, datasources);
+ }
+ if (subscriptionInfo.attributes) {
+ createDatasourceKeys(subscriptionInfo.attributes, types.dataKeyType.attribute, datasource, datasources);
+ }
+ if (subscriptionInfo.functions) {
+ createDatasourceKeys(subscriptionInfo.functions, types.dataKeyType.function, datasource, datasources);
+ }
+ }
+
+ function processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred) {
+ if (index < subscriptionsInfo.length) {
+ var subscriptionInfo = subscriptionsInfo[index];
+ if (subscriptionInfo.type === types.datasourceType.device) {
+ if (subscriptionInfo.deviceId) {
+ deviceService.getDevice(subscriptionInfo.deviceId, true, {ignoreLoading: true}).then(
+ function success(device) {
+ createDatasourceFromSubscription(subscriptionInfo, datasources, device);
+ index++;
+ processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+ },
+ function fail() {
+ index++;
+ processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+ }
+ );
+ } else if (subscriptionInfo.deviceName || subscriptionInfo.deviceNamePrefix
+ || subscriptionInfo.deviceIds) {
+ var promise;
+ if (subscriptionInfo.deviceName) {
+ promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceName, 1, false, {ignoreLoading: true});
+ } else if (subscriptionInfo.deviceNamePrefix) {
+ promise = deviceService.fetchAliasDeviceByNameFilter(subscriptionInfo.deviceNamePrefix, 100, false, {ignoreLoading: true});
+ } else if (subscriptionInfo.deviceIds) {
+ promise = deviceService.getDevices(subscriptionInfo.deviceIds, {ignoreLoading: true});
+ }
+ promise.then(
+ function success(devices) {
+ if (devices && devices.length > 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 7c7a552..131616b 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 ecf12cd..0233c95 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();
ui/src/app/components/grid.tpl.html 4(+2 -2)
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
index 1cf0085..c29c2e5 100644
--- a/ui/src/app/components/grid.tpl.html
+++ b/ui/src/app/components/grid.tpl.html
@@ -49,7 +49,7 @@
<md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
<md-tooltip md-direction="top">
- {{ action.details() }}
+ {{ action.details( rowItem[n] ) }}
</md-tooltip>
<ng-md-icon icon="{{action.icon}}"></ng-md-icon>
</md-button>
@@ -62,7 +62,7 @@
</div>
<tb-details-sidenav
header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}"
- header-subtitle="{{vm.itemDetailsText()}}"
+ header-subtitle="{{vm.itemDetailsText(vm.operatingItem())}}"
is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
is-open="vm.detailsConfig.isDetailsOpen"
is-edit="vm.detailsConfig.isDetailsEditMode"
diff --git a/ui/src/app/components/legend.directive.js b/ui/src/app/components/legend.directive.js
index fba58e4..196629c 100644
--- a/ui/src/app/components/legend.directive.js
+++ b/ui/src/app/components/legend.directive.js
@@ -44,15 +44,8 @@ function Legend($compile, $templateCache, types) {
scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
scope.legendConfig.position === types.position.top.value;
- scope.$on('legendDataUpdated', function (event, apply) {
- if (apply) {
- scope.$digest();
- }
- });
-
scope.toggleHideData = function(index) {
scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
- scope.$emit('legendDataHiddenChanged', index);
}
$compile(element.contents())(scope);
ui/src/app/components/widget.controller.js 902(+313 -589)
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index bf500b1..7c7caa9 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 += '</br>';
- $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<data.length;i++) {
- result = Math.min(result, Number(data[i][1]));
- }
- return result;
- } else {
- return null;
- }
- }
-
- function calculateMax(data) {
- if (data.length > 0) {
- var result = Number(data[0][1]);
- for (var i=1;i<data.length;i++) {
- result = Math.max(result, Number(data[i][1]));
- }
- return result;
- } else {
- return null;
- }
- }
-
- function calculateAvg(data) {
- if (data.length > 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<devicesInfo.length;d++) {
- if (devicesInfo[d].id === deviceId) {
- deviceName = devicesInfo[d].name;
- break;
- }
- }
- for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
- updateDataKeyLabel(datasource.dataKeys[dk], deviceName, aliasName);
- }
- }
- } else {
- datasource.name = types.datasourceType.function;
- }
- var listener = {
- widget: widget,
- subscriptionTimewindow: subscriptionTimewindow,
- datasource: datasource,
- deviceId: deviceId,
- dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
- dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
- },
- updateRealtimeSubscription: function() {
- this.subscriptionTimewindow = updateRealtimeSubscription();
- return this.subscriptionTimewindow;
- },
- setRealtimeSubscription: function(subscriptionTimewindow) {
- updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
- },
- datasourceIndex: index
- };
-
- for (var a = 0; a < datasource.dataKeys.length; a++) {
- widgetContext.data[index + a].data = [];
- }
-
- index += datasource.dataKeys.length;
-
- datasourceListeners.push(listener);
- datasourceService.subscribeToDatasource(listener);
- }
- } else {
- notifyDataLoaded();
- }
- }
-
}
/* eslint-enable angular/angularelement */
\ No newline at end of file
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index 61a5d14..bcffa72 100644
--- a/ui/src/app/components/widget-config.directive.js
+++ b/ui/src/app/components/widget-config.directive.js
@@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.forceExpandDatasources = false;
}
+ if (angular.isUndefined(scope.isDataEnabled)) {
+ scope.isDataEnabled = true;
+ }
+
scope.currentSettingsSchema = {};
scope.currentSettings = angular.copy(scope.emptySettingsSchema);
@@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ?
ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
scope.legendConfig = ngModelCtrl.$viewValue.legendConfig;
- if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) {
+ if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
+ && scope.isDataEnabled) {
if (scope.datasources) {
scope.datasources.splice(0, scope.datasources.length);
} else {
@@ -119,7 +124,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
}
}
- } else if (scope.widgetType === types.widgetType.rpc.value) {
+ } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
if (scope.deviceAliases[aliasId]) {
@@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var valid;
- if (scope.widgetType === types.widgetType.rpc.value) {
+ if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
- } else if (scope.widgetType !== types.widgetType.static.value) {
+ } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
valid = value && value.datasources && value.datasources.length > 0;
ngModelCtrl.$setValidity('datasources', valid);
}
@@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
scope.$watch('datasources', function () {
if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value
- && scope.widgetType !== types.widgetType.static.value) {
+ && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue;
if (value.datasources) {
value.datasources.splice(0, value.datasources.length);
@@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
}, true);
scope.$watch('targetDeviceAlias.value', function () {
- if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value) {
+ if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
var value = ngModelCtrl.$viewValue;
if (scope.targetDeviceAlias.value) {
value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
@@ -359,6 +364,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
require: "^ngModel",
scope: {
forceExpandDatasources: '=?',
+ isDataEnabled: '=?',
widgetType: '=',
widgetSettingsSchema: '=',
datakeySettingsSchema: '=',
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index 66b440f..e7c03e3 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -31,7 +31,7 @@
</section>
</div>
<v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
- ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value">
+ ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled">
<v-pane id="datasources-pane" expanded="true">
<v-pane-header>
{{ 'widget-config.datasources' | translate }}
@@ -96,7 +96,7 @@
</v-pane>
</v-accordion>
<v-accordion id="target-devices-accordion" control="targetDevicesAccordion" class="vAccordion--default"
- ng-show="widgetType === types.widgetType.rpc.value">
+ ng-show="widgetType === types.widgetType.rpc.value && isDataEnabled">
<v-pane id="target-devices-pane" expanded="true">
<v-pane-header>
{{ 'widget-config.target-device' | translate }}
ui/src/app/customer/customer.controller.js 40(+35 -5)
diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js
index 5738660..4182588 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) {
ui/src/app/customer/customer.directive.js 14(+14 -0)
diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js
index f23711a..4a4e09c 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/customer/customer-card.tpl.html b/ui/src/app/customer/customer-card.tpl.html
index 8c96313..e15ed9d 100644
--- a/ui/src/app/customer/customer-card.tpl.html
+++ b/ui/src/app/customer/customer-card.tpl.html
@@ -15,4 +15,4 @@
limitations under the License.
-->
-<div class="tb-uppercase">{{ item | contactShort }}</div>
\ No newline at end of file
+<div ng-show="item && (!item.additionalInfo || !item.additionalInfo.isPublic)" class="tb-uppercase">{{ item | contactShort }}</div>
\ 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 62ef6b3..a216571 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.
-->
-<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
+<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
<md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button>
<md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button>
-<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
+<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
<md-content class="md-padding" layout="column">
- <fieldset ng-disabled="loading || !isEdit">
+ <fieldset ng-show="!isPublic" ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
<label translate>customer.title</label>
<input required name="title" ng-model="customer.title">
ui/src/app/dashboard/dashboard.controller.js 57(+38 -19)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index fbb3e37..d100522 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;
+ });
+ }
}
);
}
ui/src/app/dashboard/dashboard.directive.js 19(+17 -2)
diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js
index 2e9f55a..b2f3117 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 2c400ea..e9fe1f2 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 ad9e52b..7e314b8 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">
</md-button>
- <tb-user-menu ng-show="forceFullscreen" display-user-info="true">
+ <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
</tb-user-menu>
<md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
ng-click="vm.exportDashboard($event)">
diff --git a/ui/src/app/dashboard/dashboard-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html
index 9f6cd00..6367867 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.
-->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</div>
+<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+<div class="tb-small" ng-show="vm.isPublic()">{{'dashboard.public' | translate}}</div>
diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
index 6325e17..1d11fb2 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.
-->
+<md-button ng-click="onExportDashboard({event: $event})"
+ ng-show="!isEdit && dashboardScope === 'tenant'"
+ class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
+<md-button ng-click="onMakePublic({event: $event})"
+ ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer && !isPublic"
+ class="md-raised md-primary">{{ 'dashboard.make-public' | translate }}</md-button>
<md-button ng-click="onAssignToCustomer({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer"
class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
-<md-button ng-click="onUnassignFromCustomer({event: $event})"
+<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer"
- class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button>
-<md-button ng-click="onExportDashboard({event: $event})"
- ng-show="!isEdit && dashboardScope === 'tenant'"
- class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
+ class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }}</md-button>
<md-button ng-click="onDeleteDashboard({event: $event})"
ng-show="!isEdit && dashboardScope === 'tenant'"
class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
-
<md-content class="md-padding" layout="column">
<md-input-container class="md-block"
- ng-show="isAssignedToCustomer && dashboardScope === 'tenant'">
+ ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'">
<label translate>dashboard.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled>
</md-input-container>
+ <div layout="row" ng-show="!isEdit && isPublic && (dashboardScope === 'customer' || dashboardScope === 'tenant')">
+ <md-input-container class="md-block" flex>
+ <label translate>dashboard.public-link</label>
+ <input ng-model="publicLink" disabled>
+ </md-input-container>
+ <md-button class="md-icon-button" style="margin-top: 14px;"
+ ngclipboard
+ data-clipboard-text="{{ publicLink }}"
+ ngclipboard-success="onPublicLinkCopied(e)">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.copy-public-link' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
<fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
<label translate>dashboard.title</label>
ui/src/app/dashboard/dashboards.controller.js 153(+126 -27)
diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js
index 3ef96a7..d0fa958 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 bae57b9..dde2f86 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)"></tb-dashboard-details>
</tb-grid>
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 1c2e461..ced39f5 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 7aa6339..1aaf861 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -18,6 +18,7 @@
<fieldset ng-disabled="loading">
<tb-widget-config widget-type="widget.type"
ng-model="widgetConfig"
+ is-data-enabled="isDataEnabled"
widget-settings-schema="settingsSchema"
datakey-settings-schema="dataKeySettingsSchema"
device-aliases="aliasesInfo.deviceAliases"
ui/src/app/dashboard/index.js 3(+2 -1)
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index dc74c76..4587001 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -35,7 +35,7 @@ import thingsboardItemBuffer from '../services/item-buffer.service';
import thingsboardImportExport from '../import-export';
import DashboardRoutes from './dashboard.routes';
-import {DashboardsController, DashboardCardController} from './dashboards.controller';
+import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
import DashboardController from './dashboard.controller';
import DeviceAliasesController from './device-aliases.controller';
import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
@@ -69,6 +69,7 @@ export default angular.module('thingsboard.dashboard', [
.config(DashboardRoutes)
.controller('DashboardsController', DashboardsController)
.controller('DashboardCardController', DashboardCardController)
+ .controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController)
.controller('DashboardController', DashboardController)
.controller('DeviceAliasesController', DeviceAliasesController)
.controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)
diff --git a/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html b/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html
new file mode 100644
index 0000000..31f49fb
--- /dev/null
+++ b/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html
@@ -0,0 +1,56 @@
+<!--
+
+ 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.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.make-public' | translate }}" style="min-width: 400px;">
+ <form>
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate="dashboard.public-dashboard-title"></h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.close()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-dialog-content>
+ <div id="make-dialog-public-content" class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <span translate="dashboard.public-dashboard-text" translate-values="{dashboardTitle: vm.dashboard.title, publicLink: vm.publicLink}"></span>
+ <div layout="row" layout-align="start center">
+ <pre class="tb-highlight" flex><code>{{ vm.publicLink }}</code></pre>
+ <md-button class="md-icon-button"
+ ngclipboard
+ data-clipboard-text="{{ vm.publicLink }}"
+ ngclipboard-success="vm.onPublicLinkCopied(e)">
+ <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.copy-public-link' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ <div class="tb-notice" translate>dashboard.public-dashboard-notice</div>
+ </md-content>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-click="vm.close()">{{ 'action.ok' |
+ translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
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 9fd0cec..c54daab 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;
ui/src/app/device/device.controller.js 125(+101 -24)
diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js
index 4de12bb..ed9ff6f 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,16 +256,33 @@ 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) {
manageCredentials($event, item);
},
name: function() { return $translate.instant('device.credentials') },
@@ -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 9c927ae..eb50a15 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: '&'
ui/src/app/device/device.routes.js 2(+1 -1)
diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js
index d0293b4..c1f5b27 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/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index 1c667f3..bb84af7 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.
-->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</div>
+<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
ui/src/app/device/device-fieldset.tpl.html 13(+10 -3)
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index 99cedbd..fd6c9d1 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.
-->
+<md-button ng-click="onMakePublic({event: $event})"
+ ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer && !isPublic"
+ class="md-raised md-primary">{{ 'device.make-public' | translate }}</md-button>
<md-button ng-click="onAssignToCustomer({event: $event})"
ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer"
class="md-raised md-primary">{{ 'device.assign-to-customer' | translate }}</md-button>
-<md-button ng-click="onUnassignFromCustomer({event: $event})"
+<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
ng-show="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer"
- class="md-raised md-primary">{{ 'device.unassign-from-customer' | translate }}</md-button>
+ class="md-raised md-primary">{{ isPublic ? 'device.make-private' : 'device.unassign-from-customer' | translate }}</md-button>
<md-button ng-click="onManageCredentials({event: $event})"
ng-show="!isEdit"
class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button>
@@ -47,10 +50,14 @@
<md-content class="md-padding" layout="column">
<md-input-container class="md-block"
- ng-show="isAssignedToCustomer && deviceScope === 'tenant'">
+ ng-show="!isEdit && isAssignedToCustomer && !isPublic && deviceScope === 'tenant'">
<label translate>device.assignedToCustomer</label>
<input ng-model="assignedCustomer.title" disabled>
</md-input-container>
+ <div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
+ ng-show="!isEdit && isPublic && (deviceScope === 'customer' || deviceScope === 'tenant')">
+ {{ 'device.device-public' | translate }}
+ </div>
<fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
<label translate>device.name</label>
ui/src/app/device/devices.tpl.html 3(+2 -1)
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 52c05f2..90909e3 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)"></tb-device>
</md-tab>
ui/src/app/locale/locale.constant.js 30(+28 -2)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 3de229e..37a2b93 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 <b>{{dashboardTitle}}</b> is now public and accessible via next public <a href='{{publicLink}}' target='_blank'>link</a>:",
+ "public-dashboard-notice": "<b>Note:</b> 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.<br/>" +
"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):<br/>{{widgetsList}}",
- "is-gateway": "Is gateway"
+ "is-gateway": "Is gateway",
+ "public": "Public",
+ "device-public": "Device is public"
},
"dialog": {
"close": "Close dialog"
ui/src/app/widget/lib/flot-widget.js 231(+126 -105)
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js
index 4cc2192..be61de7 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 = $("<div id='flot-series-tooltip' class='flot-mouse-value'></div>");
@@ -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();
}
}
ui/src/app/widget/lib/google-map.js 15(+14 -1)
diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js
index 9722839..caac144 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) {
ui/src/app/widget/lib/map-widget.js 325(+224 -101)
diff --git a/ui/src/app/widget/lib/map-widget.js b/ui/src/app/widget/lib/map-widget.js
index be7b118..c39a142 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<datasources.length;i++) {
+ var datasource = datasources[i];
+ for (var k = 0; k < datasource.dataKeys.length; k++) {
+ var dataKey = datasource.dataKeys[k];
+ if (dataKey.label === label) {
+ variableInfo.dataKeyIndex = offset + k;
+ break;
+ }
+ }
+ offset += datasource.dataKeys.length;
+ }
+ }
+ replaceInfo.variables.push(variableInfo);
+ match = tbMap.varsRegex.exec(pattern);
+ }
+ return replaceInfo;
+}
+
+
export default class TbMapWidget {
- constructor(mapProvider, drawRoutes, ctx) {
+ constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
var tbMap = this;
this.ctx = ctx;
+ if (!$element) {
+ $element = ctx.$container;
+ }
this.drawRoutes = drawRoutes;
this.markers = [];
@@ -35,6 +84,9 @@ export default class TbMapWidget {
var settings = ctx.settings;
+ this.callbacks = {};
+ this.callbacks.onLocationClick = function(){};
+
if (settings.defaultZoomLevel) {
if (settings.defaultZoomLevel > 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<ctx.datasources.length;i++) {
- var datasource = ctx.datasources[i];
- for (var k = 0; k < datasource.dataKeys.length; k++) {
- var dataKey = datasource.dataKeys[k];
- if (dataKey.label === label) {
- variableInfo.dataKeyIndex = offset + k;
- break;
- }
+ });
+ this.locations = null;
+ this.markers = [];
+ if (this.drawRoutes) {
+ this.polylines = [];
+ }
+ }
+ }
+
+ configureLocationsFromSubscription(subscription, subscriptionLocationSettings) {
+ this.subscription = subscription;
+ this.clearLocations();
+ this.locationsSettings = [];
+ var latKeyName = subscriptionLocationSettings.latKeyName;
+ var lngKeyName = subscriptionLocationSettings.lngKeyName;
+ var index = 0;
+ for (var i=0;i<subscription.datasources.length;i++) {
+ var datasource = subscription.datasources[i];
+ var dataKeys = datasource.dataKeys;
+ var latKeyIndex = -1;
+ var lngKeyIndex = -1;
+ var localLatKeyName = latKeyName;
+ var localLngKeyName = lngKeyName;
+ for (var k=0;k<dataKeys.length;k++) {
+ var dataKey = dataKeys[k];
+ if (dataKey.name === latKeyName) {
+ latKeyIndex = index;
+ localLatKeyName = localLatKeyName + index;
+ dataKey.locationAttrName = localLatKeyName;
+ } else if (dataKey.name === lngKeyName) {
+ lngKeyIndex = index;
+ localLngKeyName = localLngKeyName + index;
+ dataKey.locationAttrName = localLngKeyName;
+ }
+ if (latKeyIndex > -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: "<b>Latitude:</b> ${#"+latKeyIndex+":7}<br/><b>Longitude:</b> ${#"+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: "<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${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 || "<b>Latitude:</b> ${"+this.locationsSettings[i].latKeyName+":7}<br/><b>Longitude:</b> ${"+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<datasources.length;d++) {
+ dsDataMap[d] = {};
+ }
for (var i = 0; i < data.length; i++) {
var dataKey = data[i].dataKey;
var label = dataKey.label;
@@ -356,30 +465,44 @@ export default class TbMapWidget {
val = keyData[keyData.length-1][1];
}
dataMap[label] = val;
+ var dsIndex = datasources.indexOf(data[i].datasource);
+ dsDataMap[dsIndex][label] = val;
}
- return dataMap;
+ return {
+ dataMap: dataMap,
+ dsDataMap: dsDataMap
+ };
}
- function loadLocations(data) {
+ function loadLocations(data, datasources) {
var bounds = tbMap.map.createBounds();
tbMap.locations = [];
- var dataMap = toLabelValueMap(data);
+ var dataMap = toLabelValueMap(data, datasources);
for (var l=0; l < tbMap.locationsSettings.length; l++) {
var locationSettings = tbMap.locationsSettings[l];
var latIndex = -1;
var lngIndex = -1;
for (var i = 0; i < data.length; i++) {
var dataKey = data[i].dataKey;
- if (dataKey.label === locationSettings.latKeyName) {
+ var nameToCheck;
+ if (dataKey.locationAttrName) {
+ nameToCheck = dataKey.locationAttrName;
+ } else {
+ nameToCheck = dataKey.label;
+ }
+ if (nameToCheck === locationSettings.latKeyName) {
latIndex = i;
- } else if (dataKey.label === locationSettings.lngKeyName) {
+ } else if (nameToCheck === locationSettings.lngKeyName) {
lngIndex = i;
}
}
if (latIndex > -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);
}
}
}
ui/src/app/widget/lib/openstreet-map.js 14(+13 -1)
diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js
index aacb505..77332d5 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) {
ui/src/app/widget/lib/timeseries-table-widget.js 325(+325 -0)
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 0000000..c55c519
--- /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 0000000..99a0653
--- /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 0000000..2b6d72c
--- /dev/null
+++ b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html
@@ -0,0 +1,43 @@
+<!--
+
+ 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.
+
+-->
+
+<md-tabs md-selected="vm.sourceIndex" ng-class="{'tb-headless': vm.sources.length === 1}"
+ id="tabs" md-border-bottom flex class="tb-absolute-fill">
+ <md-tab ng-repeat="source in vm.sources" label="{{ source.datasource.name }}">
+ <md-table-container>
+ <table md-table>
+ <thead md-head md-order="source.query.order" md-on-reorder="vm.onReorder(source)">
+ <tr md-row>
+ <th ng-show="vm.showTimestamp" md-column md-order-by="0"><span>Timestamp</span></th>
+ <th md-column md-order-by="{{ h.index }}" ng-repeat="h in source.ts.header"><span>{{ h.dataKey.label }}</span></th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr md-row ng-repeat="row in source.ts.data">
+ <td ng-show="$index > 0 || ($index === 0 && vm.showTimestamp)" md-cell ng-repeat="d in row track by $index" ng-style="vm.cellStyle(source, $index, d)" ng-bind-html="vm.cellContent(source, $index, row, d)">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </md-table-container>
+ <md-table-pagination md-limit="source.query.limit" md-limit-options="[5, 10, 15]"
+ md-page="source.query.page" md-total="{{source.ts.count}}"
+ md-on-paginate="vm.onPaginate(source)" md-page-select>
+ </md-table-pagination>
+ </md-tab>
+</md-tabs>
\ No newline at end of file
ui/src/scss/main.scss 26(+26 -0)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 9f89aba..ab9b0d7 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
***********************/