diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index ae447c1a6f..b0c505b575 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/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); diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index c4396dbc7e..fa0cf7ce39 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/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 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 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 diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index f7e34fbc5b..1bea1d8727 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/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 findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId); @@ -45,16 +48,24 @@ public interface DeviceService { PageData findDevicesByTenantId(TenantId tenantId, PageLink pageLink); + PageData findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink); + PageData findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + PageData findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + ListenableFuture> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List deviceIds); void deleteDevicesByTenantId(TenantId tenantId); PageData findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + PageData findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink); + PageData findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + PageData findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + ListenableFuture> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds); void unassignCustomerDevices(TenantId tenantId, CustomerId customerId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java new file mode 100644 index 0000000000..759e7db332 --- /dev/null +++ b/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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java index 64ac69ac59..ff567b7233 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java +++ b/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 { + /** + * 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 { */ PageData 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 findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink); + /** * Find devices by tenantId, type and page link. * @@ -60,6 +79,16 @@ public interface DeviceDao extends Dao { */ PageData 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 findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + /** * Find devices by tenantId and devices Ids. * @@ -79,6 +108,16 @@ public interface DeviceDao extends Dao { */ PageData 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 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 { */ PageData 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 findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + /** * Find devices by tenantId, customerId and devices Ids. diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index ec6d85e0d2..6dca2a4b04 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/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 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 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 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> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List 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 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 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 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> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List deviceIds) { log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java new file mode 100644 index 0000000000..b023f127de --- /dev/null +++ b/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 extends BaseSqlEntity implements SearchTextEntity { + + @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; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java index 66feed0077..f2b06e091c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java +++ b/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 implements SearchTextEntity { - - @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 { 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(); } -} \ No newline at end of file +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceInfoEntity.java new file mode 100644 index 0000000000..2633f84311 --- /dev/null +++ b/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 { + + public static final Map 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); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index 4021881b88..9747f588cd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/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 { + @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 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 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 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 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 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 findTenantDeviceTypes(@Param("tenantId") String tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index cc195eb3d2..2dee5d6185 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/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 return deviceRepository; } + @Override + public DeviceInfo findDeviceInfoById(TenantId tenantId, UUID deviceId) { + return DaoUtil.getData(deviceRepository.findDeviceInfoById(fromTimeUUID(deviceId))); + } + @Override public PageData findDevicesByTenantId(UUID tenantId, PageLink pageLink) { return DaoUtil.toPageData( @@ -73,6 +76,15 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao DaoUtil.toPageable(pageLink))); } + @Override + public PageData findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + deviceRepository.findDeviceInfosByTenantId( + fromTimeUUID(tenantId), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap))); + } + @Override public ListenableFuture> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List deviceIds) { return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(UUIDConverter.fromTimeUUID(tenantId), fromTimeUUIDs(deviceIds)))); @@ -88,6 +100,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao DaoUtil.toPageable(pageLink))); } + @Override + public PageData 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> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List deviceIds) { return service.submit(() -> DaoUtil.convertDataList( @@ -110,6 +132,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao DaoUtil.toPageable(pageLink))); } + @Override + public PageData 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 findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { return DaoUtil.toPageData( @@ -121,6 +153,17 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao DaoUtil.toPageable(pageLink))); } + @Override + public PageData 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> findTenantDeviceTypesAsync(UUID tenantId) { return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId)))); diff --git a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java index 6dee664716..bde567bd7e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java +++ b/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 { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index f4f910d01e..0f62c4ef68 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/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 devicesTitle1 = new ArrayList<>(); + List 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 devicesTitle2 = new ArrayList<>(); + List 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 loadedDevicesTitle1 = new ArrayList<>(); + List loadedDevicesTitle1 = new ArrayList<>(); PageLink pageLink = new PageLink(15, 0, title1); - PageData pageData = null; + PageData 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 loadedDevicesTitle2 = new ArrayList<>(); + List 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 devices = new ArrayList<>(); + List 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 loadedDevices = new ArrayList<>(); + List loadedDevices = new ArrayList<>(); PageLink pageLink = new PageLink(23); - PageData pageData = null; + PageData 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()); diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts new file mode 100644 index 0000000000..19c34e5881 --- /dev/null +++ b/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> { + return this.http.get>(`/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> { + return this.http.get>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&type=${type}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public getDevice(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.get(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public getDeviceInfo(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.get(`/api/device/info/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public saveDevice(device: Device, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.post('/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> { + return this.http.get>('/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)); + } + +} diff --git a/ui-ngx/src/app/core/services/broadcast.models.ts b/ui-ngx/src/app/core/services/broadcast.models.ts new file mode 100644 index 0000000000..7b83dd397b --- /dev/null +++ b/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; +} + +export interface BroadcastEvent { + name: string; +} + +export type BroadcastListener = (event: BroadcastEvent, ...args: Array) => void; diff --git a/ui-ngx/src/app/core/services/broadcast.service.ts b/ui-ngx/src/app/core/services/broadcast.service.ts new file mode 100644 index 0000000000..7461ce3d3d --- /dev/null +++ b/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 = new Subject(); + + broadcast(name: string, ...args: Array) { + 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); + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts b/ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts new file mode 100644 index 0000000000..78ccb6370e --- /dev/null +++ b/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 { } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html new file mode 100644 index 0000000000..f4088933bc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html @@ -0,0 +1,23 @@ + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.scss new file mode 100644 index 0000000000..cb7fe8d04b --- /dev/null +++ b/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; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts new file mode 100644 index 0000000000..4a89ebc60c --- /dev/null +++ b/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 { + + entityType = EntityType; + + constructor(protected store: Store) { + super(store); + } + + deviceTypeChanged(deviceType: string) { + this.entitiesTableConfig.componentsData.deviceType = deviceType; + this.entitiesTableConfig.table.updateData(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html new file mode 100644 index 0000000000..879f1d2757 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -0,0 +1,107 @@ + +
+ + + + + +
+ + +
+
+
+ + device.assignedToCustomer + + +
+ {{ 'device.device-public' | translate }} +
+
+
+ + device.name + + + {{ 'device.name-required' | translate }} + + + + + + device.label + + +
+ + {{ 'device.is-gateway' | translate }} + + + device.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.scss b/ui-ngx/src/app/modules/home/pages/device/device.component.scss new file mode 100644 index 0000000000..d18a4874d0 --- /dev/null +++ b/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 { + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts new file mode 100644 index 0000000000..3d287c5c62 --- /dev/null +++ b/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 { + + entityType = EntityType; + + deviceScope: 'tenant' | 'customer' | 'customer_user'; + + constructor(protected store: Store, + 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 : ''}}); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts new file mode 100644 index 0000000000..de4f34eeef --- /dev/null +++ b/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 { } diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts new file mode 100644 index 0000000000..27e5c86a84 --- /dev/null +++ b/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> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + private customerId: string; + + constructor(private store: Store, + 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> { + 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> { + const columns: Array> = [ + new DateEntityTableColumn('createdTime', 'device.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'device.name'), + new EntityTableColumn('type', 'device.device-type'), + new EntityTableColumn('label', 'device.label') + ]; + if (deviceScope === 'tenant') { + columns.push( + new EntityTableColumn('customerTitle', 'customer.customer'), + new EntityTableColumn('customerIsPublic', 'device.public', '60px', + entity => { + return checkBoxCell(entity.customerIsPublic); + }, () => ({}), false), + ); + } + columns.push( + new EntityTableColumn('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> { + const actions: Array> = []; + 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): boolean { + switch (action.action) { + case 'makePublic': + this.makePublic(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 08cf06cf2d..b7fe5c611d 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/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 diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts index ad2ba9b4a6..41067c10e4 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts @@ -74,8 +74,6 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI filteredDashboards: Observable>; - 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(''), + 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(''), - 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}); } } diff --git a/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts b/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts index 5a6c9d26a6..28cfcd6a18 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts @@ -76,7 +76,8 @@ export class EntityTableColumn> { public title: string, public maxWidth: string = '100%', public cellContentFunction: CellContentFunction = (entity, property) => entity[property], - public cellStyleFunction: CellStyleFunction = () => ({})) { + public cellStyleFunction: CellStyleFunction = () => ({}), + public sortable: boolean = true) { } } @@ -135,3 +136,7 @@ export class EntityTableConfig, P extends PageLink = P entitiesFetchFunction: EntitiesFetchFunction = () => of(emptyPageData()); onEntityAction: EntityActionFunction = () => false; } + +export function checkBoxCell(value: boolean): string { + return `${value ? 'check_box' : 'check_box_outline_blank'}`; +} diff --git a/ui-ngx/src/app/shared/components/entity/entities-table.component.html b/ui-ngx/src/app/shared/components/entity/entities-table.component.html index d5d0d95d51..6cbcd63490 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/shared/components/entity/entities-table.component.html @@ -120,7 +120,7 @@ - {{ column.title | translate }} + {{ column.title | translate }} diff --git a/ui-ngx/src/app/shared/components/entity/entities-table.component.ts b/ui-ngx/src/app/shared/components/entity/entities-table.component.ts index 0fa6248581..a415d54ee8 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table.component.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html new file mode 100644 index 0000000000..692460d940 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.html @@ -0,0 +1,39 @@ + + + {{ entitySubtypeText | translate }} + + + + + + + + + {{ entitySubtypeRequiredText | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts new file mode 100644 index 0000000000..34044a45b2 --- /dev/null +++ b/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>; + + subTypes: Observable>; + + private broadcastSubscription: Subscription; + + private searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + 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(''), + 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> { + 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> { + 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); + } + +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html new file mode 100644 index 0000000000..651b274905 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.html @@ -0,0 +1,28 @@ + + + {{ entitySubtypeTitle | translate }} + + + {{ displaySubTypeFn(subType) }} + + + + {{ entitySubtypeRequiredText | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.scss b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.scss new file mode 100644 index 0000000000..bfd904b713 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts new file mode 100644 index 0000000000..db31c53f56 --- /dev/null +++ b/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>; + + private subTypesOptionsSubject: Subject = new Subject(); + + subTypes: Observable>; + + private broadcastSubscription: Subscription; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + 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(''), + 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> { + return this.getSubTypes().pipe( + map(subTypes => subTypes.filter( subType => { + return searchText ? (typeof subType === 'string' ? false : subType.type === searchText) : false; + })) + ); + } + + getSubTypes(): Observable> { + 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); + } + +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index b57950bef6..f92a20eed2 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/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' } }; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts new file mode 100644 index 0000000000..ebd3acb44a --- /dev/null +++ b/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 { + tenantId: TenantId; + customerId: CustomerId; + name: string; + type: string; + label: string; + additionalInfo?: any; +} + +export interface DeviceInfo extends Device { + customerTitle: string; + customerIsPublic: boolean; +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 36747d3380..4b9687f019 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/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 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( { helpLinkId: 'users' } + ], + [ + EntityType.DEVICE, + { + helpLinkId: 'devices' + } ] ] ); + +export interface EntitySubtype { + tenantId: TenantId; + entityType: EntityType; + type: string; +} diff --git a/ui-ngx/src/app/shared/models/id/device-id.ts b/ui-ngx/src/app/shared/models/id/device-id.ts new file mode 100644 index 0000000000..9c79b35342 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index ffd5591240..1390aefefb 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/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, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index f5e115aafb..34fe3394dc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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" diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index e2ea5e36c9..bd55482c9b 100644 --- a/ui-ngx/src/styles.scss +++ b/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; diff --git a/ui-ngx/src/theme.scss b/ui-ngx/src/theme.scss index 33b7b11673..662ae1632a 100644 --- a/ui-ngx/src/theme.scss +++ b/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 {