Browse Source

Merge branch 'develop/3.6' into release-3.6

release-3.6
Igor Kulikov 3 years ago
parent
commit
ad214eb53e
  1. 7
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  2. 44
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
  4. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
  5. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  7. 2
      application/src/main/resources/logback.xml
  8. 13
      application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
  9. 53
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  10. 21
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  11. 2
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java
  12. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java
  13. 13
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java
  14. 5
      dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java
  15. 158
      dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java
  16. 8
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  17. 10
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts
  18. 2
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html
  19. 5
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts
  20. 10
      ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts
  21. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  22. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts
  23. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  24. 6
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html
  25. 4
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss
  26. 7
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
  27. 4
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  28. 16
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  29. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  30. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html
  31. 3
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html
  32. 3
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts
  33. 37
      ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts
  34. 2
      ui-ngx/src/app/shared/components/json-object-edit.component.html
  35. 44
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  36. 2
      ui-ngx/src/app/shared/components/value-input.component.html
  37. 6
      ui-ngx/src/app/shared/components/value-input.component.ts
  38. 14
      ui-ngx/src/app/shared/models/edge.models.ts
  39. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -756,6 +756,13 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);"); conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) { } catch (Exception e) {
} }
try {
conn.createStatement().execute("UPDATE rule_node SET " +
"configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;");
} catch (Exception e) {
}
try { try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';"); conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';");
} catch (Exception e) { } catch (Exception e) {

44
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -78,6 +78,7 @@ import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.sql.device.DeviceProfileRepository; import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
@ -101,6 +102,8 @@ import static org.thingsboard.server.common.data.StringUtils.isBlank;
@Slf4j @Slf4j
public class DefaultDataUpdateService implements DataUpdateService { public class DefaultDataUpdateService implements DataUpdateService {
private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 100;
@Autowired @Autowired
private TenantService tenantService; private TenantService tenantService;
@ -153,6 +156,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired @Autowired
private EdgeEventDao edgeEventDao; private EdgeEventDao edgeEventDao;
@Autowired
JpaExecutorService jpaExecutorService;
@Override @Override
public void updateData(String fromVersion) throws Exception { public void updateData(String fromVersion) throws Exception {
switch (fromVersion) { switch (fromVersion) {
@ -225,13 +231,15 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Override @Override
public void upgradeRuleNodes() { public void upgradeRuleNodes() {
try { try {
var futures = new ArrayList<ListenableFuture<?>>(100);
int totalRuleNodesUpgraded = 0;
log.info("Starting rule nodes upgrade ..."); log.info("Starting rule nodes upgrade ...");
var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes(); var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes();
log.debug("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size()); log.debug("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size());
nodeClassToVersionMap.forEach(clazz -> { for (var ruleNodeClassInfo : nodeClassToVersionMap) {
var ruleNodeType = clazz.getClassName(); var ruleNodeType = ruleNodeClassInfo.getClassName();
var ruleNodeTypeForLogs = clazz.getSimpleName(); var ruleNodeTypeForLogs = ruleNodeClassInfo.getSimpleName();
var toVersion = clazz.getCurrentVersion(); var toVersion = ruleNodeClassInfo.getCurrentVersion();
log.debug("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion); log.debug("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion);
var ruleNodesToUpdate = new PageDataIterable<>( var ruleNodesToUpdate = new PageDataIterable<>(
pageLink -> ruleChainService.findAllRuleNodesByTypeAndVersionLessThan(ruleNodeType, toVersion, pageLink), 1024 pageLink -> ruleChainService.findAllRuleNodesByTypeAndVersionLessThan(ruleNodeType, toVersion, pageLink), 1024
@ -246,28 +254,44 @@ public class DefaultDataUpdateService implements DataUpdateService {
log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}", log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion); ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
try { try {
var tbVersionedNode = (TbNode) clazz.getClazz().getDeclaredConstructor().newInstance(); var tbVersionedNode = (TbNode) ruleNodeClassInfo.getClazz().getDeclaredConstructor().newInstance();
TbPair<Boolean, JsonNode> upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration); TbPair<Boolean, JsonNode> upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration);
if (upgradeRuleNodeConfigurationResult.getFirst()) { if (upgradeRuleNodeConfigurationResult.getFirst()) {
ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond()); ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond());
} }
ruleNode.setConfigurationVersion(toVersion); ruleNode.setConfigurationVersion(toVersion);
ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode); futures.add(jpaExecutorService.submit(() -> {
log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}", ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion); log.debug("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
}));
if (futures.size() >= MAX_PENDING_SAVE_RULE_NODE_FUTURES) {
log.info("{} upgraded rule nodes so far ...",
totalRuleNodesUpgraded += awaitFuturesToCompleteAndGetCount(futures));
futures.clear();
}
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {} due to: ", log.warn("Failed to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {} due to: ",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion, e); ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion, e);
} }
} }
} }
}); }
log.info("Finished rule nodes upgrade!"); log.info("Finished rule nodes upgrade. Upgraded rule nodes count: {}",
totalRuleNodesUpgraded + awaitFuturesToCompleteAndGetCount(futures));
} catch (Exception e) { } catch (Exception e) {
log.error("Unexpected error during rule nodes upgrade: ", e); log.error("Unexpected error during rule nodes upgrade: ", e);
} }
} }
private int awaitFuturesToCompleteAndGetCount(List<ListenableFuture<?>> futures) {
try {
return Futures.allAsList(futures).get().size();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException("Failed to process save rule nodes requests due to: ", e);
}
}
private final PaginatedUpdater<String, DeviceProfileEntity> deviceProfileEntityDynamicConditionsUpdater = private final PaginatedUpdater<String, DeviceProfileEntity> deviceProfileEntityDynamicConditionsUpdater =
new PaginatedUpdater<>() { new PaginatedUpdater<>() {

4
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java

@ -48,8 +48,8 @@ public abstract class BaseEntityExportService<I extends EntityId, E extends Expo
public abstract Set<EntityType> getSupportedEntityTypes(); public abstract Set<EntityType> getSupportedEntityTypes();
protected void replaceUuidsRecursively(EntitiesExportCtx<?> ctx, JsonNode node, Set<String> skipFieldsSet, Pattern includedFieldsPattern) { protected void replaceUuidsRecursively(EntitiesExportCtx<?> ctx, JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern) {
JacksonUtil.replaceUuidsRecursively(node, skipFieldsSet, includedFieldsPattern, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid)); JacksonUtil.replaceUuidsRecursively(node, skippedRootFields, includedFieldsPattern, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid), true);
} }
protected Stream<UUID> toExternalIds(Collection<UUID> internalIds, Function<UUID, EntityId> entityIdCreator, protected Stream<UUID> toExternalIds(Collection<UUID> internalIds, Function<UUID, EntityId> entityIdCreator,

5
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java

@ -26,8 +26,11 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Collections;
import java.util.Set; import java.util.Set;
import static org.thingsboard.server.service.sync.ie.importing.impl.DashboardImportService.WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN;
@Service @Service
@TbCoreComponent @TbCoreComponent
public class DashboardExportService extends BaseEntityExportService<DashboardId, Dashboard, EntityExportData<Dashboard>> { public class DashboardExportService extends BaseEntityExportService<DashboardId, Dashboard, EntityExportData<Dashboard>> {
@ -43,7 +46,7 @@ public class DashboardExportService extends BaseEntityExportService<DashboardId,
replaceUuidsRecursively(ctx, entityAlias, Set.of("id"), null); replaceUuidsRecursively(ctx, entityAlias, Set.of("id"), null);
} }
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), null); replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN);
} }
} }

6
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -388,11 +388,11 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
} }
protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json, protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json,
Set<String> skipFieldsSet, Pattern includedFieldsPattern, Set<String> skippedRootFields, Pattern includedFieldsPattern,
LinkedHashSet<EntityType> hints) { LinkedHashSet<EntityType> hints) {
JacksonUtil.replaceUuidsRecursively(json, skipFieldsSet, includedFieldsPattern, JacksonUtil.replaceUuidsRecursively(json, skippedRootFields, includedFieldsPattern,
uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints) uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints)
.map(EntityId::getId).orElse(uuid)); .map(EntityId::getId).orElse(uuid), true);
} }
} }

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java

@ -36,6 +36,7 @@ import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@ -44,6 +45,7 @@ import java.util.stream.Collectors;
public class DashboardImportService extends BaseEntityImportService<DashboardId, Dashboard, EntityExportData<Dashboard>> { public class DashboardImportService extends BaseEntityImportService<DashboardId, Dashboard, EntityExportData<Dashboard>> {
private static final LinkedHashSet<EntityType> HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.DASHBOARD, EntityType.DEVICE, EntityType.ASSET)); private static final LinkedHashSet<EntityType> HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.DASHBOARD, EntityType.DEVICE, EntityType.ASSET));
public static final Pattern WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN = Pattern.compile(".*Id.*");
private final DashboardService dashboardService; private final DashboardService dashboardService;
@ -68,7 +70,7 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), null, HINTS); replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), null, HINTS);
} }
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), null, HINTS); replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN, HINTS);
} }
return dashboard; return dashboard;
} }

2
application/src/main/resources/logback.xml

@ -30,6 +30,8 @@
<logger name="org.apache.kafka.clients" level="WARN"/> <logger name="org.apache.kafka.clients" level="WARN"/>
<!-- To enable the logging of scanned rule engine components--> <!-- To enable the logging of scanned rule engine components-->
<!-- <logger name="org.thingsboard.server.service.component.AnnotationComponentDiscoveryService" level="DEBUG" />--> <!-- <logger name="org.thingsboard.server.service.component.AnnotationComponentDiscoveryService" level="DEBUG" />-->
<!-- To enable the debug logging of rule node upgrade -->
<!-- <logger name="org.thingsboard.server.service.install.update.DefaultDataUpdateService" level="DEBUG"/>-->
<!-- Other useful logs --> <!-- Other useful logs -->
<!-- <logger name="org.springframework.jdbc.core" level="TRACE" />--> <!-- <logger name="org.springframework.jdbc.core" level="TRACE" />-->
<!-- <logger name="org.hibernate.SQL" level="DEBUG" />--> <!-- <logger name="org.hibernate.SQL" level="DEBUG" />-->

13
application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java

@ -149,6 +149,19 @@ public class TelemetryControllerTest extends AbstractControllerTest {
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
} }
@Test
public void testEmptyKeyIsProhibited() throws Exception {
loginTenantAdmin();
Device device = createDevice();
String invalidRequestBody = "{\"\": \"value\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
String invalidRequestBody2 = "{\" \": \"value\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody2, String.class, status().isBadRequest());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest());
}
private Device createDevice() throws Exception { private Device createDevice() throws Exception {
String testToken = "TEST_TOKEN"; String testToken = "TEST_TOKEN";

53
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -257,6 +257,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2"); Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2");
Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1");
Dashboard otherDashboard = createDashboard(tenantId1, null, "Dashboard 2");
DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing"); DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing");
String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c";
@ -265,20 +266,43 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
"\"" + aliasId + "\": {\n" + "\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" + "\"alias\": \"assets\",\n" +
"\"filter\": {\n" + "\"filter\": {\n" +
"\"entityList\": [\n" + " \"entityList\": [\n" +
"\"" + asset1.getId().toString() + "\",\n" + " \"" + asset1.getId() + "\",\n" +
"\"" + asset2.getId().toString() + "\",\n" + " \"" + asset2.getId() + "\",\n" +
"\"" + tenantId1.getId().toString() + "\",\n" + " \"" + tenantId1.getId() + "\",\n" +
"\"" + existingDeviceProfile.getId().toString() + "\",\n" + " \"" + existingDeviceProfile.getId() + "\",\n" +
"\"" + unknownUuid + "\"\n" + " \"" + unknownUuid + "\"\n" +
"],\n" + " ],\n" +
"\"resolveMultiple\": true\n" + " \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" +
"},\n" + "},\n" +
"\"id\": \"" + aliasId + "\"\n" + "\"id\": \"" + aliasId + "\"\n" +
"}\n" + "}\n" +
"}"; "}";
String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b";
String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae";
String widgets = "{\n" +
" \"" + widgetId + "\": {\n" +
" \"config\": {\n" +
" \"actions\": {\n" +
" \"rowClick\": [\n" +
" {\n" +
" \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"row\": 0,\n" +
" \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" +
" }\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("widgets", JacksonUtil.toJsonNode(widgets));
dashboardConfiguration.set("description", new TextNode("hallo")); dashboardConfiguration.set("description", new TextNode("hallo"));
dashboard.setConfiguration(dashboardConfiguration); dashboard.setConfiguration(dashboardConfiguration);
dashboard = dashboardService.saveDashboard(dashboard); dashboard = dashboardService.saveDashboard(dashboard);
@ -288,10 +312,12 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
EntityExportData<Asset> asset1ExportData = exportEntity(tenantAdmin1, asset1.getId()); EntityExportData<Asset> asset1ExportData = exportEntity(tenantAdmin1, asset1.getId());
EntityExportData<Asset> asset2ExportData = exportEntity(tenantAdmin1, asset2.getId()); EntityExportData<Asset> asset2ExportData = exportEntity(tenantAdmin1, asset2.getId());
EntityExportData<Dashboard> dashboardExportData = exportEntity(tenantAdmin1, dashboard.getId()); EntityExportData<Dashboard> dashboardExportData = exportEntity(tenantAdmin1, dashboard.getId());
EntityExportData<Dashboard> otherDashboardExportData = exportEntity(tenantAdmin1, otherDashboard.getId());
AssetProfile importedProfile = importEntity(tenantAdmin2, profileExportData).getSavedEntity(); AssetProfile importedProfile = importEntity(tenantAdmin2, profileExportData).getSavedEntity();
Asset importedAsset1 = importEntity(tenantAdmin2, asset1ExportData).getSavedEntity(); Asset importedAsset1 = importEntity(tenantAdmin2, asset1ExportData).getSavedEntity();
Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity(); Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity();
Dashboard importedOtherDashboard = importEntity(tenantAdmin2, otherDashboardExportData).getSavedEntity();
Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity(); Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity();
Map.Entry<String, JsonNode> entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next(); Map.Entry<String, JsonNode> entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next();
@ -311,6 +337,17 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
.isEqualTo(existingDeviceProfile.getId().toString()); .isEqualTo(existingDeviceProfile.getId().toString());
assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id") assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id")
.isEqualTo(tenantId2.toString()); .isEqualTo(tenantId2.toString());
assertThat(entityAlias.getValue().get("filter").get("id").asText()).as("external asset 1 was replaced with imported one")
.isEqualTo(importedAsset1.getId().toString());
ObjectNode widgetConfig = importedDashboard.getWidgetsConfig().get(0);
assertThat(widgetConfig.get("id").asText()).as("widget id is not replaced")
.isEqualTo(widgetId);
JsonNode actionConfig = widgetConfig.get("config").get("actions").get("rowClick").get(0);
assertThat(actionConfig.get("id").asText()).as("action id is not replaced")
.isEqualTo(actionId);
assertThat(actionConfig.get("targetDashboardId").asText()).as("dashboard id is replaced with imported one")
.isEqualTo(importedOtherDashboard.getId().toString());
} }

21
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.collect.Lists;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
@ -35,7 +36,6 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.io.Writer; import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -228,27 +228,24 @@ public class JacksonUtil {
return node; return node;
} }
public static void replaceUuidsRecursively(JsonNode node, Set<String> skipFieldsSet, Pattern includedFieldsPattern, UnaryOperator<UUID> replacer) { public static void replaceUuidsRecursively(JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern, UnaryOperator<UUID> replacer, boolean root) {
if (node == null) { if (node == null) {
return; return;
} }
if (node.isObject()) { if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node; ObjectNode objectNode = (ObjectNode) node;
List<String> fieldNames = new ArrayList<>(objectNode.size()); List<String> fieldNames = Lists.newArrayList(objectNode.fieldNames());
objectNode.fieldNames().forEachRemaining(fieldNames::add);
for (String fieldName : fieldNames) { for (String fieldName : fieldNames) {
if (skipFieldsSet.contains(fieldName)) { if (root && skippedRootFields.contains(fieldName)) {
continue; continue;
} }
if (includedFieldsPattern != null) {
if (!RegexUtils.matches(fieldName, includedFieldsPattern)) {
continue;
}
}
var child = objectNode.get(fieldName); var child = objectNode.get(fieldName);
if (child.isObject() || child.isArray()) { if (child.isObject() || child.isArray()) {
replaceUuidsRecursively(child, skipFieldsSet, includedFieldsPattern, replacer); replaceUuidsRecursively(child, skippedRootFields, includedFieldsPattern, replacer, false);
} else if (child.isTextual()) { } else if (child.isTextual()) {
if (includedFieldsPattern != null && !RegexUtils.matches(fieldName, includedFieldsPattern)) {
continue;
}
String text = child.asText(); String text = child.asText();
String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString()); String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString());
if (!text.equals(newText)) { if (!text.equals(newText)) {
@ -261,7 +258,7 @@ public class JacksonUtil {
for (int i = 0; i < array.size(); i++) { for (int i = 0; i < array.size(); i++) {
JsonNode arrayElement = array.get(i); JsonNode arrayElement = array.get(i);
if (arrayElement.isObject() || arrayElement.isArray()) { if (arrayElement.isObject() || arrayElement.isArray()) {
replaceUuidsRecursively(arrayElement, skipFieldsSet, includedFieldsPattern, replacer); replaceUuidsRecursively(arrayElement, skippedRootFields, includedFieldsPattern, replacer, false);
} else if (arrayElement.isTextual()) { } else if (arrayElement.isTextual()) {
String text = arrayElement.asText(); String text = arrayElement.asText();
String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString()); String newText = RegexUtils.replace(text, RegexUtils.UUID_PATTERN, uuid -> replacer.apply(UUID.fromString(uuid)).toString());

2
dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java

@ -30,7 +30,7 @@ import java.util.List;
*/ */
public interface RuleNodeDao extends Dao<RuleNode> { public interface RuleNodeDao extends Dao<RuleNode> {
List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search); List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String configurationSearch);
PageData<RuleNode> findAllRuleNodesByType(String type, PageLink pageLink); PageData<RuleNode> findAllRuleNodesByType(String type, PageLink pageLink);

4
dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java

@ -56,8 +56,8 @@ public class JpaRuleNodeDao extends JpaAbstractDao<RuleNodeEntity, RuleNode> imp
} }
@Override @Override
public List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search) { public List<RuleNode> findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String configurationSearch) {
return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByTenantIdAndType(tenantId.getId(), type, search)); return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByTenantIdAndType(tenantId.getId(), type, configurationSearch));
} }
@Override @Override

13
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java

@ -29,19 +29,22 @@ import java.util.UUID;
public interface RuleNodeRepository extends JpaRepository<RuleNodeEntity, UUID> { public interface RuleNodeRepository extends JpaRepository<RuleNodeEntity, UUID> {
@Query("SELECT r FROM RuleNodeEntity r WHERE r.ruleChainId in " + @Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.rule_chain_id in " +
"(select id from RuleChainEntity rc WHERE rc.tenantId = :tenantId) " + "(select id from rule_chain rc WHERE rc.tenant_id = :tenantId) AND r.type = :ruleType " +
"AND r.type = :ruleType AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ") " AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))")
List<RuleNodeEntity> findRuleNodesByTenantIdAndType(@Param("tenantId") UUID tenantId, List<RuleNodeEntity> findRuleNodesByTenantIdAndType(@Param("tenantId") UUID tenantId,
@Param("ruleType") String ruleType, @Param("ruleType") String ruleType,
@Param("searchText") String searchText); @Param("searchText") String searchText);
@Query("SELECT r FROM RuleNodeEntity r WHERE r.type = :ruleType AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ") @Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.type = :ruleType " +
" AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))")
Page<RuleNodeEntity> findAllRuleNodesByType(@Param("ruleType") String ruleType, Page<RuleNodeEntity> findAllRuleNodesByType(@Param("ruleType") String ruleType,
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query("SELECT r FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ") @Query(nativeQuery = true, value = "SELECT * FROM rule_node r WHERE r.type = :ruleType " +
" AND configuration_version < :version " +
" AND (:searchText IS NULL OR r.configuration ILIKE CONCAT('%', :searchText, '%'))")
Page<RuleNodeEntity> findAllRuleNodesByTypeAndVersionLessThan(@Param("ruleType") String ruleType, Page<RuleNodeEntity> findAllRuleNodesByTypeAndVersionLessThan(@Param("ruleType") String ruleType,
@Param("version") int version, @Param("version") int version,
@Param("searchText") String searchText, @Param("searchText") String searchText,

5
dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.util;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
@ -48,8 +49,8 @@ public class KvUtils {
String key = tsKvEntry.getKey(); String key = tsKvEntry.getKey();
if (key == null) { if (StringUtils.isBlank(key)) {
throw new DataValidationException("Key can't be null"); throw new DataValidationException("Key can't be null or empty");
} }
if (key.length() > 255) { if (key.length() > 255) {

158
dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java

@ -0,0 +1,158 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.rule;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleNodeDao;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class JpaRuleNodeDaoTest extends AbstractJpaDaoTest {
public static final int COUNT = 40;
public static final String PREFIX_FOR_RULE_NODE_NAME = "SEARCH_TEXT_";
List<UUID> ruleNodeIds;
TenantId tenantId1;
TenantId tenantId2;
RuleChainId ruleChainId1;
RuleChainId ruleChainId2;
@Autowired
private RuleChainDao ruleChainDao;
@Autowired
private RuleNodeDao ruleNodeDao;
ListeningExecutorService executor;
@Before
public void setUp() {
tenantId1 = TenantId.fromUUID(Uuids.timeBased());
ruleChainId1 = new RuleChainId(UUID.randomUUID());
tenantId2 = TenantId.fromUUID(Uuids.timeBased());
ruleChainId2 = new RuleChainId(UUID.randomUUID());
ruleNodeIds = createRuleNodes(tenantId1, tenantId2, ruleChainId1, ruleChainId2, COUNT);
}
@After
public void tearDown() throws Exception {
ruleNodeDao.removeAllByIds(ruleNodeIds);
if (executor != null) {
executor.shutdownNow();
}
}
@Test
public void testSaveRuleName0x00_thenSomeDatabaseException() {
RuleNode ruleNode = getRuleNode(ruleChainId1, "T", "\u0000");
assertThatThrownBy(() -> ruleNodeIds.add(ruleNodeDao.save(tenantId1, ruleNode).getUuidId()));
}
@Test
public void testFindRuleNodesByTenantIdAndType() {
List<RuleNode> ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", PREFIX_FOR_RULE_NODE_NAME);
assertEquals(20, ruleNodes1.size());
List<RuleNode> ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", PREFIX_FOR_RULE_NODE_NAME);
assertEquals(20, ruleNodes2.size());
ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", null);
assertEquals(20, ruleNodes1.size());
ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", null);
assertEquals(20, ruleNodes2.size());
}
@Test
public void testFindRuleNodesByType() {
PageData<RuleNode> ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
}
@Test
public void testFindRuleNodesByTypeAndVersionLessThan() {
PageData<RuleNode> ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0));
assertEquals(20, ruleNodes.getTotalElements());
assertEquals(2, ruleNodes.getTotalPages());
assertEquals(10, ruleNodes.getData().size());
}
private List<UUID> createRuleNodes(TenantId tenantId1, TenantId tenantId2, RuleChainId ruleChainId1, RuleChainId ruleChainId2, int count) {
var chain1 = new RuleChain(ruleChainId1);
chain1.setTenantId(tenantId1);
chain1.setName(ruleChainId1.toString());
ruleChainDao.save(tenantId1, chain1);
var chain2 = new RuleChain(ruleChainId2);
chain2.setTenantId(tenantId2);
chain2.setName(ruleChainId2.toString());
ruleChainDao.save(tenantId2, chain2);
List<UUID> savedRuleNodeIds = new ArrayList<>();
for (int i = 0; i < count / 2; i++) {
savedRuleNodeIds.add(ruleNodeDao.save(tenantId1, getRuleNode(ruleChainId1, "A", Integer.toString(i))).getUuidId());
savedRuleNodeIds.add(ruleNodeDao.save(tenantId2, getRuleNode(ruleChainId2, "B", Integer.toString(i + count / 2))).getUuidId());
}
return savedRuleNodeIds;
}
private RuleNode getRuleNode(RuleChainId ruleChainId, String type, String nameSuffix) {
return getRuleNode(ruleChainId, Uuids.timeBased(), type, nameSuffix);
}
private RuleNode getRuleNode(RuleChainId ruleChainId, UUID ruleNodeId, String type, String nameSuffix) {
RuleNode ruleNode = new RuleNode();
ruleNode.setId(new RuleNodeId(ruleNodeId));
ruleNode.setRuleChainId(ruleChainId);
ruleNode.setName(nameSuffix);
ruleNode.setType(type);
ruleNode.setConfiguration(JacksonUtil.newObjectNode().put("searchHint", PREFIX_FOR_RULE_NODE_NAME + nameSuffix));
ruleNode.setConfigurationVersion(0);
return ruleNode;
}
}

8
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -17,7 +17,7 @@
--> -->
<form [formGroup]="attributeFormGroup" (ngSubmit)="add()" style="min-width: 400px;"> <form [formGroup]="attributeFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ title | translate }}</h2> <h2>{{ (isTelemetry ? 'attribute.add-telemetry' : 'attribute.add') | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
@ -34,7 +34,7 @@
<mat-label translate>attribute.key</mat-label> <mat-label translate>attribute.key</mat-label>
<input matInput formControlName="key" required> <input matInput formControlName="key" required>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('required')"> <mat-error *ngIf="attributeFormGroup.get('key').hasError('required')">
{{ 'attribute.key-required' | translate }} {{ (isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required') | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('maxlength')"> <mat-error *ngIf="attributeFormGroup.get('key').hasError('maxlength')">
{{ 'attribute.key-max-length' | translate }} {{ 'attribute.key-max-length' | translate }}
@ -42,7 +42,7 @@
</mat-form-field> </mat-form-field>
<tb-value-input <tb-value-input
formControlName="value" formControlName="value"
requiredText="attribute.value-required"> [requiredText]="isTelemetry ? 'attribute.telemetry-value-required' : 'attribute.value-required'">
</tb-value-input> </tb-value-input>
</fieldset> </fieldset>
</div> </div>
@ -55,7 +55,7 @@
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty"> [disabled]="(isLoading$ | async) || invalid() || !attributeFormGroup.dirty">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
</div> </div>

10
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts

@ -45,7 +45,7 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
submitted = false; submitted = false;
title = ''; isTelemetry = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@ -62,8 +62,7 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
key: ['', [Validators.required, Validators.maxLength(255)]], key: ['', [Validators.required, Validators.maxLength(255)]],
value: [null, [Validators.required]] value: [null, [Validators.required]]
}); });
this.title = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY ? this.isTelemetry = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY;
'attribute.add-telemetry' : 'attribute.add'
} }
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -72,6 +71,11 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }
invalid(): boolean {
const value = this.attributeFormGroup.get('value').value;
return !Array.isArray(value) && this.attributeFormGroup.invalid;
}
cancel(): void { cancel(): void {
this.dialogRef.close(false); this.dialogRef.close(false);
} }

2
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html

@ -33,7 +33,7 @@
</button> </button>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty"> [disabled]="(isLoading$ | async) || invalid() || !attributeFormGroup.dirty">
{{ 'action.update' | translate }} {{ 'action.update' | translate }}
</button> </button>
</div> </div>

5
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts

@ -62,6 +62,11 @@ export class EditAttributeValuePanelComponent extends PageComponent implements O
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }
invalid(): boolean {
const value = this.attributeFormGroup.get('value').value;
return !Array.isArray(value) && this.attributeFormGroup.invalid;
}
cancel(): void { cancel(): void {
this.overlayRef.dispose(); this.overlayRef.dispose();
} }

10
ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts

@ -107,9 +107,15 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig<EdgeEvent, TimePa
this.columns.push( this.columns.push(
new DateEntityTableColumn<EdgeEvent>('createdTime', 'event.event-time', this.datePipe, '120px'), new DateEntityTableColumn<EdgeEvent>('createdTime', 'event.event-time', this.datePipe, '120px'),
new EntityTableColumn<EdgeEvent>('type', 'event.type', '25%', new EntityTableColumn<EdgeEvent>('type', 'event.type', '25%',
entity => this.translate.instant(edgeEventTypeTranslations.get(entity.type)), entity => ({}), false), entity => {
let key = edgeEventTypeTranslations.get(entity.type);
return key ? this.translate.instant(key) : entity.type;
}, entity => ({}), false),
new EntityTableColumn<EdgeEvent>('action', 'edge.event-action', '25%', new EntityTableColumn<EdgeEvent>('action', 'edge.event-action', '25%',
entity => this.translate.instant(edgeEventActionTypeTranslations.get(entity.action)), entity => ({}), false), entity => {
let key = edgeEventActionTypeTranslations.get(entity.action);
return key ? this.translate.instant(key) : entity.action;
}, entity => ({}), false),
new EntityTableColumn<EdgeEvent>('entityId', 'edge.entity-id', '40%', new EntityTableColumn<EdgeEvent>('entityId', 'edge.entity-id', '40%',
(entity) => entity.entityId ? entity.entityId : '', () => ({}), false), (entity) => entity.entityId ? entity.entityId : '', () => ({}), false),
new EntityTableColumn<EdgeEvent>('status', 'event.status', '10%', new EntityTableColumn<EdgeEvent>('status', 'event.status', '10%',

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -179,7 +179,7 @@
<tb-json-object-edit <tb-json-object-edit
fxFlex fxFlex
fxLayout="column" fxLayout="column"
required jsonRequired
label="{{ 'gateway.configuration' | translate }}" label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson"> formControlName="configurationJson">
</tb-json-object-edit> </tb-json-object-edit>

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts

@ -286,6 +286,7 @@ export class GatewayFormComponent extends PageComponent implements OnInit, OnDes
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
jsonValue: config, jsonValue: config,
required: true,
title: this.translate.instant('gateway.title-connectors-json', {typeName: type}) title: this.translate.instant('gateway.title-connectors-json', {typeName: type})
} }
}).afterClosed().subscribe( }).afterClosed().subscribe(

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -31,6 +31,7 @@ import {
JsonObjectEditDialogComponent, JsonObjectEditDialogComponent,
JsonObjectEditDialogData JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
@Component({ @Component({
@ -77,7 +78,7 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
this.commandForm = this.fb.group({ this.commandForm = this.fb.group({
command: [null,[Validators.required]], command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]], time: [60, [Validators.required, Validators.min(1)]],
params: ["{}", [Validators.required]], params: [{}, [jsonRequired]],
result: [null] result: [null]
}) })
@ -114,7 +115,8 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
jsonValue: JSON.parse(this.commandForm.get('params').value) jsonValue: JSON.parse(this.commandForm.get('params').value),
required: true
} }
}).afterClosed().subscribe( }).afterClosed().subscribe(
(res) => { (res) => {

6
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html

@ -26,14 +26,14 @@
<tb-json-object-edit <tb-json-object-edit
[editorStyle]="{minHeight: '100px'}" [editorStyle]="{minHeight: '100px'}"
fillHeight="true" fillHeight="true"
[required]="settings.attributeRequired" [jsonRequired]="settings.attributeRequired"
label="{{ settings.showLabel ? labelValue : '' }}" label="{{ settings.showLabel ? labelValue : '' }}"
formControlName="currentValue" formControlName="currentValue"
(focusin)="isFocused = true;" (focusin)="isFocused = true;"
(focusout)="isFocused = false;" (focusout)="isFocused = false;"
></tb-json-object-edit> ></tb-json-object-edit>
</fieldset> </fieldset>
<div class="tb-json-input-form__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px"> <div class="tb-json-input__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="!attributeUpdateFormGroup.dirty" [disabled]="!attributeUpdateFormGroup.dirty"
@ -42,7 +42,7 @@
matTooltipPosition="above"> matTooltipPosition="above">
{{ "action.undo" | translate }} {{ "action.undo" | translate }}
</button> </button>
<button mat-button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty"> [disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty">
{{ "action.save" | translate }} {{ "action.save" | translate }}

4
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss

@ -27,6 +27,10 @@
font-size: 18px; font-size: 18px;
color: #a0a0a0; color: #a0a0a0;
} }
&__actions {
height: 48px;
}
} }
.tb-toast { .tb-toast {

7
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts

@ -23,13 +23,14 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models'; import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { createLabelFromDatasource, isDefinedAndNotNull } from '@core/utils'; import { createLabelFromDatasource, isDefinedAndNotNull } from '@core/utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { jsonRequired } from '@shared/components/json-object-edit.component';
enum JsonInputWidgetMode { enum JsonInputWidgetMode {
ATTRIBUTE = 'ATTRIBUTE', ATTRIBUTE = 'ATTRIBUTE',
@ -131,7 +132,7 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit {
private buildForm() { private buildForm() {
const validators: ValidatorFn[] = []; const validators: ValidatorFn[] = [];
if (this.settings.attributeRequired) { if (this.settings.attributeRequired) {
validators.push(Validators.required); validators.push(jsonRequired);
} }
this.attributeUpdateFormGroup = this.fb.group({ this.attributeUpdateFormGroup = this.fb.group({
currentValue: [{}, validators] currentValue: [{}, validators]
@ -143,7 +144,7 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit {
private updateWidgetData(data: Array<DatasourceData>) { private updateWidgetData(data: Array<DatasourceData>) {
if (!this.errorMessage) { if (!this.errorMessage) {
let value = {}; let value = null;
if (data[0].data[0][1] !== '') { if (data[0].data[0][1] !== '') {
try { try {
value = JSON.parse(data[0].data[0][1]); value = JSON.parse(data[0].data[0][1]);

4
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -91,7 +91,7 @@
(click)="openEditJSONDialog($event, key, source)"> (click)="openEditJSONDialog($event, key, source)">
<mat-icon>open_in_new</mat-icon> <mat-icon>open_in_new</mat-icon>
</button> </button>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required') && !multipleInputFormGroup.get(key.formId).hasError('invalidJSON')">
{{ getErrorMessageText(key.settings,'required') }} {{ getErrorMessageText(key.settings,'required') }}
</mat-error> </mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('invalidJSON')"> <mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('invalidJSON')">
@ -193,7 +193,7 @@
</button> </button>
<button mat-button mat-raised-button color="primary" type="submit" <button mat-button mat-raised-button color="primary" type="submit"
class="tb-multiple-input--buttons-container__button" class="tb-multiple-input--buttons-container__button"
[disabled]="!multipleInputFormGroup.dirty || multipleInputFormGroup.invalid"> [disabled]="!multipleInputFormGroup.dirty || invalid()">
{{ saveButtonLabel }} {{ saveButtonLabel }}
</button> </button>
</div> </div>

16
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -604,7 +604,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
} }
public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) { public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) {
if (!this.settings.showActionButtons && !this.isSavingInProgress && this.multipleInputFormGroup.get(key.formId).valid) { const control = this.multipleInputFormGroup.get(key.formId);
if (!this.settings.showActionButtons && !this.isSavingInProgress && (Array.isArray(control.value) || control.valid)) {
this.isSavingInProgress = true; this.isSavingInProgress = true;
const dataToSave: MultipleInputWidgetSource = { const dataToSave: MultipleInputWidgetSource = {
datasource: source.datasource, datasource: source.datasource,
@ -775,6 +776,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
jsonValue: formControl.value, jsonValue: formControl.value,
required: key.settings.required,
title: key.settings.dialogTitle, title: key.settings.dialogTitle,
saveLabel: key.settings.saveButtonLabel, saveLabel: key.settings.saveButtonLabel,
cancelLabel: key.settings.cancelButtonLabel cancelLabel: key.settings.cancelButtonLabel
@ -791,4 +793,16 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
} }
); );
} }
invalid(): boolean {
for (const source of this.sources) {
for (const key of this.visibleKeys(source)) {
const control = this.multipleInputFormGroup.get(key.formId);
if (!Array.isArray(control.value) && control.invalid) {
return true;
}
}
}
return false;
}
} }

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

@ -89,7 +89,6 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-json-object-edit <tb-json-object-edit
[editorStyle]="{minHeight: '100px'}" [editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.title-style' | translate }}" label="{{ 'widget-config.title-style' | translate }}"
formControlName="titleStyle" formControlName="titleStyle"
></tb-json-object-edit> ></tb-json-object-edit>

2
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html

@ -22,7 +22,7 @@
class="tb-rule-node-configuration-json" class="tb-rule-node-configuration-json"
formControlName="configuration" formControlName="configuration"
[label]="'rulenode.configuration' | translate" [label]="'rulenode.configuration' | translate"
[required]="required" [jsonRequired]="required"
[fillHeight]="true"> [fillHeight]="true">
</tb-json-object-edit> </tb-json-object-edit>
</div> </div>

3
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html

@ -33,8 +33,7 @@
<tb-json-object-edit <tb-json-object-edit
formControlName="json" formControlName="json"
label="{{ 'value.json-value' | translate }}" label="{{ 'value.json-value' | translate }}"
validateContent="true" [jsonRequired]="required"
[required]="true"
[fillHeight]="false"> [fillHeight]="false">
</tb-json-object-edit> </tb-json-object-edit>
</fieldset> </fieldset>

3
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts

@ -26,6 +26,7 @@ import { isNotEmptyStr } from '@core/utils';
export interface JsonObjectEditDialogData { export interface JsonObjectEditDialogData {
jsonValue: object; jsonValue: object;
required?: boolean;
title?: string; title?: string;
saveLabel?: string; saveLabel?: string;
cancelLabel?: string; cancelLabel?: string;
@ -43,6 +44,8 @@ export class JsonObjectEditDialogComponent extends DialogComponent<JsonObjectEdi
saveButtonLabel = this.translate.instant('action.save'); saveButtonLabel = this.translate.instant('action.save');
cancelButtonLabel = this.translate.instant('action.cancel'); cancelButtonLabel = this.translate.instant('action.cancel');
required = this.data.required === true;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: JsonObjectEditDialogData, @Inject(MAT_DIALOG_DATA) public data: JsonObjectEditDialogData,

37
ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts

@ -17,19 +17,23 @@
import { Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf } from '@angular/core'; import { Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
UntypedFormControl,
FormGroupDirective, FormGroupDirective,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
NgForm, NgForm,
UntypedFormControl,
ValidationErrors, ValidationErrors,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { isObject } from "@core/utils"; import { isObject } from '@core/utils';
@Directive({ @Directive({
selector: '[tb-json-to-string]', selector: '[tb-json-to-string]',
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
'(blur)': 'onTouched()'
},
providers: [{ providers: [{
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TbJsonToStringDirective), useExisting: forwardRef(() => TbJsonToStringDirective),
@ -48,18 +52,26 @@ import { isObject } from "@core/utils";
export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher { export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher {
private propagateChange = null; private propagateChange = null;
public onTouched = () => {};
private parseError: boolean; private parseError: boolean;
private data: any; private data: any;
@HostListener('input', ['$event.target.value']) input(newValue: any): void { @HostListener('input', ['$event.target.value']) input(newValue: any): void {
try { try {
this.data = JSON.parse(newValue); if (newValue) {
if (isObject(this.data)) { this.data = JSON.parse(newValue);
this.parseError = false; if (isObject(this.data)) {
this.parseError = false;
} else {
this.data = null;
this.parseError = true;
}
} else { } else {
this.parseError = true; this.data = null;
this.parseError = false;
} }
} catch (e) { } catch (e) {
this.data = null;
this.parseError = true; this.parseError = true;
} }
@ -73,9 +85,7 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator,
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); return !!(control && control.invalid && !Array.isArray(control.value) && control.touched);
const customErrorState = !!(control && control.invalid && this.parseError);
return originalErrorState || customErrorState;
} }
validate(c: UntypedFormControl): ValidationErrors { validate(c: UntypedFormControl): ValidationErrors {
@ -87,11 +97,9 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator,
} }
writeValue(obj: any): void { writeValue(obj: any): void {
if (obj) { this.data = obj;
this.data = obj; this.parseError = false;
this.parseError = false; this.render.setProperty(this.element.nativeElement, 'value', obj ? JSON.stringify(obj) : '');
this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj));
}
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -99,5 +107,6 @@ export class TbJsonToStringDirective implements ControlValueAccessor, Validator,
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
this.onTouched = fn;
} }
} }

2
ui-ngx/src/app/shared/components/json-object-edit.component.html

@ -20,7 +20,7 @@
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" class="tb-json-object-toolbar"> <div fxLayout="row" fxLayoutAlign="start center" class="tb-json-object-toolbar">
<label class="tb-title no-padding" <label class="tb-title no-padding"
[ngClass]="{'tb-required': required, [ngClass]="{'tb-required': jsonRequired,
'tb-readonly': readonly, 'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label> 'tb-error': !objectValid}">{{ label }}</label>
<span fxFlex></span> <span fxFlex></span>

44
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -24,7 +24,14 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; import {
ControlValueAccessor,
UntypedFormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
AbstractControl, ValidationErrors
} from '@angular/forms';
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
@ -34,6 +41,9 @@ import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { guid, isDefinedAndNotNull, isObject, isUndefined } from '@core/utils'; import { guid, isDefinedAndNotNull, isObject, isUndefined } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { getAce } from '@shared/models/ace/ace.models'; import { getAce } from '@shared/models/ace/ace.models';
import { coerceBoolean } from '@shared/decorators/coercion';
export const jsonRequired = (control: AbstractControl): ValidationErrors | null => !control.value ? {required: true} : null;
@Component({ @Component({
selector: 'tb-json-object-edit', selector: 'tb-json-object-edit',
@ -73,27 +83,13 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
@Input() sort: (key: string, value: any) => any; @Input() sort: (key: string, value: any) => any;
private requiredValue: boolean; @coerceBoolean()
get required(): boolean {
return this.requiredValue;
}
@Input() @Input()
set required(value: boolean) { jsonRequired: boolean;
this.requiredValue = coerceBooleanProperty(value);
}
private readonlyValue: boolean;
get readonly(): boolean {
return this.readonlyValue;
}
@coerceBoolean()
@Input() @Input()
set readonly(value: boolean) { readonly: boolean;
this.readonlyValue = coerceBooleanProperty(value);
}
fullscreen = false; fullscreen = false;
@ -245,12 +241,10 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
try { try {
if (isDefinedAndNotNull(this.modelValue)) { if (isDefinedAndNotNull(this.modelValue)) {
this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined :
(key, objectValue) => { (key, objectValue) => this.sort(key, objectValue), 2);
return this.sort(key, objectValue);
}, 2);
this.objectValid = true; this.objectValid = true;
} else { } else {
this.objectValid = !this.required; this.objectValid = !this.jsonRequired;
this.validationError = 'Json object is required.'; this.validationError = 'Json object is required.';
} }
} catch (e) { } catch (e) {
@ -288,8 +282,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
this.validationError = errorInfo; this.validationError = errorInfo;
} }
} else { } else {
this.objectValid = !this.required; this.objectValid = !this.jsonRequired;
this.validationError = this.required ? 'Json object is required.' : ''; this.validationError = this.jsonRequired ? 'Json object is required.' : '';
} }
this.modelValue = data; this.modelValue = data;
this.propagateChange(data); this.propagateChange(data);

2
ui-ngx/src/app/shared/components/value-input.component.html

@ -67,7 +67,7 @@
<button matSuffix mat-icon-button (click)="openEditJSONDialog($event)"> <button matSuffix mat-icon-button (click)="openEditJSONDialog($event)">
<mat-icon>open_in_new</mat-icon> <mat-icon>open_in_new</mat-icon>
</button> </button>
<mat-error *ngIf="value.hasError('required')"> <mat-error *ngIf="value.hasError('required') && !value.hasError('invalidJSON')">
{{ (requiredText ? requiredText : 'value.json-value-required') | translate }} {{ (requiredText ? requiredText : 'value.json-value-required') | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="value.hasError('invalidJSON')"> <mat-error *ngIf="value.hasError('invalidJSON')">

6
ui-ngx/src/app/shared/components/value-input.component.ts

@ -73,7 +73,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
jsonValue: this.modelValue jsonValue: this.modelValue,
required: true
} }
}).afterClosed().subscribe( }).afterClosed().subscribe(
(res) => { (res) => {
@ -115,7 +116,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
} }
updateView() { updateView() {
if (this.inputForm.valid || this.valueType === ValueType.BOOLEAN) { if (this.inputForm.valid || this.valueType === ValueType.BOOLEAN ||
(this.valueType === ValueType.JSON && Array.isArray(this.modelValue))) {
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} else { } else {
this.propagateChange(null); this.propagateChange(null);

14
ui-ngx/src/app/shared/models/edge.models.ts

@ -59,9 +59,12 @@ export enum EdgeEventType {
CUSTOMER = 'CUSTOMER', CUSTOMER = 'CUSTOMER',
RELATION = 'RELATION', RELATION = 'RELATION',
TENANT = 'TENANT', TENANT = 'TENANT',
TENANT_PROFILE = 'TENANT_PROFILE',
WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', WIDGETS_BUNDLE = 'WIDGETS_BUNDLE',
WIDGET_TYPE = 'WIDGET_TYPE', WIDGET_TYPE = 'WIDGET_TYPE',
ADMIN_SETTINGS = 'ADMIN_SETTINGS' ADMIN_SETTINGS = 'ADMIN_SETTINGS',
OTA_PACKAGE = 'OTA_PACKAGE',
QUEUE = 'QUEUE'
} }
export enum EdgeEventActionType { export enum EdgeEventActionType {
@ -80,6 +83,8 @@ export enum EdgeEventActionType {
RPC_CALL = 'RPC_CALL', RPC_CALL = 'RPC_CALL',
ALARM_ACK = 'ALARM_ACK', ALARM_ACK = 'ALARM_ACK',
ALARM_CLEAR = 'ALARM_CLEAR', ALARM_CLEAR = 'ALARM_CLEAR',
ALARM_ASSIGNED = 'ALARM_ASSIGNED',
ALARM_UNASSIGNED = 'ALARM_UNASSIGNED',
ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE', ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE',
UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE', UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE',
CREDENTIALS_REQUEST = 'CREDENTIALS_REQUEST', CREDENTIALS_REQUEST = 'CREDENTIALS_REQUEST',
@ -107,9 +112,12 @@ export const edgeEventTypeTranslations = new Map<EdgeEventType, string>(
[EdgeEventType.CUSTOMER, 'edge-event.type-customer'], [EdgeEventType.CUSTOMER, 'edge-event.type-customer'],
[EdgeEventType.RELATION, 'edge-event.type-relation'], [EdgeEventType.RELATION, 'edge-event.type-relation'],
[EdgeEventType.TENANT, 'edge-event.type-tenant'], [EdgeEventType.TENANT, 'edge-event.type-tenant'],
[EdgeEventType.TENANT_PROFILE, 'edge-event.type-tenant-profile'],
[EdgeEventType.WIDGETS_BUNDLE, 'edge-event.type-widgets-bundle'], [EdgeEventType.WIDGETS_BUNDLE, 'edge-event.type-widgets-bundle'],
[EdgeEventType.WIDGET_TYPE, 'edge-event.type-widgets-type'], [EdgeEventType.WIDGET_TYPE, 'edge-event.type-widgets-type'],
[EdgeEventType.ADMIN_SETTINGS, 'edge-event.type-admin-settings'] [EdgeEventType.ADMIN_SETTINGS, 'edge-event.type-admin-settings'],
[EdgeEventType.OTA_PACKAGE, 'edge-event.type-ota-package'],
[EdgeEventType.QUEUE, 'edge-event.type-queue']
] ]
); );
@ -130,6 +138,8 @@ export const edgeEventActionTypeTranslations = new Map<EdgeEventActionType, stri
[EdgeEventActionType.RPC_CALL, 'edge-event.action-type-rpc-call'], [EdgeEventActionType.RPC_CALL, 'edge-event.action-type-rpc-call'],
[EdgeEventActionType.ALARM_ACK, 'edge-event.action-type-alarm-ack'], [EdgeEventActionType.ALARM_ACK, 'edge-event.action-type-alarm-ack'],
[EdgeEventActionType.ALARM_CLEAR, 'edge-event.action-type-alarm-clear'], [EdgeEventActionType.ALARM_CLEAR, 'edge-event.action-type-alarm-clear'],
[EdgeEventActionType.ALARM_ASSIGNED, 'edge-event.action-type-alarm-assigned'],
[EdgeEventActionType.ALARM_UNASSIGNED, 'edge-event.action-type-alarm-unassigned'],
[EdgeEventActionType.ASSIGNED_TO_EDGE, 'edge-event.action-type-assigned-to-edge'], [EdgeEventActionType.ASSIGNED_TO_EDGE, 'edge-event.action-type-assigned-to-edge'],
[EdgeEventActionType.UNASSIGNED_FROM_EDGE, 'edge-event.action-type-unassigned-from-edge'], [EdgeEventActionType.UNASSIGNED_FROM_EDGE, 'edge-event.action-type-unassigned-from-edge'],
[EdgeEventActionType.CREDENTIALS_REQUEST, 'edge-event.action-type-credentials-request'], [EdgeEventActionType.CREDENTIALS_REQUEST, 'edge-event.action-type-credentials-request'],

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

@ -722,6 +722,8 @@
"key-required": "Attribute key is required.", "key-required": "Attribute key is required.",
"value": "Value", "value": "Value",
"value-required": "Attribute value is required.", "value-required": "Attribute value is required.",
"telemetry-key-required": "Telemetry key is required",
"telemetry-value-required": "Telemetry value is required",
"delete-attributes-title": "Are you sure you want to delete { count, plural, =1 {1 attribute} other {# attributes} }?", "delete-attributes-title": "Are you sure you want to delete { count, plural, =1 {1 attribute} other {# attributes} }?",
"delete-attributes-text": "Be careful, after the confirmation all selected attributes will be removed.", "delete-attributes-text": "Be careful, after the confirmation all selected attributes will be removed.",
"delete-attributes": "Delete attributes", "delete-attributes": "Delete attributes",
@ -2014,11 +2016,14 @@
"type-edge": "Edge", "type-edge": "Edge",
"type-user": "User", "type-user": "User",
"type-tenant": "Tenant", "type-tenant": "Tenant",
"type-tenant-profile": "Tenant Profile",
"type-customer": "Customer", "type-customer": "Customer",
"type-relation": "Relation", "type-relation": "Relation",
"type-widgets-bundle": "Widgets Bundle", "type-widgets-bundle": "Widgets Bundle",
"type-widgets-type": "Widgets Type", "type-widgets-type": "Widgets Type",
"type-admin-settings": "Admin Settings", "type-admin-settings": "Admin Settings",
"type-ota-package": "Ota Package",
"type-queue": "Queue",
"action-type-added": "Added", "action-type-added": "Added",
"action-type-deleted": "Deleted", "action-type-deleted": "Deleted",
"action-type-updated": "Updated", "action-type-updated": "Updated",
@ -2034,6 +2039,8 @@
"action-type-rpc-call": "RPC Call", "action-type-rpc-call": "RPC Call",
"action-type-alarm-ack": "Alarm Ack", "action-type-alarm-ack": "Alarm Ack",
"action-type-alarm-clear": "Alarm Clear", "action-type-alarm-clear": "Alarm Clear",
"action-type-alarm-assigned": "Alarm Assigned",
"action-type-alarm-unassigned": "Alarm Unassigned",
"action-type-assigned-to-edge": "Assigned to Edge", "action-type-assigned-to-edge": "Assigned to Edge",
"action-type-unassigned-from-edge": "Unassigned from Edge", "action-type-unassigned-from-edge": "Unassigned from Edge",
"action-type-credentials-request": "Credentials Request", "action-type-credentials-request": "Credentials Request",

Loading…
Cancel
Save