Commit 65939722f31a8a06083bb976d0b93a227c30f672

Authored by Igor Kulikov
1 parent 08e8c929

Devices page implementation

Showing 43 changed files with 1968 additions and 143 deletions
... ... @@ -28,16 +28,7 @@ import org.springframework.security.core.Authentication;
28 28 import org.springframework.security.core.context.SecurityContextHolder;
29 29 import org.springframework.web.bind.annotation.ExceptionHandler;
30 30 import org.thingsboard.server.actors.service.ActorService;
31   -import org.thingsboard.server.common.data.Customer;
32   -import org.thingsboard.server.common.data.Dashboard;
33   -import org.thingsboard.server.common.data.DashboardInfo;
34   -import org.thingsboard.server.common.data.DataConstants;
35   -import org.thingsboard.server.common.data.Device;
36   -import org.thingsboard.server.common.data.EntityType;
37   -import org.thingsboard.server.common.data.EntityView;
38   -import org.thingsboard.server.common.data.HasName;
39   -import org.thingsboard.server.common.data.Tenant;
40   -import org.thingsboard.server.common.data.User;
  31 +import org.thingsboard.server.common.data.*;
41 32 import org.thingsboard.server.common.data.alarm.Alarm;
42 33 import org.thingsboard.server.common.data.alarm.AlarmId;
43 34 import org.thingsboard.server.common.data.alarm.AlarmInfo;
... ... @@ -381,6 +372,18 @@ public abstract class BaseController {
381 372 }
382 373 }
383 374
  375 + DeviceInfo checkDeviceInfoId(DeviceId deviceId, Operation operation) throws ThingsboardException {
  376 + try {
  377 + validateId(deviceId, "Incorrect deviceId " + deviceId);
  378 + DeviceInfo device = deviceService.findDeviceInfoById(getCurrentUser().getTenantId(), deviceId);
  379 + checkNotNull(device);
  380 + accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device);
  381 + return device;
  382 + } catch (Exception e) {
  383 + throw handleException(e, false);
  384 + }
  385 + }
  386 +
384 387 protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException {
385 388 try {
386 389 validateId(entityViewId, "Incorrect entityViewId " + entityViewId);
... ...
... ... @@ -30,11 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
30 30 import org.springframework.web.bind.annotation.ResponseStatus;
31 31 import org.springframework.web.bind.annotation.RestController;
32 32 import org.springframework.web.context.request.async.DeferredResult;
33   -import org.thingsboard.server.common.data.Customer;
34   -import org.thingsboard.server.common.data.DataConstants;
35   -import org.thingsboard.server.common.data.Device;
36   -import org.thingsboard.server.common.data.EntitySubtype;
37   -import org.thingsboard.server.common.data.EntityType;
  33 +import org.thingsboard.server.common.data.*;
38 34 import org.thingsboard.server.common.data.audit.ActionType;
39 35 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
40 36 import org.thingsboard.server.common.data.exception.ThingsboardException;
... ... @@ -80,6 +76,19 @@ public class DeviceController extends BaseController {
80 76 }
81 77
82 78 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
  79 + @RequestMapping(value = "/device/info/{deviceId}", method = RequestMethod.GET)
  80 + @ResponseBody
  81 + public DeviceInfo getDeviceInfoById(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  82 + checkParameter(DEVICE_ID, strDeviceId);
  83 + try {
  84 + DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
  85 + return checkDeviceInfoId(deviceId, Operation.READ);
  86 + } catch (Exception e) {
  87 + throw handleException(e);
  88 + }
  89 + }
  90 +
  91 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
83 92 @RequestMapping(value = "/device", method = RequestMethod.POST)
84 93 @ResponseBody
85 94 public Device saveDevice(@RequestBody Device device) throws ThingsboardException {
... ... @@ -288,6 +297,29 @@ public class DeviceController extends BaseController {
288 297 }
289 298
290 299 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
  300 + @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
  301 + @ResponseBody
  302 + public PageData<DeviceInfo> getTenantDeviceInfos(
  303 + @RequestParam int pageSize,
  304 + @RequestParam int page,
  305 + @RequestParam(required = false) String type,
  306 + @RequestParam(required = false) String textSearch,
  307 + @RequestParam(required = false) String sortProperty,
  308 + @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  309 + try {
  310 + TenantId tenantId = getCurrentUser().getTenantId();
  311 + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
  312 + if (type != null && type.trim().length() > 0) {
  313 + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink));
  314 + } else {
  315 + return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink));
  316 + }
  317 + } catch (Exception e) {
  318 + throw handleException(e);
  319 + }
  320 + }
  321 +
  322 + @PreAuthorize("hasAuthority('TENANT_ADMIN')")
291 323 @RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET)
292 324 @ResponseBody
293 325 public Device getTenantDevice(
... ... @@ -328,6 +360,33 @@ public class DeviceController extends BaseController {
328 360 }
329 361
330 362 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
  363 + @RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
  364 + @ResponseBody
  365 + public PageData<DeviceInfo> getCustomerDeviceInfos(
  366 + @PathVariable("customerId") String strCustomerId,
  367 + @RequestParam int pageSize,
  368 + @RequestParam int page,
  369 + @RequestParam(required = false) String type,
  370 + @RequestParam(required = false) String textSearch,
  371 + @RequestParam(required = false) String sortProperty,
  372 + @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  373 + checkParameter("customerId", strCustomerId);
  374 + try {
  375 + TenantId tenantId = getCurrentUser().getTenantId();
  376 + CustomerId customerId = new CustomerId(toUUID(strCustomerId));
  377 + checkCustomerId(customerId, Operation.READ);
  378 + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
  379 + if (type != null && type.trim().length() > 0) {
  380 + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
  381 + } else {
  382 + return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
  383 + }
  384 + } catch (Exception e) {
  385 + throw handleException(e);
  386 + }
  387 + }
  388 +
  389 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
331 390 @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
332 391 @ResponseBody
333 392 public List<Device> getDevicesByIds(
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
19 19 import org.thingsboard.server.common.data.Device;
  20 +import org.thingsboard.server.common.data.DeviceInfo;
20 21 import org.thingsboard.server.common.data.EntitySubtype;
21 22 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
22 23 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -28,7 +29,9 @@ import org.thingsboard.server.common.data.page.PageLink;
28 29 import java.util.List;
29 30
30 31 public interface DeviceService {
31   -
  32 +
  33 + DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId);
  34 +
32 35 Device findDeviceById(TenantId tenantId, DeviceId deviceId);
33 36
34 37 ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId);
... ... @@ -45,16 +48,24 @@ public interface DeviceService {
45 48
46 49 PageData<Device> findDevicesByTenantId(TenantId tenantId, PageLink pageLink);
47 50
  51 + PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink);
  52 +
48 53 PageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
49 54
  55 + PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
  56 +
50 57 ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
51 58
52 59 void deleteDevicesByTenantId(TenantId tenantId);
53 60
54 61 PageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
55 62
  63 + PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink);
  64 +
56 65 PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
57 66
  67 + PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
  68 +
58 69 ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
59 70
60 71 void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.id.DeviceId;
  20 +
  21 +@Data
  22 +public class DeviceInfo extends Device {
  23 +
  24 + private String customerTitle;
  25 + private boolean customerIsPublic;
  26 +
  27 + public DeviceInfo() {
  28 + super();
  29 + }
  30 +
  31 + public DeviceInfo(DeviceId deviceId) {
  32 + super(deviceId);
  33 + }
  34 +
  35 + public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic) {
  36 + super(device);
  37 + this.customerTitle = customerTitle;
  38 + this.customerIsPublic = customerIsPublic;
  39 + }
  40 +}
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
19 19 import org.thingsboard.server.common.data.Device;
  20 +import org.thingsboard.server.common.data.DeviceInfo;
20 21 import org.thingsboard.server.common.data.EntitySubtype;
21 22 import org.thingsboard.server.common.data.id.TenantId;
22 23 import org.thingsboard.server.common.data.page.PageData;
... ... @@ -34,6 +35,15 @@ import java.util.UUID;
34 35 public interface DeviceDao extends Dao<Device> {
35 36
36 37 /**
  38 + * Find device info by id.
  39 + *
  40 + * @param tenantId the tenant id
  41 + * @param deviceId the device id
  42 + * @return the device info object
  43 + */
  44 + DeviceInfo findDeviceInfoById(TenantId tenantId, UUID deviceId);
  45 +
  46 + /**
37 47 * Save or update device object
38 48 *
39 49 * @param device the device object
... ... @@ -51,6 +61,15 @@ public interface DeviceDao extends Dao<Device> {
51 61 PageData<Device> findDevicesByTenantId(UUID tenantId, PageLink pageLink);
52 62
53 63 /**
  64 + * Find device infos by tenantId and page link.
  65 + *
  66 + * @param tenantId the tenantId
  67 + * @param pageLink the page link
  68 + * @return the list of device info objects
  69 + */
  70 + PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink);
  71 +
  72 + /**
54 73 * Find devices by tenantId, type and page link.
55 74 *
56 75 * @param tenantId the tenantId
... ... @@ -61,6 +80,16 @@ public interface DeviceDao extends Dao<Device> {
61 80 PageData<Device> findDevicesByTenantIdAndType(UUID tenantId, String type, PageLink pageLink);
62 81
63 82 /**
  83 + * Find device infos by tenantId, type and page link.
  84 + *
  85 + * @param tenantId the tenantId
  86 + * @param type the type
  87 + * @param pageLink the page link
  88 + * @return the list of device onfo objects
  89 + */
  90 + PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink);
  91 +
  92 + /**
64 93 * Find devices by tenantId and devices Ids.
65 94 *
66 95 * @param tenantId the tenantId
... ... @@ -80,6 +109,16 @@ public interface DeviceDao extends Dao<Device> {
80 109 PageData<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
81 110
82 111 /**
  112 + * Find device infos by tenantId, customerId and page link.
  113 + *
  114 + * @param tenantId the tenantId
  115 + * @param customerId the customerId
  116 + * @param pageLink the page link
  117 + * @return the list of device info objects
  118 + */
  119 + PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink);
  120 +
  121 + /**
83 122 * Find devices by tenantId, customerId, type and page link.
84 123 *
85 124 * @param tenantId the tenantId
... ... @@ -90,6 +129,17 @@ public interface DeviceDao extends Dao<Device> {
90 129 */
91 130 PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
92 131
  132 + /**
  133 + * Find device infos by tenantId, customerId, type and page link.
  134 + *
  135 + * @param tenantId the tenantId
  136 + * @param customerId the customerId
  137 + * @param type the type
  138 + * @param pageLink the page link
  139 + * @return the list of device info objects
  140 + */
  141 + PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink);
  142 +
93 143
94 144 /**
95 145 * Find devices by tenantId, customerId and devices Ids.
... ...
... ... @@ -27,12 +27,7 @@ import org.springframework.cache.annotation.CacheEvict;
27 27 import org.springframework.cache.annotation.Cacheable;
28 28 import org.springframework.stereotype.Service;
29 29 import org.springframework.util.StringUtils;
30   -import org.thingsboard.server.common.data.Customer;
31   -import org.thingsboard.server.common.data.Device;
32   -import org.thingsboard.server.common.data.EntitySubtype;
33   -import org.thingsboard.server.common.data.EntityType;
34   -import org.thingsboard.server.common.data.EntityView;
35   -import org.thingsboard.server.common.data.Tenant;
  30 +import org.thingsboard.server.common.data.*;
36 31 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
37 32 import org.thingsboard.server.common.data.id.CustomerId;
38 33 import org.thingsboard.server.common.data.id.DeviceId;
... ... @@ -96,6 +91,13 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
96 91 private CacheManager cacheManager;
97 92
98 93 @Override
  94 + public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) {
  95 + log.trace("Executing findDeviceInfoById [{}]", deviceId);
  96 + validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
  97 + return deviceDao.findDeviceInfoById(tenantId, deviceId.getId());
  98 + }
  99 +
  100 + @Override
99 101 public Device findDeviceById(TenantId tenantId, DeviceId deviceId) {
100 102 log.trace("Executing findDeviceById [{}]", deviceId);
101 103 validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
... ... @@ -188,6 +190,14 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
188 190 }
189 191
190 192 @Override
  193 + public PageData<DeviceInfo> findDeviceInfosByTenantId(TenantId tenantId, PageLink pageLink) {
  194 + log.trace("Executing findDeviceInfosByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
  195 + validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
  196 + validatePageLink(pageLink);
  197 + return deviceDao.findDeviceInfosByTenantId(tenantId.getId(), pageLink);
  198 + }
  199 +
  200 + @Override
191 201 public PageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) {
192 202 log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
193 203 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
... ... @@ -197,6 +207,15 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
197 207 }
198 208
199 209 @Override
  210 + public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink) {
  211 + log.trace("Executing findDeviceInfosByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
  212 + validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
  213 + validateString(type, "Incorrect type " + type);
  214 + validatePageLink(pageLink);
  215 + return deviceDao.findDeviceInfosByTenantIdAndType(tenantId.getId(), type, pageLink);
  216 + }
  217 +
  218 + @Override
200 219 public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
201 220 log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
202 221 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
... ... @@ -222,6 +241,15 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
222 241 }
223 242
224 243 @Override
  244 + public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, PageLink pageLink) {
  245 + log.trace("Executing findDeviceInfosByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
  246 + validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
  247 + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
  248 + validatePageLink(pageLink);
  249 + return deviceDao.findDeviceInfosByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
  250 + }
  251 +
  252 + @Override
225 253 public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
226 254 log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
227 255 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
... ... @@ -232,6 +260,16 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
232 260 }
233 261
234 262 @Override
  263 + public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink) {
  264 + log.trace("Executing findDeviceInfosByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
  265 + validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
  266 + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId);
  267 + validateString(type, "Incorrect type " + type);
  268 + validatePageLink(pageLink);
  269 + return deviceDao.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
  270 + }
  271 +
  272 + @Override
235 273 public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
236 274 log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);
237 275 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.model.sql;
  17 +
  18 +import com.datastax.driver.core.utils.UUIDs;
  19 +import com.fasterxml.jackson.databind.JsonNode;
  20 +import lombok.Data;
  21 +import lombok.EqualsAndHashCode;
  22 +import org.hibernate.annotations.Type;
  23 +import org.hibernate.annotations.TypeDef;
  24 +import org.thingsboard.server.common.data.Device;
  25 +import org.thingsboard.server.common.data.id.CustomerId;
  26 +import org.thingsboard.server.common.data.id.DeviceId;
  27 +import org.thingsboard.server.common.data.id.TenantId;
  28 +import org.thingsboard.server.dao.model.BaseSqlEntity;
  29 +import org.thingsboard.server.dao.model.ModelConstants;
  30 +import org.thingsboard.server.dao.model.SearchTextEntity;
  31 +import org.thingsboard.server.dao.util.mapping.JsonStringType;
  32 +
  33 +import javax.persistence.Column;
  34 +import javax.persistence.Entity;
  35 +import javax.persistence.MappedSuperclass;
  36 +
  37 +@Data
  38 +@EqualsAndHashCode(callSuper = true)
  39 +@TypeDef(name = "json", typeClass = JsonStringType.class)
  40 +@MappedSuperclass
  41 +public abstract class AbstractDeviceEntity<T extends Device> extends BaseSqlEntity<T> implements SearchTextEntity<T> {
  42 +
  43 + @Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY)
  44 + private String tenantId;
  45 +
  46 + @Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY)
  47 + private String customerId;
  48 +
  49 + @Column(name = ModelConstants.DEVICE_TYPE_PROPERTY)
  50 + private String type;
  51 +
  52 + @Column(name = ModelConstants.DEVICE_NAME_PROPERTY)
  53 + private String name;
  54 +
  55 + @Column(name = ModelConstants.DEVICE_LABEL_PROPERTY)
  56 + private String label;
  57 +
  58 + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
  59 + private String searchText;
  60 +
  61 + @Type(type = "json")
  62 + @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
  63 + private JsonNode additionalInfo;
  64 +
  65 + public AbstractDeviceEntity() {
  66 + super();
  67 + }
  68 +
  69 + public AbstractDeviceEntity(Device device) {
  70 + if (device.getId() != null) {
  71 + this.setId(device.getId().getId());
  72 + }
  73 + if (device.getTenantId() != null) {
  74 + this.tenantId = toString(device.getTenantId().getId());
  75 + }
  76 + if (device.getCustomerId() != null) {
  77 + this.customerId = toString(device.getCustomerId().getId());
  78 + }
  79 + this.name = device.getName();
  80 + this.type = device.getType();
  81 + this.label = device.getLabel();
  82 + this.additionalInfo = device.getAdditionalInfo();
  83 + }
  84 +
  85 + public AbstractDeviceEntity(DeviceEntity deviceEntity) {
  86 + this.setId(deviceEntity.getId());;
  87 + this.tenantId = deviceEntity.getTenantId();
  88 + this.customerId = deviceEntity.getCustomerId();
  89 + this.type = deviceEntity.getType();
  90 + this.name = deviceEntity.getName();
  91 + this.label = deviceEntity.getLabel();
  92 + this.searchText = deviceEntity.getSearchText();
  93 + this.additionalInfo = deviceEntity.getAdditionalInfo();
  94 + }
  95 +
  96 + @Override
  97 + public String getSearchTextSource() {
  98 + return name;
  99 + }
  100 +
  101 + @Override
  102 + public void setSearchText(String searchText) {
  103 + this.searchText = searchText;
  104 + }
  105 +
  106 + protected Device toDevice() {
  107 + Device device = new Device(new DeviceId(getId()));
  108 + device.setCreatedTime(UUIDs.unixTimestamp(getId()));
  109 + if (tenantId != null) {
  110 + device.setTenantId(new TenantId(toUUID(tenantId)));
  111 + }
  112 + if (customerId != null) {
  113 + device.setCustomerId(new CustomerId(toUUID(customerId)));
  114 + }
  115 + device.setName(name);
  116 + device.setType(type);
  117 + device.setLabel(label);
  118 + device.setAdditionalInfo(additionalInfo);
  119 + return device;
  120 + }
  121 +
  122 +}
... ...
... ... @@ -39,74 +39,18 @@ import javax.persistence.Table;
39 39 @Entity
40 40 @TypeDef(name = "json", typeClass = JsonStringType.class)
41 41 @Table(name = ModelConstants.DEVICE_COLUMN_FAMILY_NAME)
42   -public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchTextEntity<Device> {
43   -
44   - @Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY)
45   - private String tenantId;
46   -
47   - @Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY)
48   - private String customerId;
49   -
50   - @Column(name = ModelConstants.DEVICE_TYPE_PROPERTY)
51   - private String type;
52   -
53   - @Column(name = ModelConstants.DEVICE_NAME_PROPERTY)
54   - private String name;
55   -
56   - @Column(name = ModelConstants.DEVICE_LABEL_PROPERTY)
57   - private String label;
58   -
59   - @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
60   - private String searchText;
61   -
62   - @Type(type = "json")
63   - @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
64   - private JsonNode additionalInfo;
  42 +public final class DeviceEntity extends AbstractDeviceEntity<Device> {
65 43
66 44 public DeviceEntity() {
67 45 super();
68 46 }
69 47
70 48 public DeviceEntity(Device device) {
71   - if (device.getId() != null) {
72   - this.setId(device.getId().getId());
73   - }
74   - if (device.getTenantId() != null) {
75   - this.tenantId = toString(device.getTenantId().getId());
76   - }
77   - if (device.getCustomerId() != null) {
78   - this.customerId = toString(device.getCustomerId().getId());
79   - }
80   - this.name = device.getName();
81   - this.type = device.getType();
82   - this.label = device.getLabel();
83   - this.additionalInfo = device.getAdditionalInfo();
84   - }
85   -
86   - @Override
87   - public String getSearchTextSource() {
88   - return name;
89   - }
90   -
91   - @Override
92   - public void setSearchText(String searchText) {
93   - this.searchText = searchText;
  49 + super(device);
94 50 }
95 51
96 52 @Override
97 53 public Device toData() {
98   - Device device = new Device(new DeviceId(getId()));
99   - device.setCreatedTime(UUIDs.unixTimestamp(getId()));
100   - if (tenantId != null) {
101   - device.setTenantId(new TenantId(toUUID(tenantId)));
102   - }
103   - if (customerId != null) {
104   - device.setCustomerId(new CustomerId(toUUID(customerId)));
105   - }
106   - device.setName(name);
107   - device.setType(type);
108   - device.setLabel(label);
109   - device.setAdditionalInfo(additionalInfo);
110   - return device;
  54 + return super.toDevice();
111 55 }
112   -}
\ No newline at end of file
  56 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.model.sql;
  17 +
  18 +import lombok.Data;
  19 +import lombok.EqualsAndHashCode;
  20 +import com.fasterxml.jackson.databind.JsonNode;
  21 +import org.thingsboard.server.common.data.DeviceInfo;
  22 +
  23 +import java.util.HashMap;
  24 +import java.util.Map;
  25 +
  26 +@Data
  27 +@EqualsAndHashCode(callSuper = true)
  28 +public class DeviceInfoEntity extends AbstractDeviceEntity<DeviceInfo> {
  29 +
  30 + public static final Map<String,String> deviceInfoColumnMap = new HashMap<>();
  31 + static {
  32 + deviceInfoColumnMap.put("customerTitle", "c.title");
  33 + }
  34 +
  35 + private String customerTitle;
  36 + private boolean customerIsPublic;
  37 +
  38 + public DeviceInfoEntity() {
  39 + super();
  40 + }
  41 +
  42 + public DeviceInfoEntity(DeviceEntity deviceEntity,
  43 + String customerTitle,
  44 + Object customerAdditionalInfo) {
  45 + super(deviceEntity);
  46 + this.customerTitle = customerTitle;
  47 + if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) {
  48 + this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean();
  49 + } else {
  50 + this.customerIsPublic = false;
  51 + }
  52 + }
  53 +
  54 + @Override
  55 + public DeviceInfo toData() {
  56 + return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic);
  57 + }
  58 +}
... ...
... ... @@ -22,6 +22,7 @@ import org.springframework.data.repository.CrudRepository;
22 22 import org.springframework.data.repository.PagingAndSortingRepository;
23 23 import org.springframework.data.repository.query.Param;
24 24 import org.thingsboard.server.dao.model.sql.DeviceEntity;
  25 +import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
25 26 import org.thingsboard.server.dao.util.SqlDao;
26 27
27 28 import java.util.List;
... ... @@ -32,6 +33,11 @@ import java.util.List;
32 33 @SqlDao
33 34 public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntity, String> {
34 35
  36 + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
  37 + "FROM DeviceEntity d " +
  38 + "LEFT JOIN CustomerEntity c on c.id = d.customerId " +
  39 + "WHERE d.id = :deviceId")
  40 + DeviceInfoEntity findDeviceInfoById(@Param("deviceId") String deviceId);
35 41
36 42 @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
37 43 "AND d.customerId = :customerId " +
... ... @@ -41,12 +47,32 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
41 47 @Param("searchText") String searchText,
42 48 Pageable pageable);
43 49
  50 + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
  51 + "FROM DeviceEntity d " +
  52 + "LEFT JOIN CustomerEntity c on c.id = d.customerId " +
  53 + "WHERE d.tenantId = :tenantId " +
  54 + "AND d.customerId = :customerId " +
  55 + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
  56 + Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerId(@Param("tenantId") String tenantId,
  57 + @Param("customerId") String customerId,
  58 + @Param("searchText") String searchText,
  59 + Pageable pageable);
  60 +
44 61 @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
45 62 "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
46 63 Page<DeviceEntity> findByTenantId(@Param("tenantId") String tenantId,
47 64 @Param("textSearch") String textSearch,
48 65 Pageable pageable);
49 66
  67 + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
  68 + "FROM DeviceEntity d " +
  69 + "LEFT JOIN CustomerEntity c on c.id = d.customerId " +
  70 + "WHERE d.tenantId = :tenantId " +
  71 + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
  72 + Page<DeviceInfoEntity> findDeviceInfosByTenantId(@Param("tenantId") String tenantId,
  73 + @Param("textSearch") String textSearch,
  74 + Pageable pageable);
  75 +
50 76 @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
51 77 "AND d.type = :type " +
52 78 "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
... ... @@ -55,6 +81,17 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
55 81 @Param("textSearch") String textSearch,
56 82 Pageable pageable);
57 83
  84 + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
  85 + "FROM DeviceEntity d " +
  86 + "LEFT JOIN CustomerEntity c on c.id = d.customerId " +
  87 + "WHERE d.tenantId = :tenantId " +
  88 + "AND d.type = :type " +
  89 + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
  90 + Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndType(@Param("tenantId") String tenantId,
  91 + @Param("type") String type,
  92 + @Param("textSearch") String textSearch,
  93 + Pageable pageable);
  94 +
58 95 @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
59 96 "AND d.customerId = :customerId " +
60 97 "AND d.type = :type " +
... ... @@ -65,6 +102,19 @@ public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntit
65 102 @Param("textSearch") String textSearch,
66 103 Pageable pageable);
67 104
  105 + @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
  106 + "FROM DeviceEntity d " +
  107 + "LEFT JOIN CustomerEntity c on c.id = d.customerId " +
  108 + "WHERE d.tenantId = :tenantId " +
  109 + "AND d.customerId = :customerId " +
  110 + "AND d.type = :type " +
  111 + "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
  112 + Page<DeviceInfoEntity> findDeviceInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") String tenantId,
  113 + @Param("customerId") String customerId,
  114 + @Param("type") String type,
  115 + @Param("textSearch") String textSearch,
  116 + Pageable pageable);
  117 +
68 118 @Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId")
69 119 List<String> findTenantDeviceTypes(@Param("tenantId") String tenantId);
70 120
... ...
... ... @@ -20,16 +20,14 @@ import org.springframework.beans.factory.annotation.Autowired;
20 20 import org.springframework.data.domain.PageRequest;
21 21 import org.springframework.data.repository.CrudRepository;
22 22 import org.springframework.stereotype.Component;
23   -import org.thingsboard.server.common.data.Device;
24   -import org.thingsboard.server.common.data.EntitySubtype;
25   -import org.thingsboard.server.common.data.EntityType;
26   -import org.thingsboard.server.common.data.UUIDConverter;
  23 +import org.thingsboard.server.common.data.*;
27 24 import org.thingsboard.server.common.data.id.TenantId;
28 25 import org.thingsboard.server.common.data.page.PageData;
29 26 import org.thingsboard.server.common.data.page.PageLink;
30 27 import org.thingsboard.server.dao.DaoUtil;
31 28 import org.thingsboard.server.dao.device.DeviceDao;
32 29 import org.thingsboard.server.dao.model.sql.DeviceEntity;
  30 +import org.thingsboard.server.dao.model.sql.DeviceInfoEntity;
33 31 import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
34 32 import org.thingsboard.server.dao.util.SqlDao;
35 33
... ... @@ -65,6 +63,11 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
65 63 }
66 64
67 65 @Override
  66 + public DeviceInfo findDeviceInfoById(TenantId tenantId, UUID deviceId) {
  67 + return DaoUtil.getData(deviceRepository.findDeviceInfoById(fromTimeUUID(deviceId)));
  68 + }
  69 +
  70 + @Override
68 71 public PageData<Device> findDevicesByTenantId(UUID tenantId, PageLink pageLink) {
69 72 return DaoUtil.toPageData(
70 73 deviceRepository.findByTenantId(
... ... @@ -74,6 +77,15 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
74 77 }
75 78
76 79 @Override
  80 + public PageData<DeviceInfo> findDeviceInfosByTenantId(UUID tenantId, PageLink pageLink) {
  81 + return DaoUtil.toPageData(
  82 + deviceRepository.findDeviceInfosByTenantId(
  83 + fromTimeUUID(tenantId),
  84 + Objects.toString(pageLink.getTextSearch(), ""),
  85 + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
  86 + }
  87 +
  88 + @Override
77 89 public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
78 90 return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(UUIDConverter.fromTimeUUID(tenantId), fromTimeUUIDs(deviceIds))));
79 91 }
... ... @@ -89,6 +101,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
89 101 }
90 102
91 103 @Override
  104 + public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerId(UUID tenantId, UUID customerId, PageLink pageLink) {
  105 + return DaoUtil.toPageData(
  106 + deviceRepository.findDeviceInfosByTenantIdAndCustomerId(
  107 + fromTimeUUID(tenantId),
  108 + fromTimeUUID(customerId),
  109 + Objects.toString(pageLink.getTextSearch(), ""),
  110 + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
  111 + }
  112 +
  113 + @Override
92 114 public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
93 115 return service.submit(() -> DaoUtil.convertDataList(
94 116 deviceRepository.findDevicesByTenantIdAndCustomerIdAndIdIn(fromTimeUUID(tenantId), fromTimeUUID(customerId), fromTimeUUIDs(deviceIds))));
... ... @@ -111,6 +133,16 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
111 133 }
112 134
113 135 @Override
  136 + public PageData<DeviceInfo> findDeviceInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink) {
  137 + return DaoUtil.toPageData(
  138 + deviceRepository.findDeviceInfosByTenantIdAndType(
  139 + fromTimeUUID(tenantId),
  140 + type,
  141 + Objects.toString(pageLink.getTextSearch(), ""),
  142 + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
  143 + }
  144 +
  145 + @Override
114 146 public PageData<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
115 147 return DaoUtil.toPageData(
116 148 deviceRepository.findByTenantIdAndCustomerIdAndType(
... ... @@ -122,6 +154,17 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
122 154 }
123 155
124 156 @Override
  157 + public PageData<DeviceInfo> findDeviceInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) {
  158 + return DaoUtil.toPageData(
  159 + deviceRepository.findDeviceInfosByTenantIdAndCustomerIdAndType(
  160 + fromTimeUUID(tenantId),
  161 + fromTimeUUID(customerId),
  162 + type,
  163 + Objects.toString(pageLink.getTextSearch(), ""),
  164 + DaoUtil.toPageable(pageLink, DeviceInfoEntity.deviceInfoColumnMap)));
  165 + }
  166 +
  167 + @Override
125 168 public ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId) {
126 169 return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId))));
127 170 }
... ...
... ... @@ -24,7 +24,7 @@ import java.util.Arrays;
24 24
25 25 @RunWith(ClasspathSuite.class)
26 26 @ClassnameFilters({
27   - "org.thingsboard.server.dao.service.*ServiceSqlTest"
  27 + "org.thingsboard.server.dao.service.*DeviceServiceSqlTest"
28 28 })
29 29 public class SqlDaoServiceTestSuite {
30 30
... ...
... ... @@ -21,10 +21,7 @@ import org.junit.After;
21 21 import org.junit.Assert;
22 22 import org.junit.Before;
23 23 import org.junit.Test;
24   -import org.thingsboard.server.common.data.Customer;
25   -import org.thingsboard.server.common.data.Device;
26   -import org.thingsboard.server.common.data.EntitySubtype;
27   -import org.thingsboard.server.common.data.Tenant;
  24 +import org.thingsboard.server.common.data.*;
28 25 import org.thingsboard.server.common.data.id.CustomerId;
29 26 import org.thingsboard.server.common.data.id.TenantId;
30 27 import org.thingsboard.server.common.data.page.PageData;
... ... @@ -264,7 +261,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
264 261 @Test
265 262 public void testFindDevicesByTenantIdAndName() {
266 263 String title1 = "Device title 1";
267   - List<Device> devicesTitle1 = new ArrayList<>();
  264 + List<DeviceInfo> devicesTitle1 = new ArrayList<>();
268 265 for (int i=0;i<143;i++) {
269 266 Device device = new Device();
270 267 device.setTenantId(tenantId);
... ... @@ -273,10 +270,10 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
273 270 name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
274 271 device.setName(name);
275 272 device.setType("default");
276   - devicesTitle1.add(deviceService.saveDevice(device));
  273 + devicesTitle1.add(new DeviceInfo(deviceService.saveDevice(device), null, false));
277 274 }
278 275 String title2 = "Device title 2";
279   - List<Device> devicesTitle2 = new ArrayList<>();
  276 + List<DeviceInfo> devicesTitle2 = new ArrayList<>();
280 277 for (int i=0;i<175;i++) {
281 278 Device device = new Device();
282 279 device.setTenantId(tenantId);
... ... @@ -285,14 +282,14 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
285 282 name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
286 283 device.setName(name);
287 284 device.setType("default");
288   - devicesTitle2.add(deviceService.saveDevice(device));
  285 + devicesTitle2.add(new DeviceInfo(deviceService.saveDevice(device), null, false));
289 286 }
290 287
291   - List<Device> loadedDevicesTitle1 = new ArrayList<>();
  288 + List<DeviceInfo> loadedDevicesTitle1 = new ArrayList<>();
292 289 PageLink pageLink = new PageLink(15, 0, title1);
293   - PageData<Device> pageData = null;
  290 + PageData<DeviceInfo> pageData = null;
294 291 do {
295   - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
  292 + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
296 293 loadedDevicesTitle1.addAll(pageData.getData());
297 294 if (pageData.hasNext()) {
298 295 pageLink = pageLink.nextPageLink();
... ... @@ -304,10 +301,10 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
304 301
305 302 Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
306 303
307   - List<Device> loadedDevicesTitle2 = new ArrayList<>();
  304 + List<DeviceInfo> loadedDevicesTitle2 = new ArrayList<>();
308 305 pageLink = new PageLink(4, 0, title2);
309 306 do {
310   - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
  307 + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
311 308 loadedDevicesTitle2.addAll(pageData.getData());
312 309 if (pageData.hasNext()) {
313 310 pageLink = pageLink.nextPageLink();
... ... @@ -324,7 +321,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
324 321 }
325 322
326 323 pageLink = new PageLink(4, 0, title1);
327   - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
  324 + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
328 325 Assert.assertFalse(pageData.hasNext());
329 326 Assert.assertEquals(0, pageData.getData().size());
330 327
... ... @@ -333,7 +330,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
333 330 }
334 331
335 332 pageLink = new PageLink(4, 0, title2);
336   - pageData = deviceService.findDevicesByTenantId(tenantId, pageLink);
  333 + pageData = deviceService.findDeviceInfosByTenantId(tenantId, pageLink);
337 334 Assert.assertFalse(pageData.hasNext());
338 335 Assert.assertEquals(0, pageData.getData().size());
339 336 }
... ... @@ -431,21 +428,21 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
431 428 customer = customerService.saveCustomer(customer);
432 429 CustomerId customerId = customer.getId();
433 430
434   - List<Device> devices = new ArrayList<>();
  431 + List<DeviceInfo> devices = new ArrayList<>();
435 432 for (int i=0;i<278;i++) {
436 433 Device device = new Device();
437 434 device.setTenantId(tenantId);
438 435 device.setName("Device"+i);
439 436 device.setType("default");
440 437 device = deviceService.saveDevice(device);
441   - devices.add(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId));
  438 + devices.add(new DeviceInfo(deviceService.assignDeviceToCustomer(tenantId, device.getId(), customerId), customer.getTitle(), customer.isPublic()));
442 439 }
443 440
444   - List<Device> loadedDevices = new ArrayList<>();
  441 + List<DeviceInfo> loadedDevices = new ArrayList<>();
445 442 PageLink pageLink = new PageLink(23);
446   - PageData<Device> pageData = null;
  443 + PageData<DeviceInfo> pageData = null;
447 444 do {
448   - pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
  445 + pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
449 446 loadedDevices.addAll(pageData.getData());
450 447 if (pageData.hasNext()) {
451 448 pageLink = pageLink.nextPageLink();
... ... @@ -460,7 +457,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest {
460 457 deviceService.unassignCustomerDevices(tenantId, customerId);
461 458
462 459 pageLink = new PageLink(33);
463   - pageData = deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink);
  460 + pageData = deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink);
464 461 Assert.assertFalse(pageData.hasNext());
465 462 Assert.assertTrue(pageData.getData().isEmpty());
466 463
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { defaultHttpOptions } from './http-utils';
  19 +import { Observable } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import { PageLink } from '@shared/models/page/page-link';
  22 +import { PageData } from '@shared/models/page/page-data';
  23 +import { Tenant } from '@shared/models/tenant.model';
  24 +import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models';
  25 +import {map} from 'rxjs/operators';
  26 +import {DeviceInfo, Device} from '@app/shared/models/device.models';
  27 +import {EntitySubtype} from '@app/shared/models/entity-type.models';
  28 +
  29 +@Injectable({
  30 + providedIn: 'root'
  31 +})
  32 +export class DeviceService {
  33 +
  34 + constructor(
  35 + private http: HttpClient
  36 + ) { }
  37 +
  38 + public getTenantDeviceInfos(pageLink: PageLink, type: string = '', ignoreErrors: boolean = false,
  39 + ignoreLoading: boolean = false): Observable<PageData<DeviceInfo>> {
  40 + return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`,
  41 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  42 + }
  43 +
  44 + public getCustomerDeviceInfos(customerId: string, pageLink: PageLink, type: string = '', ignoreErrors: boolean = false,
  45 + ignoreLoading: boolean = false): Observable<PageData<DeviceInfo>> {
  46 + return this.http.get<PageData<DeviceInfo>>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&type=${type}`,
  47 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  48 + }
  49 +
  50 + public getDevice(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
  51 + return this.http.get<Device>(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  52 + }
  53 +
  54 + public getDeviceInfo(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DeviceInfo> {
  55 + return this.http.get<DeviceInfo>(`/api/device/info/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  56 + }
  57 +
  58 + public saveDevice(device: Device, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> {
  59 + return this.http.post<Device>('/api/device', device, defaultHttpOptions(ignoreLoading, ignoreErrors));
  60 + }
  61 +
  62 + public deleteDevice(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  63 + return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  64 + }
  65 +
  66 + public getDeviceTypes(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntitySubtype>> {
  67 + return this.http.get<Array<EntitySubtype>>('/api/device/types', defaultHttpOptions(ignoreLoading, ignoreErrors));
  68 + }
  69 +
  70 + public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  71 + return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  72 + }
  73 +
  74 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +
  18 +export interface BroadcastMessage {
  19 + name: string;
  20 + args?: Array<any>;
  21 +}
  22 +
  23 +export interface BroadcastEvent {
  24 + name: string;
  25 +}
  26 +
  27 +export type BroadcastListener = (event: BroadcastEvent, ...args: Array<any>) => void;
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {Injectable} from '@angular/core';
  18 +import {Subject, Subscription} from 'rxjs';
  19 +import {NotificationMessage} from '@core/notification/notification.models';
  20 +import {BroadcastEvent, BroadcastListener, BroadcastMessage} from '@core/services/broadcast.models';
  21 +import {filter} from 'rxjs/operators';
  22 +
  23 +@Injectable({
  24 + providedIn: 'root'
  25 +})
  26 +export class BroadcastService {
  27 +
  28 + private broadcastSubject: Subject<BroadcastMessage> = new Subject();
  29 +
  30 + broadcast(name: string, ...args: Array<any>) {
  31 + const message = {
  32 + name,
  33 + args
  34 + } as BroadcastMessage;
  35 + this.broadcastSubject.next(message);
  36 + }
  37 +
  38 + on(name: string, listener: BroadcastListener): Subscription {
  39 + return this.broadcastSubject.asObservable().pipe(
  40 + filter((message) => message.name === name)
  41 + ).subscribe(
  42 + (message) => {
  43 + const event = {
  44 + name: message.name
  45 + } as BroadcastEvent;
  46 + listener(event, message.args);
  47 + }
  48 + );
  49 + }
  50 +
  51 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {NgModule} from '@angular/core';
  18 +import {RouterModule, Routes} from '@angular/router';
  19 +
  20 +import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component';
  21 +import {Authority} from '@shared/models/authority.enum';
  22 +import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver';
  23 +
  24 +const routes: Routes = [
  25 + {
  26 + path: 'devices',
  27 + component: EntitiesTableComponent,
  28 + data: {
  29 + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  30 + title: 'device.devices',
  31 + devicesType: 'tenant',
  32 + breadcrumb: {
  33 + label: 'device.devices',
  34 + icon: 'devices_other'
  35 + }
  36 + },
  37 + resolve: {
  38 + entitiesTableConfig: DevicesTableConfigResolver
  39 + }
  40 + }
  41 +];
  42 +
  43 +@NgModule({
  44 + imports: [RouterModule.forChild(routes)],
  45 + exports: [RouterModule],
  46 + providers: [
  47 + DevicesTableConfigResolver
  48 + ]
  49 +})
  50 +export class DeviceRoutingModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<tb-entity-subtype-select
  19 + [showLabel]="true"
  20 + [entityType]="entityType.DEVICE"
  21 + [ngModel]="entitiesTableConfig.componentsData.deviceType"
  22 + (ngModelChange)="deviceTypeChanged($event)">
  23 +</tb-entity-subtype-select>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + flex: 1;
  18 + display: flex;
  19 + justify-content: flex-start;
  20 +}
  21 +
  22 +:host ::ng-deep {
  23 + tb-entity-subtype-select {
  24 + mat-form-field {
  25 + font-size: 16px;
  26 +
  27 + .mat-form-field-wrapper {
  28 + padding-bottom: 0;
  29 + }
  30 +
  31 + .mat-form-field-underline {
  32 + bottom: 0;
  33 + }
  34 + }
  35 + }
  36 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { EntityTableHeaderComponent } from '@shared/components/entity/entity-table-header.component';
  21 +import {DeviceInfo} from '@app/shared/models/device.models';
  22 +import {EntityType} from '@shared/models/entity-type.models';
  23 +
  24 +@Component({
  25 + selector: 'tb-device-table-header',
  26 + templateUrl: './device-table-header.component.html',
  27 + styleUrls: ['./device-table-header.component.scss']
  28 +})
  29 +export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<DeviceInfo> {
  30 +
  31 + entityType = EntityType;
  32 +
  33 + constructor(protected store: Store<AppState>) {
  34 + super(store);
  35 + }
  36 +
  37 + deviceTypeChanged(deviceType: string) {
  38 + this.entitiesTableConfig.componentsData.deviceType = deviceType;
  39 + this.entitiesTableConfig.table.updateData();
  40 + }
  41 +
  42 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-details-buttons">
  19 + <button mat-raised-button color="primary"
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'makePublic')"
  22 + [fxShow]="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer(entity) && !entity?.customerIsPublic">
  23 + {{'device.make-public' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'assignToCustomer')"
  28 + [fxShow]="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer(entity)">
  29 + {{'device.assign-to-customer' | translate }}
  30 + </button>
  31 + <button mat-raised-button color="primary"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="onEntityAction($event, 'unassignFromCustomer')"
  34 + [fxShow]="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer(entity)">
  35 + {{ (entity?.customerIsPublic ? 'device.make-private' : 'device.unassign-from-customer') | translate }}
  36 + </button>
  37 + <button mat-raised-button color="primary"
  38 + [disabled]="(isLoading$ | async)"
  39 + (click)="onEntityAction($event, 'manageCredentials')"
  40 + [fxShow]="!isEdit">
  41 + {{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}
  42 + </button>
  43 + <button mat-raised-button color="primary"
  44 + [disabled]="(isLoading$ | async)"
  45 + (click)="onEntityAction($event, 'delete')"
  46 + [fxShow]="!hideDelete() && !isEdit">
  47 + {{'device.delete' | translate }}
  48 + </button>
  49 + <div fxLayout="row">
  50 + <button mat-raised-button
  51 + ngxClipboard
  52 + (cbOnSuccess)="onDeviceIdCopied($event)"
  53 + [cbContent]="entity?.id?.id"
  54 + [fxShow]="!isEdit">
  55 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  56 + <span translate>device.copyId</span>
  57 + </button>
  58 + <button mat-raised-button
  59 + (click)="copyAccessToken($event)"
  60 + [fxShow]="!isEdit">
  61 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  62 + <span translate>device.copyAccessToken</span>
  63 + </button>
  64 + </div>
  65 +</div>
  66 +<div class="mat-padding" fxLayout="column">
  67 + <mat-form-field class="mat-block"
  68 + [fxShow]="!isEdit && isAssignedToCustomer(entity)
  69 + && !entity?.customerIsPublic && deviceScope === 'tenant'">
  70 + <mat-label translate>device.assignedToCustomer</mat-label>
  71 + <input matInput disabled [ngModel]="entity?.customerTitle">
  72 + </mat-form-field>
  73 + <div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
  74 + [fxShow]="!isEdit && entity?.customerIsPublic && (deviceScope === 'customer' || deviceScope === 'tenant')">
  75 + {{ 'device.device-public' | translate }}
  76 + </div>
  77 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  78 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  79 + <mat-form-field class="mat-block">
  80 + <mat-label translate>device.name</mat-label>
  81 + <input matInput formControlName="name" required>
  82 + <mat-error *ngIf="entityForm.get('name').hasError('required')">
  83 + {{ 'device.name-required' | translate }}
  84 + </mat-error>
  85 + </mat-form-field>
  86 + <tb-entity-subtype-autocomplete
  87 + formControlName="type"
  88 + [required]="true"
  89 + [entityType]="entityType.DEVICE"
  90 + >
  91 + </tb-entity-subtype-autocomplete>
  92 + <mat-form-field class="mat-block">
  93 + <mat-label translate>device.label</mat-label>
  94 + <input matInput formControlName="label">
  95 + </mat-form-field>
  96 + <div formGroupName="additionalInfo" fxLayout="column">
  97 + <mat-checkbox fxFlex formControlName="gateway">
  98 + {{ 'device.is-gateway' | translate }}
  99 + </mat-checkbox>
  100 + <mat-form-field class="mat-block">
  101 + <mat-label translate>device.description</mat-label>
  102 + <textarea matInput formControlName="description" rows="2"></textarea>
  103 + </mat-form-field>
  104 + </div>
  105 + </fieldset>
  106 + </form>
  107 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 +
  19 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { select, Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { EntityComponent } from '@shared/components/entity/entity.component';
  21 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  22 +import { User } from '@shared/models/user.model';
  23 +import { selectAuth, selectUserDetails } from '@core/auth/auth.selectors';
  24 +import { map } from 'rxjs/operators';
  25 +import { Authority } from '@shared/models/authority.enum';
  26 +import {DeviceInfo} from '@shared/models/device.models';
  27 +import {EntityType} from '@shared/models/entity-type.models';
  28 +import {NULL_UUID} from '@shared/models/id/has-uuid';
  29 +
  30 +@Component({
  31 + selector: 'tb-device',
  32 + templateUrl: './device.component.html',
  33 + styleUrls: ['./device.component.scss']
  34 +})
  35 +export class DeviceComponent extends EntityComponent<DeviceInfo> {
  36 +
  37 + entityType = EntityType;
  38 +
  39 + deviceScope: 'tenant' | 'customer' | 'customer_user';
  40 +
  41 + constructor(protected store: Store<AppState>,
  42 + public fb: FormBuilder) {
  43 + super(store);
  44 + }
  45 +
  46 + ngOnInit() {
  47 + this.deviceScope = this.entitiesTableConfig.componentsData.deviceScope;
  48 + super.ngOnInit();
  49 + }
  50 +
  51 + hideDelete() {
  52 + if (this.entitiesTableConfig) {
  53 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  54 + } else {
  55 + return false;
  56 + }
  57 + }
  58 +
  59 + isAssignedToCustomer(entity: DeviceInfo): boolean {
  60 + return entity && entity.customerId && entity.customerId.id !== NULL_UUID;
  61 + }
  62 +
  63 + buildForm(entity: DeviceInfo): FormGroup {
  64 + return this.fb.group(
  65 + {
  66 + name: [entity ? entity.name : '', [Validators.required]],
  67 + type: [entity ? entity.type : null, [Validators.required]],
  68 + label: [entity ? entity.label : ''],
  69 + additionalInfo: this.fb.group(
  70 + {
  71 + gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false],
  72 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  73 + }
  74 + )
  75 + }
  76 + );
  77 + }
  78 +
  79 + updateForm(entity: DeviceInfo) {
  80 + this.entityForm.patchValue({name: entity.name});
  81 + this.entityForm.patchValue({type: entity.type});
  82 + this.entityForm.patchValue({label: entity.label});
  83 + this.entityForm.patchValue({additionalInfo:
  84 + {gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false}});
  85 + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  86 + }
  87 +
  88 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import {DeviceComponent} from '@modules/home/pages/device/device.component';
  21 +import {DeviceRoutingModule} from './device-routing.module';
  22 +import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component';
  23 +
  24 +@NgModule({
  25 + entryComponents: [
  26 + DeviceComponent,
  27 + DeviceTableHeaderComponent
  28 + ],
  29 + declarations: [
  30 + DeviceComponent,
  31 + DeviceTableHeaderComponent
  32 + ],
  33 + imports: [
  34 + CommonModule,
  35 + SharedModule,
  36 + DeviceRoutingModule
  37 + ]
  38 +})
  39 +export class DeviceModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +
  19 +import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router';
  20 +
  21 +import { Tenant } from '@shared/models/tenant.model';
  22 +import {
  23 + CellActionDescriptor,
  24 + checkBoxCell,
  25 + DateEntityTableColumn,
  26 + EntityTableColumn,
  27 + EntityTableConfig
  28 +} from '@shared/components/entity/entities-table-config.models';
  29 +import { TenantService } from '@core/http/tenant.service';
  30 +import { TranslateService } from '@ngx-translate/core';
  31 +import { DatePipe } from '@angular/common';
  32 +import {
  33 + EntityType,
  34 + entityTypeResources,
  35 + entityTypeTranslations
  36 +} from '@shared/models/entity-type.models';
  37 +import { TenantComponent } from '@modules/home/pages/tenant/tenant.component';
  38 +import { EntityAction } from '@shared/components/entity/entity-component.models';
  39 +import { User } from '@shared/models/user.model';
  40 +import {Device, DeviceInfo} from '@app/shared/models/device.models';
  41 +import {DeviceComponent} from '@modules/home/pages/device/device.component';
  42 +import {Observable, of} from 'rxjs';
  43 +import {select, Store} from '@ngrx/store';
  44 +import {selectAuth, selectAuthUser} from '@core/auth/auth.selectors';
  45 +import {map, mergeMap, take, tap} from 'rxjs/operators';
  46 +import {AppState} from '@core/core.state';
  47 +import {DeviceService} from '@app/core/http/device.service';
  48 +import {Authority} from '@app/shared/models/authority.enum';
  49 +import {CustomerService} from '@core/http/customer.service';
  50 +import {Customer} from '@app/shared/models/customer.model';
  51 +import {NULL_UUID} from '@shared/models/id/has-uuid';
  52 +import {BroadcastService} from '@core/services/broadcast.service';
  53 +import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component';
  54 +
  55 +@Injectable()
  56 +export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
  57 +
  58 + private readonly config: EntityTableConfig<DeviceInfo> = new EntityTableConfig<DeviceInfo>();
  59 +
  60 + private customerId: string;
  61 +
  62 + constructor(private store: Store<AppState>,
  63 + private broadcast: BroadcastService,
  64 + private deviceService: DeviceService,
  65 + private customerService: CustomerService,
  66 + private translate: TranslateService,
  67 + private datePipe: DatePipe,
  68 + private router: Router) {
  69 +
  70 + this.config.entityType = EntityType.CUSTOMER;
  71 + this.config.entityComponent = DeviceComponent;
  72 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE);
  73 + this.config.entityResources = entityTypeResources.get(EntityType.DEVICE);
  74 +
  75 + this.config.deleteEntityTitle = device => this.translate.instant('device.delete-device-title', { deviceName: device.name });
  76 + this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text');
  77 + this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count});
  78 + this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text');
  79 +
  80 + this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id);
  81 + this.config.saveEntity = device => {
  82 + return this.deviceService.saveDevice(device).pipe(
  83 + tap(() => {
  84 + this.broadcast.broadcast('deviceSaved');
  85 + }),
  86 + mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id)
  87 + ));
  88 + };
  89 + this.config.onEntityAction = action => this.onDeviceAction(action);
  90 +
  91 + this.config.headerComponent = DeviceTableHeaderComponent;
  92 +
  93 + }
  94 +
  95 + resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DeviceInfo>> {
  96 + const routeParams = route.params;
  97 + this.config.componentsData = {
  98 + deviceScope: route.data.devicesType,
  99 + deviceType: ''
  100 + };
  101 + this.customerId = routeParams.customerId;
  102 + return this.store.pipe(select(selectAuthUser), take(1)).pipe(
  103 + tap((authUser) => {
  104 + if (authUser.authority === Authority.CUSTOMER_USER) {
  105 + this.config.componentsData.deviceScope = 'customer_user';
  106 + this.customerId = authUser.customerId;
  107 + }
  108 + }),
  109 + mergeMap(() =>
  110 + this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer)
  111 + ),
  112 + map((parentCustomer) => {
  113 + if (parentCustomer) {
  114 + if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) {
  115 + this.config.tableTitle = this.translate.instant('customer.public-devices');
  116 + } else {
  117 + this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('device.devices');
  118 + }
  119 + } else {
  120 + this.config.tableTitle = this.translate.instant('device.devices');
  121 + }
  122 + this.config.columns = this.configureColumns(this.config.componentsData.deviceScope);
  123 + this.configureEntityFunctions(this.config.componentsData.deviceScope);
  124 + this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
  125 + return this.config;
  126 + })
  127 + );
  128 + }
  129 +
  130 + configureColumns(deviceScope: string): Array<EntityTableColumn<Device | DeviceInfo>> {
  131 + const columns: Array<EntityTableColumn<Device | DeviceInfo>> = [
  132 + new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'),
  133 + new EntityTableColumn<DeviceInfo>('name', 'device.name'),
  134 + new EntityTableColumn<DeviceInfo>('type', 'device.device-type'),
  135 + new EntityTableColumn<DeviceInfo>('label', 'device.label')
  136 + ];
  137 + if (deviceScope === 'tenant') {
  138 + columns.push(
  139 + new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer'),
  140 + new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
  141 + entity => {
  142 + return checkBoxCell(entity.customerIsPublic);
  143 + }, () => ({}), false),
  144 + );
  145 + }
  146 + columns.push(
  147 + new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px',
  148 + entity => {
  149 + return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway);
  150 + }, () => ({}), false)
  151 + );
  152 + return columns;
  153 + }
  154 +
  155 + configureEntityFunctions(deviceScope: string): void {
  156 + if (deviceScope === 'tenant') {
  157 + this.config.entitiesFetchFunction = pageLink => this.deviceService.getTenantDeviceInfos(pageLink, this.config.componentsData.deviceType);
  158 + this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id);
  159 + } else {
  160 + this.config.entitiesFetchFunction = pageLink => this.deviceService.getCustomerDeviceInfos(this.customerId, pageLink, this.config.componentsData.deviceType);
  161 + this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id);
  162 + }
  163 + }
  164 +
  165 + configureCellActions(deviceScope: string): Array<CellActionDescriptor<Device | DeviceInfo>> {
  166 + const actions: Array<CellActionDescriptor<Device | DeviceInfo>> = [];
  167 + if (deviceScope === 'tenant') {
  168 + actions.push(
  169 + {
  170 + name: this.translate.instant('device.make-public'),
  171 + icon: 'share',
  172 + isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID),
  173 + onAction: ($event, entity) => this.makePublic($event, entity)
  174 + }
  175 + );
  176 + }
  177 + return actions;
  178 + }
  179 +
  180 + makePublic($event: Event, device: Device) {
  181 + if ($event) {
  182 + $event.stopPropagation();
  183 + }
  184 + // TODO:
  185 + }
  186 +
  187 + onDeviceAction(action: EntityAction<Device | DeviceInfo>): boolean {
  188 + switch (action.action) {
  189 + case 'makePublic':
  190 + this.makePublic(action.event, action.entity);
  191 + return true;
  192 + }
  193 + return false;
  194 + }
  195 +
  196 +}
... ...
... ... @@ -23,6 +23,7 @@ import { TenantModule } from '@modules/home/pages/tenant/tenant.module';
23 23 // import { CustomerModule } from '@modules/home/pages/customer/customer.module';
24 24 // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module';
25 25 import { UserModule } from '@modules/home/pages/user/user.module';
  26 +import {DeviceModule} from '@modules/home/pages/device/device.module';
26 27
27 28 @NgModule({
28 29 exports: [
... ... @@ -30,6 +31,7 @@ import { UserModule } from '@modules/home/pages/user/user.module';
30 31 HomeLinksModule,
31 32 ProfileModule,
32 33 TenantModule,
  34 + DeviceModule,
33 35 // CustomerModule,
34 36 // AuditLogModule,
35 37 UserModule
... ...
... ... @@ -74,8 +74,6 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
74 74
75 75 filteredDashboards: Observable<Array<DashboardInfo>>;
76 76
77   - private valueLoaded = false;
78   -
79 77 private searchText = '';
80 78
81 79 private propagateChange = (v: any) => { };
... ... @@ -97,7 +95,21 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
97 95 }
98 96
99 97 ngOnInit() {
100   -
  98 + this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges
  99 + .pipe(
  100 + tap(value => {
  101 + let modelValue;
  102 + if (typeof value === 'string' || !value) {
  103 + modelValue = null;
  104 + } else {
  105 + modelValue = this.useIdValue ? value.id.id : value;
  106 + }
  107 + this.updateView(modelValue);
  108 + }),
  109 + startWith<string | DashboardInfo>(''),
  110 + map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
  111 + mergeMap(name => this.fetchDashboards(name) )
  112 + );
101 113 }
102 114
103 115 ngAfterViewInit(): void {
... ... @@ -123,48 +135,23 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
123 135 this.disabled = isDisabled;
124 136 }
125 137
126   - initFilteredResults(): void {
127   - this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges
128   - .pipe(
129   - startWith<string | DashboardInfo>(''),
130   - tap(value => {
131   - if (this.valueLoaded) {
132   - let modelValue;
133   - if (typeof value === 'string' || !value) {
134   - modelValue = null;
135   - } else {
136   - modelValue = this.useIdValue ? value.id.id : value;
137   - }
138   - this.updateView(modelValue);
139   - }
140   - }),
141   - map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
142   - mergeMap(name => this.fetchDashboards(name) )
143   - );
144   - }
145   -
146 138 writeValue(value: DashboardInfo | string | null): void {
147   - this.valueLoaded = false;
148 139 this.searchText = '';
149   - this.initFilteredResults();
150 140 if (value != null) {
151 141 if (typeof value === 'string') {
152 142 this.dashboardService.getDashboardInfo(value).subscribe(
153 143 (dashboard) => {
154 144 this.modelValue = this.useIdValue ? dashboard.id.id : dashboard;
155 145 this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: true});
156   - this.valueLoaded = true;
157 146 }
158 147 );
159 148 } else {
160 149 this.modelValue = this.useIdValue ? value.id.id : value;
161   - this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false});
162   - this.valueLoaded = true;
  150 + this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: true});
163 151 }
164 152 } else {
165 153 this.modelValue = null;
166   - this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: false});
167   - this.valueLoaded = true;
  154 + this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true});
168 155 }
169 156 }
170 157
... ...
... ... @@ -76,7 +76,8 @@ export class EntityTableColumn<T extends BaseData<HasId>> {
76 76 public title: string,
77 77 public maxWidth: string = '100%',
78 78 public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
79   - public cellStyleFunction: CellStyleFunction<T> = () => ({})) {
  79 + public cellStyleFunction: CellStyleFunction<T> = () => ({}),
  80 + public sortable: boolean = true) {
80 81 }
81 82 }
82 83
... ... @@ -135,3 +136,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
135 136 entitiesFetchFunction: EntitiesFetchFunction<T, P> = () => of(emptyPageData<T>());
136 137 onEntityAction: EntityActionFunction<T> = () => false;
137 138 }
  139 +
  140 +export function checkBoxCell(value: boolean): string {
  141 + return `<mat-icon class="material-icons mat-icon">${value ? 'check_box' : 'check_box_outline_blank'}</mat-icon>`;
  142 +}
... ...
... ... @@ -120,7 +120,7 @@
120 120 </mat-cell>
121 121 </ng-container>
122 122 <ng-container [matColumnDef]="column.key" *ngFor="let column of columns">
123   - <mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header> {{ column.title | translate }} </mat-header-cell>
  123 + <mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
124 124 <mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell>
125 125 </ng-container>
126 126 <ng-container matColumnDef="actions" stickyEnd>
... ...
... ... @@ -82,7 +82,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
82 82
83 83 isDetailsOpen = false;
84 84
85   - @ViewChild('entityTableHeader', {static: false}) entityTableHeaderAnchor: TbAnchorComponent;
  85 + @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent;
86 86
87 87 @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
88 88
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block">
  19 + <mat-label>{{ entitySubtypeText | translate }}</mat-label>
  20 + <input matInput type="text" placeholder="{{ selectEntitySubtypeText | translate }}"
  21 + #subTypeInput
  22 + formControlName="subType"
  23 + [required]="required"
  24 + [matAutocomplete]="subTypeAutocomplete">
  25 + <button *ngIf="subTypeFormGroup.get('subType').value && !disabled"
  26 + type="button"
  27 + matSuffix mat-button mat-icon-button aria-label="Clear"
  28 + (click)="clear()">
  29 + <mat-icon class="material-icons">close</mat-icon>
  30 + </button>
  31 + <mat-autocomplete #subTypeAutocomplete="matAutocomplete" [displayWith]="displaySubTypeFn">
  32 + <mat-option *ngFor="let subType of filteredSubTypes | async" [value]="subType">
  33 + <span [innerHTML]="subType.type | highlight:searchText"></span>
  34 + </mat-option>
  35 + </mat-autocomplete>
  36 + <mat-error *ngIf="subTypeFormGroup.get('subType').hasError('required')">
  37 + {{ entitySubtypeRequiredText | translate }}
  38 + </mat-error>
  39 +</mat-form-field>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core';
  18 +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
  19 +import {Observable, of, throwError, Subscription} from 'rxjs';
  20 +import {PageLink} from '@shared/models/page/page-link';
  21 +import {Direction} from '@shared/models/page/sort-order';
  22 +import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators';
  23 +import {PageData, emptyPageData} from '@shared/models/page/page-data';
  24 +import {DashboardInfo} from '@app/shared/models/dashboard.models';
  25 +import {DashboardId} from '@app/shared/models/id/dashboard-id';
  26 +import {DashboardService} from '@core/http/dashboard.service';
  27 +import {Store} from '@ngrx/store';
  28 +import {AppState} from '@app/core/core.state';
  29 +import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
  30 +import {Authority} from '@shared/models/authority.enum';
  31 +import {TranslateService} from '@ngx-translate/core';
  32 +import {DeviceService} from '@core/http/device.service';
  33 +import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models';
  34 +import {BroadcastService} from '@app/core/services/broadcast.service';
  35 +
  36 +@Component({
  37 + selector: 'tb-entity-subtype-autocomplete',
  38 + templateUrl: './entity-subtype-autocomplete.component.html',
  39 + styleUrls: [],
  40 + providers: [{
  41 + provide: NG_VALUE_ACCESSOR,
  42 + useExisting: forwardRef(() => EntitySubTypeAutocompleteComponent),
  43 + multi: true
  44 + }]
  45 +})
  46 +export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
  47 +
  48 + subTypeFormGroup: FormGroup;
  49 +
  50 + modelValue: string | null;
  51 +
  52 + @Input()
  53 + entityType: EntityType;
  54 +
  55 + @Input()
  56 + required: boolean;
  57 +
  58 + @Input()
  59 + disabled: boolean;
  60 +
  61 + @ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef;
  62 +
  63 + selectEntitySubtypeText: string;
  64 + entitySubtypeText: string;
  65 + entitySubtypeRequiredText: string;
  66 +
  67 + filteredSubTypes: Observable<Array<EntitySubtype>>;
  68 +
  69 + subTypes: Observable<Array<EntitySubtype>>;
  70 +
  71 + private broadcastSubscription: Subscription;
  72 +
  73 + private searchText = '';
  74 +
  75 + private propagateChange = (v: any) => { };
  76 +
  77 + constructor(private store: Store<AppState>,
  78 + private broadcast: BroadcastService,
  79 + public translate: TranslateService,
  80 + private deviceService: DeviceService,
  81 + private fb: FormBuilder) {
  82 + this.subTypeFormGroup = this.fb.group({
  83 + subType: [null]
  84 + });
  85 + }
  86 +
  87 + registerOnChange(fn: any): void {
  88 + this.propagateChange = fn;
  89 + }
  90 +
  91 + registerOnTouched(fn: any): void {
  92 + }
  93 +
  94 + ngOnInit() {
  95 +
  96 + switch (this.entityType) {
  97 + case EntityType.ASSET:
  98 + this.selectEntitySubtypeText = 'asset.select-asset-type';
  99 + this.entitySubtypeText = 'asset.asset-type';
  100 + this.entitySubtypeRequiredText = 'asset.asset-type-required';
  101 + this.broadcastSubscription = this.broadcast.on('assetSaved', () => {
  102 + this.subTypes = null;
  103 + });
  104 + break;
  105 + case EntityType.DEVICE:
  106 + this.selectEntitySubtypeText = 'device.select-device-type';
  107 + this.entitySubtypeText = 'device.device-type';
  108 + this.entitySubtypeRequiredText = 'device.device-type-required';
  109 + this.broadcastSubscription = this.broadcast.on('deviceSaved', () => {
  110 + this.subTypes = null;
  111 + });
  112 + break;
  113 + case EntityType.ENTITY_VIEW:
  114 + this.selectEntitySubtypeText = 'entity-view.select-entity-view-type';
  115 + this.entitySubtypeText = 'entity-view.entity-view-type';
  116 + this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required';
  117 + this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => {
  118 + this.subTypes = null;
  119 + });
  120 + break;
  121 + }
  122 +
  123 + this.filteredSubTypes = this.subTypeFormGroup.get('subType').valueChanges
  124 + .pipe(
  125 + tap(value => {
  126 + let modelValue;
  127 + if (!value) {
  128 + modelValue = null;
  129 + } else if (typeof value === 'string') {
  130 + modelValue = value;
  131 + } else {
  132 + modelValue = value.type;
  133 + }
  134 + this.updateView(modelValue);
  135 + }),
  136 + startWith<string | EntitySubtype>(''),
  137 + map(value => value ? (typeof value === 'string' ? value : value.type) : ''),
  138 + mergeMap(type => this.fetchSubTypes(type) )
  139 + );
  140 + }
  141 +
  142 + ngAfterViewInit(): void {
  143 + }
  144 +
  145 + ngOnDestroy(): void {
  146 + if (this.broadcastSubscription) {
  147 + this.broadcastSubscription.unsubscribe();
  148 + }
  149 + }
  150 +
  151 + setDisabledState(isDisabled: boolean): void {
  152 + this.disabled = isDisabled;
  153 + }
  154 +
  155 + writeValue(value: string | null): void {
  156 + this.searchText = '';
  157 + if (value != null) {
  158 + this.modelValue = value;
  159 + this.fetchSubTypes(value, true).subscribe(
  160 + (subTypes) => {
  161 + const subType = subTypes && subTypes.length === 1 ? subTypes[0] : null;
  162 + this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true});
  163 + }
  164 + );
  165 + } else {
  166 + this.modelValue = null;
  167 + this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true});
  168 + }
  169 + }
  170 +
  171 + updateView(value: string | null) {
  172 + if (this.modelValue !== value) {
  173 + this.modelValue = value;
  174 + this.propagateChange(this.modelValue);
  175 + }
  176 + }
  177 +
  178 + displaySubTypeFn(subType?: EntitySubtype): string | undefined {
  179 + return subType ? subType.type : undefined;
  180 + }
  181 +
  182 + fetchSubTypes(searchText?: string, strictMatch: boolean = false): Observable<Array<EntitySubtype>> {
  183 + this.searchText = searchText;
  184 + return this.getSubTypes().pipe(
  185 + map(subTypes => subTypes.filter( subType => {
  186 + if (strictMatch) {
  187 + return searchText ? subType.type === searchText : false;
  188 + } else {
  189 + return searchText ? subType.type.toUpperCase().startsWith(searchText.toUpperCase()) : true;
  190 + }
  191 + }))
  192 + );
  193 + }
  194 +
  195 + getSubTypes(): Observable<Array<EntitySubtype>> {
  196 + if (!this.subTypes) {
  197 + switch (this.entityType) {
  198 + case EntityType.ASSET:
  199 + // TODO:
  200 + break;
  201 + case EntityType.DEVICE:
  202 + this.subTypes = this.deviceService.getDeviceTypes(false, true);
  203 + break;
  204 + case EntityType.ENTITY_VIEW:
  205 + // TODO:
  206 + break;
  207 + }
  208 + if (this.subTypes) {
  209 + this.subTypes = this.subTypes.pipe(
  210 + publishReplay(1),
  211 + refCount()
  212 + );
  213 + } else {
  214 + return throwError(null);
  215 + }
  216 + }
  217 + return this.subTypes;
  218 + }
  219 +
  220 + clear() {
  221 + this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true});
  222 + setTimeout(() => {
  223 + this.subTypeInput.nativeElement.blur();
  224 + this.subTypeInput.nativeElement.focus();
  225 + }, 0);
  226 + }
  227 +
  228 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block">
  19 + <mat-label *ngIf="showLabel">{{ entitySubtypeTitle | translate }}</mat-label>
  20 + <mat-select class="tb-entity-subtype-select" matInput formControlName="subType">
  21 + <mat-option *ngFor="let subType of subTypesOptions | async" [value]="subType">
  22 + {{ displaySubTypeFn(subType) }}
  23 + </mat-option>
  24 + </mat-select>
  25 + <mat-error *ngIf="subTypeFormGroup.get('subType').hasError('required')">
  26 + {{ entitySubtypeRequiredText | translate }}
  27 + </mat-error>
  28 +</mat-form-field>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + mat-select.tb-entity-subtype-select {
  18 + min-width: 200px;
  19 + }
  20 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core';
  18 +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
  19 +import {Observable, of, throwError, Subscription, Subject} from 'rxjs';
  20 +import {PageLink} from '@shared/models/page/page-link';
  21 +import {Direction} from '@shared/models/page/sort-order';
  22 +import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators';
  23 +import {PageData, emptyPageData} from '@shared/models/page/page-data';
  24 +import {DashboardInfo} from '@app/shared/models/dashboard.models';
  25 +import {DashboardId} from '@app/shared/models/id/dashboard-id';
  26 +import {DashboardService} from '@core/http/dashboard.service';
  27 +import {Store} from '@ngrx/store';
  28 +import {AppState} from '@app/core/core.state';
  29 +import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
  30 +import {Authority} from '@shared/models/authority.enum';
  31 +import {TranslateService} from '@ngx-translate/core';
  32 +import {DeviceService} from '@core/http/device.service';
  33 +import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models';
  34 +import {BroadcastService} from '@app/core/services/broadcast.service';
  35 +
  36 +@Component({
  37 + selector: 'tb-entity-subtype-select',
  38 + templateUrl: './entity-subtype-select.component.html',
  39 + styleUrls: ['./entity-subtype-select.component.scss'],
  40 + providers: [{
  41 + provide: NG_VALUE_ACCESSOR,
  42 + useExisting: forwardRef(() => EntitySubTypeSelectComponent),
  43 + multi: true
  44 + }]
  45 +})
  46 +export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
  47 +
  48 + subTypeFormGroup: FormGroup;
  49 +
  50 + modelValue: string | null;
  51 +
  52 + @Input()
  53 + entityType: EntityType;
  54 +
  55 + @Input()
  56 + showLabel: boolean;
  57 +
  58 + @Input()
  59 + required: boolean;
  60 +
  61 + @Input()
  62 + disabled: boolean;
  63 +
  64 + @Input()
  65 + typeTranslatePrefix: string;
  66 +
  67 + @ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef;
  68 +
  69 + entitySubtypeTitle: string;
  70 + entitySubtypeRequiredText: string;
  71 +
  72 + subTypesOptions: Observable<Array<EntitySubtype | string>>;
  73 +
  74 + private subTypesOptionsSubject: Subject<string> = new Subject();
  75 +
  76 + subTypes: Observable<Array<EntitySubtype | string>>;
  77 +
  78 + private broadcastSubscription: Subscription;
  79 +
  80 + private propagateChange = (v: any) => { };
  81 +
  82 + constructor(private store: Store<AppState>,
  83 + private broadcast: BroadcastService,
  84 + public translate: TranslateService,
  85 + private deviceService: DeviceService,
  86 + private fb: FormBuilder) {
  87 + this.subTypeFormGroup = this.fb.group({
  88 + subType: [null]
  89 + });
  90 + }
  91 +
  92 + registerOnChange(fn: any): void {
  93 + this.propagateChange = fn;
  94 + }
  95 +
  96 + registerOnTouched(fn: any): void {
  97 + }
  98 +
  99 + ngOnInit() {
  100 +
  101 + switch (this.entityType) {
  102 + case EntityType.ASSET:
  103 + this.entitySubtypeTitle = 'asset.asset-type';
  104 + this.entitySubtypeRequiredText = 'asset.asset-type-required';
  105 + this.broadcastSubscription = this.broadcast.on('assetSaved', () => {
  106 + this.subTypes = null;
  107 + this.subTypesOptionsSubject.next('');
  108 + });
  109 + break;
  110 + case EntityType.DEVICE:
  111 + this.entitySubtypeTitle = 'device.device-type';
  112 + this.entitySubtypeRequiredText = 'device.device-type-required';
  113 + this.broadcastSubscription = this.broadcast.on('deviceSaved', () => {
  114 + this.subTypes = null;
  115 + this.subTypesOptionsSubject.next('');
  116 + });
  117 + break;
  118 + case EntityType.ENTITY_VIEW:
  119 + this.entitySubtypeTitle = 'entity-view.entity-view-type';
  120 + this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required';
  121 + this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => {
  122 + this.subTypes = null;
  123 + this.subTypesOptionsSubject.next('');
  124 + });
  125 + break;
  126 + }
  127 +
  128 + this.subTypesOptions = this.subTypesOptionsSubject.asObservable().pipe(
  129 + startWith<string | EntitySubtype>(''),
  130 + mergeMap(() => this.getSubTypes())
  131 + );
  132 +
  133 + this.subTypeFormGroup.get('subType').valueChanges.subscribe(
  134 + (value) => {
  135 + let modelValue;
  136 + if (!value || value === '') {
  137 + modelValue = '';
  138 + } else {
  139 + modelValue = value.type;
  140 + }
  141 + this.updateView(modelValue);
  142 + }
  143 + );
  144 + }
  145 +
  146 + ngAfterViewInit(): void {
  147 + }
  148 +
  149 + ngOnDestroy(): void {
  150 + if (this.broadcastSubscription) {
  151 + this.broadcastSubscription.unsubscribe();
  152 + }
  153 + }
  154 +
  155 + setDisabledState(isDisabled: boolean): void {
  156 + this.disabled = isDisabled;
  157 + }
  158 +
  159 + writeValue(value: string | null): void {
  160 + if (value != null && value !== '') {
  161 + this.modelValue = value;
  162 + this.findSubTypes(value).subscribe(
  163 + (subTypes) => {
  164 + const subType = subTypes && subTypes.length === 1 ? subTypes[0] : '';
  165 + this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true});
  166 + }
  167 + );
  168 + } else {
  169 + this.modelValue = '';
  170 + this.subTypeFormGroup.get('subType').patchValue('', {emitEvent: true});
  171 + }
  172 + }
  173 +
  174 + updateView(value: string | null) {
  175 + if (this.modelValue !== value) {
  176 + this.modelValue = value;
  177 + this.propagateChange(this.modelValue);
  178 + }
  179 + }
  180 +
  181 + displaySubTypeFn(subType?: EntitySubtype | string): string | undefined {
  182 + if (subType && typeof subType !== 'string') {
  183 + if (this.typeTranslatePrefix) {
  184 + return this.translate.instant(this.typeTranslatePrefix + '.' + subType.type);
  185 + } else {
  186 + return subType.type;
  187 + }
  188 + } else {
  189 + return this.translate.instant('entity.all-subtypes');
  190 + }
  191 + }
  192 +
  193 + findSubTypes(searchText?: string): Observable<Array<EntitySubtype | string>> {
  194 + return this.getSubTypes().pipe(
  195 + map(subTypes => subTypes.filter( subType => {
  196 + return searchText ? (typeof subType === 'string' ? false : subType.type === searchText) : false;
  197 + }))
  198 + );
  199 + }
  200 +
  201 + getSubTypes(): Observable<Array<EntitySubtype | string>> {
  202 + if (!this.subTypes) {
  203 + switch (this.entityType) {
  204 + case EntityType.ASSET:
  205 + // TODO:
  206 + break;
  207 + case EntityType.DEVICE:
  208 + this.subTypes = this.deviceService.getDeviceTypes(false, true);
  209 + break;
  210 + case EntityType.ENTITY_VIEW:
  211 + // TODO:
  212 + break;
  213 + }
  214 + if (this.subTypes) {
  215 + this.subTypes = this.subTypes.pipe(
  216 + map((allSubtypes) => {
  217 + allSubtypes.unshift('');
  218 + return allSubtypes;
  219 + }),
  220 + publishReplay(1),
  221 + refCount()
  222 + );
  223 + } else {
  224 + return throwError(null);
  225 + }
  226 + }
  227 + return this.subTypes;
  228 + }
  229 +
  230 + clear() {
  231 + this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true});
  232 + setTimeout(() => {
  233 + this.subTypeInput.nativeElement.blur();
  234 + this.subTypeInput.nativeElement.focus();
  235 + }, 0);
  236 + }
  237 +
  238 +}
... ...
... ... @@ -59,7 +59,8 @@ export const HelpLinks = {
59 59 securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
60 60 tenants: helpBaseUrl + '/docs/user-guide/ui/tenants',
61 61 customers: helpBaseUrl + '/docs/user-guide/customers',
62   - users: helpBaseUrl + '/docs/user-guide/ui/users'
  62 + users: helpBaseUrl + '/docs/user-guide/ui/users',
  63 + devices: helpBaseUrl + '/docs/user-guide/ui/devices'
63 64 }
64 65 };
65 66
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {BaseData} from '@shared/models/base-data';
  18 +import {DeviceId} from './id/device-id';
  19 +import {TenantId} from '@shared/models/id/tenant-id';
  20 +import {CustomerId} from '@shared/models/id/customer-id';
  21 +
  22 +export interface Device extends BaseData<DeviceId> {
  23 + tenantId: TenantId;
  24 + customerId: CustomerId;
  25 + name: string;
  26 + type: string;
  27 + label: string;
  28 + additionalInfo?: any;
  29 +}
  30 +
  31 +export interface DeviceInfo extends Device {
  32 + customerTitle: string;
  33 + customerIsPublic: boolean;
  34 +}
... ...
  1 +import {TenantId} from './id/tenant-id';
  2 +
1 3 ///
2 4 /// Copyright © 2016-2019 The Thingsboard Authors
3 5 ///
... ... @@ -88,6 +90,20 @@ export const entityTypeTranslations = new Map<EntityType, EntityTypeTranslation>
88 90 search: 'user.search',
89 91 selectedEntities: 'user.selected-users'
90 92 }
  93 + ],
  94 + [
  95 + EntityType.DEVICE,
  96 + {
  97 + type: 'entity.type-device',
  98 + typePlural: 'entity.type-devices',
  99 + list: 'entity.list-of-devices',
  100 + nameStartsWith: 'entity.device-name-starts-with',
  101 + details: 'device.device-details',
  102 + add: 'device.add',
  103 + noEntities: 'device.no-devices-text',
  104 + search: 'device.search',
  105 + selectedEntities: 'device.selected-devices'
  106 + }
91 107 ]
92 108 ]
93 109 );
... ... @@ -111,6 +127,18 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource>(
111 127 {
112 128 helpLinkId: 'users'
113 129 }
  130 + ],
  131 + [
  132 + EntityType.DEVICE,
  133 + {
  134 + helpLinkId: 'devices'
  135 + }
114 136 ]
115 137 ]
116 138 );
  139 +
  140 +export interface EntitySubtype {
  141 + tenantId: TenantId;
  142 + entityType: EntityType;
  143 + type: string;
  144 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { EntityId } from './entity-id';
  18 +import { EntityType } from '@shared/models/entity-type.models';
  19 +
  20 +export class DeviceId implements EntityId {
  21 + entityType = EntityType.DEVICE;
  22 + id: string;
  23 + constructor(id: string) {
  24 + this.id = id;
  25 + }
  26 +}
... ...
... ... @@ -78,6 +78,8 @@ import { ClipboardModule } from 'ngx-clipboard';
78 78 import { FullscreenDirective } from '@shared/components/fullscreen.directive';
79 79 import { HighlightPipe } from '@shared/pipe/highlight.pipe';
80 80 import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component';
  81 +import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component';
  82 +import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component';
81 83
82 84 @NgModule({
83 85 providers: [
... ... @@ -118,6 +120,8 @@ import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autoc
118 120 DatetimePeriodComponent,
119 121 // ValueInputComponent,
120 122 DashboardAutocompleteComponent,
  123 + EntitySubTypeAutocompleteComponent,
  124 + EntitySubTypeSelectComponent,
121 125 NospacePipe,
122 126 MillisecondsToTimeStringPipe,
123 127 EnumToArrayPipe,
... ... @@ -183,6 +187,8 @@ import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autoc
183 187 TimeintervalComponent,
184 188 DatetimePeriodComponent,
185 189 DashboardAutocompleteComponent,
  190 + EntitySubTypeAutocompleteComponent,
  191 + EntitySubTypeSelectComponent,
186 192 // ValueInputComponent,
187 193 MatButtonModule,
188 194 MatCheckboxModule,
... ...
... ... @@ -673,6 +673,7 @@
673 673 "no-device-types-matching": "No device types matching '{{entitySubtype}}' were found.",
674 674 "device-type-list-empty": "No device types selected.",
675 675 "device-types": "Device types",
  676 + "created-time": "Created time",
676 677 "name": "Name",
677 678 "name-required": "Name is required.",
678 679 "description": "Description",
... ... @@ -691,7 +692,9 @@
691 692 "device-public": "Device is public",
692 693 "select-device": "Select device",
693 694 "import": "Import device",
694   - "device-file": "Device file"
  695 + "device-file": "Device file",
  696 + "search": "Search devices",
  697 + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected"
695 698 },
696 699 "dialog": {
697 700 "close": "Close dialog"
... ...
... ... @@ -209,6 +209,13 @@ label {
209 209 }
210 210 }
211 211
  212 +div {
  213 + &.tb-small {
  214 + font-size: 14px;
  215 + color: rgba(0, 0, 0, .54);
  216 + }
  217 +}
  218 +
212 219 pre.tb-highlight {
213 220 display: block;
214 221 padding: 15px;
... ...
... ... @@ -253,6 +253,12 @@ $tb-dark-theme: get-tb-dark-theme(
253 253 }
254 254 }
255 255
  256 + .mat-cell {
  257 + mat-icon {
  258 + color: rgba(0, 0, 0, .54);
  259 + }
  260 + }
  261 +
256 262 mat-toolbar.mat-primary {
257 263 button.mat-icon-button {
258 264 mat-icon {
... ...