Browse Source

Devices page implementation

pull/1954/head
Igor Kulikov 7 years ago
parent
commit
65939722f3
  1. 23
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  2. 69
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  3. 13
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  4. 40
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java
  5. 50
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  6. 50
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  7. 122
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java
  8. 64
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
  9. 58
      dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java
  10. 50
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
  11. 51
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
  12. 2
      dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java
  13. 39
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java
  14. 74
      ui-ngx/src/app/core/http/device.service.ts
  15. 27
      ui-ngx/src/app/core/services/broadcast.models.ts
  16. 51
      ui-ngx/src/app/core/services/broadcast.service.ts
  17. 50
      ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts
  18. 23
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html
  19. 36
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss
  20. 42
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts
  21. 107
      ui-ngx/src/app/modules/home/pages/device/device.component.html
  22. 19
      ui-ngx/src/app/modules/home/pages/device/device.component.scss
  23. 88
      ui-ngx/src/app/modules/home/pages/device/device.component.ts
  24. 39
      ui-ngx/src/app/modules/home/pages/device/device.module.ts
  25. 196
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  26. 2
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  27. 47
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
  28. 7
      ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts
  29. 2
      ui-ngx/src/app/shared/components/entity/entities-table.component.html
  30. 2
      ui-ngx/src/app/shared/components/entity/entities-table.component.ts
  31. 39
      ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html
  32. 228
      ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts
  33. 28
      ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html
  34. 20
      ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.scss
  35. 238
      ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts
  36. 3
      ui-ngx/src/app/shared/models/constants.ts
  37. 34
      ui-ngx/src/app/shared/models/device.models.ts
  38. 28
      ui-ngx/src/app/shared/models/entity-type.models.ts
  39. 26
      ui-ngx/src/app/shared/models/id/device-id.ts
  40. 6
      ui-ngx/src/app/shared/shared.module.ts
  41. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  42. 7
      ui-ngx/src/styles.scss
  43. 6
      ui-ngx/src/theme.scss

23
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -28,16 +28,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.thingsboard.server.actors.service.ActorService;
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.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmId;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
@ -381,6 +372,18 @@ public abstract class BaseController {
}
}
DeviceInfo checkDeviceInfoId(DeviceId deviceId, Operation operation) throws ThingsboardException {
try {
validateId(deviceId, "Incorrect deviceId " + deviceId);
DeviceInfo device = deviceService.findDeviceInfoById(getCurrentUser().getTenantId(), deviceId);
checkNotNull(device);
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device);
return device;
} catch (Exception e) {
throw handleException(e, false);
}
}
protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException {
try {
validateId(entityViewId, "Incorrect entityViewId " + entityViewId);

69
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -30,11 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -79,6 +75,19 @@ public class DeviceController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device/info/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public DeviceInfo getDeviceInfoById(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
checkParameter(DEVICE_ID, strDeviceId);
try {
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
return checkDeviceInfoId(deviceId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device", method = RequestMethod.POST)
@ResponseBody
@ -287,6 +296,29 @@ public class DeviceController extends BaseController {
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<DeviceInfo> getTenantDeviceInfos(
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String type,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink));
}
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET)
@ResponseBody
@ -327,6 +359,33 @@ public class DeviceController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<DeviceInfo> getCustomerDeviceInfos(
@PathVariable("customerId") String strCustomerId,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String type,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
checkCustomerId(customerId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
}
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
@ResponseBody

13
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
@ -28,7 +29,9 @@ import org.thingsboard.server.common.data.page.PageLink;
import java.util.List;
public interface DeviceService {
DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId);
Device findDeviceById(TenantId tenantId, DeviceId deviceId);
ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId);
@ -45,16 +48,24 @@ public interface DeviceService {
PageData<Device> findDevicesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink);
PageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
void deleteDevicesByTenantId(TenantId tenantId);
PageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);

40
common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2019 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.common.data;
import lombok.Data;
import org.thingsboard.server.common.data.id.DeviceId;
@Data
public class DeviceInfo extends Device {
private String customerTitle;
private boolean customerIsPublic;
public DeviceInfo() {
super();
}
public DeviceInfo(DeviceId deviceId) {
super(deviceId);
}
public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic) {
super(device);
this.customerTitle = customerTitle;
this.customerIsPublic = customerIsPublic;
}
}

50
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -33,6 +34,15 @@ import java.util.UUID;
*/
public interface DeviceDao extends Dao<Device> {
/**
* Find device info by id.
*
* @param tenantId the tenant id
* @param deviceId the device id
* @return the device info object
*/
DeviceInfo findDeviceInfoById(TenantId tenantId, UUID deviceId);
/**
* Save or update device object
*
@ -50,6 +60,15 @@ public interface DeviceDao extends Dao<Device> {
*/
PageData<Device> findDevicesByTenantId(UUID tenantId, PageLink pageLink);
/**
* Find device infos by tenantId and page link.
*
* @param tenantId the tenantId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink);
/**
* Find devices by tenantId, type and page link.
*
@ -60,6 +79,16 @@ public interface DeviceDao extends Dao<Device> {
*/
PageData<Device> findDevicesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, type and page link.
*
* @param tenantId the tenantId
* @param type the type
* @param pageLink the page link
* @return the list of device onfo objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink);
/**
* Find devices by tenantId and devices Ids.
*
@ -79,6 +108,16 @@ public interface DeviceDao extends Dao<Device> {
*/
PageData<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
/**
* Find device infos by tenantId, customerId and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
/**
* Find devices by tenantId, customerId, type and page link.
*
@ -90,6 +129,17 @@ public interface DeviceDao extends Dao<Device> {
*/
PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
/**
* Find device infos by tenantId, customerId, type and page link.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param type the type
* @param pageLink the page link
* @return the list of device info objects
*/
PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
/**
* Find devices by tenantId, customerId and devices Ids.

50
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -27,12 +27,7 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -95,6 +90,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Autowired
private CacheManager cacheManager;
@Override
public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceInfoById [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
return deviceDao.findDeviceInfoById(tenantId, deviceId.getId());
}
@Override
public Device findDeviceById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId);
@ -187,6 +189,14 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantId(tenantId.getId(), pageLink);
}
@Override
public PageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) {
log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
@ -196,6 +206,15 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceDao.findDevicesByTenantIdAndType(tenantId.getId(), type, pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateString(type, "Incorrect type " + type);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndType(tenantId.getId(), type, pageLink);
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
@ -221,6 +240,15 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
}
@Override
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
@ -231,6 +259,16 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
return deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
validateString(type, "Incorrect type " + type);
validatePageLink(pageLink);
return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);

122
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java

@ -0,0 +1,122 @@
/**
* Copyright © 2016-2019 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.dao.model.sql;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.SearchTextEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
@Data
@EqualsAndHashCode(callSuper = true)
@TypeDef(name = "json", typeClass = JsonStringType.class)
@MappedSuperclass
public abstract class AbstractDeviceEntity<T extends Device> extends BaseSqlEntity<T> implements SearchTextEntity<T> {
@Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY)
private String tenantId;
@Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY)
private String customerId;
@Column(name = ModelConstants.DEVICE_TYPE_PROPERTY)
private String type;
@Column(name = ModelConstants.DEVICE_NAME_PROPERTY)
private String name;
@Column(name = ModelConstants.DEVICE_LABEL_PROPERTY)
private String label;
@Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
private String searchText;
@Type(type = "json")
@Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
public AbstractDeviceEntity() {
super();
}
public AbstractDeviceEntity(Device device) {
if (device.getId() != null) {
this.setId(device.getId().getId());
}
if (device.getTenantId() != null) {
this.tenantId = toString(device.getTenantId().getId());
}
if (device.getCustomerId() != null) {
this.customerId = toString(device.getCustomerId().getId());
}
this.name = device.getName();
this.type = device.getType();
this.label = device.getLabel();
this.additionalInfo = device.getAdditionalInfo();
}
public AbstractDeviceEntity(DeviceEntity deviceEntity) {
this.setId(deviceEntity.getId());;
this.tenantId = deviceEntity.getTenantId();
this.customerId = deviceEntity.getCustomerId();
this.type = deviceEntity.getType();
this.name = deviceEntity.getName();
this.label = deviceEntity.getLabel();
this.searchText = deviceEntity.getSearchText();
this.additionalInfo = deviceEntity.getAdditionalInfo();
}
@Override
public String getSearchTextSource() {
return name;
}
@Override
public void setSearchText(String searchText) {
this.searchText = searchText;
}
protected Device toDevice() {
Device device = new Device(new DeviceId(getId()));
device.setCreatedTime(UUIDs.unixTimestamp(getId()));
if (tenantId != null) {
device.setTenantId(new TenantId(toUUID(tenantId)));
}
if (customerId != null) {
device.setCustomerId(new CustomerId(toUUID(customerId)));
}
device.setName(name);
device.setType(type);
device.setLabel(label);
device.setAdditionalInfo(additionalInfo);
return device;
}
}

64
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java

@ -39,74 +39,18 @@ import javax.persistence.Table;
@Entity
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Table(name = ModelConstants.DEVICE_COLUMN_FAMILY_NAME)
public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchTextEntity<Device> {
@Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY)
private String tenantId;
@Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY)
private String customerId;
@Column(name = ModelConstants.DEVICE_TYPE_PROPERTY)
private String type;
@Column(name = ModelConstants.DEVICE_NAME_PROPERTY)
private String name;
@Column(name = ModelConstants.DEVICE_LABEL_PROPERTY)
private String label;
@Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
private String searchText;
@Type(type = "json")
@Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
public final class DeviceEntity extends AbstractDeviceEntity<Device> {
public DeviceEntity() {
super();
}
public DeviceEntity(Device device) {
if (device.getId() != null) {
this.setId(device.getId().getId());
}
if (device.getTenantId() != null) {
this.tenantId = toString(device.getTenantId().getId());
}
if (device.getCustomerId() != null) {
this.customerId = toString(device.getCustomerId().getId());
}
this.name = device.getName();
this.type = device.getType();
this.label = device.getLabel();
this.additionalInfo = device.getAdditionalInfo();
}
@Override
public String getSearchTextSource() {
return name;
}
@Override
public void setSearchText(String searchText) {
this.searchText = searchText;
super(device);
}
@Override
public Device toData() {
Device device = new Device(new DeviceId(getId()));
device.setCreatedTime(UUIDs.unixTimestamp(getId()));
if (tenantId != null) {
device.setTenantId(new TenantId(toUUID(tenantId)));
}
if (customerId != null) {
device.setCustomerId(new CustomerId(toUUID(customerId)));
}
device.setName(name);
device.setType(type);
device.setLabel(label);
device.setAdditionalInfo(additionalInfo);
return device;
return super.toDevice();
}
}
}

58
dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java

@ -0,0 +1,58 @@
/**
* Copyright © 2016-2019 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.dao.model.sql;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.DeviceInfo;
import java.util.HashMap;
import java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
public class DeviceInfoEntity extends AbstractDeviceEntity<DeviceInfo> {
public static final Map<String,String> deviceInfoColumnMap = new HashMap<>();
static {
deviceInfoColumnMap.put("customerTitle", "c.title");
}
private String customerTitle;
private boolean customerIsPublic;
public DeviceInfoEntity() {
super();
}
public DeviceInfoEntity(DeviceEntity deviceEntity,
String customerTitle,
Object customerAdditionalInfo) {
super(deviceEntity);
this.customerTitle = customerTitle;
if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) {
this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean();
} else {
this.customerIsPublic = false;
}
}
@Override
public DeviceInfo toData() {
return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic);
}
}

50
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java

@ -22,6 +22,7 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sql.DeviceEntity;
import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
@ -32,6 +33,11 @@ import java.util.List;
@SqlDao
public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntity, String> {
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"WHERE d.id = :deviceId")
DeviceInfoEntity findDeviceInfoById(@Param("deviceId") String deviceId);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
@ -41,12 +47,32 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerId(@Param("tenantId") String tenantId,
@Param("customerId") String customerId,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
Page<DeviceEntity> findByTenantId(@Param("tenantId") String tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"WHERE d.tenantId = :tenantId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantId(@Param("tenantId") String tenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.type = :type " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
@ -55,6 +81,17 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"WHERE d.tenantId = :tenantId " +
"AND d.type = :type " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndType(@Param("tenantId") String tenantId,
@Param("type") String type,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND d.type = :type " +
@ -65,6 +102,19 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
"FROM DeviceEntity d " +
"LEFT JOIN CustomerEntity c on c.id = d.customerId " +
"WHERE d.tenantId = :tenantId " +
"AND d.customerId = :customerId " +
"AND d.type = :type " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") String tenantId,
@Param("customerId") String customerId,
@Param("type") String type,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId")
List<String> findTenantDeviceTypes(@Param("tenantId") String tenantId);

51
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java

@ -20,16 +20,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.model.sql.DeviceEntity;
import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import org.thingsboard.server.dao.util.SqlDao;
@ -64,6 +62,11 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
return deviceRepository;
}
@Override
public DeviceInfo findDeviceInfoById(TenantId tenantId, UUID deviceId) {
return DaoUtil.getData(deviceRepository.findDeviceInfoById(fromTimeUUID(deviceId)));
}
@Override
public PageData<Device> findDevicesByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.toPageData(
@ -73,6 +76,15 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantId(
fromTimeUUID(tenantId),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(UUIDConverter.fromTimeUUID(tenantId), fromTimeUUIDs(deviceIds))));
@ -88,6 +100,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerId(
fromTimeUUID(tenantId),
fromTimeUUID(customerId),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
return service.submit(() -> DaoUtil.convertDataList(
@ -110,6 +132,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndType(
fromTimeUUID(tenantId),
type,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
@ -121,6 +153,17 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
return DaoUtil.toPageData(
deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndType(
fromTimeUUID(tenantId),
fromTimeUUID(customerId),
type,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
}
@Override
public ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId) {
return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId))));

2
dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java

@ -24,7 +24,7 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
"org.thingsboard.server.dao.service.*ServiceSqlTest"
"org.thingsboard.server.dao.service.*DeviceServiceSqlTest"
})
public class SqlDaoServiceTestSuite {

39
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java

@ -21,10 +21,7 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.*;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -264,7 +261,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
@Test
public void testFindDevicesByTenantIdAndName() {
String title1 = "Device title 1";
List<Device> devicesTitle1 = new ArrayList<>();
List<DeviceInfo> devicesTitle1 = new ArrayList<>();
for (int i=0;i<143;i++) {
Device device = new Device();
device.setTenantId(tenantId);
@ -273,10 +270,10 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
device.setName(name);
device.setType("default");
devicesTitle1.add(deviceService.saveDevice(device));
devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false));
}
String title2 = "Device title 2";
List<Device> devicesTitle2 = new ArrayList<>();
List<DeviceInfo> devicesTitle2 = new ArrayList<>();
for (int i=0;i<175;i++) {
Device device = new Device();
device.setTenantId(tenantId);
@ -285,14 +282,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
device.setName(name);
device.setType("default");
devicesTitle2.add(deviceService.saveDevice(device));
devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false));
}
List<Device> loadedDevicesTitle1 = new ArrayList<>();
List<DeviceInfo> loadedDevicesTitle1 = new ArrayList<>();
PageLink pageLink = new PageLink(15, 0, title1);
PageData<Device> pageData = null;
PageData<DeviceInfo> pageData = null;
do {
pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
loadedDevicesTitle1.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -304,10 +301,10 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
List<Device> loadedDevicesTitle2 = new ArrayList<>();
List<DeviceInfo> loadedDevicesTitle2 = new ArrayList<>();
pageLink = new PageLink(4, 0, title2);
do {
pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
loadedDevicesTitle2.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -324,7 +321,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
}
pageLink = new PageLink(4, 0, title1);
pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
@ -333,7 +330,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
}
pageLink = new PageLink(4, 0, title2);
pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
}
@ -431,21 +428,21 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
customer = customerService.saveCustomer(customer);
CustomerId customerId = customer.getId();
List<Device> devices = new ArrayList<>();
List<DeviceInfo> devices = new ArrayList<>();
for (int i=0;i<278;i++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Device"+i);
device.setType("default");
device = deviceService.saveDevice(device);
devices.add(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId));
devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic()));
}
List<Device> loadedDevices = new ArrayList<>();
List<DeviceInfo> loadedDevices = new ArrayList<>();
PageLink pageLink = new PageLink(23);
PageData<Device> pageData = null;
PageData<DeviceInfo> pageData = null;
do {
pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
loadedDevices.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -460,7 +457,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
deviceService.unassignCustomerDevices(tenantId, customerId);
pageLink = new PageLink(33);
pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertTrue(pageData.getData().isEmpty());

74
ui-ngx/src/app/core/http/device.service.ts

@ -0,0 +1,74 @@
///
/// Copyright © 2016-2019 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 { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils';
import { Observable } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { Tenant } from '@shared/models/tenant.model';
import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models';
import {map} from 'rxjs/operators';
import {DeviceInfo, Device} from '@app/shared/models/device.models';
import {EntitySubtype} from '@app/shared/models/entity-type.models';
@Injectable({
providedIn: 'root'
})
export class DeviceService {
constructor(
private http: HttpClient
) { }
public getTenantDeviceInfos(pageLink: PageLink, type: string = '', ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getCustomerDeviceInfos(customerId: string, pageLink: PageLink, type: string = '', ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&type=${type}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getDevice(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
return this.http.get<Device>(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getDeviceInfo(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DeviceInfo> {
return this.http.get<DeviceInfo>(`/api/device/info/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public saveDevice(device: Device, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
return this.http.post<Device>('/api/device', device, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public deleteDevice(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getDeviceTypes(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntitySubtype>> {
return this.http.get<Array<EntitySubtype>>('/api/device/types', defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
}

27
ui-ngx/src/app/core/services/broadcast.models.ts

@ -0,0 +1,27 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export interface BroadcastMessage {
name: string;
args?: Array<any>;
}
export interface BroadcastEvent {
name: string;
}
export type BroadcastListener = (event: BroadcastEvent, ...args: Array<any>) => void;

51
ui-ngx/src/app/core/services/broadcast.service.ts

@ -0,0 +1,51 @@
///
/// Copyright © 2016-2019 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 {Injectable} from '@angular/core';
import {Subject, Subscription} from 'rxjs';
import {NotificationMessage} from '@core/notification/notification.models';
import {BroadcastEvent, BroadcastListener, BroadcastMessage} from '@core/services/broadcast.models';
import {filter} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class BroadcastService {
private broadcastSubject: Subject<BroadcastMessage> = new Subject();
broadcast(name: string, ...args: Array<any>) {
const message = {
name,
args
} as BroadcastMessage;
this.broadcastSubject.next(message);
}
on(name: string, listener: BroadcastListener): Subscription {
return this.broadcastSubject.asObservable().pipe(
filter((message) => message.name === name)
).subscribe(
(message) => {
const event = {
name: message.name
} as BroadcastEvent;
listener(event, message.args);
}
);
}
}

50
ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts

@ -0,0 +1,50 @@
///
/// Copyright © 2016-2019 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 {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component';
import {Authority} from '@shared/models/authority.enum';
import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver';
const routes: Routes = [
{
path: 'devices',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'device.devices',
devicesType: 'tenant',
breadcrumb: {
label: 'device.devices',
icon: 'devices_other'
}
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
DevicesTableConfigResolver
]
})
export class DeviceRoutingModule { }

23
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html

@ -0,0 +1,23 @@
<!--
Copyright © 2016-2019 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-entity-subtype-select
[showLabel]="true"
[entityType]="entityType.DEVICE"
[ngModel]="entitiesTableConfig.componentsData.deviceType"
(ngModelChange)="deviceTypeChanged($event)">
</tb-entity-subtype-select>

36
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2019 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.
*/
:host {
flex: 1;
display: flex;
justify-content: flex-start;
}
:host ::ng-deep {
tb-entity-subtype-select {
mat-form-field {
font-size: 16px;
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-underline {
bottom: 0;
}
}
}
}

42
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2019 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 { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '@shared/components/entity/entity-table-header.component';
import {DeviceInfo} from '@app/shared/models/device.models';
import {EntityType} from '@shared/models/entity-type.models';
@Component({
selector: 'tb-device-table-header',
templateUrl: './device-table-header.component.html',
styleUrls: ['./device-table-header.component.scss']
})
export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<DeviceInfo> {
entityType = EntityType;
constructor(protected store: Store<AppState>) {
super(store);
}
deviceTypeChanged(deviceType: string) {
this.entitiesTableConfig.componentsData.deviceType = deviceType;
this.entitiesTableConfig.table.updateData();
}
}

107
ui-ngx/src/app/modules/home/pages/device/device.component.html

@ -0,0 +1,107 @@
<!--
Copyright © 2016-2019 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.
-->
<div class="tb-details-buttons">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')"
[fxShow]="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer(entity) && !entity?.customerIsPublic">
{{'device.make-public' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'assignToCustomer')"
[fxShow]="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer(entity)">
{{'device.assign-to-customer' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'unassignFromCustomer')"
[fxShow]="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer(entity)">
{{ (entity?.customerIsPublic ? 'device.make-private' : 'device.unassign-from-customer') | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'manageCredentials')"
[fxShow]="!isEdit">
{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'device.delete' | translate }}
</button>
<div fxLayout="row">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onDeviceIdCopied($event)"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>device.copyId</span>
</button>
<button mat-raised-button
(click)="copyAccessToken($event)"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>device.copyAccessToken</span>
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<mat-form-field class="mat-block"
[fxShow]="!isEdit && isAssignedToCustomer(entity)
&& !entity?.customerIsPublic && deviceScope === 'tenant'">
<mat-label translate>device.assignedToCustomer</mat-label>
<input matInput disabled [ngModel]="entity?.customerTitle">
</mat-form-field>
<div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
[fxShow]="!isEdit && entity?.customerIsPublic && (deviceScope === 'customer' || deviceScope === 'tenant')">
{{ 'device.device-public' | translate }}
</div>
<form #entityNgForm="ngForm" [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>device.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="entityForm.get('name').hasError('required')">
{{ 'device.name-required' | translate }}
</mat-error>
</mat-form-field>
<tb-entity-subtype-autocomplete
formControlName="type"
[required]="true"
[entityType]="entityType.DEVICE"
>
</tb-entity-subtype-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>device.label</mat-label>
<input matInput formControlName="label">
</mat-form-field>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-checkbox fxFlex formControlName="gateway">
{{ 'device.is-gateway' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
</fieldset>
</form>
</div>

19
ui-ngx/src/app/modules/home/pages/device/device.component.scss

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2019 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.
*/
:host {
}

88
ui-ngx/src/app/modules/home/pages/device/device.component.ts

@ -0,0 +1,88 @@
///
/// Copyright © 2016-2019 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 { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityComponent } from '@shared/components/entity/entity.component';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { User } from '@shared/models/user.model';
import { selectAuth, selectUserDetails } from '@core/auth/auth.selectors';
import { map } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum';
import {DeviceInfo} from '@shared/models/device.models';
import {EntityType} from '@shared/models/entity-type.models';
import {NULL_UUID} from '@shared/models/id/has-uuid';
@Component({
selector: 'tb-device',
templateUrl: './device.component.html',
styleUrls: ['./device.component.scss']
})
export class DeviceComponent extends EntityComponent<DeviceInfo> {
entityType = EntityType;
deviceScope: 'tenant' | 'customer' | 'customer_user';
constructor(protected store: Store<AppState>,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.deviceScope = this.entitiesTableConfig.componentsData.deviceScope;
super.ngOnInit();
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
isAssignedToCustomer(entity: DeviceInfo): boolean {
return entity && entity.customerId && entity.customerId.id !== NULL_UUID;
}
buildForm(entity: DeviceInfo): FormGroup {
return this.fb.group(
{
name: [entity ? entity.name : '', [Validators.required]],
type: [entity ? entity.type : null, [Validators.required]],
label: [entity ? entity.label : ''],
additionalInfo: this.fb.group(
{
gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false],
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
}
)
}
);
}
updateForm(entity: DeviceInfo) {
this.entityForm.patchValue({name: entity.name});
this.entityForm.patchValue({type: entity.type});
this.entityForm.patchValue({label: entity.label});
this.entityForm.patchValue({additionalInfo:
{gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false}});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
}
}

39
ui-ngx/src/app/modules/home/pages/device/device.module.ts

@ -0,0 +1,39 @@
///
/// Copyright © 2016-2019 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import {DeviceComponent} from '@modules/home/pages/device/device.component';
import {DeviceRoutingModule} from './device-routing.module';
import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component';
@NgModule({
entryComponents: [
DeviceComponent,
DeviceTableHeaderComponent
],
declarations: [
DeviceComponent,
DeviceTableHeaderComponent
],
imports: [
CommonModule,
SharedModule,
DeviceRoutingModule
]
})
export class DeviceModule { }

196
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -0,0 +1,196 @@
///
/// Copyright © 2016-2019 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 { Injectable } from '@angular/core';
import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router';
import { Tenant } from '@shared/models/tenant.model';
import {
CellActionDescriptor,
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@shared/components/entity/entities-table-config.models';
import { TenantService } from '@core/http/tenant.service';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import {
EntityType,
entityTypeResources,
entityTypeTranslations
} from '@shared/models/entity-type.models';
import { TenantComponent } from '@modules/home/pages/tenant/tenant.component';
import { EntityAction } from '@shared/components/entity/entity-component.models';
import { User } from '@shared/models/user.model';
import {Device, DeviceInfo} from '@app/shared/models/device.models';
import {DeviceComponent} from '@modules/home/pages/device/device.component';
import {Observable, of} from 'rxjs';
import {select, Store} from '@ngrx/store';
import {selectAuth, selectAuthUser} from '@core/auth/auth.selectors';
import {map, mergeMap, take, tap} from 'rxjs/operators';
import {AppState} from '@core/core.state';
import {DeviceService} from '@app/core/http/device.service';
import {Authority} from '@app/shared/models/authority.enum';
import {CustomerService} from '@core/http/customer.service';
import {Customer} from '@app/shared/models/customer.model';
import {NULL_UUID} from '@shared/models/id/has-uuid';
import {BroadcastService} from '@core/services/broadcast.service';
import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component';
@Injectable()
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
private readonly config: EntityTableConfig<DeviceInfo> = new EntityTableConfig<DeviceInfo>();
private customerId: string;
constructor(private store: Store<AppState>,
private broadcast: BroadcastService,
private deviceService: DeviceService,
private customerService: CustomerService,
private translate: TranslateService,
private datePipe: DatePipe,
private router: Router) {
this.config.entityType = EntityType.CUSTOMER;
this.config.entityComponent = DeviceComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE);
this.config.entityResources = entityTypeResources.get(EntityType.DEVICE);
this.config.deleteEntityTitle = device => this.translate.instant('device.delete-device-title', { deviceName: device.name });
this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text');
this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id);
this.config.saveEntity = device => {
return this.deviceService.saveDevice(device).pipe(
tap(() => {
this.broadcast.broadcast('deviceSaved');
}),
mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id)
));
};
this.config.onEntityAction = action => this.onDeviceAction(action);
this.config.headerComponent = DeviceTableHeaderComponent;
}
resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DeviceInfo>> {
const routeParams = route.params;
this.config.componentsData = {
deviceScope: route.data.devicesType,
deviceType: ''
};
this.customerId = routeParams.customerId;
return this.store.pipe(select(selectAuthUser), take(1)).pipe(
tap((authUser) => {
if (authUser.authority === Authority.CUSTOMER_USER) {
this.config.componentsData.deviceScope = 'customer_user';
this.customerId = authUser.customerId;
}
}),
mergeMap(() =>
this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer)
),
map((parentCustomer) => {
if (parentCustomer) {
if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) {
this.config.tableTitle = this.translate.instant('customer.public-devices');
} else {
this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('device.devices');
}
} else {
this.config.tableTitle = this.translate.instant('device.devices');
}
this.config.columns = this.configureColumns(this.config.componentsData.deviceScope);
this.configureEntityFunctions(this.config.componentsData.deviceScope);
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
return this.config;
})
);
}
configureColumns(deviceScope: string): Array<EntityTableColumn<Device | DeviceInfo>> {
const columns: Array<EntityTableColumn<Device | DeviceInfo>> = [
new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceInfo>('name', 'device.name'),
new EntityTableColumn<DeviceInfo>('type', 'device.device-type'),
new EntityTableColumn<DeviceInfo>('label', 'device.label')
];
if (deviceScope === 'tenant') {
columns.push(
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer'),
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
entity => {
return checkBoxCell(entity.customerIsPublic);
}, () => ({}), false),
);
}
columns.push(
new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px',
entity => {
return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway);
}, () => ({}), false)
);
return columns;
}
configureEntityFunctions(deviceScope: string): void {
if (deviceScope === 'tenant') {
this.config.entitiesFetchFunction = pageLink => this.deviceService.getTenantDeviceInfos(pageLink, this.config.componentsData.deviceType);
this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id);
} else {
this.config.entitiesFetchFunction = pageLink => this.deviceService.getCustomerDeviceInfos(this.customerId, pageLink, this.config.componentsData.deviceType);
this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id);
}
}
configureCellActions(deviceScope: string): Array<CellActionDescriptor<Device | DeviceInfo>> {
const actions: Array<CellActionDescriptor<Device | DeviceInfo>> = [];
if (deviceScope === 'tenant') {
actions.push(
{
name: this.translate.instant('device.make-public'),
icon: 'share',
isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID),
onAction: ($event, entity) => this.makePublic($event, entity)
}
);
}
return actions;
}
makePublic($event: Event, device: Device) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
onDeviceAction(action: EntityAction<Device | DeviceInfo>): boolean {
switch (action.action) {
case 'makePublic':
this.makePublic(action.event, action.entity);
return true;
}
return false;
}
}

2
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -23,6 +23,7 @@ import { TenantModule } from '@modules/home/pages/tenant/tenant.module';
// import { CustomerModule } from '@modules/home/pages/customer/customer.module';
// import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module';
import { UserModule } from '@modules/home/pages/user/user.module';
import {DeviceModule} from '@modules/home/pages/device/device.module';
@NgModule({
exports: [
@ -30,6 +31,7 @@ import { UserModule } from '@modules/home/pages/user/user.module';
HomeLinksModule,
ProfileModule,
TenantModule,
DeviceModule,
// CustomerModule,
// AuditLogModule,
UserModule

47
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts

@ -74,8 +74,6 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
filteredDashboards: Observable<Array<DashboardInfo>>;
private valueLoaded = false;
private searchText = '';
private propagateChange = (v: any) => { };
@ -97,7 +95,21 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
}
ngOnInit() {
this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges
.pipe(
tap(value => {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = this.useIdValue ? value.id.id : value;
}
this.updateView(modelValue);
}),
startWith<string | DashboardInfo>(''),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchDashboards(name) )
);
}
ngAfterViewInit(): void {
@ -123,48 +135,23 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
this.disabled = isDisabled;
}
initFilteredResults(): void {
this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges
.pipe(
startWith<string | DashboardInfo>(''),
tap(value => {
if (this.valueLoaded) {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = this.useIdValue ? value.id.id : value;
}
this.updateView(modelValue);
}
}),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchDashboards(name) )
);
}
writeValue(value: DashboardInfo | string | null): void {
this.valueLoaded = false;
this.searchText = '';
this.initFilteredResults();
if (value != null) {
if (typeof value === 'string') {
this.dashboardService.getDashboardInfo(value).subscribe(
(dashboard) => {
this.modelValue = this.useIdValue ? dashboard.id.id : dashboard;
this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: true});
this.valueLoaded = true;
}
);
} else {
this.modelValue = this.useIdValue ? value.id.id : value;
this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false});
this.valueLoaded = true;
this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: true});
}
} else {
this.modelValue = null;
this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: false});
this.valueLoaded = true;
this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true});
}
}

7
ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts

@ -76,7 +76,8 @@ export class EntityTableColumn<T extends BaseData<HasId>> {
public title: string,
public maxWidth: string = '100%',
public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
public cellStyleFunction: CellStyleFunction<T> = () => ({})) {
public cellStyleFunction: CellStyleFunction<T> = () => ({}),
public sortable: boolean = true) {
}
}
@ -135,3 +136,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
entitiesFetchFunction: EntitiesFetchFunction<T, P> = () => of(emptyPageData<T>());
onEntityAction: EntityActionFunction<T> = () => false;
}
export function checkBoxCell(value: boolean): string {
return `<mat-icon class="material-icons mat-icon">${value ? 'check_box' : 'check_box_outline_blank'}</mat-icon>`;
}

2
ui-ngx/src/app/shared/components/entity/entities-table.component.html

@ -120,7 +120,7 @@
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of columns">
<mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header> {{ column.title | translate }} </mat-header-cell>
<mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>

2
ui-ngx/src/app/shared/components/entity/entities-table.component.ts

@ -82,7 +82,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
isDetailsOpen = false;
@ViewChild('entityTableHeader', {static: false}) entityTableHeaderAnchor: TbAnchorComponent;
@ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent;
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;

39
ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html

@ -0,0 +1,39 @@
<!--
Copyright © 2016-2019 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.
-->
<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block">
<mat-label>{{ entitySubtypeText | translate }}</mat-label>
<input matInput type="text" placeholder="{{ selectEntitySubtypeText | translate }}"
#subTypeInput
formControlName="subType"
[required]="required"
[matAutocomplete]="subTypeAutocomplete">
<button *ngIf="subTypeFormGroup.get('subType').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete #subTypeAutocomplete="matAutocomplete" [displayWith]="displaySubTypeFn">
<mat-option *ngFor="let subType of filteredSubTypes | async" [value]="subType">
<span [innerHTML]="subType.type | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="subTypeFormGroup.get('subType').hasError('required')">
{{ entitySubtypeRequiredText | translate }}
</mat-error>
</mat-form-field>

228
ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts

@ -0,0 +1,228 @@
///
/// Copyright © 2016-2019 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 {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Observable, of, throwError, Subscription} from 'rxjs';
import {PageLink} from '@shared/models/page/page-link';
import {Direction} from '@shared/models/page/sort-order';
import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators';
import {PageData, emptyPageData} from '@shared/models/page/page-data';
import {DashboardInfo} from '@app/shared/models/dashboard.models';
import {DashboardId} from '@app/shared/models/id/dashboard-id';
import {DashboardService} from '@core/http/dashboard.service';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
import {Authority} from '@shared/models/authority.enum';
import {TranslateService} from '@ngx-translate/core';
import {DeviceService} from '@core/http/device.service';
import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models';
import {BroadcastService} from '@app/core/services/broadcast.service';
@Component({
selector: 'tb-entity-subtype-autocomplete',
templateUrl: './entity-subtype-autocomplete.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntitySubTypeAutocompleteComponent),
multi: true
}]
})
export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
subTypeFormGroup: FormGroup;
modelValue: string | null;
@Input()
entityType: EntityType;
@Input()
required: boolean;
@Input()
disabled: boolean;
@ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef;
selectEntitySubtypeText: string;
entitySubtypeText: string;
entitySubtypeRequiredText: string;
filteredSubTypes: Observable<Array<EntitySubtype>>;
subTypes: Observable<Array<EntitySubtype>>;
private broadcastSubscription: Subscription;
private searchText = '';
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private broadcast: BroadcastService,
public translate: TranslateService,
private deviceService: DeviceService,
private fb: FormBuilder) {
this.subTypeFormGroup = this.fb.group({
subType: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
switch (this.entityType) {
case EntityType.ASSET:
this.selectEntitySubtypeText = 'asset.select-asset-type';
this.entitySubtypeText = 'asset.asset-type';
this.entitySubtypeRequiredText = 'asset.asset-type-required';
this.broadcastSubscription = this.broadcast.on('assetSaved', () => {
this.subTypes = null;
});
break;
case EntityType.DEVICE:
this.selectEntitySubtypeText = 'device.select-device-type';
this.entitySubtypeText = 'device.device-type';
this.entitySubtypeRequiredText = 'device.device-type-required';
this.broadcastSubscription = this.broadcast.on('deviceSaved', () => {
this.subTypes = null;
});
break;
case EntityType.ENTITY_VIEW:
this.selectEntitySubtypeText = 'entity-view.select-entity-view-type';
this.entitySubtypeText = 'entity-view.entity-view-type';
this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required';
this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => {
this.subTypes = null;
});
break;
}
this.filteredSubTypes = this.subTypeFormGroup.get('subType').valueChanges
.pipe(
tap(value => {
let modelValue;
if (!value) {
modelValue = null;
} else if (typeof value === 'string') {
modelValue = value;
} else {
modelValue = value.type;
}
this.updateView(modelValue);
}),
startWith<string | EntitySubtype>(''),
map(value => value ? (typeof value === 'string' ? value : value.type) : ''),
mergeMap(type => this.fetchSubTypes(type) )
);
}
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
if (this.broadcastSubscription) {
this.broadcastSubscription.unsubscribe();
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string | null): void {
this.searchText = '';
if (value != null) {
this.modelValue = value;
this.fetchSubTypes(value, true).subscribe(
(subTypes) => {
const subType = subTypes && subTypes.length === 1 ? subTypes[0] : null;
this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true});
}
);
} else {
this.modelValue = null;
this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true});
}
}
updateView(value: string | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displaySubTypeFn(subType?: EntitySubtype): string | undefined {
return subType ? subType.type : undefined;
}
fetchSubTypes(searchText?: string, strictMatch: boolean = false): Observable<Array<EntitySubtype>> {
this.searchText = searchText;
return this.getSubTypes().pipe(
map(subTypes => subTypes.filter( subType => {
if (strictMatch) {
return searchText ? subType.type === searchText : false;
} else {
return searchText ? subType.type.toUpperCase().startsWith(searchText.toUpperCase()) : true;
}
}))
);
}
getSubTypes(): Observable<Array<EntitySubtype>> {
if (!this.subTypes) {
switch (this.entityType) {
case EntityType.ASSET:
// TODO:
break;
case EntityType.DEVICE:
this.subTypes = this.deviceService.getDeviceTypes(false, true);
break;
case EntityType.ENTITY_VIEW:
// TODO:
break;
}
if (this.subTypes) {
this.subTypes = this.subTypes.pipe(
publishReplay(1),
refCount()
);
} else {
return throwError(null);
}
}
return this.subTypes;
}
clear() {
this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.subTypeInput.nativeElement.blur();
this.subTypeInput.nativeElement.focus();
}, 0);
}
}

28
ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2019 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.
-->
<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block">
<mat-label *ngIf="showLabel">{{ entitySubtypeTitle | translate }}</mat-label>
<mat-select class="tb-entity-subtype-select" matInput formControlName="subType">
<mat-option *ngFor="let subType of subTypesOptions | async" [value]="subType">
{{ displaySubTypeFn(subType) }}
</mat-option>
</mat-select>
<mat-error *ngIf="subTypeFormGroup.get('subType').hasError('required')">
{{ entitySubtypeRequiredText | translate }}
</mat-error>
</mat-form-field>

20
ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.scss

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2019 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.
*/
:host {
mat-select.tb-entity-subtype-select {
min-width: 200px;
}
}

238
ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts

@ -0,0 +1,238 @@
///
/// Copyright © 2016-2019 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 {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Observable, of, throwError, Subscription, Subject} from 'rxjs';
import {PageLink} from '@shared/models/page/page-link';
import {Direction} from '@shared/models/page/sort-order';
import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators';
import {PageData, emptyPageData} from '@shared/models/page/page-data';
import {DashboardInfo} from '@app/shared/models/dashboard.models';
import {DashboardId} from '@app/shared/models/id/dashboard-id';
import {DashboardService} from '@core/http/dashboard.service';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
import {Authority} from '@shared/models/authority.enum';
import {TranslateService} from '@ngx-translate/core';
import {DeviceService} from '@core/http/device.service';
import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models';
import {BroadcastService} from '@app/core/services/broadcast.service';
@Component({
selector: 'tb-entity-subtype-select',
templateUrl: './entity-subtype-select.component.html',
styleUrls: ['./entity-subtype-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntitySubTypeSelectComponent),
multi: true
}]
})
export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
subTypeFormGroup: FormGroup;
modelValue: string | null;
@Input()
entityType: EntityType;
@Input()
showLabel: boolean;
@Input()
required: boolean;
@Input()
disabled: boolean;
@Input()
typeTranslatePrefix: string;
@ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef;
entitySubtypeTitle: string;
entitySubtypeRequiredText: string;
subTypesOptions: Observable<Array<EntitySubtype | string>>;
private subTypesOptionsSubject: Subject<string> = new Subject();
subTypes: Observable<Array<EntitySubtype | string>>;
private broadcastSubscription: Subscription;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private broadcast: BroadcastService,
public translate: TranslateService,
private deviceService: DeviceService,
private fb: FormBuilder) {
this.subTypeFormGroup = this.fb.group({
subType: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
switch (this.entityType) {
case EntityType.ASSET:
this.entitySubtypeTitle = 'asset.asset-type';
this.entitySubtypeRequiredText = 'asset.asset-type-required';
this.broadcastSubscription = this.broadcast.on('assetSaved', () => {
this.subTypes = null;
this.subTypesOptionsSubject.next('');
});
break;
case EntityType.DEVICE:
this.entitySubtypeTitle = 'device.device-type';
this.entitySubtypeRequiredText = 'device.device-type-required';
this.broadcastSubscription = this.broadcast.on('deviceSaved', () => {
this.subTypes = null;
this.subTypesOptionsSubject.next('');
});
break;
case EntityType.ENTITY_VIEW:
this.entitySubtypeTitle = 'entity-view.entity-view-type';
this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required';
this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => {
this.subTypes = null;
this.subTypesOptionsSubject.next('');
});
break;
}
this.subTypesOptions = this.subTypesOptionsSubject.asObservable().pipe(
startWith<string | EntitySubtype>(''),
mergeMap(() => this.getSubTypes())
);
this.subTypeFormGroup.get('subType').valueChanges.subscribe(
(value) => {
let modelValue;
if (!value || value === '') {
modelValue = '';
} else {
modelValue = value.type;
}
this.updateView(modelValue);
}
);
}
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
if (this.broadcastSubscription) {
this.broadcastSubscription.unsubscribe();
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string | null): void {
if (value != null && value !== '') {
this.modelValue = value;
this.findSubTypes(value).subscribe(
(subTypes) => {
const subType = subTypes && subTypes.length === 1 ? subTypes[0] : '';
this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true});
}
);
} else {
this.modelValue = '';
this.subTypeFormGroup.get('subType').patchValue('', {emitEvent: true});
}
}
updateView(value: string | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displaySubTypeFn(subType?: EntitySubtype | string): string | undefined {
if (subType && typeof subType !== 'string') {
if (this.typeTranslatePrefix) {
return this.translate.instant(this.typeTranslatePrefix + '.' + subType.type);
} else {
return subType.type;
}
} else {
return this.translate.instant('entity.all-subtypes');
}
}
findSubTypes(searchText?: string): Observable<Array<EntitySubtype | string>> {
return this.getSubTypes().pipe(
map(subTypes => subTypes.filter( subType => {
return searchText ? (typeof subType === 'string' ? false : subType.type === searchText) : false;
}))
);
}
getSubTypes(): Observable<Array<EntitySubtype | string>> {
if (!this.subTypes) {
switch (this.entityType) {
case EntityType.ASSET:
// TODO:
break;
case EntityType.DEVICE:
this.subTypes = this.deviceService.getDeviceTypes(false, true);
break;
case EntityType.ENTITY_VIEW:
// TODO:
break;
}
if (this.subTypes) {
this.subTypes = this.subTypes.pipe(
map((allSubtypes) => {
allSubtypes.unshift('');
return allSubtypes;
}),
publishReplay(1),
refCount()
);
} else {
return throwError(null);
}
}
return this.subTypes;
}
clear() {
this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.subTypeInput.nativeElement.blur();
this.subTypeInput.nativeElement.focus();
}, 0);
}
}

3
ui-ngx/src/app/shared/models/constants.ts

@ -59,7 +59,8 @@ export const HelpLinks = {
securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
tenants: helpBaseUrl + '/docs/user-guide/ui/tenants',
customers: helpBaseUrl + '/docs/user-guide/customers',
users: helpBaseUrl + '/docs/user-guide/ui/users'
users: helpBaseUrl + '/docs/user-guide/ui/users',
devices: helpBaseUrl + '/docs/user-guide/ui/devices'
}
};

34
ui-ngx/src/app/shared/models/device.models.ts

@ -0,0 +1,34 @@
///
/// Copyright © 2016-2019 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 {BaseData} from '@shared/models/base-data';
import {DeviceId} from './id/device-id';
import {TenantId} from '@shared/models/id/tenant-id';
import {CustomerId} from '@shared/models/id/customer-id';
export interface Device extends BaseData<DeviceId> {
tenantId: TenantId;
customerId: CustomerId;
name: string;
type: string;
label: string;
additionalInfo?: any;
}
export interface DeviceInfo extends Device {
customerTitle: string;
customerIsPublic: boolean;
}

28
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -1,3 +1,5 @@
import {TenantId} from './id/tenant-id';
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
@ -88,6 +90,20 @@ export const entityTypeTranslations = new Map<EntityType, EntityTypeTranslation>
search: 'user.search',
selectedEntities: 'user.selected-users'
}
],
[
EntityType.DEVICE,
{
type: 'entity.type-device',
typePlural: 'entity.type-devices',
list: 'entity.list-of-devices',
nameStartsWith: 'entity.device-name-starts-with',
details: 'device.device-details',
add: 'device.add',
noEntities: 'device.no-devices-text',
search: 'device.search',
selectedEntities: 'device.selected-devices'
}
]
]
);
@ -111,6 +127,18 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource>(
{
helpLinkId: 'users'
}
],
[
EntityType.DEVICE,
{
helpLinkId: 'devices'
}
]
]
);
export interface EntitySubtype {
tenantId: TenantId;
entityType: EntityType;
type: string;
}

26
ui-ngx/src/app/shared/models/id/device-id.ts

@ -0,0 +1,26 @@
///
/// Copyright © 2016-2019 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 { EntityId } from './entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export class DeviceId implements EntityId {
entityType = EntityType.DEVICE;
id: string;
constructor(id: string) {
this.id = id;
}
}

6
ui-ngx/src/app/shared/shared.module.ts

@ -78,6 +78,8 @@ import { ClipboardModule } from 'ngx-clipboard';
import { FullscreenDirective } from '@shared/components/fullscreen.directive';
import { HighlightPipe } from '@shared/pipe/highlight.pipe';
import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component';
import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component';
import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component';
@NgModule({
providers: [
@ -118,6 +120,8 @@ import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autoc
DatetimePeriodComponent,
// ValueInputComponent,
DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -183,6 +187,8 @@ import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autoc
TimeintervalComponent,
DatetimePeriodComponent,
DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent,
// ValueInputComponent,
MatButtonModule,
MatCheckboxModule,

5
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -673,6 +673,7 @@
"no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.",
"device-type-list-empty": "No device types selected.",
"device-types": "Device types",
"created-time": "Created time",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
@ -691,7 +692,9 @@
"device-public": "Device is public",
"select-device": "Select device",
"import": "Import device",
"device-file": "Device file"
"device-file": "Device file",
"search": "Search devices",
"selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected"
},
"dialog": {
"close": "Close dialog"

7
ui-ngx/src/styles.scss

@ -209,6 +209,13 @@ label {
}
}
div {
&.tb-small {
font-size: 14px;
color: rgba(0, 0, 0, .54);
}
}
pre.tb-highlight {
display: block;
padding: 15px;

6
ui-ngx/src/theme.scss

@ -253,6 +253,12 @@ $tb-dark-theme: get-tb-dark-theme(
}
}
.mat-cell {
mat-icon {
color: rgba(0, 0, 0, .54);
}
}
mat-toolbar.mat-primary {
button.mat-icon-button {
mat-icon {

Loading…
Cancel
Save