Browse Source

Merge branch 'master' into lwm2m_fix_bug_release

pull/5871/head
Igor Kulikov 5 years ago
parent
commit
4a6007e2b1
  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. 1
      application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java
  4. 207
      application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java
  5. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java
  6. 4
      dao/src/main/java/org/thingsboard/server/dao/Dao.java
  7. 3
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
  8. 10
      dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java
  9. 4
      dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java
  10. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  11. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java
  12. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java
  13. 18
      dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java
  14. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  15. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java
  16. 18
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
  17. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java
  18. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  19. 5
      ui-ngx/angular.json
  20. 3
      ui-ngx/package.json
  21. 11
      ui-ngx/src/app/core/api/widget-api.models.ts
  22. 13
      ui-ngx/src/app/core/api/widget-subscription.ts
  23. 15
      ui-ngx/src/app/core/http/device.service.ts
  24. 2142
      ui-ngx/src/app/core/services/material-icons-codepoints.raw
  25. 2
      ui-ngx/src/app/core/services/utils.service.ts
  26. 5
      ui-ngx/src/app/core/utils.ts
  27. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  28. 1
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  29. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  30. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  31. 25
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  32. 4
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts
  33. 8
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts
  34. 1
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  35. 1
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss
  36. 143
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  37. 57
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html
  38. 119
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss
  39. 182
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts
  40. 16
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  41. 2
      ui-ngx/src/app/modules/home/components/entity/entity.component.ts
  42. 1
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  43. 7
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  44. 6
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  45. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  46. 7
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  47. 21
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.scss
  48. 9
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
  49. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts
  50. 8
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  51. 7
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html
  52. 21
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.scss
  53. 16
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.ts
  54. 6
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html
  55. 11
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  56. 4
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss
  57. 34
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts
  58. 3
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  59. 1
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
  60. 35
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  61. 12
      ui-ngx/src/app/modules/home/components/widget/legend.component.ts
  62. 1
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  63. 1
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss
  64. 19
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  65. 3
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  66. 1
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss
  67. 28
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  68. 52
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  69. 19
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  70. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  71. 29
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  72. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts
  73. 58
      ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts
  74. 92
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html
  75. 31
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss
  76. 75
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts
  77. 118
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html
  78. 34
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss
  79. 126
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts
  80. 44
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html
  81. 37
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss
  82. 67
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts
  83. 126
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html
  84. 53
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss
  85. 580
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts
  86. 15
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts
  87. 1
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  88. 3
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  89. 1
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss
  90. 24
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  91. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  92. 4
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  93. 5
      ui-ngx/src/app/modules/home/models/datasource/relation-datasource.ts
  94. 4
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  95. 1
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  96. 10
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  97. 38
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  98. 13
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  99. 6
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  100. 5
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

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,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = "Status of the RPC", required = true, allowableValues = RPC_STATUS_ALLOWABLE_VALUES)
@RequestParam RpcStatus rpcStatus,
@ApiParam(value = "Status of the RPC", allowableValues = RPC_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) RpcStatus rpcStatus,
@ApiParam(value = RPC_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@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 {
checkParameter("DeviceId", strDeviceId);
try {
if (rpcStatus.equals(RpcStatus.DELETED)) {
if (rpcStatus != null && rpcStatus.equals(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<>() {
@Override
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));
}

1
application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java

@ -23,6 +23,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.service.resource.sql.*Test",
"org.thingsboard.server.service.sql.*Test"
})
public class ServiceSqlTestSuite {

207
application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java

@ -0,0 +1,207 @@
/**
* 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.
*/
package org.thingsboard.server.service.sql;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode;
import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest {
static final int TIMEOUT = 30;
final String TOTALIZER = "Totalizer";
final int TTL = 99999;
final String GENERIC_CUMULATIVE_OBJ = "genericCumulativeObj";
final List<Long> ts = List.of(10L, 20L, 30L, 40L, 60L, 70L, 50L, 80L);
final List<Long> msgValue = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L);
@Autowired
TimeseriesService timeseriesService;
TbMsgTimeseriesNodeConfiguration configuration;
Tenant savedTenant;
User tenantAdmin;
@Before
public void beforeTest() throws Exception {
configuration = new TbMsgTimeseriesNodeConfiguration();
configuration.setUseServerTs(true);
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
}
@Test
public void testSequentialTimeseriesPersistence() throws Exception {
Asset asset = saveAsset("Asset");
Device deviceA = saveDevice("Device A");
Device deviceB = saveDevice("Device B");
Device deviceC = saveDevice("Device C");
Device deviceD = saveDevice("Device D");
List<Device> devices = List.of(deviceA, deviceB, deviceC, deviceD);
for (int i = 0; i < 2; i++) {
int idx = i * devices.size();
saveLatestTsForAssetAndDevice(devices, asset, idx);
checkDiffBetweenLatestTsForDevicesAndAsset(devices, asset);
}
}
Device saveDevice(String name) throws Exception {
Device device = new Device();
device.setName(name);
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
Assert.assertNotNull(savedDevice);
return savedDevice;
}
Asset saveAsset(String name) throws Exception {
Asset asset = new Asset();
asset.setName(name);
asset.setType("default");
Asset savedAsset = doPost("/api/asset", asset, Asset.class);
Assert.assertNotNull(savedAsset);
return savedAsset;
}
void saveLatestTsForAssetAndDevice(List<Device> devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException {
for (Device device : devices) {
TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(),
device.getId(),
getTbMsgMetadata(device.getName(), ts.get(idx)),
TbMsgDataType.JSON,
getTbMsgData(msgValue.get(idx)));
saveDeviceTsEntry(device.getId(), tbMsg, msgValue.get(idx));
saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs()));
idx++;
}
}
void checkDiffBetweenLatestTsForDevicesAndAsset(List<Device> devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException {
TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ);
Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent());
JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject();
for (Device device : devices) {
Long assetValue = assetJsonObject.get(device.getName()).getAsLong();
TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER);
Assert.assertTrue(deviceLatest.getLongValue().isPresent());
Long deviceValue = deviceLatest.getLongValue().get();
Assert.assertEquals(assetValue, deviceValue);
}
}
String getTbMsgData(long value) {
return "{\"Totalizer\": " + value + "}";
}
TbMsgMetaData getTbMsgMetadata(String name, long ts) {
Map<String, String> metadata = new HashMap<>();
metadata.put("deviceName", name);
metadata.put("ts", String.valueOf(ts));
return new TbMsgMetaData(metadata);
}
void saveDeviceTsEntry(EntityId entityId, TbMsg tbMsg, long value) throws ExecutionException, InterruptedException, TimeoutException {
TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs()), new LongDataEntry(TOTALIZER, value));
saveTimeseries(entityId, tsKvEntry);
}
void saveAssetTsEntry(Asset asset, String key, long value, long ts) throws ExecutionException, InterruptedException, TimeoutException {
Optional<String> tsKvEntryOpt = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ).getJsonValue();
TsKvEntry saveTsKvEntry = new BasicTsKvEntry(ts, new JsonDataEntry(GENERIC_CUMULATIVE_OBJ, getJsonObject(key, value, tsKvEntryOpt).toString()));
saveTimeseries(asset.getId(), saveTsKvEntry);
}
@NotNull
JsonObject getJsonObject(String key, long value, Optional<String> tsKvEntryOpt) {
JsonObject jsonObject = new JsonObject();
if (tsKvEntryOpt.isPresent()) {
jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject();
}
jsonObject.addProperty(key, value);
return jsonObject;
}
void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException {
timeseriesService.save(savedTenant.getId(), entityId, List.of(saveTsKvEntry), TTL).get(TIMEOUT, TimeUnit.SECONDS);
}
TsKvEntry getTsKvLatest(EntityId entityId, String key) throws InterruptedException, ExecutionException, TimeoutException {
List<TsKvEntry> tsKvEntries = timeseriesService.findLatest(
savedTenant.getTenantId(),
entityId,
List.of(key)).get(TIMEOUT, TimeUnit.SECONDS);
Assert.assertEquals(1, tsKvEntries.size());
return tsKvEntries.get(0);
}
}

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);
PageData<Rpc> findAllByDeviceId(TenantId tenantId, DeviceId deviceId, 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);
boolean existsById(TenantId tenantId, UUID id);
ListenableFuture<Boolean> existsByIdAsync(TenantId tenantId, UUID id);
T save(TenantId tenantId, T t);
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 java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class AuditLogLevelFilter {
@ -28,7 +29,7 @@ public class AuditLogLevelFilter {
public AuditLogLevelFilter(Map<String, String> mask) {
entityTypeMask.clear();
mask.forEach((entityTypeStr, logLevelMaskStr) -> {
EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase());
EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase(Locale.ENGLISH));
AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase());
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);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
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 =

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;
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);

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)));
}
@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
@Transactional
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
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)));
}

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;
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> 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.tenant.TenantDao;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
/**
@ -75,4 +76,19 @@ public class JpaTenantDaoTest extends AbstractJpaDaoTest {
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 {
MqttClientConfig config = new MqttClientConfig(getSslContext());
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());

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 connectTimeoutSec;
private String clientId;
private boolean appendClientIdSuffix;
private boolean cleanSession;
private boolean ssl;

18
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java

@ -46,8 +46,16 @@ import java.util.concurrent.TimeUnit;
configClazz = TbMsgTimeseriesNodeConfiguration.class,
nodeDescription = "Saves timeseries data",
nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " +
"Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' timestamp will be applied. " +
"Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.",
"Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' message timestamp will be applied. " +
"Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.\n " +
"<br/>" +
"Enable 'useServerTs' param to use the timestamp of the message processing instead of the timestamp from the message. " +
"Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).\n" +
"<br/>" +
"In the case of sequential processing, the platform guarantees that the messages are processed in the order of their submission to the queue. " +
"However, the timestamp of the messages originated by multiple devices/servers may be unsynchronized long before they are pushed to the queue. " +
"The DB layer has certain optimizations to ignore the updates of the \"attributes\" and \"latest values\" tables if the new record has a timestamp that is older than the previous record. " +
"So, to make sure that all the messages will be processed correctly, one should enable this parameter for sequential message processing scenarios.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeTimeseriesConfig",
icon = "file_upload"
@ -77,7 +85,7 @@ public class TbMsgTimeseriesNode implements TbNode {
ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
return;
}
long ts = getTs(msg);
long ts = computeTs(msg, config.isUseServerTs());
String src = msg.getData();
Map<Long, List<KvEntry>> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts);
if (tsKvMap.isEmpty()) {
@ -102,6 +110,10 @@ public class TbMsgTimeseriesNode implements TbNode {
}
}
public static long computeTs(TbMsg msg, boolean ignoreMetadataTs) {
return ignoreMetadataTs ? System.currentTimeMillis() : getTs(msg);
}
public static long getTs(TbMsg msg) {
long ts = -1;
String tsStr = msg.getMetaData().getValue("ts");

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java

@ -23,12 +23,14 @@ public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration<TbMsg
private long defaultTTL;
private boolean skipLatestPersistence;
private boolean useServerTs;
@Override
public TbMsgTimeseriesNodeConfiguration defaultConfiguration() {
TbMsgTimeseriesNodeConfiguration configuration = new TbMsgTimeseriesNodeConfiguration();
configuration.setDefaultTTL(0L);
configuration.setSkipLatestPersistence(false);
configuration.setUseServerTs(false);
return configuration;
}
}

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

5
ui-ngx/angular.json

@ -63,11 +63,6 @@
"glob": "marker-shadow.png",
"input": "node_modules/leaflet/dist/images/",
"output": "/"
},
{
"glob": "**/*",
"input": "node_modules/material-design-icons/iconfont/",
"output": "assets/fonts"
}
],
"styles": [

3
ui-ngx/package.json

@ -28,7 +28,7 @@
"@date-io/date-fns": "^2.11.0",
"@flowjs/flow.js": "^2.14.1",
"@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",
"@mat-datetimepicker/core": "~7.0.1",
"@material-ui/core": "^4.12.3",
@ -61,7 +61,6 @@
"leaflet-providers": "^1.13.0",
"leaflet.gridlayer.googlemutant": "^0.13.4",
"leaflet.markercluster": "^1.5.3",
"material-design-icons": "^3.0.1",
"messageformat": "^2.3.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",

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 { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models';
import { PopoverPlacement } from '@shared/components/popover.models';
import { PersistentRpc } from '@shared/models/rpc.models';
export interface TimewindowFunctions {
onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void;
@ -71,9 +72,9 @@ export interface WidgetSubscriptionApi {
export interface RpcApi {
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,
persistentPollingInterval?: number, requestUUID?: string) => Observable<any>;
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable<any>;
completedCommand: () => void;
}
@ -288,6 +289,8 @@ export interface IWidgetSubscription {
comparisonEnabled?: boolean;
comparisonTimeWindow?: WidgetTimewindow;
persistentRequests?: PageData<PersistentRpc>;
alarms?: PageData<AlarmData>;
alarmSource?: Datasource;
@ -314,9 +317,9 @@ export interface IWidgetSubscription {
updateTimewindowConfig(newTimewindow: Timewindow): void;
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,
persistentPollingInterval?: number, requestUUID?: string): Observable<any>;
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any>;
clearRpcError(): 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,
persistentPollingInterval?: number, requestUUID?: string): Observable<any> {
return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, requestUUID);
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any> {
return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID);
}
sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean,
persistentPollingInterval?: number, requestUUID?: string): Observable<any> {
return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, requestUUID);
persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable<any> {
return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID);
}
clearRpcError(): void {
@ -679,7 +679,8 @@ export class WidgetSubscription implements IWidgetSubscription {
}
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) {
return throwError(new Error('Rpc disabled!'));
} else {
@ -692,6 +693,8 @@ export class WidgetSubscription implements IWidgetSubscription {
method,
params,
persistent,
retries,
additionalInfo,
requestUUID
};
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 { AuthService } from '@core/auth/auth.service';
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({
providedIn: 'root'
@ -143,6 +143,19 @@ export class DeviceService {
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,
config?: RequestConfig): Observable<Array<Device>> {
return this.http.post<Array<Device>>('/api/devices', query, defaultHttpOptionsFromConfig(config));

2142
ui-ngx/src/app/core/services/material-icons-codepoints.raw

File diff suppressed because it is too large

2
ui-ngx/src/app/core/services/utils.service.ts

@ -41,7 +41,7 @@ import { alarmFields } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults';
import materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints';
import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw';
import { Observable, of, ReplaySubject } from 'rxjs';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');

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

@ -20,6 +20,7 @@ import { finalize, share } from 'rxjs/operators';
import { Datasource } from '@app/shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType, baseDetailsPageByEntityType } from '@shared/models/entity-type.models';
const varsRegex = /\${([^}]*)}/g;
@ -460,3 +461,7 @@ export function randomAlphanumeric(length: number): string {
}
return result;
}
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 SocialSharePanelComponent from '@shared/components/socialshare-panel.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 JsFuncComponent from '@shared/components/js-func.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/socialshare-panel.component': SocialSharePanelComponent,
'@shared/components/json-object-edit.component': JsonObjectEditComponent,
'@shared/components/json-object-view.component': JsonObjectViewComponent,
'@shared/components/json-content.component': JsonContentComponent,
'@shared/components/js-func.component': JsFuncComponent,
'@shared/components/fab-toolbar.component': FabToolbarComponent,

1
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -60,6 +60,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.loadDataOnInit = false;
this.tableTitle = '';
this.useTimePageLink = true;
this.pageMode = false;
this.defaultTimewindowInterval = historyInterval(DAY * 30);
this.detailsPanelEnabled = false;
this.selectionEnabled = false;

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

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

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

@ -18,6 +18,7 @@
:host {
width: 100%;
height: 100%;
display: block;
.tb-entity-table {
.tb-entity-table-content {
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';
import { deepClone } from '@core/utils';
import { Filters } from '@shared/models/query/query.models';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { ResizeObserver } from '@juggle/resize-observer';
@Component({
@ -109,6 +111,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
pageLink: PageLink;
textSearchMode = false;
dataSource: AttributeDatasource;
hidePageSize = false;
activeValue = false;
dirtyValue = false;
@ -127,10 +130,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
aliasController: IAliasController;
private widgetDatasource: Datasource;
private widgetResize$: ResizeObserver;
private disableAttributeScopeSelectionValue: boolean;
get disableAttributeScopeSelection(): boolean {
return this.disableAttributeScopeSelectionValue;
}
@Input()
set disableAttributeScopeSelection(value: boolean) {
this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value);
@ -184,7 +191,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
private dashboardUtils: DashboardUtilsService,
private widgetService: WidgetService,
private zone: NgZone,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(store);
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC };
@ -193,6 +201,20 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
}
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) {
@ -263,6 +285,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
this.attributeScopeSelectionReadonly = true;
}
this.mode = 'default';
this.selectedWidgetsBundleAlias = null;
this.attributeScope = this.defaultAttributeScope;
this.pageLink.textSearch = null;
if (this.viewsInited) {

4
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts

@ -52,11 +52,13 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
public entityId: EntityId = null,
public userId: UserId = null,
public customerId: CustomerId = null,
updateOnInit = true) {
updateOnInit = true,
pageMode = false) {
super();
this.loadDataOnInit = updateOnInit;
this.tableTitle = '';
this.useTimePageLink = true;
this.pageMode = pageMode;
this.detailsPanelEnabled = false;
this.selectionEnabled = false;
this.searchEnabled = true;

8
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts

@ -29,6 +29,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'tb-audit-log-table',
@ -104,7 +105,8 @@ export class AuditLogTableComponent implements OnInit {
private translate: TranslateService,
private datePipe: DatePipe,
private dialog: MatDialog,
private store: Store<AppState>) {
private store: Store<AppState>,
private route: ActivatedRoute) {
}
ngOnInit() {
@ -117,6 +119,7 @@ export class AuditLogTableComponent implements OnInit {
}
updateOnInit = true;
}
const pageMode = !!this.route.snapshot.data.isPage;
this.auditLogTableConfig = new AuditLogTableConfig(
this.auditLogService,
this.translate,
@ -126,7 +129,8 @@ export class AuditLogTableComponent implements OnInit {
this.entityIdValue,
this.userIdValue,
this.customerIdValue,
updateOnInit
updateOnInit,
pageMode
);
}

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

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

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

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

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

@ -31,19 +31,20 @@ import {
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { MAX_SAFE_PAGE_SIZE, PageLink, TimePageLink } from '@shared/models/page/page-link';
import { MAX_SAFE_PAGE_SIZE, PageLink, PageQueryParam, TimePageLink } from '@shared/models/page/page-link';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatSort, SortDirection } from '@angular/material/sort';
import { EntitiesDataSource } from '@home/models/datasource/entity-datasource';
import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { catchError, debounceTime, distinctUntilChanged, map, skip, tap } from 'rxjs/operators';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseData, HasId } from '@shared/models/base-data';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router';
import {
CellActionDescriptor, CellActionDescriptorType,
CellActionDescriptor,
CellActionDescriptorType,
EntityActionTableColumn,
EntityColumn,
EntityTableColumn,
@ -55,15 +56,13 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models';
import { DialogService } from '@core/services/dialog.service';
import { AddEntityDialogComponent } from './add-entity-dialog.component';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import {
calculateIntervalStartEndTime,
HistoryWindowType,
Timewindow
} from '@shared/models/time/time.models';
import { calculateIntervalStartEndTime, HistoryWindowType, Timewindow } from '@shared/models/time/time.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { isDefined, isUndefined } from '@core/utils';
import { isDefined, isEmptyStr, isUndefined } from '@core/utils';
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';
@Component({
@ -99,8 +98,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
defaultPageSize = 10;
displayPagination = true;
hidePageSize = false;
pageSizeOptions;
pageLink: PageLink;
pageMode = true;
textSearchMode = false;
timewindow: Timewindow;
dataSource: EntitiesDataSource<BaseData<HasId>>;
@ -117,10 +118,11 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
private sortSubscription: Subscription;
private updateDataSubscription: Subscription;
private viewInited = false;
private widgetResize$: ResizeObserver;
constructor(protected store: Store<AppState>,
public route: ActivatedRoute,
public translate: TranslateService,
@ -128,7 +130,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
private dialogService: DialogService,
private domSanitizer: DomSanitizer,
private cd: ChangeDetectorRef,
private componentFactoryResolver: ComponentFactoryResolver) {
private router: Router,
private componentFactoryResolver: ComponentFactoryResolver,
private elementRef: ElementRef) {
super(store);
}
@ -138,6 +142,20 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} else {
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 {
@ -196,16 +214,19 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.columnsUpdated();
const routerQueryParams: PageQueryParam = this.route.snapshot.queryParams;
let sortOrder: SortOrder = null;
if (this.entitiesTableConfig.defaultSortOrder) {
if (this.entitiesTableConfig.defaultSortOrder || routerQueryParams.hasOwnProperty('direction') || routerQueryParams.hasOwnProperty('property')) {
sortOrder = {
property: this.entitiesTableConfig.defaultSortOrder.property,
direction: this.entitiesTableConfig.defaultSortOrder.direction
property: routerQueryParams?.property || this.entitiesTableConfig.defaultSortOrder.property,
direction: routerQueryParams?.direction || this.entitiesTableConfig.defaultSortOrder.direction
};
}
this.displayPagination = this.entitiesTableConfig.displayPagination;
this.defaultPageSize = this.entitiesTableConfig.defaultPageSize;
this.pageMode = this.entitiesTableConfig.pageMode;
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
if (this.entitiesTableConfig.useTimePageLink) {
@ -217,6 +238,16 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.pageLink = new PageLink(10, 0, null, sortOrder);
}
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE;
if (routerQueryParams.hasOwnProperty('page')) {
this.pageLink.page = Number(routerQueryParams.page);
}
if (routerQueryParams.hasOwnProperty('pageSize')) {
this.pageLink.pageSize = Number(routerQueryParams.pageSize);
}
if (routerQueryParams.hasOwnProperty('textSearch') && !isEmptyStr(routerQueryParams.textSearch)) {
this.textSearchMode = true;
this.pageLink.textSearch = decodeURI(routerQueryParams.textSearch);
}
this.dataSource = this.entitiesTableConfig.dataSource(this.dataLoaded.bind(this));
if (this.entitiesTableConfig.onLoadAction) {
this.entitiesTableConfig.onLoadAction(this.route);
@ -238,36 +269,73 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
debounceTime(150),
distinctUntilChanged(),
tap(() => {
const queryParams: PageQueryParam = {
textSearch: encodeURI(this.pageLink.textSearch) || null
};
if (this.displayPagination) {
this.paginator.pageIndex = 0;
queryParams.page = null;
}
this.updateData();
this.updatedRouterParamsAndData(queryParams);
})
)
.subscribe();
this.route.queryParams.pipe(skip(1)).subscribe((params: PageQueryParam) => {
this.paginator.pageIndex = Number(params.page) || 0;
this.paginator.pageSize = Number(params.pageSize) || this.defaultPageSize;
this.sort.active = params.property || this.entitiesTableConfig.defaultSortOrder.property;
this.sort.direction = (params.direction || this.entitiesTableConfig.defaultSortOrder.direction).toLowerCase() as SortDirection;
if (params.hasOwnProperty('textSearch') && !isEmptyStr(params.textSearch)) {
this.textSearchMode = true;
this.pageLink.textSearch = decodeURI(params.textSearch);
} else {
this.textSearchMode = false;
this.pageLink.textSearch = null;
}
this.updateData();
});
this.updatePaginationSubscriptions();
this.viewInited = true;
}
private updatePaginationSubscriptions() {
if (this.sortSubscription) {
this.sortSubscription.unsubscribe();
this.sortSubscription = null;
}
if (this.updateDataSubscription) {
this.updateDataSubscription.unsubscribe();
this.updateDataSubscription = null;
}
let paginatorSubscription$: Observable<object>;
const sortSubscription$: Observable<object> = this.sort.sortChange.asObservable().pipe(
map((data) => {
const direction = data.direction.toUpperCase();
const queryParams: PageQueryParam = {
direction: (this.entitiesTableConfig?.defaultSortOrder?.direction === direction ? null : direction) as Direction,
property: this.entitiesTableConfig?.defaultSortOrder?.property === data.active ? null : data.active
};
if (this.displayPagination) {
queryParams.page = null;
this.paginator.pageIndex = 0;
}
return queryParams;
})
);
if (this.displayPagination) {
this.sortSubscription = this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
paginatorSubscription$ = this.paginator.page.asObservable().pipe(
map((data) => {
return {
page: data.pageIndex === 0 ? null : data.pageIndex,
pageSize: data.pageSize === this.defaultPageSize ? null : data.pageSize
};
})
);
}
this.updateDataSubscription = ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page)
: this.sort.sortChange) as Observable<any>)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.updateDataSubscription = ((this.displayPagination ? merge(sortSubscription$, paginatorSubscription$)
: sortSubscription$) as Observable<PageQueryParam>).pipe(
tap((queryParams) => {
this.updatedRouterParamsAndData(queryParams);
})
).subscribe();
}
addEnabled() {
@ -451,10 +519,14 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
const queryParams: PageQueryParam = {
textSearch: null
};
if (this.displayPagination) {
this.paginator.pageIndex = 0;
queryParams.page = null;
}
this.updateData();
this.updatedRouterParamsAndData(queryParams);
}
resetSortAndFilter(update: boolean = true, preserveTimewindow: boolean = false) {
@ -469,7 +541,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.sort.active = sortable.id;
this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc';
if (update) {
this.updateData();
this.updatedRouterParamsAndData({}, 'preserve');
}
}
@ -587,4 +659,15 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
return entity.id.id;
}
protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') {
if (this.pageMode) {
this.router.navigate([], {
relativeTo: this.route,
queryParams,
queryParamsHandling
});
} else {
this.updateData();
}
}
}

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

1
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -92,6 +92,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.searchEnabled = false;
this.addEnabled = false;
this.entitiesDeleteEnabled = false;
this.pageMode = false;
this.headerComponent = EventTableHeaderComponent;

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() {
const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.BOOLEAN;
let predicate: BooleanFilterPredicate = null;
if (this.booleanFilterPredicateFormGroup.valid) {
predicate = this.booleanFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.BOOLEAN;
}
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(
@ -148,7 +152,7 @@ export class KeyFilterDialogComponent extends
this.showAutocomplete = false;
}
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').updateValueAndValidity();
} else {

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

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

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

@ -38,6 +38,13 @@
(click)="editDeviceProfile($event)">
<mat-icon class="material-icons">edit</mat-icon>
</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
class="tb-autocomplete"
(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 { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component';
import { emptyPageData } from '@shared/models/page/page-data';
import { getEntityDetailsPageURL } from '@core/utils';
@Component({
selector: 'tb-device-profile-autocomplete',
templateUrl: './device-profile-autocomplete.component.html',
styleUrls: [],
styleUrls: ['./device-profile-autocomplete.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceProfileAutocompleteComponent),
@ -79,6 +80,9 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
@Input()
addNewProfile = true;
@Input()
showDetailsPageLink = false;
@Input()
transportType: DeviceTransportType = null;
@ -110,6 +114,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
filteredDeviceProfiles: Observable<Array<DeviceProfileInfo>>;
searchText = '';
deviceProfileURL: string;
private dirty = false;
@ -240,6 +245,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe(
(profile) => {
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.deviceProfileChanged.emit(profile);
}
@ -278,6 +284,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
const idValue = deviceProfile && deviceProfile.id ? new DeviceProfileId(deviceProfile.id.id) : null;
if (!entityIdEquals(this.modelValue, idValue)) {
this.modelValue = idValue;
this.deviceProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType);
this.propagateChange(this.modelValue);
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){
this.disabled = isDisabled;
if (this.disabled){
this.provisionConfigurationFormGroup.disable();
this.provisionConfigurationFormGroup.disable({emitEvent: false});
} else {
if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) {
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">
<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"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
@ -73,12 +79,14 @@
</tb-queue-type-list>
<tb-ota-package-autocomplete
[useFullEntityId]="true"
[showDetailsPageLink]="true"
[type]="otaUpdateType.FIRMWARE"
[deviceProfileId]="deviceProfileId?.id"
formControlName="firmwareId">
</tb-ota-package-autocomplete>
<tb-ota-package-autocomplete
[useFullEntityId]="true"
[showDetailsPageLink]="true"
[type]="otaUpdateType.SOFTWARE"
[deviceProfileId]="deviceProfileId?.id"
formControlName="softwareId">

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

@ -38,6 +38,13 @@
(click)="editTenantProfile($event)">
<mat-icon class="material-icons">edit</mat-icon>
</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
class="tb-autocomplete"
#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 { MatDialog } from '@angular/material/dialog';
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({
selector: 'tb-tenant-profile-autocomplete',
templateUrl: './tenant-profile-autocomplete.component.html',
styleUrls: [],
styleUrls: ['./tenant-profile-autocomplete.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TenantProfileAutocompleteComponent),
@ -66,6 +67,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
@Input()
disabled: boolean;
@Input()
showDetailsPageLink = false;
@Output()
tenantProfileUpdated = new EventEmitter<TenantProfileId>();
@ -74,6 +78,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
filteredTenantProfiles: Observable<Array<EntityInfoData>>;
searchText = '';
tenantProfileURL: string;
private dirty = false;
@ -123,6 +128,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
(profile) => {
if (profile) {
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.propagateChange(this.modelValue);
}
@ -133,6 +139,11 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.selectTenantProfileFormGroup.disable();
} else {
this.selectTenantProfileFormGroup.enable();
}
}
writeValue(value: TenantProfileId | null): void {
@ -141,6 +152,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor,
this.tenantProfileService.getTenantProfileInfo(value.id).subscribe(
(profile) => {
this.modelValue = new TenantProfileId(profile.id.id);
this.tenantProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType);
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">
<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"
[disabled]="(isLoading$ | async)"
(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">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.to-entity-name' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.toName }}
<a class="link" [routerLink]="relation.entityURL">
{{ relation.toName }}
</a>
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.TO" matColumnDef="fromEntityTypeName">
@ -129,7 +131,9 @@
<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-cell *matCellDef="let relation">
{{ relation.fromName }}
<a class="link" [routerLink]="relation.entityURL">
{{ relation.fromName }}
</a>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
@ -165,6 +169,7 @@
<mat-paginator [length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>
[pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize"></mat-paginator>
</div>
</div>

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

@ -18,6 +18,7 @@
:host {
width: 100%;
height: 100%;
display: block;
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
@ -41,6 +42,9 @@
.table-container {
overflow: auto;
.link {
border-bottom: none;
}
}
.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.
///
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 { PageLink } from '@shared/models/page/page-link';
import { MatPaginator } from '@angular/material/paginator';
@ -38,6 +47,8 @@ import {
import { EntityId } from '@shared/models/id/entity-id';
import { RelationsDatasource } from '../../models/datasource/relation-datasource';
import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { ResizeObserver } from '@juggle/resize-observer';
@Component({
selector: 'tb-relation-table',
@ -56,6 +67,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
displayedColumns: string[];
direction: EntitySearchDirection;
pageLink: PageLink;
hidePageSize = false;
textSearchMode = false;
dataSource: RelationsDatasource;
@ -65,6 +77,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
viewsInited = false;
private widgetResize$: ResizeObserver;
@Input()
set active(active: boolean) {
if (this.activeValue !== active) {
@ -100,7 +114,9 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
private entityRelationService: EntityRelationService,
public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService) {
private dialogService: DialogService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(store);
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC };
@ -111,6 +127,20 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn
}
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() {

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"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>
[pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize"></mat-paginator>
</div>
</div>

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

@ -16,6 +16,7 @@
:host {
width: 100%;
height: 100%;
display: block;
.tb-entity-table {
.tb-entity-table-content {
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.
///
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 { TranslateService } from '@ngx-translate/core';
import { PageComponent } from '@shared/components/page.component';
@ -36,13 +46,14 @@ import {
WidgetActionsDatasource
} from '@home/components/widget/action/manage-widget-actions.component.models';
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 {
WidgetActionDialogComponent,
WidgetActionDialogData
} from '@home/components/widget/action/widget-action-dialog.component';
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({
selector: 'tb-manage-widget-actions',
@ -69,11 +80,14 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
displayedColumns: string[];
pageLink: PageLink;
textSearchMode = false;
hidePageSize = false;
dataSource: WidgetActionsDatasource;
viewsInited = false;
dirtyValue = false;
private widgetResize$: ResizeObserver;
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ -85,7 +99,9 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
private translate: TranslateService,
private utils: UtilsService,
private dialog: MatDialog,
private dialogs: DialogService) {
private dialogs: DialogService,
private cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(store);
const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder);
@ -94,9 +110,20 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
}
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 {
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
}
ngAfterViewInit() {

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

@ -60,11 +60,13 @@ export class LegendComponent implements OnInit {
}
legendKeys(): LegendKey[] {
let keys = this.legendData.keys;
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);
try {
let keys = this.legendData.keys;
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);
} catch (e) {}
}
}

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

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

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

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

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

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

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

@ -41,7 +41,7 @@
<table mat-table [dataSource]="entityDatasource" [trackBy]="trackByEntityId"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header [disabled]="!column.sortable"> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column, row)"
[ngStyle]="cellStyle(entity, column, row)">
@ -106,6 +106,7 @@
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
</div>
</div>

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

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

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

@ -16,6 +16,7 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Injector,
@ -107,6 +108,8 @@ import { sortItems } from '@shared/models/page/page-link';
import { entityFields } from '@shared/models/entity.models';
import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string;
@ -140,6 +143,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public pageLink: EntityDataPageLink;
public sortOrderProperty: string;
public textSearchMode = false;
public hidePageSize = false;
public columns: Array<EntityColumn> = [];
public displayedColumns: string[] = [];
public entityDatasource: EntityDatasource;
@ -153,6 +157,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private settings: EntitiesTableWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private widgetResize$: ResizeObserver;
private entitiesTitlePattern: string;
@ -193,7 +198,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private utils: UtilsService,
private datePipe: DatePipe,
private translate: TranslateService,
private domSanitizer: DomSanitizer) {
private domSanitizer: DomSanitizer,
private cd: ChangeDetectorRef) {
super(store);
this.pageLink = {
page: 0,
@ -211,6 +217,22 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.initializeConfig();
this.updateDatasources();
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 {
@ -324,6 +346,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
label: 'entityName',
def: 'entityName',
title: entityNameColumnTitle,
sortable: true,
entityKey: {
key: 'name',
type: EntityKeyType.ENTITY_FIELD
@ -347,6 +370,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
label: 'entityLabel',
def: 'entityLabel',
title: entityLabelColumnTitle,
sortable: true,
entityKey: {
key: 'label',
type: EntityKeyType.ENTITY_FIELD
@ -370,6 +394,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
label: 'entityType',
def: 'entityType',
title: this.translate.instant('entity.entity-type'),
sortable: true,
entityKey: {
key: 'entityType',
type: EntityKeyType.ENTITY_FIELD
@ -403,6 +428,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label);
dataKey.title = dataKey.label;
dataKey.def = 'def' + this.columns.length;
dataKey.sortable = !dataKey.usePostProcessing;
const keySettings: TableWidgetDataKeySettings = dataKey.settings;
if (dataKey.type === DataKeyType.entityField &&
!isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') {

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

@ -24,7 +24,9 @@ import {
defaultSettings,
FormattedData,
MapSettings,
MarkerSettings, MarkerIconInfo, MarkerImageInfo,
MarkerIconInfo,
MarkerImageInfo,
MarkerSettings,
PolygonSettings,
PolylineSettings,
ReplaceInfo,
@ -182,7 +184,7 @@ export default abstract class LeafletMap {
addEditControl() {
// Customize edit marker
if (this.options.draggableMarker) {
if (this.options.draggableMarker && !this.options.hideDrawControlButton) {
const actions = [{
text: L.PM.Utils.getTranslation('actions.cancel'),
onClick: () => this.toggleDrawMode('tbMarker')
@ -197,7 +199,7 @@ export default abstract class LeafletMap {
}
// Customize edit polygon
if (this.editPolygons) {
if (this.editPolygons && !this.options.hideDrawControlButton) {
const rectangleActions = [
{
text: L.PM.Utils.getTranslation('actions.cancel'),
@ -231,18 +233,27 @@ export default abstract class LeafletMap {
const translateService = this.ctx.$injector.get(TranslateService);
this.map.pm.setLang('en', translateService.instant('widgets.maps'), 'en');
this.map.pm.addControls({
position: 'topleft',
drawMarker: false,
drawCircle: false,
drawCircleMarker: false,
drawRectangle: false,
drawPolyline: false,
drawPolygon: false,
editMode: this.editPolygons,
cutPolygon: this.editPolygons,
rotateMode: this.editPolygons
});
if (!this.options.hideAllControlButton) {
this.map.pm.addControls({
position: 'topleft',
drawControls: !this.options.hideDrawControlButton,
drawMarker: false,
drawCircle: false,
drawCircleMarker: false,
drawRectangle: false,
drawPolyline: false,
drawPolygon: false,
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) => {
if (e.shape === 'tbMarker') {
@ -344,6 +355,7 @@ export default abstract class LeafletMap {
}
if (this.options.draggableMarker || this.editPolygons) {
map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions);
map.pm.applyGlobalOptions();
this.addEditControl();
} else {
this.map.pm.disableDraw();
@ -555,7 +567,7 @@ export default abstract class LeafletMap {
updatedMarkers.push(m);
}
} 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) {
createdMarkers.push(m);
}
@ -569,7 +581,7 @@ export default abstract class LeafletMap {
}
});
this.markersData = markersData;
if ((this.options as MarkerSettings).useClusterMarkers) {
if (this.options.useClusterMarkers) {
if (createdMarkers.length) {
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();
}
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings,
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings,
updateBounds = true, callback?): Marker {
const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker);
if (callback) {
@ -597,7 +609,7 @@ export default abstract class LeafletMap {
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.markers.set(key, newMarker);
@ -786,7 +798,7 @@ export default abstract class LeafletMap {
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);
if (updateBounds) {
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;
};
export interface EditorSettings {
snappable: boolean;
initDragMode: boolean;
hideAllControlButton: boolean;
hideDrawControlButton: boolean;
hideEditControlButton: boolean;
hideRemoveControlButton: boolean;
}
export interface HistorySelectSettings {
buttonColor: string;
}
@ -231,7 +240,7 @@ export interface TripAnimationSettings extends PolygonSettings {
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 = {
xPosKeyName: 'xPos',
@ -271,7 +280,13 @@ export const defaultSettings: any = {
draggableMarker: false,
editablePolygon: false,
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 = [

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 {
commonMapSettingsSchema,
editorSettingSchema,
mapPolygonSchema,
markerClusteringSettingsSchema,
markerClusteringSettingsSchemaLeaflet,
@ -115,6 +116,8 @@ export class MapWidgetController implements MapWidgetInterface {
`model.useClusterMarkers === true && model.provider !== "image-map"`)]);
addToSchema(schema, clusteringSchema);
addGroupInfo(schema, 'Markers Clustering Settings');
addToSchema(schema, addCondition(editorSettingSchema, '(model.editablePolygon === true || model.draggableMarker === true)'));
addGroupInfo(schema, 'Editor settings');
}
return schema;
}

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

@ -14,19 +14,17 @@
/// limitations under the License.
///
import L, { Icon, LeafletMouseEvent } from 'leaflet';
import { FormattedData, MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, MarkerSettings } from './map-models';
import L, { LeafletMouseEvent } from 'leaflet';
import {
bindPopupActions,
createTooltip,
} from './maps-utils';
import {
aspectCache,
fillPattern,
parseWithTranslation,
processPattern,
safeExecute
} from './common-maps-utils';
FormattedData,
MarkerIconInfo,
MarkerIconReadyFunction,
MarkerImageInfo,
MarkerSettings,
UnitedMapSettings
} from './map-models';
import { bindPopupActions, createTooltip, } from './maps-utils';
import { aspectCache, fillPattern, parseWithTranslation, processPattern, safeExecute } from './common-maps-utils';
import tinycolor from 'tinycolor2';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import LeafletMap from './leaflet-map';
@ -40,10 +38,13 @@ export class Marker {
data: 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?) {
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 = [
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,
safeExecute
} from './common-maps-utils';
import { FormattedData, MarkerSettings, PolygonSettings } from './map-models';
import { FormattedData, PolygonSettings, UnitedMapSettings } from './map-models';
export class Polygon {
@ -32,7 +32,7 @@ export class Polygon {
data: 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?) {
this.dataSources = dataSources;
this.data = data;
@ -47,7 +47,8 @@ export class Polygon {
weight: settings.polygonStrokeWeight,
fillOpacity: settings.polygonOpacity,
opacity: settings.polygonStrokeOpacity,
pmIgnore: !settings.editablePolygon
pmIgnore: !settings.editablePolygon,
snapIgnore: !settings.snappable
}).addTo(this.map);
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'
}
};
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 { SwitchComponent } from '@home/components/widget/lib/rpc/switch.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({
declarations:
@ -28,7 +32,11 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
LedIndicatorComponent,
RoundSwitchComponent,
SwitchComponent,
KnobComponent
KnobComponent,
PersistentTableComponent,
PersistentDetailsDialogComponent,
PersistentAddDialogComponent,
PersistentFilterPanelComponent
],
imports: [
CommonModule,
@ -38,7 +46,10 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component';
LedIndicatorComponent,
RoundSwitchComponent,
SwitchComponent,
KnobComponent
KnobComponent,
PersistentTableComponent,
PersistentDetailsDialogComponent,
PersistentAddDialogComponent
]
})
export class RpcWidgetsModule { }

1
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -73,6 +73,7 @@ export interface EntityData {
export interface EntityColumn extends DataKey {
def: string;
title: string;
sortable: boolean;
entityKey?: EntityKey;
}

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

@ -52,7 +52,7 @@
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ h.dataKey.label }} </mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header [disabled]="!h.sortable"> {{ h.dataKey.label }} </mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, h.index, row, row[h.index], rowIndex)"
[ngStyle]="cellStyle(source, h.index, row, row[h.index], rowIndex)">
@ -112,6 +112,7 @@
[pageIndex]="source.pageLink.page"
[pageSize]="source.pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
</ng-template>
</mat-tab>

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

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

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

@ -16,6 +16,7 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
@ -71,6 +72,8 @@ import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { DatePipe } from '@angular/common';
import { parseData } from '@home/components/widget/lib/maps/common-maps-utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean;
@ -88,6 +91,7 @@ interface TimeseriesRow {
interface TimeseriesHeader {
index: number;
dataKey: DataKey;
sortable: boolean;
}
interface TimeseriesTableSource {
@ -124,6 +128,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public enableStickyAction = true;
public pageSizeOptions;
public textSearchMode = false;
public hidePageSize = false;
public textSearch: string = null;
public sources: TimeseriesTableSource[];
public sourceIndex: number;
@ -150,6 +155,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private subscriptions: Subscription[] = [];
private widgetTimewindowChanged$: Subscription;
private widgetResize$: ResizeObserver;
private searchAction: WidgetAction = {
name: 'action.search',
@ -167,7 +173,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private utils: UtilsService,
private translate: TranslateService,
private domSanitizer: DomSanitizer,
private datePipe: DatePipe) {
private datePipe: DatePipe,
private cd: ChangeDetectorRef) {
super(store);
}
@ -190,6 +197,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 +213,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.widgetTimewindowChanged$.unsubscribe();
this.widgetTimewindowChanged$ = null;
}
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
}
ngAfterViewInit(): void {
@ -308,10 +326,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
for (let a = 0; a < datasource.dataKeys.length; a++ ) {
const dataKey = datasource.dataKeys[a];
const keySettings: TableWidgetDataKeySettings = dataKey.settings;
const sortable = !dataKey.usePostProcessing;
const index = a + 1;
source.header.push({
index,
dataKey
dataKey,
sortable
});
source.displayedColumns.push(index + '');
source.rowDataTemplate[dataKey.label] = null;

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

@ -425,7 +425,7 @@
</ng-template>
</mat-expansion-panel>
</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>
<mat-expansion-panel class="tb-settings">
<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;
showLegendFieldset = true;
private propagateChange = null;
public dataSettings: FormGroup;
@ -323,6 +325,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.datasourceTypes.push(DatasourceType.entityCount);
}
}
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = 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.widgetType !== this.modelValue.widgetType) {
this.widgetType = this.modelValue.widgetType;
this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest);
this.buildForms();
}
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 { EntityId } from '@app/shared/models/id/entity-id';
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> {
@ -92,8 +93,10 @@ export class RelationsDatasource implements DataSource<EntityRelationInfo> {
relations.forEach(relation => {
if (direction === EntitySearchDirection.FROM) {
relation.toEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.to.entityType).type);
relation.entityURL = getEntityDetailsPageURL(relation.to.id, relation.to.entityType as EntityType);
} else {
relation.fromEntityTypeName = this.translate.instant(entityTypeTranslations.get(relation.from.entityType).type);
relation.entityURL = getEntityDetailsPageURL(relation.from.id, relation.from.entityType as EntityType);
}
});
return relations;

4
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -25,8 +25,7 @@ import { EntityComponent } from '@home/components/entity/entity.component';
import { Type } from '@angular/core';
import { EntityAction } from './entity-component.models';
import { HasUUID } from '@shared/models/id/has-uuid';
import { PageLink, TimePageLink } from '@shared/models/page/page-link';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { PageLink } from '@shared/models/page/page-link';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { ActivatedRoute } from '@angular/router';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
@ -160,6 +159,7 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
addDialogStyle = {};
defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC};
displayPagination = true;
pageMode = true;
defaultPageSize = 10;
columns: Array<EntityColumn<L>> = [];
cellActionDescriptors: Array<CellActionDescriptor<L>> = [];

1
ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts

@ -50,6 +50,7 @@ export interface IEntitiesTableComponent {
displayPagination: boolean;
pageSizeOptions: number[];
pageLink: PageLink;
pageMode: boolean;
textSearchMode: boolean;
timewindow: Timewindow;
dataSource: EntitiesDataSource<BaseData<HasId>>;

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

@ -197,16 +197,18 @@ export class WidgetContext {
};
controlApi: RpcApi = {
sendOneWayCommand: (method, params, timeout, persistent, requestUUID) => {
sendOneWayCommand: (method, params, timeout, persistent,
retries, additionalInfo, requestUUID) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, requestUUID);
return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID);
} else {
return of(null);
}
},
sendTwoWayCommand: (method, params, timeout, persistent, requestUUID) => {
sendTwoWayCommand: (method, params, timeout, persistent,
retries, additionalInfo, requestUUID) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, requestUUID);
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID);
} else {
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 { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
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 '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -145,18 +148,41 @@ const routes: Routes = [
},
{
path: 'resources-library',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'resource.resources-library',
breadcrumb: {
label: 'resource.resources-library',
icon: 'folder'
}
},
resolve: {
entitiesTableConfig: ResourcesLibraryTableConfigResolver
}
children: [
{
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,
EntityTableConfig
} 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 { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -46,6 +46,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
constructor(private store: Store<AppState>,
private resourceService: ResourceService,
private translate: TranslateService,
private router: Router,
private datePipe: DatePipe) {
this.config.entityType = EntityType.TB_RESOURCE;
@ -118,6 +119,13 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
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) {
if ($event) {
$event.stopPropagation();
@ -127,6 +135,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
onResourceAction(action: EntityAction<ResourceInfo>): boolean {
switch (action.action) {
case 'open':
this.openResource(action.event, action.entity);
return true;
case 'downloadResource':
this.downloadResource(action.event, action.entity);
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">
<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
[disabled]="(isLoading$ | async)"
(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.patchValue({title: ''}, {emitEvent: false});
} else {
this.entityForm.get('title').enable({emitEvent: false})
this.entityForm.get('title').enable({emitEvent: false});
}
this.entityForm.patchValue({
data: null,
@ -90,7 +90,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
buildForm(entity: Resource): FormGroup {
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]],
fileName: [entity ? entity.fileName : null, [Validators.required]],
}
@ -102,6 +102,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
}
updateForm(entity: Resource) {
this.entity.name = entity.title;
if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false});
this.entityForm.get('fileName').disable({emitEvent: false});

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

Loading…
Cancel
Save