Browse Source

Merge remote-tracking branch 'upstream/master' into feature/persistent-page-link

pull/5856/head
Vladyslav_Prykhodko 5 years ago
parent
commit
a5aa5c999a
  1. 18
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  2. 13
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  3. 30
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  4. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java
  5. 4
      dao/src/main/java/org/thingsboard/server/dao/Dao.java
  6. 3
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
  7. 10
      dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java
  8. 4
      dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java
  9. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  10. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java
  11. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java
  12. 18
      dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
  13. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  14. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java
  15. 2
      ui-ngx/package.json
  16. 11
      ui-ngx/src/app/core/api/widget-api.models.ts
  17. 13
      ui-ngx/src/app/core/api/widget-subscription.ts
  18. 15
      ui-ngx/src/app/core/http/device.service.ts
  19. 14
      ui-ngx/src/app/core/utils.ts
  20. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  21. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  22. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  23. 25
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  24. 1
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  25. 1
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss
  26. 22
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  27. 57
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html
  28. 119
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss
  29. 182
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts
  30. 16
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  31. 2
      ui-ngx/src/app/modules/home/components/entity/entity.component.ts
  32. 7
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  33. 6
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  34. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  35. 7
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  36. 21
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.scss
  37. 9
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
  38. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts
  39. 8
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  40. 7
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html
  41. 21
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.scss
  42. 16
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts
  43. 6
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html
  44. 11
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  45. 4
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss
  46. 34
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts
  47. 3
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  48. 1
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
  49. 35
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  50. 12
      ui-ngx/src/app/modules/home/components/widget/legend.component.ts
  51. 1
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  52. 1
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss
  53. 19
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  54. 1
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  55. 1
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss
  56. 24
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  57. 52
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  58. 19
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  59. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  60. 29
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  61. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts
  62. 58
      ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts
  63. 92
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html
  64. 31
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss
  65. 75
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts
  66. 118
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html
  67. 34
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss
  68. 126
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts
  69. 44
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html
  70. 37
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss
  71. 67
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts
  72. 126
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html
  73. 53
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss
  74. 580
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts
  75. 15
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts
  76. 1
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  77. 1
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss
  78. 19
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  79. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  80. 4
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  81. 5
      ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts
  82. 10
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  83. 38
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  84. 13
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  85. 6
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  86. 5
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  87. 42
      ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts
  88. 6
      ui-ngx/src/app/modules/home/pages/asset/asset.component.html
  89. 10
      ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts
  90. 167
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  91. 6
      ui-ngx/src/app/modules/home/pages/customer/customer.component.html
  92. 10
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  93. 20
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts
  94. 13
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  95. 42
      ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts
  96. 7
      ui-ngx/src/app/modules/home/pages/device/device.component.html
  97. 1
      ui-ngx/src/app/modules/home/pages/device/device.component.ts
  98. 10
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  99. 131
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  100. 6
      ui-ngx/src/app/modules/home/pages/edge/edge.component.html

18
application/src/main/data/json/system/widget_bundles/control_widgets.json

File diff suppressed because one or more lines are too long

13
application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java

@ -178,8 +178,8 @@ public class RpcV2Controller extends AbstractRpcController {
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page, @RequestParam int page,
@ApiParam(value = "Status of the RPC", required = true, allowableValues = RPC_STATUS_ALLOWABLE_VALUES) @ApiParam(value = "Status of the RPC", allowableValues = RPC_STATUS_ALLOWABLE_VALUES)
@RequestParam RpcStatus rpcStatus, @RequestParam(required = false) RpcStatus rpcStatus,
@ApiParam(value = RPC_TEXT_SEARCH_DESCRIPTION) @ApiParam(value = RPC_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RPC_SORT_PROPERTY_ALLOWABLE_VALUES) @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RPC_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -188,7 +188,7 @@ public class RpcV2Controller extends AbstractRpcController {
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("DeviceId", strDeviceId); checkParameter("DeviceId", strDeviceId);
try { try {
if (rpcStatus.equals(RpcStatus.DELETED)) { if (rpcStatus != null && rpcStatus.equals(RpcStatus.DELETED)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "RpcStatus: DELETED"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "RpcStatus: DELETED");
} }
@ -200,7 +200,12 @@ public class RpcV2Controller extends AbstractRpcController {
accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() { accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) { public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
PageData<Rpc> rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); PageData<Rpc> rpcCalls;
if (rpcStatus != null) {
rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink);
} else {
rpcCalls = rpcService.findAllByDeviceId(tenantId, deviceId, pageLink);
}
response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK)); response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK));
} }

30
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -475,12 +475,30 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.info("Updating schema ..."); log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn); loadSql(schemaUpdateFile, conn);
log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); try {
conn.createStatement().execute("call update_profile_bootstrap();"); conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, alarm_type, customer_id, alarm_id)" +
log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); " select tenant_id, originator_id, created_time, type, customer_id, id from alarm;");
log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, alarm_type, customer_id, alarm_id)" +
conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();"); " select a.tenant_id, r.from_id, created_time, type, customer_id, id" +
log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated."); " from alarm a inner join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id ON CONFLICT DO NOTHING;");
conn.createStatement().execute("delete from relation r where r.relation_type_group = 'ALARM';");
} catch (Exception e) {
log.error("Failed to update alarm relations!!!", e);
}
log.info("Updating lwm2m device profiles ...");
try {
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql");
loadSql(schemaUpdateFile, conn);
log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M...");
conn.createStatement().execute("call update_profile_bootstrap();");
log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated.");
log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M...");
conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();");
log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated.");
} catch (Exception e) {
log.error("Failed to update lwm2m profiles!!!", e);
}
log.info("Updating schema settings..."); log.info("Updating schema settings...");
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003003;"); conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003003;");
log.info("Schema updated."); log.info("Schema updated.");

2
common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java

@ -35,5 +35,7 @@ public interface RpcService {
ListenableFuture<Rpc> findRpcByIdAsync(TenantId tenantId, RpcId id); ListenableFuture<Rpc> findRpcByIdAsync(TenantId tenantId, RpcId id);
PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink);
PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
} }

4
dao/src/main/java/org/thingsboard/server/dao/Dao.java

@ -30,6 +30,10 @@ public interface Dao<T> {
ListenableFuture<T> findByIdAsync(TenantId tenantId, UUID id); ListenableFuture<T> findByIdAsync(TenantId tenantId, UUID id);
boolean existsById(TenantId tenantId, UUID id);
ListenableFuture<Boolean> existsByIdAsync(TenantId tenantId, UUID id);
T save(TenantId tenantId, T t); T save(TenantId tenantId, T t);
boolean removeById(TenantId tenantId, UUID id); boolean removeById(TenantId tenantId, UUID id);

3
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
public class AuditLogLevelFilter { public class AuditLogLevelFilter {
@ -28,7 +29,7 @@ public class AuditLogLevelFilter {
public AuditLogLevelFilter(Map<String, String> mask) { public AuditLogLevelFilter(Map<String, String> mask) {
entityTypeMask.clear(); entityTypeMask.clear();
mask.forEach((entityTypeStr, logLevelMaskStr) -> { mask.forEach((entityTypeStr, logLevelMaskStr) -> {
EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase()); EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase(Locale.ENGLISH));
AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase()); AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase());
entityTypeMask.put(entityType, logLevelMask); entityTypeMask.put(entityType, logLevelMask);
}); });

10
dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java

@ -82,7 +82,15 @@ public class BaseRpcService implements RpcService {
log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], rpcStatus [{}], pageLink [{}]", tenantId, deviceId, rpcStatus, pageLink); log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], rpcStatus [{}], pageLink [{}]", tenantId, deviceId, rpcStatus, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validatePageLink(pageLink); validatePageLink(pageLink);
return rpcDao.findAllByDeviceId(tenantId, deviceId, rpcStatus, pageLink); return rpcDao.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink);
}
@Override
public PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) {
log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], pageLink [{}]", tenantId, deviceId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validatePageLink(pageLink);
return rpcDao.findAllByDeviceId(tenantId, deviceId, pageLink);
} }
private PaginatedRemover<TenantId, Rpc> tenantRpcRemover = private PaginatedRemover<TenantId, Rpc> tenantRpcRemover =

4
dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java

@ -24,7 +24,9 @@ import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
public interface RpcDao extends Dao<Rpc> { public interface RpcDao extends Dao<Rpc> {
PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink);
PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
PageData<Rpc> findAllRpcByTenantId(TenantId tenantId, PageLink pageLink); PageData<Rpc> findAllRpcByTenantId(TenantId tenantId, PageLink pageLink);

12
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java

@ -80,6 +80,18 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
return service.submit(() -> DaoUtil.getData(getCrudRepository().findById(key))); return service.submit(() -> DaoUtil.getData(getCrudRepository().findById(key)));
} }
@Override
public boolean existsById(TenantId tenantId, UUID key) {
log.debug("Exists by key {}", key);
return getCrudRepository().existsById(key);
}
@Override
public ListenableFuture<Boolean> existsByIdAsync(TenantId tenantId, UUID key) {
log.debug("Exists by key async {}", key);
return service.submit(() -> getCrudRepository().existsById(key));
}
@Override @Override
@Transactional @Transactional
public boolean removeById(TenantId tenantId, UUID id) { public boolean removeById(TenantId tenantId, UUID id) {

7
dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java

@ -50,7 +50,12 @@ public class JpaRpcDao extends JpaAbstractDao<RpcEntity, Rpc> implements RpcDao
} }
@Override @Override
public PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) { public PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) {
return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceId(tenantId.getId(), deviceId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) {
return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceIdAndStatus(tenantId.getId(), deviceId.getId(), rpcStatus, DaoUtil.toPageable(pageLink))); return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceIdAndStatus(tenantId.getId(), deviceId.getId(), rpcStatus, DaoUtil.toPageable(pageLink)));
} }

2
dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java

@ -26,6 +26,8 @@ import org.thingsboard.server.dao.model.sql.RpcEntity;
import java.util.UUID; import java.util.UUID;
public interface RpcRepository extends CrudRepository<RpcEntity, UUID> { public interface RpcRepository extends CrudRepository<RpcEntity, UUID> {
Page<RpcEntity> findAllByTenantIdAndDeviceId(UUID tenantId, UUID deviceId, Pageable pageable);
Page<RpcEntity> findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable); Page<RpcEntity> findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable);
Page<RpcEntity> findAllByTenantId(UUID tenantId, Pageable pageable); Page<RpcEntity> findAllByTenantId(UUID tenantId, Pageable pageable);

18
dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java

@ -27,8 +27,9 @@ import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.service.AbstractServiceTest; import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.tenant.TenantDao;
import java.util.List; import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
/** /**
@ -75,4 +76,19 @@ public class JpaTenantDaoTest extends AbstractJpaDaoTest {
tenantDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, tenant); tenantDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, tenant);
} }
@Test
@DatabaseSetup("classpath:dbunit/empty_dataset.xml")
public void testIsExistsTenantById() {
final UUID uuid = Uuids.timeBased();
final TenantId tenantId = new TenantId(uuid);
assertThat(tenantDao.existsById(tenantId, uuid)).as("Is tenant exists before save").isFalse();
final Tenant tenant = new Tenant();
tenant.setId(tenantId);
tenant.setTitle("Tenant " + uuid);
tenantDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, tenant);
assertThat(tenantDao.existsById(tenantId, uuid)).as("Is tenant exists after save").isTrue();
}
} }

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -104,7 +104,8 @@ public class TbMqttNode implements TbNode {
protected MqttClient initClient(TbContext ctx) throws Exception { protected MqttClient initClient(TbContext ctx) throws Exception {
MqttClientConfig config = new MqttClientConfig(getSslContext()); MqttClientConfig config = new MqttClientConfig(getSslContext());
if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) {
config.setClientId(this.mqttNodeConfiguration.getClientId()); config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId());
} }
config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); config.setCleanSession(this.mqttNodeConfiguration.isCleanSession());

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java

@ -28,6 +28,7 @@ public class TbMqttNodeConfiguration implements NodeConfiguration<TbMqttNodeConf
private int port; private int port;
private int connectTimeoutSec; private int connectTimeoutSec;
private String clientId; private String clientId;
private boolean appendClientIdSuffix;
private boolean cleanSession; private boolean cleanSession;
private boolean ssl; private boolean ssl;

2
ui-ngx/package.json

@ -28,7 +28,7 @@
"@date-io/date-fns": "^2.11.0", "@date-io/date-fns": "^2.11.0",
"@flowjs/flow.js": "^2.14.1", "@flowjs/flow.js": "^2.14.1",
"@flowjs/ngx-flow": "~0.4.6", "@flowjs/ngx-flow": "~0.4.6",
"@geoman-io/leaflet-geoman-free": "^2.11.3", "@geoman-io/leaflet-geoman-free": "^2.11.4",
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.3.1",
"@mat-datetimepicker/core": "~7.0.1", "@mat-datetimepicker/core": "~7.0.1",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",

11
ui-ngx/src/app/core/api/widget-api.models.ts

@ -55,6 +55,7 @@ import { TranslateService } from '@ngx-translate/core';
import { AlarmDataService } from '@core/api/alarm-data.service'; import { AlarmDataService } from '@core/api/alarm-data.service';
import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models';
import { PopoverPlacement } from '@shared/components/popover.models'; import { PopoverPlacement } from '@shared/components/popover.models';
import { PersistentRpc } from '@shared/models/rpc.models';
export interface TimewindowFunctions { export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@ -71,9 +72,9 @@ export interface WidgetSubscriptionApi {
export interface RpcApi { export interface RpcApi {
sendOneWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, sendOneWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string) => Observable<any>; persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable<any>;
sendTwoWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, sendTwoWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string) => Observable<any>; persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable<any>;
completedCommand: () => void; completedCommand: () => void;
} }
@ -288,6 +289,8 @@ export interface IWidgetSubscription {
comparisonEnabled?: boolean; comparisonEnabled?: boolean;
comparisonTimeWindow?: WidgetTimewindow; comparisonTimeWindow?: WidgetTimewindow;
persistentRequests?: PageData<PersistentRpc>;
alarms?: PageData<AlarmData>; alarms?: PageData<AlarmData>;
alarmSource?: Datasource; alarmSource?: Datasource;
@ -314,9 +317,9 @@ export interface IWidgetSubscription {
updateTimewindowConfig(newTimewindow: Timewindow): void; updateTimewindowConfig(newTimewindow: Timewindow): void;
sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any>; persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any>;
sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any>; persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any>;
clearRpcError(): void; clearRpcError(): void;
subscribe(): void; subscribe(): void;

13
ui-ngx/src/app/core/api/widget-subscription.ts

@ -656,13 +656,13 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any> { persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any> {
return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, requestUUID); return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID);
} }
sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any> { persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any> {
return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, requestUUID); return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID);
} }
clearRpcError(): void { clearRpcError(): void {
@ -679,7 +679,8 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number, sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number,
persistent?: boolean, persistentPollingInterval?: number, requestUUID?: string): Observable<any> { persistent?: boolean, persistentPollingInterval?: number, retries?: number,
additionalInfo?: any, requestUUID?: string): Observable<any> {
if (!this.rpcEnabled) { if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!')); return throwError(new Error('Rpc disabled!'));
} else { } else {
@ -692,6 +693,8 @@ export class WidgetSubscription implements IWidgetSubscription {
method, method,
params, params,
persistent, persistent,
retries,
additionalInfo,
requestUUID requestUUID
}; };
if (timeout && timeout > 0) { if (timeout && timeout > 0) {

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

@ -31,7 +31,7 @@ import {
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models'; import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
import { PersistentRpc } from '@shared/models/rpc.models'; import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -143,6 +143,19 @@ export class DeviceService {
return this.http.get<PersistentRpc>(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<PersistentRpc>(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config));
} }
public deletePersistedRpc(rpcId: string, config?: RequestConfig) {
return this.http.delete<PersistentRpc>(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config));
}
public getPersistedRpcRequests(deviceId: string, pageLink: PageLink,
rpcStatus?: RpcStatus, config?: RequestConfig): Observable<PageData<PersistentRpc>> {
let url = `/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}`;
if (rpcStatus && rpcStatus.length) {
url += `&rpcStatus=${rpcStatus}`;
}
return this.http.get<PageData<PersistentRpc>>(url, defaultHttpOptionsFromConfig(config));
}
public findByQuery(query: DeviceSearchQuery, public findByQuery(query: DeviceSearchQuery,
config?: RequestConfig): Observable<Array<Device>> { config?: RequestConfig): Observable<Array<Device>> {
return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptionsFromConfig(config)); return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptionsFromConfig(config));

14
ui-ngx/src/app/core/utils.ts

@ -20,6 +20,10 @@ import { finalize, share } from 'rxjs/operators';
import { Datasource } from '@app/shared/models/widget.models'; import { Datasource } from '@app/shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { baseDetailsPageByEntityType } from '@shared/models/constants';
import { EntityType } from '@shared/models/entity-type.models';
const varsRegex = /\${([^}]*)}/g; const varsRegex = /\${([^}]*)}/g;
@ -460,3 +464,13 @@ export function randomAlphanumeric(length: number): string {
} }
return result; return result;
} }
export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction<EntityDetailsPageComponent>
= ((route, translate, component) => {
return component.entity?.name || component.headerSubtitle;
});
export function getEntityDetailsPageURL(id: string, entityType: EntityType): string {
return `${baseDetailsPageByEntityType.get(entityType)}/${id}`;
}

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -139,6 +139,7 @@ import * as QueueTypeListComponent from '@shared/components/queue/queue-type-lis
import * as RelationTypeAutocompleteComponent from '@shared/components/relation/relation-type-autocomplete.component'; import * as RelationTypeAutocompleteComponent from '@shared/components/relation/relation-type-autocomplete.component';
import * as SocialSharePanelComponent from '@shared/components/socialshare-panel.component'; import * as SocialSharePanelComponent from '@shared/components/socialshare-panel.component';
import * as JsonObjectEditComponent from '@shared/components/json-object-edit.component'; import * as JsonObjectEditComponent from '@shared/components/json-object-edit.component';
import * as JsonObjectViewComponent from '@shared/components/json-object-view.component';
import * as JsonContentComponent from '@shared/components/json-content.component'; import * as JsonContentComponent from '@shared/components/json-content.component';
import * as JsFuncComponent from '@shared/components/js-func.component'; import * as JsFuncComponent from '@shared/components/js-func.component';
import * as FabToolbarComponent from '@shared/components/fab-toolbar.component'; import * as FabToolbarComponent from '@shared/components/fab-toolbar.component';
@ -420,6 +421,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/relation/relation-type-autocomplete.component': RelationTypeAutocompleteComponent, '@shared/components/relation/relation-type-autocomplete.component': RelationTypeAutocompleteComponent,
'@shared/components/socialshare-panel.component': SocialSharePanelComponent, '@shared/components/socialshare-panel.component': SocialSharePanelComponent,
'@shared/components/json-object-edit.component': JsonObjectEditComponent, '@shared/components/json-object-edit.component': JsonObjectEditComponent,
'@shared/components/json-object-view.component': JsonObjectViewComponent,
'@shared/components/json-content.component': JsonContentComponent, '@shared/components/json-content.component': JsonContentComponent,
'@shared/components/js-func.component': JsFuncComponent, '@shared/components/js-func.component': JsFuncComponent,
'@shared/components/fab-toolbar.component': FabToolbarComponent, '@shared/components/fab-toolbar.component': FabToolbarComponent,

1
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -196,6 +196,7 @@
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]" [pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator> showFirstLastButtons></mat-paginator>
<ngx-hm-carousel fxFlex *ngIf="mode === 'widget' && widgetsList.length > 0" <ngx-hm-carousel fxFlex *ngIf="mode === 'widget' && widgetsList.length > 0"
#carousel #carousel

1
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -18,6 +18,7 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
.tb-entity-table { .tb-entity-table {
.tb-entity-table-content { .tb-entity-table-content {
width: 100%; width: 100%;

25
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -84,6 +84,8 @@ import {
} from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; } from '@home/components/attribute/add-widget-to-dashboard-dialog.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { Filters } from '@shared/models/query/query.models'; import { Filters } from '@shared/models/query/query.models';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { ResizeObserver } from '@juggle/resize-observer';
@Component({ @Component({
@ -109,6 +111,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
pageLink: PageLink; pageLink: PageLink;
textSearchMode = false; textSearchMode = false;
dataSource: AttributeDatasource; dataSource: AttributeDatasource;
hidePageSize = false;
activeValue = false; activeValue = false;
dirtyValue = false; dirtyValue = false;
@ -127,10 +130,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
aliasController: IAliasController; aliasController: IAliasController;
private widgetDatasource: Datasource; private widgetDatasource: Datasource;
private widgetResize$: ResizeObserver;
private disableAttributeScopeSelectionValue: boolean; private disableAttributeScopeSelectionValue: boolean;
get disableAttributeScopeSelection(): boolean { get disableAttributeScopeSelection(): boolean {
return this.disableAttributeScopeSelectionValue; return this.disableAttributeScopeSelectionValue;
} }
@Input() @Input()
set disableAttributeScopeSelection(value: boolean) { set disableAttributeScopeSelection(value: boolean) {
this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value); this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value);
@ -184,7 +191,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private widgetService: WidgetService, private widgetService: WidgetService,
private zone: NgZone, private zone: NgZone,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(store); super(store);
this.dirtyValue = !this.activeValue; this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC };
@ -193,6 +201,20 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
} }
ngOnInit() { ngOnInit() {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
ngOnDestroy() {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
} }
attributeScopeChanged(attributeScope: TelemetryType) { attributeScopeChanged(attributeScope: TelemetryType) {
@ -263,6 +285,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
this.attributeScopeSelectionReadonly = true; this.attributeScopeSelectionReadonly = true;
} }
this.mode = 'default'; this.mode = 'default';
this.selectedWidgetsBundleAlias = null;
this.attributeScope = this.defaultAttributeScope; this.attributeScope = this.defaultAttributeScope;
this.pageLink.textSearch = null; this.pageLink.textSearch = null;
if (this.viewsInited) { if (this.viewsInited) {

1
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -266,6 +266,7 @@
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator> showFirstLastButtons></mat-paginator>
</div> </div>
</div> </div>

1
ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss

@ -18,6 +18,7 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
.tb-entity-table { .tb-entity-table {
.tb-entity-table-content { .tb-entity-table-content {
width: 100%; width: 100%;

22
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -61,6 +61,8 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { isDefined, isEmptyStr, isUndefined } from '@core/utils'; import { isDefined, isEmptyStr, isUndefined } from '@core/utils';
import { HasUUID } from '@shared/models/id/has-uuid'; import { HasUUID } from '@shared/models/id/has-uuid';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models'; import { IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models';
@Component({ @Component({
@ -96,6 +98,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
defaultPageSize = 10; defaultPageSize = 10;
displayPagination = true; displayPagination = true;
hidePageSize = false;
pageSizeOptions; pageSizeOptions;
pageLink: PageLink; pageLink: PageLink;
pageMode = true; pageMode = true;
@ -118,6 +121,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
private updateDataSubscription: Subscription; private updateDataSubscription: Subscription;
private viewInited = false; private viewInited = false;
private widgetResize$: ResizeObserver;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
public route: ActivatedRoute, public route: ActivatedRoute,
public translate: TranslateService, public translate: TranslateService,
@ -126,7 +131,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
private domSanitizer: DomSanitizer, private domSanitizer: DomSanitizer,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private router: Router, private router: Router,
private componentFactoryResolver: ComponentFactoryResolver) { private componentFactoryResolver: ComponentFactoryResolver,
private elementRef: ElementRef) {
super(store); super(store);
} }
@ -136,6 +142,20 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} else { } else {
this.init(this.route.snapshot.data.entitiesTableConfig); this.init(this.route.snapshot.data.entitiesTableConfig);
} }
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
ngOnDestroy() {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {

57
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-card class="settings-card">
<mat-toolbar class="details-toolbar">
<div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center">
<div class="tb-details-title-header" fxLayout="column" fxLayoutAlign="center start">
<div class="tb-details-title tb-ellipsis">{{ headerTitle }}</div>
<div class="tb-details-subtitle tb-ellipsis">{{ headerSubtitle }}</div>
</div>
<div class="tb-help" [tb-help]="helpLinkId()"></div>
<span fxFlex></span>
<section *ngIf="!isReadOnly" fxLayout="row" class="tb-header-button" fxLayoutGap="8px">
<button [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"
mat-fab
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header"
[ngClass]="{'tb-hide': !isEdit}"
(click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async)"
mat-fab
matTooltip="{{ 'action.decline-changes' | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header"
(click)="onToggleDetailsEditMode()">
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</button>
</section>
</div>
</mat-toolbar>
<mat-card-content fxFlex="100">
<mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': hideDetailsTabs()}" [(selectedIndex)]="selectedTab" fxFill>
<mat-tab label="{{ 'details.details' | translate }}">
<tb-anchor #entityDetailsForm></tb-anchor>
</mat-tab>
<tb-anchor #entityTabs></tb-anchor>
</mat-tab-group>
</mat-card-content>
</mat-card>

119
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss

@ -0,0 +1,119 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.settings-card {
margin: 8px;
padding: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.details-toolbar {
height: 84px;
min-height: 84px;
border-radius: 4px 4px 0 0;
background: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
.mat-toolbar-tools {
padding: 0 8px;
}
.tb-details-title-header {
min-width: 0;
width: auto;
}
.tb-details-title {
font-size: 1rem;
font-weight: 500;
@media #{$mat-gt-sm} {
font-size: 1.2rem;
}
}
.tb-details-subtitle {
font-size: 0.9rem;
opacity: .8;
}
.tb-ellipsis {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@media #{$mat-md} {
width: 80%;
}
@media #{$mat-gt-md} {
width: 60%;
}
.tb-header-button {
.tb-btn-header {
position: relative !important;
display: inline-block !important;
animation: tbMoveFromTopFade .3s ease both;
&.tb-hide {
animation: tbMoveToTopFade .3s ease both;
}
}
}
}
}
:host ::ng-deep {
.tb-help {
.mat-icon-button.mat-primary {
color: rgba(0, 0, 0, 0.52);
}
}
.mat-card-content {
position: relative;
overflow: hidden;
> .mat-tab-group {
> .mat-tab-body-wrapper {
position: absolute;
top: 49px;
left: 0;
right: 0;
bottom: 0;
}
> .mat-tab-header {
.mat-tab-label {
min-width: 40px;
}
}
}
}
}

182
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts

@ -0,0 +1,182 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
HostBinding,
Injector,
OnDestroy,
OnInit
} from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { BaseData, HasId } from '@shared/models/base-data';
import { ActivatedRoute, Router } from '@angular/router';
import { FormGroup } from '@angular/forms';
import { AssetId } from '@shared/models/id/asset-id';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, mergeDeep } from '@core/utils';
import { BroadcastService } from '@core/services/broadcast.service';
import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component';
import { DialogService } from '@core/services/dialog.service';
@Component({
selector: 'tb-entity-details-page',
templateUrl: './entity-details-page.component.html',
styleUrls: ['./entity-details-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EntityDetailsPageComponent extends EntityDetailsPanelComponent implements OnInit, OnDestroy {
headerTitle: string;
headerSubtitle: string;
isReadOnly = false;
set entitiesTableConfig(entitiesTableConfig: EntityTableConfig<BaseData<HasId>>) {
if (this.entitiesTableConfigValue !== entitiesTableConfig) {
this.entitiesTableConfigValue = entitiesTableConfig;
if (this.entitiesTableConfigValue) {
this.isEdit = false;
this.entity = null;
}
}
}
get entitiesTableConfig(): EntityTableConfig<BaseData<HasId>> {
return this.entitiesTableConfigValue;
}
@HostBinding('class') 'tb-absolute-fill';
constructor(private route: ActivatedRoute,
private router: Router,
protected injector: Injector,
protected cd: ChangeDetectorRef,
protected componentFactoryResolver: ComponentFactoryResolver,
private broadcast: BroadcastService,
private translate: TranslateService,
private dialogService: DialogService,
protected store: Store<AppState>) {
super(store, injector, cd, componentFactoryResolver);
this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig;
}
ngOnInit() {
this.headerSubtitle = '';
this.route.paramMap.subscribe( paramMap => {
this.entityId = new AssetId(paramMap.get('entityId'));
});
this.headerSubtitle = this.translate.instant(this.entitiesTableConfig.entityTranslations.details);
super.init();
this.entityComponent.isDetailsPage = true;
this.subscriptions.push(this.entityAction.subscribe((action) => {
if (action.action === 'delete') {
this.deleteEntity(action.event, action.entity);
}
}));
}
ngOnDestroy() {
super.ngOnDestroy();
}
reload(): void {
this.isEdit = false;
this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe(
(entity) => {
this.entity = entity;
this.broadcast.broadcast('updateBreadcrumb');
this.isReadOnly = this.entitiesTableConfig.detailsReadonly(entity);
this.headerTitle = this.entitiesTableConfig.entityTitle(entity);
this.entityComponent.entity = entity;
this.entityComponent.isEdit = false;
if (this.entityTabsComponent) {
this.entityTabsComponent.entity = entity;
}
}
);
}
onToggleDetailsEditMode() {
if (this.isEdit) {
this.entityComponent.entity = this.entity;
if (this.entityTabsComponent) {
this.entityTabsComponent.entity = this.entity;
}
this.isEdit = !this.isEdit;
} else {
this.isEdit = !this.isEdit;
this.editingEntity = deepClone(this.entity);
this.entityComponent.entity = this.editingEntity;
if (this.entityTabsComponent) {
this.entityTabsComponent.entity = this.editingEntity;
}
if (this.entitiesTableConfig.hideDetailsTabsOnEdit) {
this.selectedTab = 0;
}
}
}
onApplyDetails() {
if (this.detailsForm && this.detailsForm.valid) {
const editingEntity = {...this.editingEntity, ...this.detailsForm.getRawValue()};
if (this.detailsForm.hasOwnProperty('additionalInfo')) {
editingEntity.additionalInfo =
mergeDeep((this.editingEntity as any).additionalInfo, this.detailsForm.getRawValue()?.additionalInfo);
}
this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe(
(entity) => {
this.entity = entity;
this.entityComponent.entity = entity;
if (this.entityTabsComponent) {
this.entityTabsComponent.entity = entity;
}
this.isEdit = false;
}
);
}
}
confirmForm(): FormGroup {
return this.detailsForm;
}
private deleteEntity($event: Event, entity: BaseData<HasId>) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.entitiesTableConfig.deleteEntityTitle(entity),
this.entitiesTableConfig.deleteEntityContent(entity),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.entitiesTableConfig.deleteEntity(entity.id).subscribe(
() => {
this.router.navigate(['../'], {relativeTo: this.route});
}
);
}
});
}
}

16
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts

@ -88,15 +88,15 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
entity: BaseData<HasId>; entity: BaseData<HasId>;
editingEntity: BaseData<HasId>; editingEntity: BaseData<HasId>;
private currentEntityId: HasId; protected currentEntityId: HasId;
private subscriptions: Subscription[] = []; protected subscriptions: Subscription[] = [];
private viewInited = false; protected viewInited = false;
private pendingTabs: MatTab[]; protected pendingTabs: MatTab[];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private injector: Injector, protected injector: Injector,
private cd: ChangeDetectorRef, protected cd: ChangeDetectorRef,
private componentFactoryResolver: ComponentFactoryResolver) { protected componentFactoryResolver: ComponentFactoryResolver) {
super(store); super(store);
} }
@ -139,7 +139,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
return this.isEditValue; return this.isEditValue;
} }
private init() { protected init() {
this.translations = this.entitiesTableConfig.entityTranslations; this.translations = this.entitiesTableConfig.entityTranslations;
this.resources = this.entitiesTableConfig.entityResources; this.resources = this.entitiesTableConfig.entityResources;
this.buildEntityComponent(); this.buildEntityComponent();

2
ui-ngx/src/app/modules/home/components/entity/entity.component.ts

@ -38,6 +38,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
isEditValue: boolean; isEditValue: boolean;
isDetailsPage = false;
@Input() @Input()
set entitiesTableConfig(entitiesTableConfig: C) { set entitiesTableConfig(entitiesTableConfig: C) {
this.setEntitiesTableConfig(entitiesTableConfig); this.setEntitiesTableConfig(entitiesTableConfig);

7
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts

@ -109,8 +109,11 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, Va
} }
private updateModel() { private updateModel() {
const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue(); let predicate: BooleanFilterPredicate = null;
predicate.type = FilterPredicateType.BOOLEAN; if (this.booleanFilterPredicateFormGroup.valid) {
predicate = this.booleanFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.BOOLEAN;
}
this.propagateChange(predicate); this.propagateChange(predicate);
} }

6
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts

@ -134,6 +134,10 @@ export class KeyFilterDialogComponent extends
} }
); );
} }
if (valueType === EntityKeyValueType.BOOLEAN && this.isConstantKeyType) {
this.keyFilterFormGroup.get('value').clearValidators();
this.keyFilterFormGroup.get('value').updateValueAndValidity();
}
}); });
this.keyFilterFormGroup.get('key.type').valueChanges.pipe( this.keyFilterFormGroup.get('key.type').valueChanges.pipe(
@ -148,7 +152,7 @@ export class KeyFilterDialogComponent extends
this.showAutocomplete = false; this.showAutocomplete = false;
} }
if (this.data.telemetryKeysOnly) { if (this.data.telemetryKeysOnly) {
if (type === EntityKeyType.CONSTANT) { if (type === EntityKeyType.CONSTANT && (this.keyFilterFormGroup.get('valueType').value !== EntityKeyValueType.BOOLEAN)) {
this.keyFilterFormGroup.get('value').setValidators(Validators.required); this.keyFilterFormGroup.get('value').setValidators(Validators.required);
this.keyFilterFormGroup.get('value').updateValueAndValidity(); this.keyFilterFormGroup.get('value').updateValueAndValidity();
} else { } else {

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -147,6 +147,7 @@ import {
HOME_COMPONENTS_MODULE_TOKEN HOME_COMPONENTS_MODULE_TOKEN
} from '@home/components/tokens'; } from '@home/components/tokens';
import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component'; import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -155,6 +156,7 @@ import { DashboardStateComponent } from '@home/components/dashboard-page/dashboa
AddEntityDialogComponent, AddEntityDialogComponent,
DetailsPanelComponent, DetailsPanelComponent,
EntityDetailsPanelComponent, EntityDetailsPanelComponent,
EntityDetailsPageComponent,
AuditLogTableComponent, AuditLogTableComponent,
AuditLogDetailsDialogComponent, AuditLogDetailsDialogComponent,
EventContentDialogComponent, EventContentDialogComponent,
@ -282,6 +284,7 @@ import { DashboardStateComponent } from '@home/components/dashboard-page/dashboa
AddEntityDialogComponent, AddEntityDialogComponent,
DetailsPanelComponent, DetailsPanelComponent,
EntityDetailsPanelComponent, EntityDetailsPanelComponent,
EntityDetailsPageComponent,
AuditLogTableComponent, AuditLogTableComponent,
EventTableComponent, EventTableComponent,
EdgeDownlinkTableHeaderComponent, EdgeDownlinkTableHeaderComponent,

7
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

@ -38,6 +38,13 @@
(click)="editDeviceProfile($event)"> (click)="editDeviceProfile($event)">
<mat-icon class="material-icons">edit</mat-icon> <mat-icon class="material-icons">edit</mat-icon>
</button> </button>
<button *ngIf="showDetailsPageLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Open device profile">
<a [routerLink]=deviceProfileURL>
<mat-icon class="material-icons">open_in_new</mat-icon>
</a>
</button>
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
(closed)="onPanelClosed()" (closed)="onPanelClosed()"

21
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.scss

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host{
.mat-icon-button a {
border-bottom: none;
color: inherit;
}
}

9
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts

@ -47,11 +47,12 @@ import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-
import { MatAutocomplete } from '@angular/material/autocomplete'; import { MatAutocomplete } from '@angular/material/autocomplete';
import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component'; import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component';
import { emptyPageData } from '@shared/models/page/page-data'; import { emptyPageData } from '@shared/models/page/page-data';
import { getEntityDetailsPageURL } from '@core/utils';
@Component({ @Component({
selector: 'tb-device-profile-autocomplete', selector: 'tb-device-profile-autocomplete',
templateUrl: './device-profile-autocomplete.component.html', templateUrl: './device-profile-autocomplete.component.html',
styleUrls: [], styleUrls: ['./device-profile-autocomplete.component.scss'],
providers: [{ providers: [{
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), useExisting: forwardRef(() => DeviceProfileAutocompleteComponent),
@ -79,6 +80,9 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
@Input() @Input()
addNewProfile = true; addNewProfile = true;
@Input()
showDetailsPageLink = false;
@Input() @Input()
transportType: DeviceTransportType = null; transportType: DeviceTransportType = null;
@ -110,6 +114,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
filteredDeviceProfiles: Observable<Array<DeviceProfileInfo>>; filteredDeviceProfiles: Observable<Array<DeviceProfileInfo>>;
searchText = ''; searchText = '';
deviceProfileURL: string;
private dirty = false; private dirty = false;
@ -240,6 +245,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe(
(profile) => { (profile) => {
this.modelValue = new DeviceProfileId(profile.id.id); this.modelValue = new DeviceProfileId(profile.id.id);
this.deviceProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType);
this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false});
this.deviceProfileChanged.emit(profile); this.deviceProfileChanged.emit(profile);
} }
@ -278,6 +284,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
const idValue = deviceProfile && deviceProfile.id ? new DeviceProfileId(deviceProfile.id.id) : null; const idValue = deviceProfile && deviceProfile.id ? new DeviceProfileId(deviceProfile.id.id) : null;
if (!entityIdEquals(this.modelValue, idValue)) { if (!entityIdEquals(this.modelValue, idValue)) {
this.modelValue = idValue; this.modelValue = idValue;
this.deviceProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType);
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
this.deviceProfileChanged.emit(deviceProfile); this.deviceProfileChanged.emit(deviceProfile);
} }

2
ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts

@ -130,7 +130,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu
setDisabledState(isDisabled: boolean){ setDisabledState(isDisabled: boolean){
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled){ if (this.disabled){
this.provisionConfigurationFormGroup.disable(); this.provisionConfigurationFormGroup.disable({emitEvent: false});
} else { } else {
if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) { if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) {
this.provisionConfigurationFormGroup.enable({emitEvent: false}); this.provisionConfigurationFormGroup.enable({emitEvent: false});

8
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column" *ngIf="!standalone"> <div class="tb-details-buttons" fxLayout.xs="column" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')" (click)="onEntityAction($event, 'export')"
@ -73,12 +79,14 @@
</tb-queue-type-list> </tb-queue-type-list>
<tb-ota-package-autocomplete <tb-ota-package-autocomplete
[useFullEntityId]="true" [useFullEntityId]="true"
[showDetailsPageLink]="true"
[type]="otaUpdateType.FIRMWARE" [type]="otaUpdateType.FIRMWARE"
[deviceProfileId]="deviceProfileId?.id" [deviceProfileId]="deviceProfileId?.id"
formControlName="firmwareId"> formControlName="firmwareId">
</tb-ota-package-autocomplete> </tb-ota-package-autocomplete>
<tb-ota-package-autocomplete <tb-ota-package-autocomplete
[useFullEntityId]="true" [useFullEntityId]="true"
[showDetailsPageLink]="true"
[type]="otaUpdateType.SOFTWARE" [type]="otaUpdateType.SOFTWARE"
[deviceProfileId]="deviceProfileId?.id" [deviceProfileId]="deviceProfileId?.id"
formControlName="softwareId"> formControlName="softwareId">

7
ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html

@ -38,6 +38,13 @@
(click)="editTenantProfile($event)"> (click)="editTenantProfile($event)">
<mat-icon class="material-icons">edit</mat-icon> <mat-icon class="material-icons">edit</mat-icon>
</button> </button>
<button *ngIf="showDetailsPageLink && selectTenantProfileFormGroup.get('tenantProfile').value && disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Open device profile">
<a [routerLink]=tenantProfileURL>
<mat-icon class="material-icons">open_in_new</mat-icon>
</a>
</button>
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
#tenantProfileAutocomplete="matAutocomplete" #tenantProfileAutocomplete="matAutocomplete"

21
ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.scss

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host{
.mat-icon-button a {
border-bottom: none;
color: inherit;
}
}

16
ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts

@ -33,12 +33,13 @@ import { ENTER } from '@angular/cdk/keycodes';
import { TenantProfile } from '@shared/models/tenant.model'; import { TenantProfile } from '@shared/models/tenant.model';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { TenantProfileDialogComponent, TenantProfileDialogData } from './tenant-profile-dialog.component'; import { TenantProfileDialogComponent, TenantProfileDialogData } from './tenant-profile-dialog.component';
import { emptyPageData } from "@shared/models/page/page-data"; import { emptyPageData } from '@shared/models/page/page-data';
import { getEntityDetailsPageURL } from '@core/utils';
@Component({ @Component({
selector: 'tb-tenant-profile-autocomplete', selector: 'tb-tenant-profile-autocomplete',
templateUrl: './tenant-profile-autocomplete.component.html', templateUrl: './tenant-profile-autocomplete.component.html',
styleUrls: [], styleUrls: ['./tenant-profile-autocomplete.component.scss'],
providers: [{ providers: [{
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TenantProfileAutocompleteComponent), useExisting: forwardRef(() => TenantProfileAutocompleteComponent),
@ -66,6 +67,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
showDetailsPageLink = false;
@Output() @Output()
tenantProfileUpdated = new EventEmitter<TenantProfileId>(); tenantProfileUpdated = new EventEmitter<TenantProfileId>();
@ -74,6 +78,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
filteredTenantProfiles: Observable<Array<EntityInfoData>>; filteredTenantProfiles: Observable<Array<EntityInfoData>>;
searchText = ''; searchText = '';
tenantProfileURL: string;
private dirty = false; private dirty = false;
@ -123,6 +128,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
(profile) => { (profile) => {
if (profile) { if (profile) {
this.modelValue = new TenantProfileId(profile.id.id); this.modelValue = new TenantProfileId(profile.id.id);
this.tenantProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType);
this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false});
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
@ -133,6 +139,11 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) {
this.selectTenantProfileFormGroup.disable();
} else {
this.selectTenantProfileFormGroup.enable();
}
} }
writeValue(value: TenantProfileId | null): void { writeValue(value: TenantProfileId | null): void {
@ -141,6 +152,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
this.tenantProfileService.getTenantProfileInfo(value.id).subscribe( this.tenantProfileService.getTenantProfileInfo(value.id).subscribe(
(profile) => { (profile) => {
this.modelValue = new TenantProfileId(profile.id.id); this.modelValue = new TenantProfileId(profile.id.id);
this.tenantProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType);
this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false});
} }
); );

6
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column" *ngIf="!standalone"> <div class="tb-details-buttons" fxLayout.xs="column" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setDefault')" (click)="onEntityAction($event, 'setDefault')"

11
ui-ngx/src/app/modules/home/components/relation/relation-table.component.html

@ -117,7 +117,9 @@
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toName"> <ng-container *ngIf="direction === directions.FROM" matColumnDef="toName">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.to-entity-name' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.to-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation"> <mat-cell *matCellDef="let relation">
{{ relation.toName }} <a class="link" [routerLink]="relation.entityURL">
{{ relation.toName }}
</a>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName"> <ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName">
@ -129,7 +131,9 @@
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromName"> <ng-container *ngIf="direction === directions.TO" matColumnDef="fromName">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.from-entity-name' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.from-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation"> <mat-cell *matCellDef="let relation">
{{ relation.fromName }} <a class="link" [routerLink]="relation.entityURL">
{{ relation.fromName }}
</a>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
@ -165,6 +169,7 @@
<mat-paginator [length]="dataSource.total() | async" <mat-paginator [length]="dataSource.total() | async"
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> [pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize"></mat-paginator>
</div> </div>
</div> </div>

4
ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss

@ -18,6 +18,7 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
.tb-entity-table { .tb-entity-table {
.tb-entity-table-content { .tb-entity-table-content {
width: 100%; width: 100%;
@ -41,6 +42,9 @@
.table-container { .table-container {
overflow: auto; overflow: auto;
.link {
border-bottom: none;
}
} }
.tb-entity-table-info{ .tb-entity-table-info{

34
ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts

@ -14,7 +14,16 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnInit,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
@ -38,6 +47,8 @@ import {
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { RelationsDatasource } from '../../models/datasource/relation-datasource'; import { RelationsDatasource } from '../../models/datasource/relation-datasource';
import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { ResizeObserver } from '@juggle/resize-observer';
@Component({ @Component({
selector: 'tb-relation-table', selector: 'tb-relation-table',
@ -56,6 +67,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
displayedColumns: string[]; displayedColumns: string[];
direction: EntitySearchDirection; direction: EntitySearchDirection;
pageLink: PageLink; pageLink: PageLink;
hidePageSize = false;
textSearchMode = false; textSearchMode = false;
dataSource: RelationsDatasource; dataSource: RelationsDatasource;
@ -65,6 +77,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
viewsInited = false; viewsInited = false;
private widgetResize$: ResizeObserver;
@Input() @Input()
set active(active: boolean) { set active(active: boolean) {
if (this.activeValue !== active) { if (this.activeValue !== active) {
@ -100,7 +114,9 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
private entityRelationService: EntityRelationService, private entityRelationService: EntityRelationService,
public translate: TranslateService, public translate: TranslateService,
public dialog: MatDialog, public dialog: MatDialog,
private dialogService: DialogService) { private dialogService: DialogService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(store); super(store);
this.dirtyValue = !this.activeValue; this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC };
@ -111,6 +127,20 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
} }
ngOnInit() { ngOnInit() {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
ngOnDestroy() {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
} }
updateColumns() { updateColumns() {

3
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html

@ -119,6 +119,7 @@
<mat-paginator [length]="dataSource.total() | async" <mat-paginator [length]="dataSource.total() | async"
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> [pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize"></mat-paginator>
</div> </div>
</div> </div>

1
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss

@ -16,6 +16,7 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
.tb-entity-table { .tb-entity-table {
.tb-entity-table-content { .tb-entity-table-content {
width: 100%; width: 100%;

35
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts

@ -14,7 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -36,13 +46,14 @@ import {
WidgetActionsDatasource WidgetActionsDatasource
} from '@home/components/widget/action/manage-widget-actions.component.models'; } from '@home/components/widget/action/manage-widget-actions.component.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; import { WidgetActionDescriptor, WidgetActionSource, widgetType } from '@shared/models/widget.models';
import { import {
WidgetActionDialogComponent, WidgetActionDialogComponent,
WidgetActionDialogData WidgetActionDialogData
} from '@home/components/widget/action/widget-action-dialog.component'; } from '@home/components/widget/action/widget-action-dialog.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { widgetType } from '@shared/models/widget.models'; import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
@Component({ @Component({
selector: 'tb-manage-widget-actions', selector: 'tb-manage-widget-actions',
@ -69,11 +80,14 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
displayedColumns: string[]; displayedColumns: string[];
pageLink: PageLink; pageLink: PageLink;
textSearchMode = false; textSearchMode = false;
hidePageSize = false;
dataSource: WidgetActionsDatasource; dataSource: WidgetActionsDatasource;
viewsInited = false; viewsInited = false;
dirtyValue = false; dirtyValue = false;
private widgetResize$: ResizeObserver;
@ViewChild('searchInput') searchInputField: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ -85,7 +99,9 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
private translate: TranslateService, private translate: TranslateService,
private utils: UtilsService, private utils: UtilsService,
private dialog: MatDialog, private dialog: MatDialog,
private dialogs: DialogService) { private dialogs: DialogService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(store); super(store);
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
@ -94,9 +110,20 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
} }
ngOnInit(): void { ngOnInit(): void {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
} }
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
} }
ngAfterViewInit() { ngAfterViewInit() {

12
ui-ngx/src/app/modules/home/components/widget/legend.component.ts

@ -60,11 +60,13 @@ export class LegendComponent implements OnInit {
} }
legendKeys(): LegendKey[] { legendKeys(): LegendKey[] {
let keys = this.legendData.keys; try {
if (this.legendConfig.sortDataKeys) { let keys = this.legendData.keys;
keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); if (this.legendConfig.sortDataKeys) {
} keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); }
return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend);
} catch (e) {}
} }
} }

1
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html

@ -147,6 +147,7 @@
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator> showFirstLastButtons></mat-paginator>
</div> </div>
</div> </div>

1
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss

@ -16,6 +16,7 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
.tb-table-widget { .tb-table-widget {
.table-container { .table-container {
position: relative; position: relative;

19
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -16,6 +16,7 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
@ -123,6 +124,8 @@ import {
} from '@home/components/widget/lib/alarm-filter-panel.component'; } from '@home/components/widget/lib/alarm-filter-panel.component';
import { entityFields } from '@shared/models/entity.models'; import { entityFields } from '@shared/models/entity.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
interface AlarmsTableWidgetSettings extends TableWidgetSettings { interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string; alarmsTitle: string;
@ -164,6 +167,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public pageLink: AlarmDataPageLink; public pageLink: AlarmDataPageLink;
public sortOrderProperty: string; public sortOrderProperty: string;
public textSearchMode = false; public textSearchMode = false;
public hidePageSize = false;
public columns: Array<EntityColumn> = []; public columns: Array<EntityColumn> = [];
public displayedColumns: string[] = []; public displayedColumns: string[] = [];
public alarmsDatasource: AlarmsDatasource; public alarmsDatasource: AlarmsDatasource;
@ -177,6 +181,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private settings: AlarmsTableWidgetSettings; private settings: AlarmsTableWidgetSettings;
private widgetConfig: WidgetConfig; private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private widgetResize$: ResizeObserver;
private alarmsTitlePattern: string; private alarmsTitlePattern: string;
@ -235,7 +240,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog,
private dialogService: DialogService, private dialogService: DialogService,
private alarmService: AlarmService) { private alarmService: AlarmService,
private cd: ChangeDetectorRef) {
super(store); super(store);
this.pageLink = { this.pageLink = {
page: 0, page: 0,
@ -257,6 +263,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.widgetTimewindowChanged$ = this.ctx.defaultSubscription.widgetTimewindowChanged$.subscribe( this.widgetTimewindowChanged$ = this.ctx.defaultSubscription.widgetTimewindowChanged$.subscribe(
() => this.pageLink.page = 0 () => this.pageLink.page = 0
); );
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
} }
} }
@ -265,6 +279,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.widgetTimewindowChanged$.unsubscribe(); this.widgetTimewindowChanged$.unsubscribe();
this.widgetTimewindowChanged$ = null; this.widgetTimewindowChanged$ = null;
} }
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {

1
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -106,6 +106,7 @@
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator> showFirstLastButtons></mat-paginator>
</div> </div>
</div> </div>

1
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss

@ -16,6 +16,7 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
.tb-table-widget { .tb-table-widget {
.table-container { .table-container {
position: relative; position: relative;

24
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -16,6 +16,7 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Injector, Injector,
@ -107,6 +108,8 @@ import { sortItems } from '@shared/models/page/page-link';
import { entityFields } from '@shared/models/entity.models'; import { entityFields } from '@shared/models/entity.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
interface EntitiesTableWidgetSettings extends TableWidgetSettings { interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string; entitiesTitle: string;
@ -140,6 +143,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public pageLink: EntityDataPageLink; public pageLink: EntityDataPageLink;
public sortOrderProperty: string; public sortOrderProperty: string;
public textSearchMode = false; public textSearchMode = false;
public hidePageSize = false;
public columns: Array<EntityColumn> = []; public columns: Array<EntityColumn> = [];
public displayedColumns: string[] = []; public displayedColumns: string[] = [];
public entityDatasource: EntityDatasource; public entityDatasource: EntityDatasource;
@ -153,6 +157,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private settings: EntitiesTableWidgetSettings; private settings: EntitiesTableWidgetSettings;
private widgetConfig: WidgetConfig; private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private widgetResize$: ResizeObserver;
private entitiesTitlePattern: string; private entitiesTitlePattern: string;
@ -193,7 +198,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private utils: UtilsService, private utils: UtilsService,
private datePipe: DatePipe, private datePipe: DatePipe,
private translate: TranslateService, private translate: TranslateService,
private domSanitizer: DomSanitizer) { private domSanitizer: DomSanitizer,
private cd: ChangeDetectorRef) {
super(store); super(store);
this.pageLink = { this.pageLink = {
page: 0, page: 0,
@ -211,6 +217,22 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.initializeConfig(); this.initializeConfig();
this.updateDatasources(); this.updateDatasources();
this.ctx.updateWidgetParams(); this.ctx.updateWidgetParams();
if (this.displayPagination) {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
}
ngOnDestroy(): void {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {

52
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -24,7 +24,9 @@ import {
defaultSettings, defaultSettings,
FormattedData, FormattedData,
MapSettings, MapSettings,
MarkerSettings, MarkerIconInfo, MarkerImageInfo, MarkerIconInfo,
MarkerImageInfo,
MarkerSettings,
PolygonSettings, PolygonSettings,
PolylineSettings, PolylineSettings,
ReplaceInfo, ReplaceInfo,
@ -182,7 +184,7 @@ export default abstract class LeafletMap {
addEditControl() { addEditControl() {
// Customize edit marker // Customize edit marker
if (this.options.draggableMarker) { if (this.options.draggableMarker && !this.options.hideDrawControlButton) {
const actions = [{ const actions = [{
text: L.PM.Utils.getTranslation('actions.cancel'), text: L.PM.Utils.getTranslation('actions.cancel'),
onClick: () => this.toggleDrawMode('tbMarker') onClick: () => this.toggleDrawMode('tbMarker')
@ -197,7 +199,7 @@ export default abstract class LeafletMap {
} }
// Customize edit polygon // Customize edit polygon
if (this.editPolygons) { if (this.editPolygons && !this.options.hideDrawControlButton) {
const rectangleActions = [ const rectangleActions = [
{ {
text: L.PM.Utils.getTranslation('actions.cancel'), text: L.PM.Utils.getTranslation('actions.cancel'),
@ -231,18 +233,27 @@ export default abstract class LeafletMap {
const translateService = this.ctx.$injector.get(TranslateService); const translateService = this.ctx.$injector.get(TranslateService);
this.map.pm.setLang('en', translateService.instant('widgets.maps'), 'en'); this.map.pm.setLang('en', translateService.instant('widgets.maps'), 'en');
this.map.pm.addControls({ if (!this.options.hideAllControlButton) {
position: 'topleft', this.map.pm.addControls({
drawMarker: false, position: 'topleft',
drawCircle: false, drawControls: !this.options.hideDrawControlButton,
drawCircleMarker: false, drawMarker: false,
drawRectangle: false, drawCircle: false,
drawPolyline: false, drawCircleMarker: false,
drawPolygon: false, drawRectangle: false,
editMode: this.editPolygons, drawPolyline: false,
cutPolygon: this.editPolygons, drawPolygon: false,
rotateMode: this.editPolygons dragMode: !this.options.hideEditControlButton,
}); editMode: this.editPolygons && !this.options.hideEditControlButton,
cutPolygon: this.editPolygons && !this.options.hideEditControlButton,
removalMode: !this.options.hideRemoveControlButton,
rotateMode: this.editPolygons && !this.options.hideEditControlButton
});
}
if (this.options.initDragMode) {
this.map.pm.enableGlobalDragMode();
}
this.map.on('pm:create', (e) => { this.map.on('pm:create', (e) => {
if (e.shape === 'tbMarker') { if (e.shape === 'tbMarker') {
@ -344,6 +355,7 @@ export default abstract class LeafletMap {
} }
if (this.options.draggableMarker || this.editPolygons) { if (this.options.draggableMarker || this.editPolygons) {
map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions);
map.pm.applyGlobalOptions();
this.addEditControl(); this.addEditControl();
} else { } else {
this.map.pm.disableDraw(); this.map.pm.disableDraw();
@ -555,7 +567,7 @@ export default abstract class LeafletMap {
updatedMarkers.push(m); updatedMarkers.push(m);
} }
} else { } else {
m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); m = this.createMarker(data.entityName, data, markersData, this.options, updateBounds, callback);
if (m) { if (m) {
createdMarkers.push(m); createdMarkers.push(m);
} }
@ -569,7 +581,7 @@ export default abstract class LeafletMap {
} }
}); });
this.markersData = markersData; this.markersData = markersData;
if ((this.options as MarkerSettings).useClusterMarkers) { if (this.options.useClusterMarkers) {
if (createdMarkers.length) { if (createdMarkers.length) {
this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker));
} }
@ -589,7 +601,7 @@ export default abstract class LeafletMap {
this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe(); this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe();
} }
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings,
updateBounds = true, callback?): Marker { updateBounds = true, callback?): Marker {
const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker); const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker);
if (callback) { if (callback) {
@ -597,7 +609,7 @@ export default abstract class LeafletMap {
callback(data, true); callback(data, true);
}); });
} }
if (this.bounds && updateBounds && !(this.options as MarkerSettings).useClusterMarkers) { if (this.bounds && updateBounds && !this.options.useClusterMarkers) {
this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()));
} }
this.markers.set(key, newMarker); this.markers.set(key, newMarker);
@ -786,7 +798,7 @@ export default abstract class LeafletMap {
this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {});
} }
createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true) {
const polygon = new Polygon(this.map, polyData, dataSources, settings, this.dragPolygonVertex); const polygon = new Polygon(this.map, polyData, dataSources, settings, this.dragPolygonVertex);
if (updateBounds) { if (updateBounds) {
const bounds = polygon.leafletPoly.getBounds(); const bounds = polygon.leafletPoly.getBounds();

19
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts

@ -192,6 +192,15 @@ export type PolylineSettings = {
strokeWeightFunction: GenericFunction; strokeWeightFunction: GenericFunction;
}; };
export interface EditorSettings {
snappable: boolean;
initDragMode: boolean;
hideAllControlButton: boolean;
hideDrawControlButton: boolean;
hideEditControlButton: boolean;
hideRemoveControlButton: boolean;
}
export interface HistorySelectSettings { export interface HistorySelectSettings {
buttonColor: string; buttonColor: string;
} }
@ -231,7 +240,7 @@ export interface TripAnimationSettings extends PolygonSettings {
export type actionsHandler = ($event: Event, datasource: Datasource) => void; export type actionsHandler = ($event: Event, datasource: Datasource) => void;
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings; export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings & EditorSettings;
export const defaultSettings: any = { export const defaultSettings: any = {
xPosKeyName: 'xPos', xPosKeyName: 'xPos',
@ -271,7 +280,13 @@ export const defaultSettings: any = {
draggableMarker: false, draggableMarker: false,
editablePolygon: false, editablePolygon: false,
fitMapBounds: true, fitMapBounds: true,
mapPageSize: DEFAULT_MAP_PAGE_SIZE mapPageSize: DEFAULT_MAP_PAGE_SIZE,
snappable: false,
initDragMode: false,
hideAllControlButton: false,
hideDrawControlButton: false,
hideEditControlButton: false,
hideRemoveControlButton: false
}; };
export const hereProviders = [ export const hereProviders = [

3
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -18,6 +18,7 @@ import { defaultSettings, FormattedData, hereProviders, MapProviders, UnitedMapS
import LeafletMap from './leaflet-map'; import LeafletMap from './leaflet-map';
import { import {
commonMapSettingsSchema, commonMapSettingsSchema,
editorSettingSchema,
mapPolygonSchema, mapPolygonSchema,
markerClusteringSettingsSchema, markerClusteringSettingsSchema,
markerClusteringSettingsSchemaLeaflet, markerClusteringSettingsSchemaLeaflet,
@ -115,6 +116,8 @@ export class MapWidgetController implements MapWidgetInterface {
`model.useClusterMarkers === true && model.provider !== "image-map"`)]); `model.useClusterMarkers === true && model.provider !== "image-map"`)]);
addToSchema(schema, clusteringSchema); addToSchema(schema, clusteringSchema);
addGroupInfo(schema, 'Markers Clustering Settings'); addGroupInfo(schema, 'Markers Clustering Settings');
addToSchema(schema, addCondition(editorSettingSchema, '(model.editablePolygon === true || model.draggableMarker === true)'));
addGroupInfo(schema, 'Editor settings');
} }
return schema; return schema;
} }

29
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts

@ -14,19 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import L, { Icon, LeafletMouseEvent } from 'leaflet'; import L, { LeafletMouseEvent } from 'leaflet';
import { FormattedData, MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, MarkerSettings } from './map-models';
import { import {
bindPopupActions, FormattedData,
createTooltip, MarkerIconInfo,
} from './maps-utils'; MarkerIconReadyFunction,
import { MarkerImageInfo,
aspectCache, MarkerSettings,
fillPattern, UnitedMapSettings
parseWithTranslation, } from './map-models';
processPattern, import { bindPopupActions, createTooltip, } from './maps-utils';
safeExecute import { aspectCache, fillPattern, parseWithTranslation, processPattern, safeExecute } from './common-maps-utils';
} from './common-maps-utils';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { isDefined, isDefinedAndNotNull } from '@core/utils'; import { isDefined, isDefinedAndNotNull } from '@core/utils';
import LeafletMap from './leaflet-map'; import LeafletMap from './leaflet-map';
@ -40,10 +38,13 @@ export class Marker {
data: FormattedData; data: FormattedData;
dataSources: FormattedData[]; dataSources: FormattedData[];
constructor(private map: LeafletMap, private location: L.LatLng, public settings: MarkerSettings, constructor(private map: LeafletMap, private location: L.LatLng, public settings: UnitedMapSettings,
data?: FormattedData, dataSources?, onDragendListener?) { data?: FormattedData, dataSources?, onDragendListener?) {
this.setDataSources(data, dataSources); this.setDataSources(data, dataSources);
this.leafletMarker = L.marker(location, {pmIgnore: !settings.draggableMarker}); this.leafletMarker = L.marker(location, {
pmIgnore: !settings.draggableMarker,
snapIgnore: !settings.snappable
});
this.markerOffset = [ this.markerOffset = [
isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5, isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5,

7
ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts

@ -23,7 +23,7 @@ import {
processPattern, processPattern,
safeExecute safeExecute
} from './common-maps-utils'; } from './common-maps-utils';
import { FormattedData, MarkerSettings, PolygonSettings } from './map-models'; import { FormattedData, PolygonSettings, UnitedMapSettings } from './map-models';
export class Polygon { export class Polygon {
@ -32,7 +32,7 @@ export class Polygon {
data: FormattedData; data: FormattedData;
dataSources: FormattedData[]; dataSources: FormattedData[];
constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: PolygonSettings, constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: UnitedMapSettings,
private onDragendListener?) { private onDragendListener?) {
this.dataSources = dataSources; this.dataSources = dataSources;
this.data = data; this.data = data;
@ -47,7 +47,8 @@ export class Polygon {
weight: settings.polygonStrokeWeight, weight: settings.polygonStrokeWeight,
fillOpacity: settings.polygonOpacity, fillOpacity: settings.polygonOpacity,
opacity: settings.polygonStrokeOpacity, opacity: settings.polygonStrokeOpacity,
pmIgnore: !settings.editablePolygon pmIgnore: !settings.editablePolygon,
snapIgnore: !settings.snappable
}).addTo(this.map); }).addTo(this.map);
this.updateLabel(settings); this.updateLabel(settings);

58
ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts

@ -1311,3 +1311,61 @@ export const providerSets: { [key: string]: IProvider } = {
name: 'image-map' name: 'image-map'
} }
}; };
export const editorSettingSchema =
{
schema: {
title: 'Editor settings',
type: 'object',
properties: {
snappable: {
title: 'Enable snapping to other vertices for precision drawing',
type: 'boolean',
default: false
},
initDragMode: {
title: 'Initialize map in draggable mode',
type: 'boolean',
default: false
},
hideAllControlButton: {
title: 'Hide all button',
type: 'boolean',
default: false
},
hideDrawControlButton: {
title: 'Hide draw buttons',
type: 'boolean',
default: false
},
hideEditControlButton: {
title: 'Hide edit buttons',
type: 'boolean',
default: false
},
hideRemoveControlButton: {
title: 'Hide remove button',
type: 'boolean',
default: false
},
},
required: []
},
form: [
'snappable',
'initDragMode',
'hideAllControlButton',
{
key: 'hideDrawControlButton',
condition: 'model.hideAllControlButton == false'
},
{
key: 'hideEditControlButton',
condition: 'model.hideAllControlButton == false'
},
{
key: 'hideRemoveControlButton',
condition: 'model.hideAllControlButton == false'
}
]
};

92
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html

@ -0,0 +1,92 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="persistentFormGroup" (ngSubmit)="save()" style="min-width: 480px; max-width: 600px;">
<mat-toolbar color="primary">
<h2>{{ 'widgets.persistent-table.add-title' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content class="add-dialog">
<fieldset [disabled]="isLoading$ | async">
<div fxLayout="row" fxLayoutGap="6px">
<mat-slide-toggle fxFlex class="mat-block" formControlName="oneWayElseTwoWay">
{{ rpcMessageTypeText }}
</mat-slide-toggle>
</div>
<div fxLayout="row wrap" fxLayout.xs="column" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.method</mat-label>
<input matInput formControlName="method" required>
<mat-error *ngIf="this.persistentFormGroup.get('method').hasError('required')">
{{'widgets.persistent-table.method-error' | translate}}
</mat-error>
<mat-error *ngIf="this.persistentFormGroup.get('method').hasError('pattern')">
{{'widgets.persistent-table.white-space-error' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.retries</mat-label>
<input matInput type="number" formControlName="retries">
</mat-form-field>
</div>
<div class="params-json-editor">
<tb-json-object-edit formControlName="params"
[editorStyle]="{minHeight: '130px'}"
label="{{ 'widgets.persistent-table.params' | translate }}">
</tb-json-object-edit>
</div>
<mat-expansion-panel class="additional-json-editor">
<mat-expansion-panel-header>
<mat-panel-title translate>
widgets.persistent-table.additional-info
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit formControlName="additionalInfo"
[editorStyle]="{minHeight: '130px'}">
</tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</fieldset>
</div>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-actions fxLayout="row">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()">
{{ 'action.close' | translate }}
</button>
<span fxFlex></span>
<div fxLayout="row" fxLayoutGap="8px">
<button mat-raised-button
color="primary"
type="submit"
[disabled]="(isLoading$ | async) || persistentFormGroup.invalid || !persistentFormGroup.dirty">
{{ 'widgets.persistent-table.send-request' | translate }}
</button>
</div>
</div>
</form>

31
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.add-dialog {
.params-json-editor,
.additional-json-editor {
.tb-json-object-panel {
margin: 0 0 16px;
}
.mat-expansion-panel-body {
padding-bottom: 0 !important;
}
}
}
}

75
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts

@ -0,0 +1,75 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { RequestData } from '@shared/models/rpc.models';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-persistent-add-dialog',
templateUrl: './persistent-add-dialog.component.html',
styleUrls: ['./persistent-add-dialog.component.scss']
})
export class PersistentAddDialogComponent extends DialogComponent<PersistentAddDialogComponent, RequestData> implements OnInit {
public persistentFormGroup: FormGroup;
public rpcMessageTypeText: string;
private requestData: RequestData = null;
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<PersistentAddDialogComponent, RequestData>,
private fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
this.persistentFormGroup = this.fb.group(
{
method: ['', [Validators.required, Validators.pattern(/^\S+$/)]],
oneWayElseTwoWay: [false],
retries: [null, [Validators.pattern(/^-?[0-9]+$/), Validators.min(0)]],
params: [null],
additionalInfo: [null]
}
);
}
save() {
this.requestData = this.persistentFormGroup.value;
this.close();
}
ngOnInit(): void {
this.rpcMessageTypeText = this.translate.instant('widgets.persistent-table.message-types.false');
this.persistentFormGroup.get('oneWayElseTwoWay').valueChanges.subscribe(
() => {
this.rpcMessageTypeText = this.translate.instant(`widgets.persistent-table.message-types.${this.persistentFormGroup.get('oneWayElseTwoWay').value}`);
}
);
}
close(): void {
this.dialogRef.close(this.requestData);
}
}

118
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html

@ -0,0 +1,118 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="persistentFormGroup" style="min-width: 480px;">
<mat-toolbar color="primary">
<h2>{{ persistentFormGroup.get('rpcId').value }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.created-time</mat-label>
<input matInput formControlName="createdTime" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.expiration-time</mat-label>
<input matInput formControlName="expirationTime" readonly>
</mat-form-field>
</div>
<div fxLayout="row wrap" fxLayout.xs="column" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.message-type</mat-label>
<input matInput formControlName="messageType" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.status</mat-label>
<input matInput formControlName="status" readonly
[ngStyle]="{ fontWeight: 'bold', color: rpcStatusColorsMap.get(data.persistentRequest.status) }">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.persistent-table.method</mat-label>
<input matInput formControlName="method" readonly>
</mat-form-field>
<mat-form-field fxFlex class="mat-block"
*ngIf="persistentFormGroup.get('retries').value">
<mat-label translate>widgets.persistent-table.retries</mat-label>
<input matInput formControlName="retries" readonly>
</mat-form-field>
</div>
<mat-accordion class="rpc-dialog" multi>
<mat-expansion-panel expanded>
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widgets.persistent-table.response' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<tb-json-object-view formControlName="response" autoHeight></tb-json-object-view>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widgets.persistent-table.params' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-view formControlName="params" autoHeight></tb-json-object-view>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widgets.persistent-table.additional-info' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-view
formControlName="additionalInfo"
autoHeight>
</tb-json-object-view>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<button mat-raised-button
*ngIf="allowDelete"
color="primary"
type="button"
(click)="deleteRpcRequest()"
[disabled]="(isLoading$ | async)">
{{ 'widgets.persistent-table.delete' | translate }}
</button>
<span fxFlex></span>
<div fxLayout="row" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="close()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</div>
</form>

34
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.rpc-dialog {
.mat-expansion-panel-body {
padding-bottom: 0 !important;
}
.tb-json-object-panel {
margin: 0 0 16px 0;
}
}
.tb-audit-log-response-data {
width: 100%;
min-width: 400px;
height: 100%;
min-height: 100px;
border: 1px solid #c0c0c0;
}
}

126
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts

@ -0,0 +1,126 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DeviceService } from '@core/http/device.service';
import { PersistentRpc, RpcStatus, rpcStatusColors, rpcStatusTranslation } from '@shared/models/rpc.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DialogService } from '@core/services/dialog.service';
export interface PersistentDetailsDialogData {
persistentRequest: PersistentRpc;
allowDelete: boolean;
}
@Component({
selector: 'tb-persistent-details-dialog',
templateUrl: './persistent-details-dialog.component.html',
styleUrls: ['./persistent-details-dialog.component.scss']
})
export class PersistentDetailsDialogComponent extends DialogComponent<PersistentDetailsDialogComponent, boolean> implements OnInit {
@ViewChild('responseDataEditor', {static: true})
responseDataEditorElmRef: ElementRef;
public persistentFormGroup: FormGroup;
public rpcStatusColorsMap = rpcStatusColors;
public rpcStatus = RpcStatus;
public allowDelete: boolean;
private persistentUpdated = false;
private responseData: string;
constructor(protected store: Store<AppState>,
protected router: Router,
private datePipe: DatePipe,
private translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: PersistentDetailsDialogData,
public dialogRef: MatDialogRef<PersistentDetailsDialogComponent, boolean>,
private dialogService: DialogService,
private deviceService: DeviceService,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.allowDelete = data.allowDelete;
this.persistentFormGroup = this.fb.group(
{
rpcId: [''],
createdTime: [''],
expirationTime: [''],
messageType: [''],
status: [''],
method: [''],
params: [''],
retries: [''],
response: [''],
additionalInfo: ['']
}
);
this.loadPersistentFields(data.persistentRequest);
this.responseData = JSON.stringify(data.persistentRequest.response, null, 2);
}
loadPersistentFields(request: PersistentRpc) {
this.persistentFormGroup.patchValue({
rpcId: this.translate.instant('widgets.persistent-table.details-title') + request.id.id,
createdTime: this.datePipe.transform(request.createdTime, 'yyyy-MM-dd HH:mm:ss'),
expirationTime: this.datePipe.transform(request.expirationTime, 'yyyy-MM-dd HH:mm:ss'),
messageType: this.translate.instant('widgets.persistent-table.message-types.' + request.request.oneway),
status: this.translate.instant(rpcStatusTranslation.get(request.status)),
method: request.request.body.method,
retries: request.request.retries || null,
response: request.response || null,
params: JSON.parse(request.request.body.params) || null,
additionalInfo: request.additionalInfo || null
}, {emitEvent: false});
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close(this.persistentUpdated);
}
deleteRpcRequest() {
const persistentRpc = this.data.persistentRequest;
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) {
this.dialogService.confirm(
this.translate.instant('widgets.persistent-table.delete-request-title'),
this.translate.instant('widgets.persistent-table.delete-request-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => {
this.persistentUpdated = true;
this.close();
});
}
});
}
}
}

44
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="persistentFilterFormGroup" (ngSubmit)="update()">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>widgets.persistent-table.rpc-status-list</mat-label>
<mat-select formControlName="rpcStatus"
placeholder="{{ rpcSearchPlaceholder }}">
<mat-option [value]="null">
{{ 'widgets.persistent-table.rpc-search-status-all' | translate }}
</mat-option>
<mat-option *ngFor="let searchStatus of persistentSearchStatuses" [value]="searchStatus">
{{ rpcSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="persistentFilterFormGroup.invalid || !persistentFilterFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>

37
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
min-width: 300px;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
.mat-content {
overflow: hidden;
background-color: #fff;
}
.mat-padding {
padding: 16px;
}
}

67
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts

@ -0,0 +1,67 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { OverlayRef } from '@angular/cdk/overlay';
import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models';
import { TranslateService } from '@ngx-translate/core';
export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken<any>('AlarmFilterPanelData');
export interface PersistentFilterPanelData {
rpcStatus: RpcStatus;
}
@Component({
selector: 'tb-persistent-filter-panel',
templateUrl: './persistent-filter-panel.component.html',
styleUrls: ['./persistent-filter-panel.component.scss']
})
export class PersistentFilterPanelComponent {
public persistentFilterFormGroup: FormGroup;
public result: PersistentFilterPanelData;
public rpcSearchStatusTranslationMap = rpcStatusTranslation;
public rpcSearchPlaceholder: string;
public persistentSearchStatuses = Object.keys(RpcStatus);
constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA)
public data: PersistentFilterPanelData,
public overlayRef: OverlayRef,
private fb: FormBuilder,
private translate: TranslateService) {
this.persistentFilterFormGroup = this.fb.group(
{
rpcStatus: this.data.rpcStatus
}
);
this.rpcSearchPlaceholder = this.translate.instant('widgets.persistent-table.any-status');
}
update() {
this.result = {
rpcStatus: this.persistentFilterFormGroup.get('rpcStatus').value
};
this.overlayRef.dispose();
}
cancel() {
this.overlayRef.dispose();
}
}

126
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html

@ -0,0 +1,126 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-table-widget tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-absolute-fill">
<div fxFlex class="table-container">
<table mat-table [dataSource]="persistentDatasource"
matSort [matSortActive]="pageLink.sortOrder.property"
[matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="rpcId">
<mat-header-cell *matHeaderCellDef class="column-id">
{{ 'widgets.persistent-table.rpc-id' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.id.id }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="createdTime">
<mat-header-cell *matHeaderCellDef class="column-time" mat-sort-header>
{{ 'widgets.persistent-table.created-time' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.createdTime | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="expirationTime">
<mat-header-cell *matHeaderCellDef class="column-time" mat-sort-header>
{{ 'widgets.persistent-table.expiration-time' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.expirationTime | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header>
{{ 'widgets.persistent-table.status' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column"
[ngStyle]="{fontWeight: 'bold', color: rpcStatusColor.get((column.status))}">
{{ rpcStatusTranslation.get(column.status) | translate }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="messageType">
<mat-header-cell *matHeaderCellDef>
{{ 'widgets.persistent-table.message-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ 'widgets.persistent-table.message-types.' + column.request.oneway | translate }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="method">
<mat-header-cell *matHeaderCellDef>
{{ 'widgets.persistent-table.method' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let column">
{{ column.request.body.method }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
<mat-header-cell *matHeaderCellDef>
</mat-header-cell>
<mat-cell *matCellDef="let column">
<div fxHide fxShow.gt-md fxLayout="row" fxLayoutAlign="end">
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{ actionDescriptor.icon }}</mat-icon>
</button>
</ng-container>
</div>
<div fxHide fxShow.lt-lg *ngIf="actionCellButtonAction.length">
<button mat-button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction">
<button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="(isLoading$ | async)"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<span>{{ actionDescriptor.displayName }}</span>
</button>
</ng-container>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row *matRowDef="let column; columns: displayedColumns"
[fxShow]="!persistentDatasource.dataLoading"></mat-row>
</table>
<span [fxShow]="(persistentDatasource.isEmpty() | async) && !persistentDatasource.dataLoading"
fxLayoutAlign="center center"
class="no-data-found">{{ noDataDisplayMessageText }}</span>
<span [fxShow]="persistentDatasource.dataLoading"
fxLayoutAlign="center center"
class="no-data-found">{{ 'common.loading' | translate }}</span>
</div>
<mat-divider *ngIf="displayPagination"></mat-divider>
<mat-paginator *ngIf="displayPagination"
[length]="persistentDatasource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
</div>
</div>

53
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: block;
.tb-table-widget {
.table-container {
position: relative;
}
.mat-table {
.mat-row {
&.invisible {
visibility: hidden;
}
}
}
span.no-data-found {
position: absolute;
top: 60px;
bottom: 0;
left: 0;
right: 0;
}
.column-id {
min-width: 250px;
max-width: 250px;
width: 250px;
}
.column-time {
min-width: 120px;
max-width: 120px;
width: 120px;
}
}
}

580
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts

@ -0,0 +1,580 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
ElementRef,
Injector,
Input,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetContext } from '@home/models/widget-component.models';
import { WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { BehaviorSubject, merge, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import {
constructTableCssString,
noDataMessage,
TableCellButtonActionDescriptor,
TableWidgetSettings
} from '@home/components/widget/lib/table-widget.models';
import cssjs from '@core/css/css';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { hashCode, isDefined, isNumber } from '@core/utils';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import {
PersistentRpc,
PersistentRpcData,
RequestData,
RpcStatus,
rpcStatusColors,
rpcStatusTranslation
} from '@shared/models/rpc.models';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DialogService } from '@core/services/dialog.service';
import { DeviceService } from '@core/http/device.service';
import { MatDialog } from '@angular/material/dialog';
import {
PersistentDetailsDialogComponent,
PersistentDetailsDialogData
} from '@home/components/widget/lib/rpc/persistent-details-dialog.component';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import {
PERSISTENT_FILTER_PANEL_DATA,
PersistentFilterPanelComponent,
PersistentFilterPanelData
} from '@home/components/widget/lib/rpc/persistent-filter-panel.component';
import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { HttpErrorResponse } from '@angular/common/http';
interface PersistentTableWidgetSettings extends TableWidgetSettings {
defaultSortOrder: string;
defaultPageSize: number;
displayPagination: boolean;
enableStickyAction: boolean;
enableStickyHeader: boolean;
enableFilter: boolean;
displayColumns: string[];
displayDetails: boolean;
allowDelete: boolean;
allowSendRequest: boolean;
}
interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDescriptor {
details?: boolean;
delete?: boolean;
}
@Component({
selector: 'tb-persistent-table-widget',
templateUrl: './persistent-table.component.html',
styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss']
})
export class PersistentTableComponent extends PageComponent implements OnInit {
@Input()
ctx: WidgetContext;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
private settings: PersistentTableWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private enableFilterAction = true;
private allowSendRequest = true;
private defaultPageSize = 10;
private defaultSortOrder = '-createdTime';
private rpcStatusFilter: RpcStatus;
private displayDetails = true;
private allowDelete = true;
private displayTableColumns: string[];
private widgetResize$: ResizeObserver;
public persistentDatasource: PersistentDatasource;
public noDataDisplayMessageText: string;
public rpcStatusColor = rpcStatusColors;
public rpcStatusTranslation = rpcStatusTranslation;
public displayPagination = true;
public enableStickyHeader = true;
public enableStickyAction = true;
public pageLink: PageLink;
public pageSizeOptions;
public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = [];
public displayedColumns: string[];
public hidePageSize = false;
constructor(protected store: Store<AppState>,
private elementRef: ElementRef,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
private translate: TranslateService,
private dialogService: DialogService,
private deviceService: DeviceService,
private dialog: MatDialog,
private cd: ChangeDetectorRef) {
super(store);
}
ngOnInit() {
this.ctx.$scope.persistentTableWidget = this;
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.initializeConfig();
this.ctx.updateWidgetParams();
if (this.displayPagination) {
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
}
}
ngOnDestroy() {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
}
ngAfterViewInit(): void {
if (this.displayPagination) {
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
}
((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable<any>)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.updateData();
}
private initializeConfig() {
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
this.displayTableColumns = isDefined(this.settings.displayColumns) ? this.settings.displayColumns : [];
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true;
this.enableFilterAction = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true;
this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true;
this.allowDelete = isDefined(this.settings.allowDelete) ? this.settings.allowDelete : true;
this.allowSendRequest = isDefined(this.settings.allowSendRequest) ? this.settings.allowSendRequest : true;
this.noDataDisplayMessageText =
noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widgets.persistent-table.no-request-prompt', this.utils, this.translate);
this.displayedColumns = [...this.displayTableColumns];
const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
this.defaultPageSize = pageSize;
}
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder;
}
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024;
this.ctx.widgetActions = [
{
name: 'widgets.persistent-table.add',
show: this.allowSendRequest,
icon: 'add',
onAction: $event => this.addPersistentRpcRequest($event)
},
{
name: 'widgets.persistent-table.refresh',
show: true,
icon: 'refresh',
onAction: () => this.reloadPersistentRequests()
},
{
name: 'widgets.persistent-table.filter',
show: this.enableFilterAction,
icon: 'filter_list',
onAction: $event => this.editFilter($event)
}
];
if (this.settings.displayDetails) {
this.actionCellButtonAction.push(
{
displayName: this.translate.instant('widgets.persistent-table.details'),
icon: 'more_horiz',
details: true
} as PersistentTableWidgetActionDescriptor
);
}
if (this.settings.allowDelete) {
this.actionCellButtonAction.push(
{
displayName: this.translate.instant('widgets.persistent-table.delete'),
icon: 'delete',
delete: true
} as PersistentTableWidgetActionDescriptor
);
}
if (this.actionCellButtonAction.length) {
this.displayedColumns.push('actions');
}
this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription, this.ctx);
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'persistent-table-' + hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);
}
private updateData() {
if (this.displayPagination) {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
} else {
this.pageLink.page = 0;
}
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder);
}
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.persistentDatasource.loadPersistent(this.pageLink, this.rpcStatusFilter);
this.ctx.detectChanges();
}
public onDataUpdated() {
this.ctx.detectChanges();
}
reloadPersistentRequests() {
if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData();
}
deleteRpcRequest($event: Event, persistentRpc: PersistentRpc) {
if ($event) {
$event.stopPropagation();
}
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) {
this.dialogService.confirm(
this.translate.instant('widgets.persistent-table.delete-request-title'),
this.translate.instant('widgets.persistent-table.delete-request-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => {
this.reloadPersistentRequests();
});
}
});
}
}
openRequestDetails($event: Event, persistentRpc: PersistentRpc) {
if ($event) {
$event.stopPropagation();
}
if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) {
this.dialog.open<PersistentDetailsDialogComponent, PersistentDetailsDialogData, boolean>
(PersistentDetailsDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
persistentRequest: persistentRpc,
allowDelete: this.allowDelete
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.reloadPersistentRequests();
}
}
);
}
}
addPersistentRpcRequest($event: Event){
if ($event) {
$event.stopPropagation();
}
this.dialog.open<PersistentAddDialogComponent, RequestData>
(PersistentAddDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(requestData) => {
if (requestData) {
this.sendRequests(requestData);
}
}
);
}
private sendRequests(requestData: RequestData) {
let commandPromise;
if (requestData.oneWayElseTwoWay) {
commandPromise = this.ctx.controlApi.sendOneWayCommand(
requestData.method,
requestData.params, null,
true, null,
requestData.retries,
requestData.additionalInfo
);
} else {
commandPromise = this.ctx.controlApi.sendTwoWayCommand(
requestData.method,
requestData.params,
null,
true, null,
requestData.retries,
requestData.additionalInfo
);
}
commandPromise.subscribe(
() => {
this.reloadPersistentRequests();
}
);
}
public onActionButtonClick($event: Event, persistentRpc: PersistentRpc, actionDescriptor: PersistentTableWidgetActionDescriptor) {
if (actionDescriptor.details) {
this.openRequestDetails($event, persistentRpc);
}
if (actionDescriptor.delete) {
this.deleteRpcRequest($event, persistentRpc);
}
}
private editFilter($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: PERSISTENT_FILTER_PANEL_DATA,
useValue: {
rpcStatus: this.rpcStatusFilter
} as PersistentFilterPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(PersistentFilterPanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.result) {
const result = componentRef.instance.result;
this.rpcStatusFilter = result.rpcStatus;
this.reloadPersistentRequests();
}
});
this.ctx.detectChanges();
}
}
class PersistentDatasource implements DataSource<PersistentRpcData> {
private persistentSubject = new BehaviorSubject<PersistentRpcData[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<PersistentRpcData>>(emptyPageData<PersistentRpcData>());
private rpcErrorText: string;
private executingSubjects: Array<Subject<any>>;
private executingRpcRequest = false;
public dataLoading = true;
public pageData$ = this.pageDataSubject.asObservable();
constructor(private translate: TranslateService,
private subscription: IWidgetSubscription,
private ctx: WidgetContext) {
}
connect(collectionViewer: CollectionViewer): Observable<PersistentRpcData[] | ReadonlyArray<PersistentRpcData>> {
return this.persistentSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.persistentSubject.complete();
this.pageDataSubject.complete();
}
reset() {
const pageData = emptyPageData<PersistentRpcData>();
this.persistentSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
}
loadPersistent(pageLink: PageLink, rpcStatusFilter: RpcStatus) {
this.dataLoading = true;
const result = new ReplaySubject<PageData<PersistentRpcData>>();
this.fetchEntities(pageLink, rpcStatusFilter).pipe(
catchError(() => of(emptyPageData<PersistentRpcData>())),
).subscribe(
(pageData) => {
this.persistentSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
this.dataLoading = false;
}
);
return result;
}
fetchEntities(pageLink: PageLink, rpcStatusFilter: RpcStatus): Observable<PageData<PersistentRpcData>> {
if (!this.subscription.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
} else if (!this.subscription.targetDeviceId) {
return throwError(new Error('Target device is not set!'));
}
const rpcSubject: Subject<any> = new Subject<any>();
this.ctx.deviceService.getPersistedRpcRequests(this.subscription.targetDeviceId, pageLink, rpcStatusFilter).subscribe(
(responseBody) => {
rpcSubject.next(responseBody);
rpcSubject.complete();
},
(rejection: HttpErrorResponse) => {
this.rpcErrorText = null;
this.executingSubjects = [];
const index = this.executingSubjects.indexOf(rpcSubject);
if (index >= 0) {
this.executingSubjects.splice(index, 1);
}
this.executingRpcRequest = this.executingSubjects.length > 0;
this.subscription.options.callbacks.rpcStateChanged(this.subscription);
if (!this.executingRpcRequest || rejection.status === 504) {
this.subscription.rpcRejection = rejection;
if (rejection.status === 504) {
this.subscription.rpcErrorText = 'Request Timeout.';
} else {
this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText;
const error = this.extractRejectionErrorText(rejection);
if (error) {
this.subscription.rpcErrorText += '</br>';
this.subscription.rpcErrorText += error.message || '';
}
}
this.subscription.callbacks.onRpcFailed(this.subscription);
}
rpcSubject.error(rejection);
}
);
return rpcSubject.asObservable();
}
extractRejectionErrorText(rejection: HttpErrorResponse) {
let error = null;
if (rejection.error) {
error = rejection.error;
try {
error = rejection.error ? JSON.parse(rejection.error) : null;
} catch (e) {}
}
if (error && !error.message) {
error = this.prepareMessageFromData(error);
} else if (error && error.message) {
error = error.message;
}
return error;
}
prepareMessageFromData(data) {
if (typeof data === 'object' && data.constructor === ArrayBuffer) {
const msg = String.fromCharCode.apply(null, new Uint8Array(data));
try {
const msgObj = JSON.parse(msg);
if (msgObj.message) {
return msgObj.message;
} else {
return msg;
}
} catch (e) {
return msg;
}
} else {
return data;
}
}
isEmpty(): Observable<boolean> {
return this.persistentSubject.pipe(
map((requests) => !requests.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
}

15
ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts

@ -21,6 +21,10 @@ import { LedIndicatorComponent } from '@home/components/widget/lib/rpc/led-indic
import { RoundSwitchComponent } from '@home/components/widget/lib/rpc/round-switch.component'; import { RoundSwitchComponent } from '@home/components/widget/lib/rpc/round-switch.component';
import { SwitchComponent } from '@home/components/widget/lib/rpc/switch.component'; import { SwitchComponent } from '@home/components/widget/lib/rpc/switch.component';
import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
import { PersistentTableComponent } from '@home/components/widget/lib/rpc/persistent-table.component';
import { PersistentDetailsDialogComponent } from '@home/components/widget/lib/rpc/persistent-details-dialog.component';
import { PersistentFilterPanelComponent } from '@home/components/widget/lib/rpc/persistent-filter-panel.component';
import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -28,7 +32,11 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
LedIndicatorComponent, LedIndicatorComponent,
RoundSwitchComponent, RoundSwitchComponent,
SwitchComponent, SwitchComponent,
KnobComponent KnobComponent,
PersistentTableComponent,
PersistentDetailsDialogComponent,
PersistentAddDialogComponent,
PersistentFilterPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -38,7 +46,10 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
LedIndicatorComponent, LedIndicatorComponent,
RoundSwitchComponent, RoundSwitchComponent,
SwitchComponent, SwitchComponent,
KnobComponent KnobComponent,
PersistentTableComponent,
PersistentDetailsDialogComponent,
PersistentAddDialogComponent
] ]
}) })
export class RpcWidgetsModule { } export class RpcWidgetsModule { }

1
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -112,6 +112,7 @@
[pageIndex]="source.pageLink.page" [pageIndex]="source.pageLink.page"
[pageSize]="source.pageLink.pageSize" [pageSize]="source.pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions" [pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator> showFirstLastButtons></mat-paginator>
</ng-template> </ng-template>
</mat-tab> </mat-tab>

1
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss

@ -16,6 +16,7 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
.tb-table-widget { .tb-table-widget {
mat-footer-row, mat-row { mat-footer-row, mat-row {
height: 38px; height: 38px;

19
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -16,6 +16,7 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Input, Input,
@ -71,6 +72,8 @@ import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { parseData } from '@home/components/widget/lib/maps/common-maps-utils'; import { parseData } from '@home/components/widget/lib/maps/common-maps-utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean; showTimestamp: boolean;
@ -124,6 +127,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public enableStickyAction = true; public enableStickyAction = true;
public pageSizeOptions; public pageSizeOptions;
public textSearchMode = false; public textSearchMode = false;
public hidePageSize = false;
public textSearch: string = null; public textSearch: string = null;
public sources: TimeseriesTableSource[]; public sources: TimeseriesTableSource[];
public sourceIndex: number; public sourceIndex: number;
@ -150,6 +154,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
private widgetTimewindowChanged$: Subscription; private widgetTimewindowChanged$: Subscription;
private widgetResize$: ResizeObserver;
private searchAction: WidgetAction = { private searchAction: WidgetAction = {
name: 'action.search', name: 'action.search',
@ -167,7 +172,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private utils: UtilsService, private utils: UtilsService,
private translate: TranslateService, private translate: TranslateService,
private domSanitizer: DomSanitizer, private domSanitizer: DomSanitizer,
private datePipe: DatePipe) { private datePipe: DatePipe,
private cd: ChangeDetectorRef) {
super(store); super(store);
} }
@ -190,6 +196,14 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
}); });
} }
); );
this.widgetResize$ = new ResizeObserver(() => {
const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;
if (showHidePageSize !== this.hidePageSize) {
this.hidePageSize = showHidePageSize;
this.cd.markForCheck();
}
});
this.widgetResize$.observe(this.elementRef.nativeElement);
} }
} }
@ -198,6 +212,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.widgetTimewindowChanged$.unsubscribe(); this.widgetTimewindowChanged$.unsubscribe();
this.widgetTimewindowChanged$ = null; this.widgetTimewindowChanged$ = null;
} }
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {

2
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -425,7 +425,7 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
<fieldset class="fields-group fields-group-slider"> <fieldset class="fields-group fields-group-slider" *ngIf="showLegendFieldset">
<legend class="group-title" translate>widget-config.legend</legend> <legend class="group-title" translate>widget-config.legend</legend>
<mat-expansion-panel class="tb-settings"> <mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">

4
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -159,6 +159,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
modelValue: WidgetConfigComponentData; modelValue: WidgetConfigComponentData;
showLegendFieldset = true;
private propagateChange = null; private propagateChange = null;
public dataSettings: FormGroup; public dataSettings: FormGroup;
@ -323,6 +325,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.datasourceTypes.push(DatasourceType.entityCount); this.datasourceTypes.push(DatasourceType.entityCount);
} }
} }
this.dataSettings = this.fb.group({}); this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({});
@ -386,6 +389,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
if (this.modelValue) { if (this.modelValue) {
if (this.widgetType !== this.modelValue.widgetType) { if (this.widgetType !== this.modelValue.widgetType) {
this.widgetType = this.modelValue.widgetType; this.widgetType = this.modelValue.widgetType;
this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest);
this.buildForms(); this.buildForms();
} }
const config = this.modelValue.config; const config = this.modelValue.config;

5
ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts

@ -23,7 +23,8 @@ import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators'; import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators';
import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityId } from '@app/shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
export class RelationsDatasource implements DataSource<EntityRelationInfo> { export class RelationsDatasource implements DataSource<EntityRelationInfo> {
@ -92,8 +93,10 @@ export class RelationsDatasource implements DataSource<EntityRelationInfo> {
relations.forEach(relation => { relations.forEach(relation => {
if (direction === EntitySearchDirection.FROM) { if (direction === EntitySearchDirection.FROM) {
relation.toEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.to.entityType).type); relation.toEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.to.entityType).type);
relation.entityURL = getEntityDetailsPageURL(relation.to.id, relation.to.entityType as EntityType);
} else { } else {
relation.fromEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.from.entityType).type); relation.fromEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.from.entityType).type);
relation.entityURL = getEntityDetailsPageURL(relation.from.id, relation.from.entityType as EntityType);
} }
}); });
return relations; return relations;

10
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -197,16 +197,18 @@ export class WidgetContext {
}; };
controlApi: RpcApi = { controlApi: RpcApi = {
sendOneWayCommand: (method, params, timeout, persistent, requestUUID) => { sendOneWayCommand: (method, params, timeout, persistent,
retries, additionalInfo, requestUUID) => {
if (this.defaultSubscription) { if (this.defaultSubscription) {
return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, requestUUID); return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID);
} else { } else {
return of(null); return of(null);
} }
}, },
sendTwoWayCommand: (method, params, timeout, persistent, requestUUID) => { sendTwoWayCommand: (method, params, timeout, persistent,
retries, additionalInfo, requestUUID) => {
if (this.defaultSubscription) { if (this.defaultSubscription) {
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, requestUUID); return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID);
} else { } else {
return of(null); return of(null);
} }

38
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -29,6 +29,9 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/resources-library-table-config.resolve'; import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/resources-library-table-config.resolve';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { entityDetailsPageBreadcrumbLabelFunction } from '@core/utils';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
@Injectable() @Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -145,18 +148,41 @@ const routes: Routes = [
}, },
{ {
path: 'resources-library', path: 'resources-library',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library',
breadcrumb: { breadcrumb: {
label: 'resource.resources-library', label: 'resource.resources-library',
icon: 'folder' icon: 'folder'
} }
}, },
resolve: { children: [
entitiesTableConfig: ResourcesLibraryTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library',
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'folder'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library'
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
}
]
} }
] ]
} }

13
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -21,7 +21,7 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { Resolve } from '@angular/router'; import { Resolve, Router } from '@angular/router';
import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models'; import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -46,6 +46,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private resourceService: ResourceService, private resourceService: ResourceService,
private translate: TranslateService, private translate: TranslateService,
private router: Router,
private datePipe: DatePipe) { private datePipe: DatePipe) {
this.config.entityType = EntityType.TB_RESOURCE; this.config.entityType = EntityType.TB_RESOURCE;
@ -118,6 +119,13 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
return this.config; return this.config;
} }
private openResource($event: Event, resourceInfo: ResourceInfo) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`settings/resources-library/${resourceInfo.id.id}`);
}
downloadResource($event: Event, resource: ResourceInfo) { downloadResource($event: Event, resource: ResourceInfo) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -127,6 +135,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
onResourceAction(action: EntityAction<ResourceInfo>): boolean { onResourceAction(action: EntityAction<ResourceInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open':
this.openResource(action.event, action.entity);
return true;
case 'downloadResource': case 'downloadResource':
this.downloadResource(action.event, action.entity); this.downloadResource(action.event, action.entity);
return true; return true;

6
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column"> <div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" fxFlex.xs <button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'downloadResource')" (click)="onEntityAction($event, 'downloadResource')"

5
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -64,7 +64,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
this.entityForm.get('title').disable({emitEvent: false}); this.entityForm.get('title').disable({emitEvent: false});
this.entityForm.patchValue({title: ''}, {emitEvent: false}); this.entityForm.patchValue({title: ''}, {emitEvent: false});
} else { } else {
this.entityForm.get('title').enable({emitEvent: false}) this.entityForm.get('title').enable({emitEvent: false});
} }
this.entityForm.patchValue({ this.entityForm.patchValue({
data: null, data: null,
@ -90,7 +90,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
buildForm(entity: Resource): FormGroup { buildForm(entity: Resource): FormGroup {
const form = this.fb.group( const form = this.fb.group(
{ {
title: [entity ? entity.title : "", [Validators.required, Validators.maxLength(255)]], title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, [Validators.required]], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, [Validators.required]],
fileName: [entity ? entity.fileName : null, [Validators.required]], fileName: [entity ? entity.fileName : null, [Validators.required]],
} }
@ -102,6 +102,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
} }
updateForm(entity: Resource) { updateForm(entity: Resource) {
this.entity.name = entity.title;
if (this.isEdit) { if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false}); this.entityForm.get('resourceType').disable({emitEvent: false});
this.entityForm.get('fileName').disable({emitEvent: false}); this.entityForm.get('fileName').disable({emitEvent: false});

42
ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts

@ -20,23 +20,51 @@ import { RouterModule, Routes } from '@angular/router';
import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { AssetsTableConfigResolver } from './assets-table-config.resolver'; import { AssetsTableConfigResolver } from './assets-table-config.resolver';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@core/utils';
const routes: Routes = [ const routes: Routes = [
{ {
path: 'assets', path: 'assets',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'asset.assets',
assetsType: 'tenant',
breadcrumb: { breadcrumb: {
label: 'asset.assets', label: 'asset.assets',
icon: 'domain' icon: 'domain'
} }
}, },
resolve: { children: [
entitiesTableConfig: AssetsTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'asset.assets',
assetsType: 'tenant'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'domain'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'asset.assets',
assetsType: 'tenant'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
}
]
} }
]; ];

6
ui-ngx/src/app/modules/home/pages/asset/asset.component.html

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column"> <div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')" (click)="onEntityAction($event, 'makePublic')"

10
ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts

@ -337,6 +337,13 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
}); });
} }
private openAsset($event: Event, asset: Asset) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`${this.router.url}/${asset.id.id}`);
}
addAssetsToCustomer($event: Event) { addAssetsToCustomer($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -461,6 +468,9 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
onAssetAction(action: EntityAction<AssetInfo>): boolean { onAssetAction(action: EntityAction<AssetInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open':
this.openAsset(action.event, action.entity);
return true;
case 'makePublic': case 'makePublic':
this.makePublic(action.event, action.entity); this.makePublic(action.event, action.entity);
return true; return true;

167
ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts

@ -28,6 +28,9 @@ import { DashboardPageComponent } from '@home/components/dashboard-page/dashboar
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module';
import { EdgesTableConfigResolver } from '@home/pages/edge/edges-table-config.resolver'; import { EdgesTableConfigResolver } from '@home/pages/edge/edges-table-config.resolver';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@core/utils';
const routes: Routes = [ const routes: Routes = [
{ {
@ -51,67 +54,177 @@ const routes: Routes = [
} }
}, },
{ {
path: ':customerId/users', path: ':entityId',
component: EntitiesTableComponent, component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'supervisor_account'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'user.customer-users', title: 'customer.customers'
},
resolve: {
entitiesTableConfig: CustomersTableConfigResolver
}
},
{
path: ':customerId/users',
data: {
breadcrumb: { breadcrumb: {
label: 'user.customer-users', label: 'user.customer-users',
icon: 'account_circle' icon: 'account_circle'
} }
}, },
resolve: { children: [
entitiesTableConfig: UsersTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'user.customer-users'
},
resolve: {
entitiesTableConfig: UsersTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'account_circle'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'user.customer-users'
},
resolve: {
entitiesTableConfig: UsersTableConfigResolver
}
}
]
}, },
{ {
path: ':customerId/devices', path: ':customerId/devices',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.devices',
devicesType: 'customer',
breadcrumb: { breadcrumb: {
label: 'customer.devices', label: 'customer.devices',
icon: 'devices_other' icon: 'devices_other'
} }
}, },
resolve: { children: [
entitiesTableConfig: DevicesTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.devices',
devicesType: 'customer'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'customer.devices',
devicesType: 'customer'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
}
]
}, },
{ {
path: ':customerId/assets', path: ':customerId/assets',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.assets',
assetsType: 'customer',
breadcrumb: { breadcrumb: {
label: 'customer.assets', label: 'customer.assets',
icon: 'domain' icon: 'domain'
} }
}, },
resolve: { children: [
entitiesTableConfig: AssetsTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.assets',
assetsType: 'customer'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'domain'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'customer.assets'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
}
]
}, },
{ {
path: ':customerId/edgeInstances', path: ':customerId/edgeInstances',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.edges',
edgesType: 'customer',
breadcrumb: { breadcrumb: {
label: 'customer.edges', label: 'customer.edges',
icon: 'router' icon: 'router'
} }
}, },
resolve: { children: [
entitiesTableConfig: EdgesTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'customer.edges',
edgesType: 'customer'
},
resolve: {
entitiesTableConfig: EdgesTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'router'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'customer.edges',
edgesType: 'customer'
},
resolve: {
entitiesTableConfig: EdgesTableConfigResolver
}
}
]
}, },
{ {
path: ':customerId/dashboards', path: ':customerId/dashboards',

6
ui-ngx/src/app/modules/home/pages/customer/customer.component.html

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column"> <div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'manageUsers')" (click)="onEntityAction($event, 'manageUsers')"

10
ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts

@ -137,6 +137,13 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
return this.config; return this.config;
} }
private openCustomer($event: Event, customer: Customer) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`${this.router.url}/${customer.id.id}`);
}
manageCustomerUsers($event: Event, customer: Customer) { manageCustomerUsers($event: Event, customer: Customer) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -174,6 +181,9 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
onCustomerAction(action: EntityAction<Customer>): boolean { onCustomerAction(action: EntityAction<Customer>): boolean {
switch (action.action) { switch (action.action) {
case 'open':
this.openCustomer(action.event, action.entity);
return true;
case 'manageUsers': case 'manageUsers':
this.manageCustomerUsers(action.event, action.entity); this.manageCustomerUsers(action.event, action.entity);
return true; return true;

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

@ -20,6 +20,10 @@ import { RouterModule, Routes } from '@angular/router';
import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { DeviceProfilesTableConfigResolver } from './device-profiles-table-config.resolver'; import { DeviceProfilesTableConfigResolver } from './device-profiles-table-config.resolver';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@core/utils';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
const routes: Routes = [ const routes: Routes = [
{ {
@ -41,6 +45,22 @@ const routes: Routes = [
resolve: { resolve: {
entitiesTableConfig: DeviceProfilesTableConfigResolver entitiesTableConfig: DeviceProfilesTableConfigResolver
} }
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'mdi:alpha-d-box'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'device-profile.device-profiles'
},
resolve: {
entitiesTableConfig: DeviceProfilesTableConfigResolver
}
} }
] ]
} }

13
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -15,7 +15,7 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router'; import { Resolve, Router } from '@angular/router';
import { import {
checkBoxCell, checkBoxCell,
DateEntityTableColumn, DateEntityTableColumn,
@ -53,6 +53,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialogService: DialogService, private dialogService: DialogService,
private router: Router,
private dialog: MatDialog) { private dialog: MatDialog) {
this.config.entityType = EntityType.DEVICE_PROFILE; this.config.entityType = EntityType.DEVICE_PROFILE;
@ -178,6 +179,13 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
); );
} }
private openDeviceProfile($event: Event, deviceProfile: DeviceProfile) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`deviceProfiles/${deviceProfile.id.id}`);
}
importDeviceProfile($event: Event) { importDeviceProfile($event: Event) {
this.importExport.importDeviceProfile().subscribe( this.importExport.importDeviceProfile().subscribe(
(deviceProfile) => { (deviceProfile) => {
@ -197,6 +205,9 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
onDeviceProfileAction(action: EntityAction<DeviceProfile>): boolean { onDeviceProfileAction(action: EntityAction<DeviceProfile>): boolean {
switch (action.action) { switch (action.action) {
case 'open':
this.openDeviceProfile(action.event, action.entity);
return true;
case 'setDefault': case 'setDefault':
this.setDefaultDeviceProfile(action.event, action.entity); this.setDefaultDeviceProfile(action.event, action.entity);
return true; return true;

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

@ -20,23 +20,51 @@ import { RouterModule, Routes } from '@angular/router';
import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { DevicesTableConfigResolver } from '@modules/home/pages/device/devices-table-config.resolver'; import { DevicesTableConfigResolver } from '@modules/home/pages/device/devices-table-config.resolver';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@core/utils';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
const routes: Routes = [ const routes: Routes = [
{ {
path: 'devices', path: 'devices',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'device.devices',
devicesType: 'tenant',
breadcrumb: { breadcrumb: {
label: 'device.devices', label: 'device.devices',
icon: 'devices_other' icon: 'devices_other'
} }
}, },
resolve: { children: [
entitiesTableConfig: DevicesTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'device.devices',
devicesType: 'tenant'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'device.devices',
devicesType: 'tenant'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
}
]
} }
]; ];

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

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column"> <div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')" (click)="onEntityAction($event, 'makePublic')"
@ -97,6 +103,7 @@
[selectDefaultProfile]="isAdd" [selectDefaultProfile]="isAdd"
required required
formControlName="deviceProfileId" formControlName="deviceProfileId"
[showDetailsPageLink]="true"
(deviceProfileUpdated)="onDeviceProfileUpdated()" (deviceProfileUpdated)="onDeviceProfileUpdated()"
(deviceProfileChanged)="onDeviceProfileChanged($event)"> (deviceProfileChanged)="onDeviceProfileChanged($event)">
</tb-device-profile-autocomplete> </tb-device-profile-autocomplete>

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

@ -36,6 +36,7 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.mod
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { OtaUpdateType } from '@shared/models/ota-package.models'; import { OtaUpdateType } from '@shared/models/ota-package.models';
import { distinctUntilChanged } from 'rxjs/operators'; import { distinctUntilChanged } from 'rxjs/operators';
import { getEntityDetailsPageURL } from '@core/utils';
@Component({ @Component({
selector: 'tb-device', selector: 'tb-device',

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

@ -371,6 +371,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
return actions; return actions;
} }
private openDevice($event: Event, device: Device) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`${this.router.url}/${device.id.id}`);
}
importDevices($event: Event) { importDevices($event: Event) {
this.homeDialogs.importEntities(EntityType.DEVICE).subscribe((res) => { this.homeDialogs.importEntities(EntityType.DEVICE).subscribe((res) => {
if (res) { if (res) {
@ -541,6 +548,9 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
onDeviceAction(action: EntityAction<DeviceInfo>): boolean { onDeviceAction(action: EntityAction<DeviceInfo>): boolean {
switch (action.action) { switch (action.action) {
case 'open':
this.openDevice(action.event, action.entity);
return true;
case 'makePublic': case 'makePublic':
this.makePublic(action.event, action.entity); this.makePublic(action.event, action.entity);
return true; return true;

131
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -39,6 +39,8 @@ import {
RuleNodeComponentsResolver, RuleNodeComponentsResolver,
TooltipsterResolver TooltipsterResolver
} from '@home/pages/rulechain/rulechain-routing.module'; } from '@home/pages/rulechain/rulechain-routing.module';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { entityDetailsPageBreadcrumbLabelFunction } from '@core/utils';
const routes: Routes = [ const routes: Routes = [
{ {
@ -55,6 +57,7 @@ const routes: Routes = [
component: EntitiesTableComponent, component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.edge-instances',
edgesType: 'tenant' edgesType: 'tenant'
}, },
resolve: { resolve: {
@ -62,49 +65,141 @@ const routes: Routes = [
} }
}, },
{ {
path: ':edgeId/assets', path: ':entityId',
component: EntitiesTableComponent, component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'router'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
assetsType: 'edge', title: 'edge.edge-instances',
edgesType: 'tenant'
},
resolve: {
entitiesTableConfig: EdgesTableConfigResolver
}
},
{
path: ':edgeId/assets',
data: {
breadcrumb: { breadcrumb: {
label: 'edge.assets', label: 'edge.assets',
icon: 'domain' icon: 'domain'
} }
}, },
resolve: { children: [
entitiesTableConfig: AssetsTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets',
assetsType: 'edge'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'domain'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.assets',
assetsType: 'edge'
},
resolve: {
entitiesTableConfig: AssetsTableConfigResolver
}
}
]
}, },
{ {
path: ':edgeId/devices', path: ':edgeId/devices',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
devicesType: 'edge',
breadcrumb: { breadcrumb: {
label: 'edge.devices', label: 'edge.devices',
icon: 'devices_other' icon: 'devices_other'
} }
}, },
resolve: { children: [
entitiesTableConfig: DevicesTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.devices',
devicesType: 'edge'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.devices',
devicesType: 'edge'
},
resolve: {
entitiesTableConfig: DevicesTableConfigResolver
}
}
]
}, },
{ {
path: ':edgeId/entityViews', path: ':edgeId/entityViews',
component: EntitiesTableComponent,
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
entityViewsType: 'edge',
breadcrumb: { breadcrumb: {
label: 'edge.entity-views', label: 'edge.entity-views',
icon: 'view_quilt' icon: 'view_quilt'
}, },
}, },
resolve: { children: [
entitiesTableConfig: EntityViewsTableConfigResolver {
} path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.entity-views',
entityViewsType: 'edge'
},
resolve: {
entitiesTableConfig: EntityViewsTableConfigResolver
}
},
{
path: ':entityId',
component: EntityDetailsPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: entityDetailsPageBreadcrumbLabelFunction,
icon: 'devices_other'
} as BreadCrumbConfig<EntityDetailsPageComponent>,
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'edge.entity-views',
entityViewsType: 'edge'
},
resolve: {
entitiesTableConfig: EntityViewsTableConfigResolver
}
}
]
}, },
{ {
path: ':edgeId/dashboards', path: ':edgeId/dashboards',

6
ui-ngx/src/app/modules/home/pages/edge/edge.component.html

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column"> <div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && !isDetailsPage">
{{'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'makePublic')" (click)="onEntityAction($event, 'makePublic')"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save