Browse Source

Merge branch 'rc' of github.com:thingsboard/thingsboard into cf-fixes

pull/13040/head
IrynaMatveieva 2 years ago
parent
commit
d10b928bbc
  1. 5
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  2. 8
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  3. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  5. 25
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java
  6. 14
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  7. 21
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  8. 16
      application/src/main/resources/banner.txt
  9. 33
      application/src/main/resources/thingsboard.yml
  10. 8
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  11. 4
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  12. 11
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  13. 4
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  14. 14
      application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java
  15. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  16. 4
      common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java
  18. 5
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java
  19. 2
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
  20. 3
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java
  21. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java
  22. 2
      common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java
  23. 5
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java
  24. 34
      common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java
  25. 92
      common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java
  26. 81
      common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java
  27. 45
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java
  28. 2
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java
  29. 24
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
  30. 1
      common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java
  31. 41
      common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java
  32. 33
      common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java
  33. 12
      common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java
  34. 2
      common/util/src/main/java/org/thingsboard/common/util/TbBytePool.java
  35. 2
      common/util/src/main/java/org/thingsboard/common/util/TbStringPool.java
  36. 3
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  37. 19
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  38. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  39. 48
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  40. 2
      edqs/src/main/resources/edqs.yml
  41. 4
      edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java
  42. 1
      ui-ngx/src/app/core/auth/auth.models.ts
  43. 8
      ui-ngx/src/app/core/auth/auth.service.ts
  44. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  45. 4
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  46. 14
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  47. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html
  48. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts
  49. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  50. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  51. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html
  52. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts
  53. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  54. 11
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  55. 1
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html
  56. 7
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts
  57. 4
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html
  58. 25
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts
  59. 6
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts
  60. 4
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  61. 6
      ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html
  62. 14
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  63. 6
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  64. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts
  65. 2
      ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts
  66. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  67. 34
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts
  68. 10
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
  69. 2
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts
  70. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  71. 9
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts
  72. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.html
  73. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html
  74. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts
  75. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts
  76. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts
  77. 2
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  78. 6
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  79. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  80. 94
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  81. 7
      ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts
  82. 4
      ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts
  83. 5
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  84. 13
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  85. 7
      ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts
  86. 8
      ui-ngx/src/app/shared/components/file-input.component.html
  87. 4
      ui-ngx/src/app/shared/components/file-input.component.scss
  88. 12
      ui-ngx/src/app/shared/components/popover.component.ts
  89. 4
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
  90. 2
      ui-ngx/src/app/shared/components/time/timezone.component.ts
  91. 50
      ui-ngx/src/app/shared/import-export/import-dialog.component.html
  92. 2
      ui-ngx/src/app/shared/models/tenant.model.ts
  93. 4
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  94. 2
      ui-ngx/src/assets/dashboard/customer_user_home_page.json
  95. 56
      ui-ngx/src/assets/help/en_US/math/math-methods_fn.md
  96. 27
      ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md
  97. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  98. 198
      ui-ngx/src/assets/locale/locale.constant-ja_JP.json
  99. 969
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  100. 9
      ui-ngx/src/typings/leaflet-extend-tb.d.ts

5
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -228,10 +228,9 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Reset password (resetPassword)", @ApiOperation(value = "Reset password (resetPassword)",
notes = "Checks the password reset token and updates the password. " + notes = "Checks the password reset token and updates the password. " +
"If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
"If token is not valid, returns '400 Bad Request'.") "If token is not valid, returns '400 Bad Request'.")
@PostMapping(value = "/noauth/resetPassword") @PostMapping(value = "/noauth/resetPassword")
public JwtPair resetPassword(@Parameter(description = "Reset password request.") public void resetPassword(@Parameter(description = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest, @RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
String resetToken = resetPasswordRequest.getResetToken(); String resetToken = resetPasswordRequest.getResetToken();
@ -263,8 +262,6 @@ public class AuthController extends BaseController {
} }
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
return tokenFactory.createTokenPair(securityUser);
} else { } else {
throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }

8
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -507,7 +507,7 @@ public class EdgeMsgConstructorUtils {
JsonObject data = entityData.getAsJsonObject(); JsonObject data = entityData.getAsJsonObject();
builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts)); builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts));
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Can't convert to telemetry proto, entityData [{}]", tenantId, entityId, entityData, e); log.trace("[{}][{}] Can't convert to telemetry proto, entityData [{}]", tenantId, entityId, entityData, e);
} }
break; break;
case ATTRIBUTES_UPDATED: case ATTRIBUTES_UPDATED:
@ -522,7 +522,7 @@ public class EdgeMsgConstructorUtils {
builder.setPostAttributeScope(getScopeOfDefault(data)); builder.setPostAttributeScope(getScopeOfDefault(data));
builder.setAttributeTs(ts); builder.setAttributeTs(ts);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", tenantId, entityId, entityData, e); log.trace("[{}][{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", tenantId, entityId, entityData, e);
} }
break; break;
case POST_ATTRIBUTES: case POST_ATTRIBUTES:
@ -533,7 +533,7 @@ public class EdgeMsgConstructorUtils {
builder.setPostAttributeScope(getScopeOfDefault(data)); builder.setPostAttributeScope(getScopeOfDefault(data));
builder.setAttributeTs(ts); builder.setAttributeTs(ts);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Can't convert to PostAttributesMsg, entityData [{}]", tenantId, entityId, entityData, e); log.trace("[{}][{}] Can't convert to PostAttributesMsg, entityData [{}]", tenantId, entityId, entityData, e);
} }
break; break;
case ATTRIBUTES_DELETED: case ATTRIBUTES_DELETED:
@ -546,7 +546,7 @@ public class EdgeMsgConstructorUtils {
attributeDeleteMsg.build(); attributeDeleteMsg.build();
builder.setAttributeDeleteMsg(attributeDeleteMsg); builder.setAttributeDeleteMsg(attributeDeleteMsg);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", tenantId, entityId, entityData, e); log.trace("[{}][{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", tenantId, entityId, entityData, e);
} }
break; break;
} }

10
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -203,7 +203,7 @@ public abstract class EdgeGrpcSession implements Closeable {
@Override @Override
public void onError(Throwable t) { public void onError(Throwable t) {
log.error("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); log.trace("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t);
closeSession(); closeSession();
} }
@ -255,7 +255,7 @@ public abstract class EdgeGrpcSession implements Closeable {
private void doSync(EdgeSyncCursor cursor) { private void doSync(EdgeSyncCursor cursor) {
if (cursor.hasNext()) { if (cursor.hasNext()) {
EdgeEventFetcher next = cursor.getNext(); EdgeEventFetcher next = cursor.getNext();
log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", log.debug("[{}][{}] starting sync process, cursor current idx = {}, class = {}",
tenantId, edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); tenantId, edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName());
ListenableFuture<Pair<Long, Long>> future = startProcessingEdgeEvents(next); ListenableFuture<Pair<Long, Long>> future = startProcessingEdgeEvents(next);
Futures.addCallback(future, new FutureCallback<>() { Futures.addCallback(future, new FutureCallback<>() {
@ -651,7 +651,7 @@ public abstract class EdgeGrpcSession implements Closeable {
default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, sessionId, edgeEvent.getAction()); default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, sessionId, edgeEvent.getAction());
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e);
} }
if (downlinkMsg != null) { if (downlinkMsg != null) {
result.add(downlinkMsg); result.add(downlinkMsg);
@ -763,7 +763,7 @@ public abstract class EdgeGrpcSession implements Closeable {
try { try {
outputStream.onNext(responseMsg); outputStream.onNext(responseMsg);
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e);
connected = false; connected = false;
sessionCloseListener.accept(edge, sessionId); sessionCloseListener.accept(edge, sessionId);
} finally { } finally {
@ -909,7 +909,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.error("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e);
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -65,8 +65,10 @@ public class EdgeSyncCursor {
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
} }
Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); Customer publicCustomer = ctx.getCustomerService().findPublicCustomer(edge.getTenantId());
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); if (publicCustomer != null) {
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));
}
if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) {
fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId()));
fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId()));

25
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java

@ -81,13 +81,16 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor {
UUID uuid = new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()); UUID uuid = new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB());
CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId()); CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId());
switch (actionType) { switch (actionType) {
case UPDATED: case ADDED:
List<ListenableFuture<Void>> futures = new ArrayList<>(); Customer customerById = edgeCtx.getCustomerService().findCustomerById(tenantId, customerId);
PageDataIterable<Edge> edges = new PageDataIterable<>(link -> edgeCtx.getEdgeService().findEdgesByTenantIdAndCustomerId(tenantId, customerId, link), 1024); if (customerById != null && customerById.isPublic()) {
for (Edge edge : edges) { return findEdgesAndSaveEdgeEvents(link -> edgeCtx.getEdgeService().findEdgesByTenantId(tenantId, link),
futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null)); tenantId, type, actionType, customerId);
} }
return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); return Futures.immediateFuture(null);
case UPDATED:
return findEdgesAndSaveEdgeEvents(link -> edgeCtx.getEdgeService().findEdgesByTenantIdAndCustomerId(tenantId, customerId, link),
tenantId, type, actionType, customerId);
case DELETED: case DELETED:
EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB())); EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB()));
return saveEdgeEvent(tenantId, edgeId, type, actionType, customerId, null); return saveEdgeEvent(tenantId, edgeId, type, actionType, customerId, null);
@ -96,6 +99,16 @@ public class CustomerEdgeProcessor extends BaseEdgeProcessor {
} }
} }
public ListenableFuture<Void> findEdgesAndSaveEdgeEvents(PageDataIterable.FetchFunction<Edge> edgeFetcher, TenantId tenantId,
EdgeEventType type, EdgeEventActionType actionType, CustomerId customerId) {
List<ListenableFuture<Void>> futures = new ArrayList<>();
PageDataIterable<Edge> edges = new PageDataIterable<>(edgeFetcher, 1024);
for (Edge edge : edges) {
futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null));
}
return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
}
@Override @Override
public EdgeEventType getEdgeEventType() { public EdgeEventType getEdgeEventType() {
return EdgeEventType.CUSTOMER; return EdgeEventType.CUSTOMER;

14
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -551,11 +551,15 @@ public class DefaultTbClusterService implements TbClusterService {
} }
private void processEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) { private void processEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) {
var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId)); if (edgesEnabled) {
serviceIdOpt.ifPresentOrElse( var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId));
serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()), serviceIdOpt.ifPresentOrElse(
() -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg) serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()),
); () -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg)
);
} else {
log.trace("Edges disabled. Ignoring edge notification {} for edgeId: {}", toEdgeNotificationMsg, edgeId);
}
} }
private void pushMsgToEdgeNotification(ToEdgeNotificationMsg toEdgeNotificationMsg, String serviceId) { private void pushMsgToEdgeNotification(ToEdgeNotificationMsg toEdgeNotificationMsg, String serviceId) {

21
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -50,6 +50,7 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.queue.processing.IdMsgPair;
@ -194,36 +195,42 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
protected void handleNotification(UUID id, TbProtoQueueMsg<ToEdgeNotificationMsg> msg, TbCallback callback) { protected void handleNotification(UUID id, TbProtoQueueMsg<ToEdgeNotificationMsg> msg, TbCallback callback) {
ToEdgeNotificationMsg toEdgeNotificationMsg = msg.getValue(); ToEdgeNotificationMsg toEdgeNotificationMsg = msg.getValue();
try { try {
EdgeRpcService edgeRpcService = edgeCtx.getEdgeRpcService();
if (edgeRpcService == null) {
log.debug("No EdgeRpcService available (edge functionality disabled), ignoring msg: {}", toEdgeNotificationMsg);
callback.onSuccess();
return;
}
if (toEdgeNotificationMsg.hasEdgeHighPriority()) { if (toEdgeNotificationMsg.hasEdgeHighPriority()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeHighPriority()); EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeHighPriority());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess(); callback.onSuccess();
} else if (toEdgeNotificationMsg.hasEdgeEventUpdate()) { } else if (toEdgeNotificationMsg.hasEdgeEventUpdate()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeEventUpdate()); EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeEventUpdate());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess(); callback.onSuccess();
} else if (toEdgeNotificationMsg.hasToEdgeSyncRequest()) { } else if (toEdgeNotificationMsg.hasToEdgeSyncRequest()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getToEdgeSyncRequest()); EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getToEdgeSyncRequest());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess(); callback.onSuccess();
} else if (toEdgeNotificationMsg.hasFromEdgeSyncResponse()) { } else if (toEdgeNotificationMsg.hasFromEdgeSyncResponse()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getFromEdgeSyncResponse()); EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getFromEdgeSyncResponse());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess(); callback.onSuccess();
} else if (toEdgeNotificationMsg.hasComponentLifecycle()) { } else if (toEdgeNotificationMsg.hasComponentLifecycle()) {
ComponentLifecycleMsg componentLifecycle = ProtoUtils.fromProto(toEdgeNotificationMsg.getComponentLifecycle()); ComponentLifecycleMsg componentLifecycle = ProtoUtils.fromProto(toEdgeNotificationMsg.getComponentLifecycle());
TenantId tenantId = componentLifecycle.getTenantId(); TenantId tenantId = componentLifecycle.getTenantId();
EdgeId edgeId = new EdgeId(componentLifecycle.getEntityId().getId()); EdgeId edgeId = new EdgeId(componentLifecycle.getEntityId().getId());
if (ComponentLifecycleEvent.DELETED.equals(componentLifecycle.getEvent())) { if (ComponentLifecycleEvent.DELETED.equals(componentLifecycle.getEvent())) {
edgeCtx.getEdgeRpcService().deleteEdge(tenantId, edgeId); edgeRpcService.deleteEdge(tenantId, edgeId);
} else if (ComponentLifecycleEvent.UPDATED.equals(componentLifecycle.getEvent())) { } else if (ComponentLifecycleEvent.UPDATED.equals(componentLifecycle.getEvent())) {
Edge edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edgeId); Edge edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edgeId);
edgeCtx.getEdgeRpcService().updateEdge(tenantId, edge); edgeRpcService.updateEdge(tenantId, edge);
} }
callback.onSuccess(); callback.onSuccess();
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Error processing edge notification message", e); log.error("Error processing edge notification message {}", toEdgeNotificationMsg, e);
callback.onFailure(e); callback.onFailure(e);
} }

16
application/src/main/resources/banner.txt

@ -1,10 +1,10 @@
______ __ _ ____ __ _____ _ _ ____ _
/_ __/ / /_ (_) ____ ____ _ _____ / __ ) ____ ____ _ _____ ____/ / |_ _| |__ (_)_ __ __ _ ___| __ ) ___ __ _ _ __ __| |
/ / / __ \ / / / __ \ / __ `/ / ___/ / __ | / __ \ / __ `/ / ___/ / __ / | | | '_ \| | '_ \ / _` / __| _ \ / _ \ / _` | '__/ _` |
/ / / / / / / / / / / / / /_/ / (__ ) / /_/ / / /_/ // /_/ / / / / /_/ / | | | | | | | | | | (_| \__ \ |_) | (_) | (_| | | | (_| |
/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_____/ \____/ \__,_/ /_/ \__,_/ |_| |_| |_|_|_| |_|\__, |___/____/ \___/ \__,_|_| \__,_|
/____/ |___/
=================================================== ===========================================================
:: ${application.title} :: ${application.formatted-version} :: ${application.title} :: ${application.formatted-version}
=================================================== ===========================================================

33
application/src/main/resources/thingsboard.yml

@ -1423,7 +1423,7 @@ device:
pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}" pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}"
gateway: gateway:
# The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch # The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch
image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:latest}" image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.7-stable}"
# Edges parameters # Edges parameters
edges: edges:
@ -1590,11 +1590,24 @@ queue:
# tb_rule_engine.sq: # tb_rule_engine.sq:
# - key: max.poll.records # - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}"
tb_edge:
# Properties for consumers targeting edge service update topics.
- key: max.poll.records
# Define the maximum number of records that can be polled from tb_edge topics per request.
value: "${TB_QUEUE_KAFKA_EDGE_EVENTS_MAX_POLL_RECORDS:10}"
tb_edge.notifications:
# Properties for consumers targeting high-priority edge notifications.
# These notifications include RPC calls, lifecycle events, and new queue messages,
# requiring minimal latency and swift processing.
- key: max.poll.records
# Define the maximum number of records that can be polled from tb_edge.notifications.<SERVICE_ID> topics.
value: "${TB_QUEUE_KAFKA_EDGE_HP_EVENTS_MAX_POLL_RECORDS:10}"
tb_edge_event.notifications: tb_edge_event.notifications:
# Example of specific consumer properties value per topic for edge event # Properties for consumers targeting downlinks meant for specific edge topics.
# Topic names are dynamically constructed using tenant and edge identifiers.
- key: max.poll.records - key: max.poll.records
# Example of specific consumer properties value per topic for edge event # Define the maximum number of records that can be polled from tb_edge_event.notifications.<TENANT_ID>.<EDGE_ID> topics.
value: "${TB_QUEUE_KAFKA_EDGE_EVENT_MAX_POLL_RECORDS:50}" value: "${TB_QUEUE_KAFKA_EDGE_NOTIFICATIONS_MAX_POLL_RECORDS:10}"
tb_housekeeper: tb_housekeeper:
# Consumer properties for Housekeeper tasks topic # Consumer properties for Housekeeper tasks topic
- key: max.poll.records - key: max.poll.records
@ -1765,9 +1778,13 @@ queue:
max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
# Maximum timeout for requests to EDQS # Maximum timeout for requests to EDQS
max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}"
# Strings longer than this threshold will be compressed
string_compression_length_threshold: "${TB_EDQS_STRING_COMPRESSION_LENGTH_THRESHOLD:512}"
stats: stats:
# Enable/disable statistics for EDQS # Enable/disable statistics for EDQS
enabled: "${TB_EDQS_STATS_ENABLED:true}" enabled: "${TB_EDQS_STATS_ENABLED:true}"
# Threshold for slow queries to log, in milliseconds
slow_query_threshold: "${TB_EDQS_SLOW_QUERY_THRESHOLD_MS:3000}"
vc: vc:
# Default topic name # Default topic name
topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}"
@ -1837,11 +1854,13 @@ queue:
# Interval in milliseconds to poll messages # Interval in milliseconds to poll messages
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
edge: edge:
# Default topic name # Topic name to notify edge service on entity updates, assignment, etc.
topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}" topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}"
# For high-priority notifications that require minimum latency and processing time # Topic prefix for high-priority edge notifications (rpc, lifecycle, new messages in queue) that require minimum latency and processing time.
# Each tb-core has its own topic: <PREFIX>.<SERVICE_ID>
notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}" notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}"
# For edge events messages # Topic prefix for downlinks to be pushed to specific edge.
# Every edge has its own unique topic: <PREFIX>.<TENANT_ID>.<EDGE_ID>
event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}" event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}"
# Amount of partitions used by Edge services # Amount of partitions used by Edge services
partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}" partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}"

8
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -23,6 +23,7 @@ import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -94,6 +95,9 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId mqttDeviceProfileId;
private DeviceProfileId coapDeviceProfileId; private DeviceProfileId coapDeviceProfileId;
@Value("${device.connectivity.gateway.image_version:3.7-stable}")
private String gatewayImageVersion;
@Before @Before
public void beforeTest() throws Exception { public void beforeTest() throws Exception {
loginSysAdmin(); loginSysAdmin();
@ -298,7 +302,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"services:\n" + "services:\n" +
" # ThingsBoard IoT Gateway Service Configuration\n" + " # ThingsBoard IoT Gateway Service Configuration\n" +
" tb-gateway:\n" + " tb-gateway:\n" +
" image: thingsboard/tb-gateway:latest\n" + " image: thingsboard/tb-gateway:" + gatewayImageVersion + "\n" +
" container_name: tb-gateway\n" + " container_name: tb-gateway\n" +
" restart: always\n" + " restart: always\n" +
"\n" + "\n" +
@ -847,7 +851,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"-t \"application/json\" -e \"{temperature:25}\" coap://test.domain:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); "-t \"application/json\" -e \"{temperature:25}\" coap://test.domain:5683/api/v1/%s/telemetry", credentials.getCredentialsId()));
assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download");
assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " +
"-R "+ CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); "-R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry", credentials.getCredentialsId()));
JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER);
assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it " + assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it " +

4
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -885,6 +885,10 @@ public class EdgeControllerTest extends AbstractControllerTest {
device.setType("default"); device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class); Device savedDevice = doPost("/api/device", device, Device.class);
// create public customer
doPost("/api/customer/public/device/" + savedDevice.getId().getId(), Device.class);
doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class);
simulateEdgeActivation(edge); simulateEdgeActivation(edge);
doPost("/api/edge/" + edge.getId().getId().toString() doPost("/api/edge/" + edge.getId().getId().toString()

11
application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java

@ -233,16 +233,9 @@ public class UserControllerTest extends AbstractControllerTest {
.put("password", "testPassword2"); .put("password", "testPassword2");
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString()); Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
JsonNode tokenInfo = readResponse( doPost("/api/noauth/resetPassword", resetPasswordRequest)
doPost("/api/noauth/resetPassword", resetPasswordRequest) .andExpect(status().isOk());
.andExpect(status().isOk()), JsonNode.class);
Mockito.verify(mailService).sendPasswordWasResetEmail(anyString(), anyString()); Mockito.verify(mailService).sendPasswordWasResetEmail(anyString(), anyString());
validateAndSetJwtToken(tokenInfo, email);
doGet("/api/auth/user")
.andExpect(status().isOk())
.andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name())))
.andExpect(jsonPath("$.email", is(email)));
resetTokens(); resetTokens();

4
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -168,6 +168,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Device savedDevice = saveDevice("Edge Device 1", THERMOSTAT_DEVICE_PROFILE_NAME); Device savedDevice = saveDevice("Edge Device 1", THERMOSTAT_DEVICE_PROFILE_NAME);
// create public customer
doPost("/api/customer/public/device/" + savedDevice.getId().getId(), Device.class);
doDelete("/api/customer/device/" + savedDevice.getId().getId(), Device.class);
Asset savedAsset = saveAsset("Edge Asset 1"); Asset savedAsset = saveAsset("Edge Asset 1");
updateRootRuleChainMetadata(); updateRootRuleChainMetadata();

14
application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java

@ -44,9 +44,8 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest {
@Test @Test
public void testTenantProfiles() throws Exception { public void testTenantProfiles() throws Exception {
loginSysAdmin(); loginSysAdmin();
TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
// save current values into tmp to revert after test TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile);
TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
// updated edge tenant profile // updated edge tenant profile
edgeTenantProfile.setName("Tenant Profile Edge Test"); edgeTenantProfile.setName("Tenant Profile Edge Test");
@ -64,14 +63,15 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest {
Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileMsg.getDescription()); Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileMsg.getDescription());
Assert.assertEquals("Tenant Profile Edge Test", tenantProfileMsg.getName()); Assert.assertEquals("Tenant Profile Edge Test", tenantProfileMsg.getName());
doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class);
loginTenantAdmin(); loginTenantAdmin();
} }
@Test @Test
public void testIsolatedTenantProfile() throws Exception { public void testIsolatedTenantProfile() throws Exception {
loginSysAdmin(); loginSysAdmin();
TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile);
// set tenant profile isolated and add 2 queues - main and isolated // set tenant profile isolated and add 2 queues - main and isolated
edgeTenantProfile.setIsolatedTbRuleEngine(true); edgeTenantProfile.setIsolatedTbRuleEngine(true);
@ -110,6 +110,10 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest {
Assert.assertNotNull(queue); Assert.assertNotNull(queue);
Assert.assertEquals(tenantId, queue.getTenantId()); Assert.assertEquals(tenantId, queue.getTenantId());
} }
loginSysAdmin();
doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class);
loginTenantAdmin();
} }
private TenantProfileQueueConfiguration createQueueConfig(String queueName, String queueTopic) { private TenantProfileQueueConfiguration createQueueConfig(String queueName, String queueTopic) {

2
common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java

@ -41,6 +41,8 @@ public interface CustomerService extends EntityDaoService {
Customer findOrCreatePublicCustomer(TenantId tenantId); Customer findOrCreatePublicCustomer(TenantId tenantId);
Customer findPublicCustomer(TenantId tenantId);
PageData<Customer> findCustomersByTenantId(TenantId tenantId, PageLink pageLink); PageData<Customer> findCustomersByTenantId(TenantId tenantId, PageLink pageLink);
void deleteCustomersByTenantId(TenantId tenantId); void deleteCustomersByTenantId(TenantId tenantId);

4
common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java

@ -36,7 +36,7 @@ public class JavaSerDesUtil {
try (ObjectInputStream ois = new ObjectInputStream(is)) { try (ObjectInputStream ois = new ObjectInputStream(is)) {
return (T) ois.readObject(); return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
log.error("Error during deserialization message, [{}]", e.getMessage()); log.error("Error during deserialization", e);
return null; return null;
} }
} }
@ -50,7 +50,7 @@ public class JavaSerDesUtil {
ois.writeObject(msq); ois.writeObject(msq);
return boas.toByteArray(); return boas.toByteArray();
} catch (IOException e) { } catch (IOException e) {
log.error("Error during serialization message, [{}]", e.getMessage()); log.error("Error during serialization", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/FieldsUtil.java

@ -289,7 +289,7 @@ public class FieldsUtil {
} }
public static String getText(JsonNode node) { public static String getText(JsonNode node) {
return node != null ? node.toString() : ""; return node != null && !node.isNull() ? node.toString() : "";
} }
private static UUID getCustomerId(CustomerId customerId) { private static UUID getCustomerId(CustomerId customerId) {

5
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java

@ -35,6 +35,11 @@ public class UserFields extends AbstractEntityFields {
private String phone; private String phone;
private String additionalInfo; private String additionalInfo;
@Override
public String getName() {
return super.getEmail();
}
public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId, public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId,
Long version, String firstName, String lastName, String email, Long version, String firstName, String lastName, String email,
String phone, JsonNode additionalInfo) { String phone, JsonNode additionalInfo) {

2
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java

@ -107,7 +107,7 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
@JsonIgnore @JsonIgnore
public boolean hasCalculatedFields() { public boolean hasCalculatedFields() {
return calculatedFields != null; return calculatedFields != null && !calculatedFields.isEmpty();
} }
} }

3
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java

@ -17,13 +17,12 @@ package org.thingsboard.server.edqs.data.dp;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.thingsboard.common.util.TbBytePool;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.edqs.util.TbBytePool;
import org.xerial.snappy.Snappy; import org.xerial.snappy.Snappy;
public class CompressedStringDataPoint extends AbstractDataPoint { public class CompressedStringDataPoint extends AbstractDataPoint {
public static final int MIN_STR_SIZE_TO_COMPRESS = 512;
@Getter @Getter
private final byte[] compressedValue; private final byte[] compressedValue;

2
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java

@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.edqs.util.TbStringPool; import org.thingsboard.common.util.TbStringPool;
public class JsonDataPoint extends AbstractDataPoint { public class JsonDataPoint extends AbstractDataPoint {

2
common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java

@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.edqs.util.TbStringPool; import org.thingsboard.common.util.TbStringPool;
public class StringDataPoint extends AbstractDataPoint { public class StringDataPoint extends AbstractDataPoint {

5
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java

@ -27,10 +27,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.edqs.stats.EdqsStatsService; import org.thingsboard.server.common.stats.EdqsStatsService;
import org.thingsboard.server.queue.edqs.EdqsComponent; import org.thingsboard.server.queue.edqs.EdqsComponent;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -42,7 +41,7 @@ import java.util.function.Predicate;
public class DefaultEdqsRepository implements EdqsRepository { public class DefaultEdqsRepository implements EdqsRepository {
private final static ConcurrentMap<TenantId, TenantRepo> repos = new ConcurrentHashMap<>(); private final static ConcurrentMap<TenantId, TenantRepo> repos = new ConcurrentHashMap<>();
private final Optional<EdqsStatsService> statsService; private final EdqsStatsService statsService;
public TenantRepo get(TenantId tenantId) { public TenantRepo get(TenantId tenantId) {
return repos.computeIfAbsent(tenantId, id -> new TenantRepo(id, statsService)); return repos.computeIfAbsent(tenantId, id -> new TenantRepo(id, statsService));

34
common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java

@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.edqs.EdqsEventType;
import org.thingsboard.server.common.data.edqs.EdqsObject; import org.thingsboard.server.common.data.edqs.EdqsObject;
import org.thingsboard.server.common.data.edqs.Entity; import org.thingsboard.server.common.data.edqs.Entity;
import org.thingsboard.server.common.data.edqs.LatestTsKv; import org.thingsboard.server.common.data.edqs.LatestTsKv;
import org.thingsboard.server.common.data.edqs.fields.AssetFields;
import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.edqs.fields.EntityFields;
import org.thingsboard.server.common.data.edqs.query.QueryResult; import org.thingsboard.server.common.data.edqs.query.QueryResult;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -41,6 +40,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.stats.EdqsStatsService;
import org.thingsboard.server.edqs.data.ApiUsageStateData; import org.thingsboard.server.edqs.data.ApiUsageStateData;
import org.thingsboard.server.edqs.data.AssetData; import org.thingsboard.server.edqs.data.AssetData;
import org.thingsboard.server.edqs.data.CustomerData; import org.thingsboard.server.edqs.data.CustomerData;
@ -55,9 +55,7 @@ import org.thingsboard.server.edqs.query.EdqsQuery;
import org.thingsboard.server.edqs.query.SortableEntityData; import org.thingsboard.server.edqs.query.SortableEntityData;
import org.thingsboard.server.edqs.query.processor.EntityQueryProcessor; import org.thingsboard.server.edqs.query.processor.EntityQueryProcessor;
import org.thingsboard.server.edqs.query.processor.EntityQueryProcessorFactory; import org.thingsboard.server.edqs.query.processor.EntityQueryProcessorFactory;
import org.thingsboard.server.edqs.stats.EdqsStatsService;
import org.thingsboard.server.edqs.util.RepositoryUtils; import org.thingsboard.server.edqs.util.RepositoryUtils;
import org.thingsboard.server.edqs.util.TbStringPool;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -65,7 +63,6 @@ import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.UUID; import java.util.UUID;
@ -96,9 +93,9 @@ public class TenantRepo {
private final Lock entityUpdateLock = new ReentrantLock(); private final Lock entityUpdateLock = new ReentrantLock();
private final TenantId tenantId; private final TenantId tenantId;
private final Optional<EdqsStatsService> edqsStatsService; private final EdqsStatsService edqsStatsService;
public TenantRepo(TenantId tenantId, Optional<EdqsStatsService> edqsStatsService) { public TenantRepo(TenantId tenantId, EdqsStatsService edqsStatsService) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.edqsStatsService = edqsStatsService; this.edqsStatsService = edqsStatsService;
} }
@ -144,9 +141,9 @@ public class TenantRepo {
RelationsRepo repo = relations.computeIfAbsent(entity.getTypeGroup(), tg -> new RelationsRepo()); RelationsRepo repo = relations.computeIfAbsent(entity.getTypeGroup(), tg -> new RelationsRepo());
EntityData<?> from = getOrCreate(entity.getFrom()); EntityData<?> from = getOrCreate(entity.getFrom());
EntityData<?> to = getOrCreate(entity.getTo()); EntityData<?> to = getOrCreate(entity.getTo());
boolean added = repo.add(from, to, TbStringPool.intern(entity.getType())); boolean added = repo.add(from, to, entity.getType());
if (added) { if (added) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.UPDATED)); edqsStatsService.reportAdded(ObjectType.RELATION);
} }
} else if (RelationTypeGroup.DASHBOARD.equals(entity.getTypeGroup())) { } else if (RelationTypeGroup.DASHBOARD.equals(entity.getTypeGroup())) {
if (EntityRelation.CONTAINS_TYPE.equals(entity.getType()) && entity.getFrom().getEntityType() == EntityType.CUSTOMER) { if (EntityRelation.CONTAINS_TYPE.equals(entity.getType()) && entity.getFrom().getEntityType() == EntityType.CUSTOMER) {
@ -166,7 +163,7 @@ public class TenantRepo {
if (relationsRepo != null) { if (relationsRepo != null) {
boolean removed = relationsRepo.remove(entityRelation.getFrom().getId(), entityRelation.getTo().getId(), entityRelation.getType()); boolean removed = relationsRepo.remove(entityRelation.getFrom().getId(), entityRelation.getTo().getId(), entityRelation.getType());
if (removed) { if (removed) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.DELETED)); edqsStatsService.reportRemoved(ObjectType.RELATION);
} }
} }
} else if (RelationTypeGroup.DASHBOARD.equals(entityRelation.getTypeGroup())) { } else if (RelationTypeGroup.DASHBOARD.equals(entityRelation.getTypeGroup())) {
@ -188,7 +185,6 @@ public class TenantRepo {
EntityType entityType = entity.getType(); EntityType entityType = entity.getType();
EntityData entityData = getOrCreate(entityType, entityId); EntityData entityData = getOrCreate(entityType, entityId);
processFields(fields);
EntityFields oldFields = entityData.getFields(); EntityFields oldFields = entityData.getFields();
entityData.setFields(fields); entityData.setFields(fields);
if (oldFields == null) { if (oldFields == null) {
@ -225,7 +221,7 @@ public class TenantRepo {
if (removed.getFields() != null) { if (removed.getFields() != null) {
getEntitySet(entityType).remove(removed); getEntitySet(entityType).remove(removed);
} }
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); edqsStatsService.reportRemoved(entity.type());
UUID customerId = removed.getCustomerId(); UUID customerId = removed.getCustomerId();
if (customerId != null) { if (customerId != null) {
@ -246,7 +242,7 @@ public class TenantRepo {
Integer keyId = KeyDictionary.get(attributeKv.getKey()); Integer keyId = KeyDictionary.get(attributeKv.getKey());
boolean added = entityData.putAttr(keyId, attributeKv.getScope(), attributeKv.getDataPoint()); boolean added = entityData.putAttr(keyId, attributeKv.getScope(), attributeKv.getDataPoint());
if (added) { if (added) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.UPDATED)); edqsStatsService.reportAdded(ObjectType.ATTRIBUTE_KV);
} }
} }
} }
@ -256,7 +252,7 @@ public class TenantRepo {
if (entityData != null) { if (entityData != null) {
boolean removed = entityData.removeAttr(KeyDictionary.get(attributeKv.getKey()), attributeKv.getScope()); boolean removed = entityData.removeAttr(KeyDictionary.get(attributeKv.getKey()), attributeKv.getScope());
if (removed) { if (removed) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.DELETED)); edqsStatsService.reportRemoved(ObjectType.ATTRIBUTE_KV);
} }
} }
} }
@ -267,7 +263,7 @@ public class TenantRepo {
Integer keyId = KeyDictionary.get(latestTsKv.getKey()); Integer keyId = KeyDictionary.get(latestTsKv.getKey());
boolean added = entityData.putTs(keyId, latestTsKv.getDataPoint()); boolean added = entityData.putTs(keyId, latestTsKv.getDataPoint());
if (added) { if (added) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.UPDATED)); edqsStatsService.reportAdded(ObjectType.LATEST_TS_KV);
} }
} }
} }
@ -277,17 +273,11 @@ public class TenantRepo {
if (entityData != null) { if (entityData != null) {
boolean removed = entityData.removeTs(KeyDictionary.get(latestTsKv.getKey())); boolean removed = entityData.removeTs(KeyDictionary.get(latestTsKv.getKey()));
if (removed) { if (removed) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.DELETED)); edqsStatsService.reportRemoved(ObjectType.LATEST_TS_KV);
} }
} }
} }
public void processFields(EntityFields fields) {
if (fields instanceof AssetFields assetFields) {
assetFields.setType(TbStringPool.intern(assetFields.getType()));
}
}
public ConcurrentMap<UUID, EntityData<?>> getEntityMap(EntityType entityType) { public ConcurrentMap<UUID, EntityData<?>> getEntityMap(EntityType entityType) {
return entityMapByType.computeIfAbsent(entityType, et -> new ConcurrentHashMap<>()); return entityMapByType.computeIfAbsent(entityType, et -> new ConcurrentHashMap<>());
} }
@ -301,7 +291,7 @@ public class TenantRepo {
return getEntityMap(entityType).computeIfAbsent(entityId, id -> { return getEntityMap(entityType).computeIfAbsent(entityId, id -> {
log.debug("[{}] Adding {} {}", tenantId, entityType, id); log.debug("[{}] Adding {} {}", tenantId, entityType, id);
EntityData<?> entityData = constructEntityData(entityType, entityId); EntityData<?> entityData = constructEntityData(entityType, entityId);
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED)); edqsStatsService.reportAdded(ObjectType.fromEntityType(entityType));
return entityData; return entityData;
}); });
} }

92
common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2025 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.edqs.stats;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.stats.EdqsStatsService;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsTimer;
import org.thingsboard.server.common.stats.StatsType;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Service
@Slf4j
@ConditionalOnExpression("'${queue.edqs.api.supported:true}' == 'true' && '${queue.edqs.stats.enabled:true}' == 'true'")
public class DefaultEdqsStatsService implements EdqsStatsService {
private final StatsFactory statsFactory;
@Value("${queue.edqs.stats.slow_query_threshold:3000}")
private int slowQueryThreshold;
private final ConcurrentHashMap<ObjectType, AtomicInteger> objectCounters = new ConcurrentHashMap<>();
private final StatsTimer dataQueryTimer;
private final StatsTimer countQueryTimer;
private DefaultEdqsStatsService(StatsFactory statsFactory) {
this.statsFactory = statsFactory;
dataQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityDataQueryTimer");
countQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityCountQueryTimer");
}
@Override
public void reportAdded(ObjectType objectType) {
getObjectCounter(objectType).incrementAndGet();
}
@Override
public void reportRemoved(ObjectType objectType) {
getObjectCounter(objectType).decrementAndGet();
}
@Override
public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {
double timingMs = timingNanos / 1000_000.0;
if (timingMs < slowQueryThreshold) {
log.debug("[{}] Executed data query in {} ms: {}", tenantId, timingMs, query);
} else {
log.warn("[{}] Executed slow data query in {} ms: {}", tenantId, timingMs, query);
}
dataQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS);
}
@Override
public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {
double timingMs = timingNanos / 1000_000.0;
if (timingMs < slowQueryThreshold) {
log.debug("[{}] Executed count query in {} ms: {}", tenantId, timingMs, query);
} else {
log.warn("[{}] Executed slow count query in {} ms: {}", tenantId, timingMs, query);
}
countQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS);
}
private AtomicInteger getObjectCounter(ObjectType objectType) {
return objectCounters.computeIfAbsent(objectType, type ->
statsFactory.createGauge("edqsObjectsCount", new AtomicInteger(), "objectType", type.name()));
}
}

81
common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java

@ -1,81 +0,0 @@
/**
* Copyright © 2016-2025 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.edqs.stats;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.edqs.EdqsEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.queue.edqs.EdqsComponent;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@EdqsComponent
@Service
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(name = "queue.edqs.stats.enabled", havingValue = "true", matchIfMissing = true)
public class EdqsStatsService {
private final ConcurrentHashMap<TenantId, EdqsStats> statsMap = new ConcurrentHashMap<>();
private final StatsFactory statsFactory;
public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) {
statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory))
.reportEvent(objectType, eventType);
}
@Getter
@AllArgsConstructor
static class EdqsStats {
private final TenantId tenantId;
private final ConcurrentHashMap<ObjectType, AtomicInteger> entityCounters = new ConcurrentHashMap<>();
private final StatsFactory statsFactory;
private AtomicInteger getOrCreateObjectCounter(ObjectType objectType) {
return entityCounters.computeIfAbsent(objectType,
type -> statsFactory.createGauge(StatsType.EDQS.getName() + "_object_count", new AtomicInteger(),
"tenantId", tenantId.toString(), "objectType", type.name()));
}
@Override
public String toString() {
return entityCounters.entrySet().stream()
.map(counters -> counters.getKey().name()+ " total = [" + counters.getValue() + "]")
.collect(Collectors.joining(", "));
}
public void reportEvent(ObjectType objectType, EdqsEventType eventType) {
AtomicInteger objectCounter = getOrCreateObjectCounter(objectType);
if (eventType == EdqsEventType.UPDATED){
objectCounter.incrementAndGet();
} else if (eventType == EdqsEventType.DELETED) {
objectCounter.decrementAndGet();
}
}
}
}

45
common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java

@ -17,13 +17,19 @@ package org.thingsboard.server.edqs.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.TbStringPool;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.ObjectType;
@ -49,6 +55,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto; import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto;
import org.xerial.snappy.Snappy; import org.xerial.snappy.Snappy;
import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -57,6 +64,9 @@ import java.util.UUID;
@Slf4j @Slf4j
public class EdqsConverter { public class EdqsConverter {
@Value("${queue.edqs.string_compression_length_threshold:512}")
private int stringCompressionLengthThreshold;
private final Map<ObjectType, Converter<? extends EdqsObject>> converters = new HashMap<>(); private final Map<ObjectType, Converter<? extends EdqsObject>> converters = new HashMap<>();
private final Converter<Entity> defaultConverter = new JsonConverter<>(Entity.class); private final Converter<Entity> defaultConverter = new JsonConverter<>(Entity.class);
@ -125,7 +135,7 @@ public class EdqsConverter {
}); });
} }
public static DataPointProto toDataPointProto(long ts, KvEntry kvEntry) { public DataPointProto toDataPointProto(long ts, KvEntry kvEntry) {
DataPointProto.Builder proto = DataPointProto.newBuilder(); DataPointProto.Builder proto = DataPointProto.newBuilder();
proto.setTs(ts); proto.setTs(ts);
switch (kvEntry.getDataType()) { switch (kvEntry.getDataType()) {
@ -134,7 +144,7 @@ public class EdqsConverter {
case DOUBLE -> proto.setDoubleV(kvEntry.getDoubleValue().get()); case DOUBLE -> proto.setDoubleV(kvEntry.getDoubleValue().get());
case STRING -> { case STRING -> {
String strValue = kvEntry.getStrValue().get(); String strValue = kvEntry.getStrValue().get();
if (strValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { if (strValue.length() < stringCompressionLengthThreshold) {
proto.setStringV(strValue); proto.setStringV(strValue);
} else { } else {
proto.setCompressedStringV(ByteString.copyFrom(compress(strValue))); proto.setCompressedStringV(ByteString.copyFrom(compress(strValue)));
@ -142,7 +152,7 @@ public class EdqsConverter {
} }
case JSON -> { case JSON -> {
String jsonValue = kvEntry.getJsonValue().get(); String jsonValue = kvEntry.getJsonValue().get();
if (jsonValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { if (jsonValue.length() < stringCompressionLengthThreshold) {
proto.setJsonV(jsonValue); proto.setJsonV(jsonValue);
} else { } else {
proto.setCompressedJsonV(ByteString.copyFrom(compress(jsonValue))); proto.setCompressedJsonV(ByteString.copyFrom(compress(jsonValue)));
@ -152,7 +162,7 @@ public class EdqsConverter {
return proto.build(); return proto.build();
} }
public static DataPoint fromDataPointProto(DataPointProto proto) { public DataPoint fromDataPointProto(DataPointProto proto) {
long ts = proto.getTs(); long ts = proto.getTs();
if (proto.hasBoolV()) { if (proto.hasBoolV()) {
return new BoolDataPoint(ts, proto.getBoolV()); return new BoolDataPoint(ts, proto.getBoolV());
@ -188,14 +198,6 @@ public class EdqsConverter {
return edqsEntity; return edqsEntity;
} }
public EdqsObject check(ObjectType type, Object object) {
if (object instanceof EdqsObject edqsObject) {
return edqsObject;
} else {
return toEntity(type.toEntityType(), object);
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@SneakyThrows @SneakyThrows
public <T extends EdqsObject> byte[] serialize(ObjectType type, T value) { public <T extends EdqsObject> byte[] serialize(ObjectType type, T value) {
@ -220,12 +222,18 @@ public class EdqsConverter {
@RequiredArgsConstructor @RequiredArgsConstructor
private static class JsonConverter<T> implements Converter<T> { private static class JsonConverter<T> implements Converter<T> {
private static final SimpleModule module = new SimpleModule();
private static final ObjectMapper mapper = JsonMapper.builder() private static final ObjectMapper mapper = JsonMapper.builder()
.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
.visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) .visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
.visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE) .visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE)
.build(); .build();
static {
module.addDeserializer(String.class, new InterningStringDeserializer());
mapper.registerModule(module);
}
private final Class<T> type; private final Class<T> type;
@SneakyThrows @SneakyThrows
@ -250,4 +258,17 @@ public class EdqsConverter {
} }
public static class InterningStringDeserializer extends StdDeserializer<String> {
public InterningStringDeserializer() {
super(String.class);
}
@Override
public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
return TbStringPool.intern(p.getText());
}
}
} }

2
common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java

@ -38,7 +38,7 @@ public class TopicService {
@Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}") @Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}")
private String tbRuleEngineNotificationsTopic; private String tbRuleEngineNotificationsTopic;
@Value("${queue.transport.notifications-topics:tb_transport.notifications}") @Value("${queue.transport.notifications-topic:tb_transport.notifications}")
private String tbTransportNotificationsTopic; private String tbTransportNotificationsTopic;
@Value("${queue.edge.notifications-topic:tb_edge.notifications}") @Value("${queue.edge.notifications-topic:tb_edge.notifications}")

24
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java

@ -50,8 +50,6 @@ import java.util.Properties;
@Component @Component
public class TbKafkaSettings { public class TbKafkaSettings {
private static final List<String> DYNAMIC_TOPICS = List.of("tb_edge_event.notifications");
@Value("${queue.kafka.bootstrap.servers}") @Value("${queue.kafka.bootstrap.servers}")
private String servers; private String servers;
@ -163,18 +161,20 @@ public class TbKafkaSettings {
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
consumerPropertiesPerTopic
.getOrDefault(topic, Collections.emptyList())
.forEach(kv -> props.put(kv.getKey(), kv.getValue()));
if (topic != null) { if (topic != null) {
DYNAMIC_TOPICS.stream() List<TbProperty> properties = consumerPropertiesPerTopic.get(topic);
.filter(topic::startsWith) if (properties == null) {
.findFirst() for (Map.Entry<String, List<TbProperty>> entry : consumerPropertiesPerTopic.entrySet()) {
.ifPresent(prefix -> consumerPropertiesPerTopic.getOrDefault(prefix, Collections.emptyList()) if (topic.startsWith(entry.getKey())) {
.forEach(kv -> props.put(kv.getKey(), kv.getValue()))); properties = entry.getValue();
break;
}
}
}
if (properties != null) {
properties.forEach(kv -> props.put(kv.getKey(), kv.getValue()));
}
} }
return props; return props;
} }

1
common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java

@ -115,7 +115,6 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider {
return toHousekeeper; return toHousekeeper;
} }
@Override @Override
public TbQueueProducer<TbProtoQueueMsg<ToEdgeMsg>> getTbEdgeMsgProducer() { public TbQueueProducer<TbProtoQueueMsg<ToEdgeMsg>> getTbEdgeMsgProducer() {
return toEdge; return toEdge;

41
common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.stats;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@Service
@ConditionalOnMissingBean(value = EdqsStatsService.class, ignored = DummyEdqsStatsService.class)
public class DummyEdqsStatsService implements EdqsStatsService {
@Override
public void reportAdded(ObjectType objectType) {}
@Override
public void reportRemoved(ObjectType objectType) {}
@Override
public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {}
@Override
public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {}
}

33
common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.stats;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityDataQuery;
public interface EdqsStatsService {
void reportAdded(ObjectType objectType);
void reportRemoved(ObjectType objectType);
void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos);
void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos);
}

12
common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java

@ -34,10 +34,14 @@ public class StatsTimer {
this.timer = micrometerTimer; this.timer = micrometerTimer;
} }
public void record(long timeMs) { public void record(long timeMs) {
record(timeMs, TimeUnit.MILLISECONDS);
}
public void record(long timing, TimeUnit timeUnit) {
count++; count++;
totalTime += timeMs; totalTime += timeUnit.toMillis(timing);
timer.record(timeMs, TimeUnit.MILLISECONDS); timer.record(timing, timeUnit);
} }
public double getAvg() { public double getAvg() {
@ -47,7 +51,7 @@ public class StatsTimer {
return (double) totalTime / count; return (double) totalTime / count;
} }
public void reset() { public void reset() {
count = 0; count = 0;
totalTime = 0; totalTime = 0;
} }

2
common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java → common/util/src/main/java/org/thingsboard/common/util/TbBytePool.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.edqs.util; package org.thingsboard.common.util;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap;

2
common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java → common/util/src/main/java/org/thingsboard/common/util/TbStringPool.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.edqs.util; package org.thingsboard.common.util;
import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap;

3
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -42,7 +42,6 @@ import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
@ -158,7 +157,7 @@ public class GitRepository {
return repository; return repository;
} catch (RepositoryNotFoundException e) { } catch (RepositoryNotFoundException e) {
log.warn("{} not a git repository, reinitializing", directory); log.warn("{} not a git repository, reinitializing", directory);
} catch (TransportException e) { } catch (org.eclipse.jgit.errors.TransportException | org.eclipse.jgit.api.errors.TransportException e) {
if (StringUtils.containsIgnoreCase(e.getMessage(), "missing commit")) { if (StringUtils.containsIgnoreCase(e.getMessage(), "missing commit")) {
log.warn("Couldn't fetch {} due to {}, reinitializing", directory, e.getMessage()); log.warn("Couldn't fetch {} due to {}, reinitializing", directory, e.getMessage());
} else { } else {

19
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -213,12 +213,11 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
@Override @Override
public Customer findOrCreatePublicCustomer(TenantId tenantId) { public Customer findOrCreatePublicCustomer(TenantId tenantId) {
log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId); log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id); var publicCustomer = findPublicCustomer(tenantId);
Optional<Customer> publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId()); if (publicCustomer != null) {
if (publicCustomerOpt.isPresent()) { return publicCustomer;
return publicCustomerOpt.get();
} }
var publicCustomer = new Customer(); publicCustomer = new Customer();
publicCustomer.setTenantId(tenantId); publicCustomer.setTenantId(tenantId);
publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE); publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
try { try {
@ -230,7 +229,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
return saveCustomer(publicCustomer, false); return saveCustomer(publicCustomer, false);
} catch (DataValidationException e) { } catch (DataValidationException e) {
if (CUSTOMER_UNIQUE_TITLE_EX_MSG.equals(e.getMessage())) { if (CUSTOMER_UNIQUE_TITLE_EX_MSG.equals(e.getMessage())) {
publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId()); Optional<Customer> publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
if (publicCustomerOpt.isPresent()) { if (publicCustomerOpt.isPresent()) {
return publicCustomerOpt.get(); return publicCustomerOpt.get();
} }
@ -239,6 +238,14 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
} }
} }
@Override
public Customer findPublicCustomer(TenantId tenantId) {
log.trace("Executing findPublicCustomer, tenantId [{}]", tenantId);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Optional<Customer> publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
return publicCustomerOpt.orElse(null);
}
@Override @Override
public PageData<Customer> findCustomersByTenantId(TenantId tenantId, PageLink pageLink) { public PageData<Customer> findCustomersByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -85,7 +85,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String mqttsPemCertFile; private String mqttsPemCertFile;
@Value("${device.connectivity.coaps.pem_cert_file:}") @Value("${device.connectivity.coaps.pem_cert_file:}")
private String coapsPemCertFile; private String coapsPemCertFile;
@Value("${device.connectivity.gateway.image_version:latest}") @Value("${device.connectivity.gateway.image_version:3.7-stable}")
private String gatewayImageVersion; private String gatewayImageVersion;
@Override @Override

48
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasEmail; import org.thingsboard.server.common.data.HasEmail;
@ -46,6 +47,7 @@ import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.common.msg.edqs.EdqsApiService; import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.stats.EdqsStatsService;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import java.util.ArrayList; import java.util.ArrayList;
@ -89,6 +91,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
@Lazy @Lazy
private EdqsApiService edqsApiService; private EdqsApiService edqsApiService;
@Autowired
private EdqsStatsService edqsStatsService;
@Override @Override
public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) {
log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query);
@ -96,14 +101,19 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id);
validateEntityCountQuery(query); validateEntityCountQuery(query);
TbStopWatch stopWatch = TbStopWatch.create();
Long result;
if (edqsApiService.isEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { if (edqsApiService.isEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) {
EdqsRequest request = EdqsRequest.builder() EdqsRequest request = EdqsRequest.builder()
.entityCountQuery(query) .entityCountQuery(query)
.build(); .build();
EdqsResponse response = processEdqsRequest(tenantId, customerId, request); EdqsResponse response = processEdqsRequest(tenantId, customerId, request);
return response.getEntityCountQueryResult(); result = response.getEntityCountQueryResult();
} else {
result = entityQueryDao.countEntitiesByQuery(tenantId, customerId, query);
} }
return this.entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); edqsStatsService.reportCountQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos());
return result;
} }
@Override @Override
@ -113,27 +123,31 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id);
validateEntityDataQuery(query); validateEntityDataQuery(query);
TbStopWatch stopWatch = TbStopWatch.create();
PageData<EntityData> result;
if (edqsApiService.isEnabled() && validForEdqs(query)) { if (edqsApiService.isEnabled() && validForEdqs(query)) {
EdqsRequest request = EdqsRequest.builder() EdqsRequest request = EdqsRequest.builder()
.entityDataQuery(query) .entityDataQuery(query)
.build(); .build();
EdqsResponse response = processEdqsRequest(tenantId, customerId, request); EdqsResponse response = processEdqsRequest(tenantId, customerId, request);
return response.getEntityDataQueryResult(); result = response.getEntityDataQueryResult();
} } else {
if (!isValidForOptimization(query)) {
if (!isValidForOptimization(query)) { result = entityQueryDao.findEntityDataByQuery(tenantId, customerId, query);
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); } else {
} // 1 step - find entity data by filter and sort columns
PageData<EntityData> entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query);
// 1 step - find entity data by filter and sort columns if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) {
PageData<EntityData> entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); result = entityDataByQuery;
if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { } else {
return entityDataByQuery; // 2 step - find entity data by entity ids from the 1st step
List<EntityData> entities = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData());
result = new PageData<>(entities, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext());
}
}
} }
edqsStatsService.reportDataQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos());
// 2 step - find entity data by entity ids from the 1st step return result;
List<EntityData> result = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData());
return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext());
} }
private boolean validForEdqs(EntityCountQuery query) { // for compatibility with PE private boolean validForEdqs(EntityCountQuery query) { // for compatibility with PE

2
edqs/src/main/resources/edqs.yml

@ -71,6 +71,8 @@ queue:
max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
# Maximum timeout for requests to EDQS # Maximum timeout for requests to EDQS
max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}"
# Strings longer than this threshold will be compressed
string_compression_length_threshold: "${TB_EDQS_STRING_COMPRESSION_LENGTH_THRESHOLD:512}"
stats: stats:
# Enable/disable statistics for EDQS # Enable/disable statistics for EDQS
enabled: "${TB_EDQS_STATS_ENABLED:true}" enabled: "${TB_EDQS_STATS_ENABLED:true}"

4
edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java

@ -20,6 +20,7 @@ import org.junit.Before;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.TestExecutionListeners;
@ -58,6 +59,7 @@ import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.stats.DummyEdqsStatsService;
import org.thingsboard.server.edqs.util.EdqsConverter; import org.thingsboard.server.edqs.util.EdqsConverter;
import java.util.Collections; import java.util.Collections;
@ -78,6 +80,8 @@ public abstract class AbstractEDQTest {
protected DefaultEdqsRepository repository; protected DefaultEdqsRepository repository;
@Autowired @Autowired
protected EdqsConverter edqsConverter; protected EdqsConverter edqsConverter;
@MockBean
private DummyEdqsStatsService edqsStatsService;
protected final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); protected final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
protected final CustomerId customerId = new CustomerId(UUID.randomUUID()); protected final CustomerId customerId = new CustomerId(UUID.randomUUID());

1
ui-ngx/src/app/core/auth/auth.models.ts

@ -31,6 +31,7 @@ export interface SysParamsState {
maxDataPointsPerRollingArg: number; maxDataPointsPerRollingArg: number;
maxArgumentsPerCF: number; maxArgumentsPerCF: number;
ruleChainDebugPerTenantLimitsConfiguration?: string; ruleChainDebugPerTenantLimitsConfiguration?: string;
calculatedFieldDebugPerTenantLimitsConfiguration?: string;
} }
export interface SysParams extends SysParamsState { export interface SysParams extends SysParamsState {

8
ui-ngx/src/app/core/auth/auth.service.ts

@ -152,12 +152,8 @@ export class AuthService {
)); ));
} }
public resetPassword(resetToken: string, password: string): Observable<LoginResponse> { public resetPassword(resetToken: string, password: string): Observable<void> {
return this.http.post<LoginResponse>('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).pipe( return this.http.post<void>('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions());
tap((loginResponse: LoginResponse) => {
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true);
}
));
} }
public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) { public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) {

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

@ -243,7 +243,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true; config.hasBackdrop = true;

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

@ -338,7 +338,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
if (this.isClientSideTelemetryTypeMap.get(this.attributeScope)) { if (this.isClientSideTelemetryTypeMap.get(this.attributeScope)) {
return; return;
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true; config.hasBackdrop = true;
@ -389,7 +389,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-filter-panel', panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',

14
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -27,10 +27,9 @@ import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { MINUTE } from '@shared/models/time/time.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { DestroyRef, Renderer2 } from '@angular/core'; import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -47,7 +46,8 @@ import {
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { import {
CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, CalculatedFieldDebugDialogComponent,
CalculatedFieldDebugDialogData,
CalculatedFieldDialogComponent, CalculatedFieldDialogComponent,
CalculatedFieldDialogData, CalculatedFieldDialogData,
CalculatedFieldScriptTestDialogComponent, CalculatedFieldScriptTestDialogComponent,
@ -60,9 +60,6 @@ import { DatePipe } from '@angular/common';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
readonly calculatedFieldsDebugPerTenantLimitsConfiguration =
getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'];
readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
readonly tenantId = getCurrentAuthUser(this.store).tenantId; readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = { additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'), title: this.translate.instant('calculated-fields.see-debug-events'),
@ -189,9 +186,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.entityDebugSettingsService.openDebugStrategyPanel({ this.entityDebugSettingsService.openDebugStrategyPanel({
debugSettings, debugSettings,
debugConfig: { debugConfig: {
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration, entityType: EntityType.CALCULATED_FIELD,
maxDebugModeDuration: this.maxDebugModeDuration,
entityLabel: this.translate.instant('debug-settings.calculated-field'),
additionalActionConfig, additionalActionConfig,
}, },
onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings)
@ -215,7 +210,6 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
value, value,
buttonTitle, buttonTitle,
entityId: this.entityId, entityId: this.entityId,
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName, entityName: this.entityName,
additionalDebugActionConfig: this.additionalDebugActionConfig, additionalDebugActionConfig: this.additionalDebugActionConfig,

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html

@ -33,7 +33,6 @@
[defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD" [defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[active]="true" [active]="true"
[entityId]="data.value.id" [entityId]="data.value.id"
[functionTestButtonLabel]="'common.test-function' | translate"
(debugEventSelected)="onDebugEventSelected($event)" (debugEventSelected)="onDebugEventSelected($event)"
/> />
</div> </div>

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts

@ -55,8 +55,8 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent<Calcula
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments);
this.eventsTable.entitiesTable.updateData(); this.eventsTable.entitiesTable.updateData();
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments)
} }
cancel(): void { cancel(): void {

13
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -49,8 +49,7 @@
<tb-entity-debug-settings-button <tb-entity-debug-settings-button
formControlName="debugSettings" formControlName="debugSettings"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched" [class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityLabel]="'debug-settings.calculated-field' | translate" [entityType]="EntityType.CALCULATED_FIELD"
[debugLimitsConfiguration]="data.debugLimitsConfiguration"
[additionalActionConfig]="additionalDebugActionConfig" [additionalActionConfig]="additionalDebugActionConfig"
/> />
</div> </div>
@ -80,6 +79,12 @@
</div> </div>
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic" [class.hidden]="fieldFormGroup.get('type').value !== CalculatedFieldType.SIMPLE"> <mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic" [class.hidden]="fieldFormGroup.get('type').value !== CalculatedFieldType.SIMPLE">
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'" required> <input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'" required>
<div matSuffix
class="pr-2"
[tb-help-popup]="'math/math-methods_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) {
<mat-error> <mat-error>
@if (configFormGroup.get('expressionSIMPLE').hasError('required')) { @if (configFormGroup.get('expressionSIMPLE').hasError('required')) {
@ -110,7 +115,7 @@
<div toolbarPrefixButton class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}</div> <div toolbarPrefixButton class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}</div>
<button toolbarSuffixButton <button toolbarSuffixButton
mat-icon-button mat-icon-button
matTooltip="{{ 'common.test-function' | translate }}" matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
class="tb-mat-32" class="tb-mat-32"
[disabled]="configFormGroup.get('arguments').invalid" [disabled]="configFormGroup.get('arguments').invalid"
@ -123,7 +128,7 @@
type="button" type="button"
(click)="onTestScript()" (click)="onTestScript()"
[disabled]="configFormGroup.get('arguments').invalid"> [disabled]="configFormGroup.get('arguments').invalid">
{{ 'common.test-function' | translate }} {{ 'calculated-fields.test-script-function' | translate }}
</button> </button>
</div> </div>
</div> </div>

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -48,7 +48,6 @@ export interface CalculatedFieldDialogData {
value?: CalculatedField; value?: CalculatedField;
buttonTitle: string; buttonTitle: string;
entityId: EntityId; entityId: EntityId;
debugLimitsConfiguration: string;
tenantId: string; tenantId: string;
entityName?: string; entityName?: string;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html

@ -55,11 +55,11 @@
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('equalCtx')) { } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) {
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-ctx' | translate" [matTooltip]="'calculated-fields.hint.argument-name-forbidden' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>

7
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts

@ -67,7 +67,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10);
argumentFormGroup = this.fb.group({ argumentFormGroup = this.fb.group({
argumentName: ['', [Validators.required, this.uniqNameRequired(), this.notEqualCtxValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]],
refEntityId: this.fb.group({ refEntityId: this.fb.group({
entityType: [ArgumentEntityType.Current], entityType: [ArgumentEntityType.Current],
id: [''] id: ['']
@ -254,10 +254,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
} }
} }
private notEqualCtxValidator(): ValidatorFn { private forbiddenArgumentNameValidator(): ValidatorFn {
return (control: FormControl) => { return (control: FormControl) => {
const trimmedValue = control.value.trim().toLowerCase(); const trimmedValue = control.value.trim().toLowerCase();
return trimmedValue === 'ctx' ? { equalCtx: true } : null; const forbiddenArgumentNames = ['ctx', 'e', 'pi'];
return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null;
}; };
} }

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -365,7 +365,7 @@
<mat-icon>close</mat-icon></button> <mat-icon>close</mat-icon></button>
</section> </section>
<section *ngIf="!embedded" data-html2canvas-ignore class="tb-powered-by-footer" [style.color]="dashboard.configuration.settings.titleColor"> <section *ngIf="!embedded" data-html2canvas-ignore class="tb-powered-by-footer" [style.color]="dashboard.configuration.settings.titleColor">
<span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ thingsboardVersion }}</a></span> <span>Powered by <a href="https://thingsboard.io" target="_blank">ThingsBoard v.{{ thingsboardVersion }}</a></span>
</section> </section>
</mat-drawer-content> </mat-drawer-content>
<mat-drawer class="tb-details-drawer" <mat-drawer class="tb-details-drawer"

11
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -1335,8 +1335,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
addWidgetFromType(widget: WidgetInfo) { addWidgetFromType(widget: WidgetInfo) {
this.onAddWidgetClosed(); this.onAddWidgetClosed();
this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe( this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe({
(widgetTypeInfo) => { next: (widgetTypeInfo) => {
const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo); const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
if (!config.title) { if (!config.title) {
config.title = 'New ' + widgetTypeInfo.widgetName; config.title = 'New ' + widgetTypeInfo.widgetName;
@ -1389,8 +1389,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
}); });
} }
},
error: (errorData) => {
const errorMessages: string[] = errorData.errorMessages;
this.dialogService.alert(this.translate.instant('widget.widget-type-load-error'),
errorMessages.join('<br>').replace(/\n/g, '<br>'));
} }
); });
} }
onRevertWidgetEdit() { onRevertWidgetEdit() {

1
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html

@ -16,6 +16,7 @@
--> -->
<button mat-stroked-button <button mat-stroked-button
type="button"
class="tb-rounded-btn w-36 flex-1" class="tb-rounded-btn w-36 flex-1"
color="primary" color="primary"
#matButton #matButton

7
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts

@ -36,6 +36,7 @@ import { Store } from '@ngrx/store';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { EntityType } from '@shared/models/entity-type.models';
@Component({ @Component({
selector: 'tb-entity-debug-settings-button', selector: 'tb-entity-debug-settings-button',
@ -58,9 +59,8 @@ import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entit
}) })
export class EntityDebugSettingsButtonComponent implements ControlValueAccessor { export class EntityDebugSettingsButtonComponent implements ControlValueAccessor {
@Input() debugLimitsConfiguration: string;
@Input() entityLabel: string;
@Input() additionalActionConfig: AdditionalDebugActionConfig; @Input() additionalActionConfig: AdditionalDebugActionConfig;
@Input({required: true}) entityType: EntityType;
debugSettingsFormGroup = this.fb.group({ debugSettingsFormGroup = this.fb.group({
failuresEnabled: [false], failuresEnabled: [false],
@ -123,8 +123,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor
debugSettings: this.debugSettingsFormGroup.value, debugSettings: this.debugSettingsFormGroup.value,
debugConfig: { debugConfig: {
maxDebugModeDuration: this.maxDebugModeDuration, maxDebugModeDuration: this.maxDebugModeDuration,
debugLimitsConfiguration: this.debugLimitsConfiguration, entityType: this.entityType,
entityLabel: this.entityLabel,
additionalActionConfig: this.additionalActionConfig, additionalActionConfig: this.additionalActionConfig,
}, },
onSettingsAppliedFn: settings => { onSettingsAppliedFn: settings => {

4
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html

@ -17,10 +17,10 @@
--> -->
<div class="flex w-96 flex-col gap-3 p-2"> <div class="flex w-96 flex-col gap-3 p-2">
<div class="tb-form-panel-title" translate>debug-settings.label</div> <div class="tb-form-panel-title" translate>debug-settings.label</div>
@if (debugLimitsConfiguration) { @if (maxMessagesCount) {
<div class="hint-container"> <div class="hint-container">
<div class="tb-form-hint tb-primary-fill tb-flex center"> <div class="tb-form-hint tb-primary-fill tb-flex center">
{{ 'debug-settings.hint.main-limited' | translate: { entity: entityLabel ?? ('debug-settings.entity' | translate), msg: maxMessagesCount, time: (maxTimeFrameDuration | milliSecondsToTimeString: true : true) } }} {{ 'debug-settings.hint.main-limited' | translate: { entity: (entityLabel | translate | lowercase), msg: maxMessagesCount, time: (maxTimeFrameDuration | milliSecondsToTimeString: true : true) } }}
</div> </div>
</div> </div>
} }

25
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts

@ -28,13 +28,15 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { SECOND } from '@shared/models/time/time.models'; import { MINUTE, SECOND } from '@shared/models/time/time.models';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { of, shareReplay, timer } from 'rxjs'; import { of, shareReplay, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators'; import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
@Component({ @Component({
selector: 'tb-entity-debug-settings-panel', selector: 'tb-entity-debug-settings-panel',
@ -51,10 +53,9 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
@Input({ transform: booleanAttribute }) failuresEnabled = false; @Input({ transform: booleanAttribute }) failuresEnabled = false;
@Input({ transform: booleanAttribute }) allEnabled = false; @Input({ transform: booleanAttribute }) allEnabled = false;
@Input() entityLabel: string; @Input() entityType: EntityType;
@Input() allEnabledUntil = 0; @Input() allEnabledUntil = 0;
@Input() maxDebugModeDuration: number; @Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
@Input() debugLimitsConfiguration: string;
@Input() additionalActionConfig: AdditionalDebugActionConfig; @Input() additionalActionConfig: AdditionalDebugActionConfig;
onFailuresControl = this.fb.control(false); onFailuresControl = this.fb.control(false);
@ -63,6 +64,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
maxMessagesCount: string; maxMessagesCount: string;
maxTimeFrameDuration: number; maxTimeFrameDuration: number;
initialAllEnabled: boolean; initialAllEnabled: boolean;
entityLabel: string;
isDebugAllActive$ = this.debugAllControl.valueChanges.pipe( isDebugAllActive$ = this.debugAllControl.valueChanges.pipe(
startWith(this.debugAllControl.value), startWith(this.debugAllControl.value),
@ -101,11 +103,13 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
} }
ngOnInit(): void { ngOnInit(): void {
this.maxMessagesCount = this.debugLimitsConfiguration?.split(':')[0]; const debugLimitsConfiguration = this.getByEntityTypeDebugLimit(this.entityType);
this.maxTimeFrameDuration = parseInt(this.debugLimitsConfiguration?.split(':')[1]) * SECOND; this.maxMessagesCount = debugLimitsConfiguration?.split(':')[0];
this.maxTimeFrameDuration = parseInt(debugLimitsConfiguration?.split(':')[1]) * SECOND;
this.onFailuresControl.patchValue(this.failuresEnabled); this.onFailuresControl.patchValue(this.failuresEnabled);
this.debugAllControl.patchValue(this.allEnabled); this.debugAllControl.patchValue(this.allEnabled);
this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime(); this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime();
this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity';
} }
onCancel(): void { onCancel(): void {
@ -135,4 +139,13 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
this.allEnabledUntil = 0; this.allEnabledUntil = 0;
this.cd.markForCheck(); this.cd.markForCheck();
} }
private getByEntityTypeDebugLimit(entityType: EntityType): string {
switch (entityType) {
case EntityType.RULE_NODE:
return getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration;
case EntityType.CALCULATED_FIELD:
return getCurrentAuthState(this.store).calculatedFieldDebugPerTenantLimitsConfiguration;
}
}
} }

6
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts

@ -15,6 +15,7 @@
/// ///
import { EntityDebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { EntityType } from '@shared/models/entity-type.models';
export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => void> { export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => void> {
action: Action; action: Action;
@ -24,10 +25,9 @@ export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => vo
export interface EntityDebugSettingPanelConfig { export interface EntityDebugSettingPanelConfig {
debugSettings: EntityDebugSettings; debugSettings: EntityDebugSettings;
debugConfig: { debugConfig: {
maxDebugModeDuration: number; maxDebugModeDuration?: number;
debugLimitsConfiguration: string;
entityLabel?: string;
additionalActionConfig?: AdditionalDebugActionConfig; additionalActionConfig?: AdditionalDebugActionConfig;
entityType: EntityType;
} }
onSettingsAppliedFn: (settings: EntityDebugSettings) => void; onSettingsAppliedFn: (settings: EntityDebugSettings) => void;
} }

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

@ -462,7 +462,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
break; break;
case DebugEventType.DEBUG_CALCULATED_FIELD: case DebugEventType.DEBUG_CALCULATED_FIELD:
this.cellActionDescriptors.push({ this.cellActionDescriptors.push({
name: this.translate.instant('common.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}), name: this.translate.instant('calculated-fields.test-with-this-message'),
icon: 'bug_report', icon: 'bug_report',
isEnabled: () => true, isEnabled: () => true,
onAction: (_, entity) => this.debugEventSelected.next(entity.body) onAction: (_, entity) => this.debugEventSelected.next(entity.body)
@ -571,7 +571,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-panel-container', panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',

6
ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html

@ -32,6 +32,12 @@
<legend class="group-title">{{'rule-node-config.custom-expression-field-input' | translate }} *</legend> <legend class="group-title">{{'rule-node-config.custom-expression-field-input' | translate }} *</legend>
<mat-form-field class="mat-block no-margin-top flex-1" subscriptSizing="dynamic"> <mat-form-field class="mat-block no-margin-top flex-1" subscriptSizing="dynamic">
<input matInput formControlName="customFunction" required/> <input matInput formControlName="customFunction" required/>
<div matSuffix
class="pr-2"
[tb-help-popup]="'math/math-methods_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
<mat-error *ngIf="mathFunctionConfigForm.get('customFunction').hasError('required')" translate> <mat-error *ngIf="mathFunctionConfigForm.get('customFunction').hasError('required')" translate>
rule-node-config.custom-expression-field-input-required rule-node-config.custom-expression-field-input-required
</mat-error> </mat-error>

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

@ -150,7 +150,8 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
datasourcesMode: DatasourceType; datasourcesMode: DatasourceType;
private propagateChange = (_val: any) => {}; private propagateChange: (value: any) => void;
private propagateChangePending = false;
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private utils: UtilsService, private utils: UtilsService,
@ -161,7 +162,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
if (this.validate(null)) { if (this.propagateChangePending) {
setTimeout(() => { setTimeout(() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}, 0); }, 0);
@ -227,7 +228,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
if (this.singleDatasource && !this.datasourcesFormArray.length) { if (this.singleDatasource && !this.datasourcesFormArray.length) {
this.addDatasource(false); this.addDatasource(false);
} }
if (changed) { if (changed || this.validate(null)) {
setTimeout(() => { setTimeout(() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}, 0); }, 0);
@ -329,7 +330,12 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
if (this.datasourcesOptional) { if (this.datasourcesOptional) {
datasources = datasources ? datasources.filter(d => datasourceValid(d)) : []; datasources = datasources ? datasources.filter(d => datasourceValid(d)) : [];
} }
this.propagateChange(datasources); if (this.propagateChange) {
this.propagateChange(datasources);
this.propagateChangePending = false;
} else {
this.propagateChangePending = true;
}
} }
public onDatasourceDrop(event: CdkDragDrop<string[]>) { public onDatasourceDrop(event: CdkDragDrop<string[]>) {

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

@ -550,7 +550,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-panel-container', panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',
@ -621,7 +621,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-filter-panel', panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',
@ -1189,7 +1189,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true; config.hasBackdrop = true;

2
ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts

@ -237,7 +237,7 @@ export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-panel-container', panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',

2
ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts

@ -126,7 +126,7 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements
$event.stopPropagation(); $event.stopPropagation();
} }
this.datePickerSelect.close(); this.datePickerSelect.close();
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true; config.hasBackdrop = true;

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

@ -513,7 +513,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-panel-container', panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',

34
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts

@ -49,9 +49,10 @@ interface ShapePatternInfo {
image: string; image: string;
width: number; width: number;
height: number; height: number;
opacity?: number; preserveAspectRatio: boolean;
angle?: number; opacity: number;
scale?: number; angle: number;
scale: number;
}; };
fillStripe?: { fillStripe?: {
weight: number; weight: number;
@ -122,23 +123,35 @@ abstract class ShapePatternProcessor<S = any> {
fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor}); fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor});
pattern.addElement(fillRect); pattern.addElement(fillRect);
} else if (patternInfo.type === ShapeFillType.image) { } else if (patternInfo.type === ShapeFillType.image) {
pattern = new L.TB.Pattern({ const patternOptions: L.TB.PatternOptions = {
width: 1, width: 1,
height: 1, height: 1,
patternUnits: 'objectBoundingBox', patternUnits: 'objectBoundingBox',
patternContentUnits: 'objectBoundingBox', patternContentUnits: 'objectBoundingBox',
preserveAspectRatioAlign: 'xMidYMid',
preserveAspectRatioMeetOrSlice: 'slice',
viewBox: [0,0,patternInfo.fillImage.width,patternInfo.fillImage.height] viewBox: [0,0,patternInfo.fillImage.width,patternInfo.fillImage.height]
}); };
const imagePatternShape = new L.TB.PatternImage({ if (patternInfo.fillImage.preserveAspectRatio) {
patternOptions.preserveAspectRatioAlign = 'xMidYMid';
patternOptions.preserveAspectRatioMeetOrSlice = 'slice';
} else {
patternOptions.preserveAspectRatioAlign = 'none';
}
pattern = new L.TB.Pattern(patternOptions);
const imagePatternOptions: L.TB.PatternImageOptions = {
imageUrl: patternInfo.fillImage.image, imageUrl: patternInfo.fillImage.image,
width: patternInfo.fillImage.width, width: patternInfo.fillImage.width,
height: patternInfo.fillImage.height, height: patternInfo.fillImage.height,
opacity: patternInfo.fillImage.opacity, opacity: patternInfo.fillImage.opacity,
angle: patternInfo.fillImage.angle, angle: patternInfo.fillImage.angle,
scale: patternInfo.fillImage.scale scale: patternInfo.fillImage.scale
}); };
if (patternInfo.fillImage.preserveAspectRatio) {
imagePatternOptions.preserveAspectRatioAlign = 'xMidYMid';
imagePatternOptions.preserveAspectRatioMeetOrSlice = 'slice';
} else {
imagePatternOptions.preserveAspectRatioAlign = 'none';
}
const imagePatternShape = new L.TB.PatternImage(imagePatternOptions);
pattern.addElement(imagePatternShape); pattern.addElement(imagePatternShape);
} else if (patternInfo.type === ShapeFillType.stripe) { } else if (patternInfo.type === ShapeFillType.stripe) {
const stripeInfo = patternInfo.fillStripe; const stripeInfo = patternInfo.fillStripe;
@ -221,6 +234,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor<ShapeFillImageSet
if (!currentImage?.url) { if (!currentImage?.url) {
currentImage = { currentImage = {
url: this.settings.image, url: this.settings.image,
preserveAspectRatio: this.settings.preserveAspectRatio,
opacity: this.settings.opacity, opacity: this.settings.opacity,
angle: this.settings.angle, angle: this.settings.angle,
scale: this.settings.scale scale: this.settings.scale
@ -231,6 +245,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor<ShapeFillImageSet
private loadPatternInfoFromImage(image: ShapeFillImageInfo): Observable<ShapePatternInfo> { private loadPatternInfoFromImage(image: ShapeFillImageInfo): Observable<ShapePatternInfo> {
const imageUrl = image?.url || '/assets/widget-preview-empty.svg'; const imageUrl = image?.url || '/assets/widget-preview-empty.svg';
const preserveAspectRatio = isDefinedAndNotNull(image?.preserveAspectRatio) ? image.preserveAspectRatio : true;
const opacity = isDefinedAndNotNull(image?.opacity) ? image.opacity : 1; const opacity = isDefinedAndNotNull(image?.opacity) ? image.opacity : 1;
const imagePipe = this.dataLayer.getCtx().$injector.get(ImagePipe); const imagePipe = this.dataLayer.getCtx().$injector.get(ImagePipe);
return loadImageWithAspect(imagePipe, imageUrl).pipe( return loadImageWithAspect(imagePipe, imageUrl).pipe(
@ -241,6 +256,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor<ShapeFillImageSet
image: res.url, image: res.url,
width: res.width, width: res.width,
height: res.height, height: res.height,
preserveAspectRatio,
opacity, opacity,
angle: image?.angle, angle: image?.angle,
scale: image?.scale scale: image?.scale

10
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts

@ -974,7 +974,15 @@ class PatternImage extends PatternElement<L.TB.PatternImageOptions> implements L
this._dom.setAttribute('y', '0'); this._dom.setAttribute('y', '0');
this._dom.setAttribute('width', `${options.width}`); this._dom.setAttribute('width', `${options.width}`);
this._dom.setAttribute('height', `${options.height}`); this._dom.setAttribute('height', `${options.height}`);
this._dom.setAttribute('preserveAspectRatio', 'xMidYMid slice'); if (options.preserveAspectRatioAlign) {
let preserveAspectRatioValue = options.preserveAspectRatioAlign;
if (preserveAspectRatioValue !== 'none' && options.preserveAspectRatioMeetOrSlice) {
preserveAspectRatioValue += (' ' + options.preserveAspectRatioMeetOrSlice);
}
dom.setAttribute('preserveAspectRatio', preserveAspectRatioValue);
} else {
dom.removeAttribute('preserveAspectRatio');
}
const transforms: string[] = []; const transforms: string[] = [];
if (options.angle) { if (options.angle) {
transforms.push(`rotate(${options.angle})`); transforms.push(`rotate(${options.angle})`);

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

@ -418,7 +418,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit, O
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true; config.hasBackdrop = true;

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts

@ -110,8 +110,28 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
this.colorSettingsFormGroup.get('type').valueChanges.pipe( this.colorSettingsFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => { ).subscribe(() => {
this.updateValidators();
setTimeout(() => {this.popover?.updatePosition();}, 0); setTimeout(() => {this.popover?.updatePosition();}, 0);
}); });
this.updateValidators();
}
updateValidators() {
const type: ColorType = this.colorSettingsFormGroup.get('type').value;
this.colorSettingsFormGroup.get('gradient').disable({emitEvent: false});
this.colorSettingsFormGroup.get('rangeList').disable({emitEvent: false});
this.colorSettingsFormGroup.get('colorFunction').disable({emitEvent: false});
switch (type) {
case ColorType.gradient:
this.colorSettingsFormGroup.get('gradient').enable({emitEvent: false});
break;
case ColorType.range:
this.colorSettingsFormGroup.get('rangeList').enable({emitEvent: false});
break;
case ColorType.function:
this.colorSettingsFormGroup.get('colorFunction').enable({emitEvent: false});
break;
}
} }
copyColorSettings(comp: ColorSettingsComponent) { copyColorSettings(comp: ColorSettingsComponent) {
@ -131,7 +151,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
} }
applyColorSettings() { applyColorSettings() {
const colorSettings = this.colorSettingsFormGroup.value; const colorSettings: ColorSettings = this.colorSettingsFormGroup.getRawValue();
this.colorSettingsApplied.emit(colorSettings); this.colorSettingsApplied.emit(colorSettings);
} }

9
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts

@ -101,7 +101,7 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen
} }
applyColorSettings() { applyColorSettings() {
const colorSettings: DataLayerColorSettings = this.colorSettingsFormGroup.value; const colorSettings: DataLayerColorSettings = this.colorSettingsFormGroup.getRawValue();
this.colorSettingsApplied.emit(colorSettings); this.colorSettingsApplied.emit(colorSettings);
} }
@ -122,8 +122,15 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen
const type: DataLayerColorType = this.colorSettingsFormGroup.get('type').value; const type: DataLayerColorType = this.colorSettingsFormGroup.get('type').value;
if (type === DataLayerColorType.range) { if (type === DataLayerColorType.range) {
this.colorSettingsFormGroup.get('rangeKey').enable({emitEvent: false}); this.colorSettingsFormGroup.get('rangeKey').enable({emitEvent: false});
this.colorSettingsFormGroup.get('range').enable({emitEvent: false});
} else { } else {
this.colorSettingsFormGroup.get('rangeKey').disable({emitEvent: false}); this.colorSettingsFormGroup.get('rangeKey').disable({emitEvent: false});
this.colorSettingsFormGroup.get('range').disable({emitEvent: false});
}
if (type === DataLayerColorType.function) {
this.colorSettingsFormGroup.get('colorFunction').enable({emitEvent: false});
} else {
this.colorSettingsFormGroup.get('colorFunction').disable({emitEvent: false});
} }
} }
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.html

@ -46,7 +46,8 @@
[functionArgs]="['data', 'images', 'dsData']" [functionArgs]="['data', 'images', 'dsData']"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
functionTitle="{{ 'widgets.maps.data-layer.marker.marker-image-function' | translate }}" functionTitle="{{ 'widgets.maps.data-layer.marker.marker-image-function' | translate }}"
helpId="widget/lib/map/marker_image_fn"> helpId="widget/lib/map/marker_image_fn"
[helpPopupStyle]="{width: '900px'}">
</tb-js-func> </tb-js-func>
<tb-multiple-gallery-image-input label="{{ 'widgets.maps.data-layer.marker.marker-images' | translate }}" <tb-multiple-gallery-image-input label="{{ 'widgets.maps.data-layer.marker.marker-images' | translate }}"
formControlName="images"> formControlName="images">

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html

@ -30,6 +30,11 @@
<div class="tb-shape-fill-image-settings-panel-body" [class.!hidden]="shapeFillImageSettingsFormGroup.get('type').value !== ShapeFillImageType.image"> <div class="tb-shape-fill-image-settings-panel-body" [class.!hidden]="shapeFillImageSettingsFormGroup.get('type').value !== ShapeFillImageType.image">
<div class="tb-form-panel no-padding no-border"> <div class="tb-form-panel no-padding no-border">
<tb-gallery-image-input required formControlName="image"></tb-gallery-image-input> <tb-gallery-image-input required formControlName="image"></tb-gallery-image-input>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="preserveAspectRatio">
{{ 'widgets.maps.data-layer.shape.preserve-aspect-ratio' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widgets.maps.data-layer.shape.opacity</div> <div translate>widgets.maps.data-layer.shape.opacity</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
@ -58,7 +63,8 @@
[functionArgs]="['data', 'images', 'dsData']" [functionArgs]="['data', 'images', 'dsData']"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
functionTitle="{{ 'widgets.maps.data-layer.shape.fill-image-function' | translate }}" functionTitle="{{ 'widgets.maps.data-layer.shape.fill-image-function' | translate }}"
helpId="widget/lib/map/shape_fill_image_fn"> helpId="widget/lib/map/shape_fill_image_fn"
[helpPopupStyle]="{width: '900px'}">
</tb-js-func> </tb-js-func>
<tb-multiple-gallery-image-input label="{{ 'widgets.maps.data-layer.shape.fill-images' | translate }}" <tb-multiple-gallery-image-input label="{{ 'widgets.maps.data-layer.shape.fill-images' | translate }}"
formControlName="images"> formControlName="images">

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts

@ -53,6 +53,7 @@ export class ShapeFillImageSettingsPanelComponent implements OnInit {
{ {
type: [this.shapeFillImageSettings?.type || ShapeFillImageType.image, []], type: [this.shapeFillImageSettings?.type || ShapeFillImageType.image, []],
image: [this.shapeFillImageSettings?.image, [Validators.required]], image: [this.shapeFillImageSettings?.image, [Validators.required]],
preserveAspectRatio: [this.shapeFillImageSettings?.preserveAspectRatio, []],
opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]], opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]],
angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]], angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]],
scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]], scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]],
@ -83,6 +84,7 @@ export class ShapeFillImageSettingsPanelComponent implements OnInit {
const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value; const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value;
if (type === ShapeFillImageType.image) { if (type === ShapeFillImageType.image) {
this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('angle').enable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('angle').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('scale').enable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('scale').enable({emitEvent: false});
@ -90,6 +92,7 @@ export class ShapeFillImageSettingsPanelComponent implements OnInit {
this.shapeFillImageSettingsFormGroup.get('images').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('images').disable({emitEvent: false});
} else { } else {
this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('angle').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('angle').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('scale').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('scale').disable({emitEvent: false});

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts

@ -97,7 +97,7 @@ export class ShapeFillStripeSettingsPanelComponent implements OnInit {
private updatePreview() { private updatePreview() {
const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value;
const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings); const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings, 136, 118);
this.stripePreviewStyle = { this.stripePreviewStyle = {
background: this.sanitizer.bypassSecurityTrustStyle(`url(${previewUrl}) no-repeat 50% 50% / cover`) background: this.sanitizer.bypassSecurityTrustStyle(`url(${previewUrl}) no-repeat 50% 50% / cover`)
}; };

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts

@ -126,15 +126,15 @@ export class ShapeFillStripeSettingsComponent implements ControlValueAccessor {
} }
} }
export const generateStripePreviewUrl = (settings: ShapeFillStripeSettings): string => { export const generateStripePreviewUrl = (settings: ShapeFillStripeSettings, previewWidth = 48, previewHeight = 48): string => {
const weight = isDefinedAndNotNull(settings?.weight) ? settings.weight : 3; const weight = isDefinedAndNotNull(settings?.weight) ? settings.weight : 3;
const spaceWeight = isDefinedAndNotNull(settings?.spaceWeight) ? settings.spaceWeight : 9; const spaceWeight = isDefinedAndNotNull(settings?.spaceWeight) ? settings.spaceWeight : 9;
const angle = isDefinedAndNotNull(settings?.angle) ? settings.angle : 45; const angle = isDefinedAndNotNull(settings?.angle) ? settings.angle : 45;
const height = weight + spaceWeight; const height = weight + spaceWeight;
const color = settings?.color?.color || '#8f8f8f'; const color = settings?.color?.color || '#8f8f8f';
const spaceColor = settings?.spaceColor?.color || 'rgba(143,143,143,0)'; const spaceColor = settings?.spaceColor?.color || 'rgba(143,143,143,0)';
const svgStr = `<svg x="0" y="0" width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> const svgStr = `<svg x="0" y="0" width="${previewWidth}" height="${previewHeight}" viewBox="0 0 ${previewWidth} ${previewHeight}" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="48" height="48" fill="url(#stripePattern)" fill-opacity="1"></rect> <rect x="0" y="0" width="${previewWidth}" height="${previewHeight}" fill="url(#stripePattern)" fill-opacity="1"></rect>
<defs> <defs>
<pattern id="stripePattern" x="0" y="0" width="8" height="${height}" patternUnits="userSpaceOnUse" <pattern id="stripePattern" x="0" y="0" width="8" height="${height}" patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse" patternTransform="rotate(${angle})"> patternContentUnits="userSpaceOnUse" patternTransform="rotate(${angle})">

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

@ -455,7 +455,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.sources.length) { if (this.sources.length) {
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-panel-container', panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',

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

@ -243,7 +243,11 @@ export class WidgetComponentService {
if (widgetInfo) { if (widgetInfo) {
return widgetInfo; return widgetInfo;
} else { } else {
return {} as WidgetInfo; return {
typeParameters: {
hideDataTab: true
}
} as WidgetInfo;
} }
} }

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

@ -165,7 +165,7 @@
[widgetType] = "modelValue.widgetType" [widgetType] = "modelValue.widgetType"
[defaultIconColor]="widgetSettings.get('color').value" [defaultIconColor]="widgetSettings.get('color').value"
[actionSources]="modelValue.actionSources" [actionSources]="modelValue.actionSources"
[additionalWidgetActionTypes]="modelValue.typeParameters.additionalWidgetActionTypes" [additionalWidgetActionTypes]="modelValue.typeParameters?.additionalWidgetActionTypes"
formControlName="actions"> formControlName="actions">
</tb-manage-widget-actions> </tb-manage-widget-actions>
</div> </div>

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

@ -54,7 +54,6 @@ import {
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
import { import {
@ -205,7 +204,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
@Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>,
@Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>, @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>,
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap, @Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
private widgetService: WidgetService,
private resources: ResourcesService, private resources: ResourcesService,
private timeService: TimeService, private timeService: TimeService,
private deviceService: DeviceService, private deviceService: DeviceService,
@ -346,17 +344,17 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.subscriptionContext.widgetUtils = this.widgetContext.utils; this.subscriptionContext.widgetUtils = this.widgetContext.utils;
this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService);
this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe( this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe({
(widgetInfo) => { next: (widgetInfo) => {
this.widgetInfo = widgetInfo; this.widgetInfo = widgetInfo;
this.loadFromWidgetInfo(); this.loadFromWidgetInfo();
}, },
(errorData) => { error: (errorData) => {
this.widgetInfo = errorData.widgetInfo; this.widgetInfo = errorData.widgetInfo;
this.errorMessages = errorData.errorMessages; this.errorMessages = errorData.errorMessages;
this.loadFromWidgetInfo(); this.loadFromWidgetInfo();
} }
); });
const noDataDisplayMessage = this.widget.config.noDataDisplayMessage; const noDataDisplayMessage = this.widget.config.noDataDisplayMessage;
if (isNotEmptyStr(noDataDisplayMessage)) { if (isNotEmptyStr(noDataDisplayMessage)) {
@ -521,15 +519,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.widgetTypeInstance.onDestroy = () => {}; this.widgetTypeInstance.onDestroy = () => {};
} }
this.initialize().subscribe( this.initialize().subscribe({
() => { next: () => {
this.onInit(); this.onInit();
}, },
(err) => { error: () => {
this.widgetContext.inited = true; this.widgetContext.inited = true;
// console.log(err); // console.log(err);
} }
); });
} }
private detectChanges(detectContainerChanges = false) { private detectChanges(detectContainerChanges = false) {
@ -681,8 +679,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
private reInitImpl() { private reInitImpl() {
this.onDestroy(); this.onDestroy();
if (!this.typeParameters.useCustomDatasources) { if (!this.typeParameters.useCustomDatasources) {
this.createDefaultSubscription().subscribe( this.createDefaultSubscription().subscribe({
() => { next: () => {
if (this.destroyed) { if (this.destroyed) {
this.onDestroy(); this.onDestroy();
} else { } else {
@ -692,7 +690,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.onInit(); this.onInit();
} }
}, },
() => { error: () => {
if (this.destroyed) { if (this.destroyed) {
this.onDestroy(); this.onDestroy();
} else { } else {
@ -701,7 +699,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.onInit(); this.onInit();
} }
} }
); });
} else { } else {
this.widgetContext.reset(); this.widgetContext.reset();
this.subscriptionInited = true; this.subscriptionInited = true;
@ -751,8 +749,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} }
)); ));
if (!this.typeParameters.useCustomDatasources) { if (!this.typeParameters.useCustomDatasources) {
this.createDefaultSubscription().subscribe( this.createDefaultSubscription().subscribe({
() => { next: () => {
this.subscriptionInited = true; this.subscriptionInited = true;
try { try {
this.configureDynamicWidgetComponent(); this.configureDynamicWidgetComponent();
@ -762,11 +760,11 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
initSubject.error(err); initSubject.error(err);
} }
}, },
(err) => { error: (err) => {
this.subscriptionInited = true; this.subscriptionInited = true;
initSubject.error(err); initSubject.error(err);
} }
); });
} else { } else {
this.loadingData = false; this.loadingData = false;
this.subscriptionInited = true; this.subscriptionInited = true;
@ -791,7 +789,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} }
} }
private handleWidgetException(e) { private handleWidgetException(e: any) {
console.error(e); console.error(e);
this.widgetErrorData = this.utils.processWidgetException(e); this.widgetErrorData = this.utils.processWidgetException(e);
this.detectChanges(); this.detectChanges();
@ -866,8 +864,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>(); const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>();
options.dashboardTimewindow = this.widgetContext.dashboardTimewindow; options.dashboardTimewindow = this.widgetContext.dashboardTimewindow;
const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options); const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options);
subscription.init$.subscribe( subscription.init$.subscribe({
() => { next: () => {
this.widgetContext.subscriptions[subscription.id] = subscription; this.widgetContext.subscriptions[subscription.id] = subscription;
if (subscribe) { if (subscribe) {
subscription.subscribe(); subscription.subscribe();
@ -875,10 +873,10 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
createSubscriptionSubject.next(subscription); createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete(); createSubscriptionSubject.complete();
}, },
(err) => { error: (err) => {
createSubscriptionSubject.error(err); createSubscriptionSubject.error(err);
} }
); });
return createSubscriptionSubject.asObservable(); return createSubscriptionSubject.asObservable();
} }
@ -900,15 +898,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} else { } else {
options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo); options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo);
} }
this.createSubscription(options, subscribe).subscribe( this.createSubscription(options, subscribe).subscribe({
(subscription) => { next: (subscription) => {
createSubscriptionSubject.next(subscription); createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete(); createSubscriptionSubject.complete();
}, },
(err) => { error: (err) => {
createSubscriptionSubject.error(err); createSubscriptionSubject.error(err);
} }
); });
return createSubscriptionSubject.asObservable(); return createSubscriptionSubject.asObservable();
} }
@ -937,7 +935,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.dataUpdatePending = true; this.dataUpdatePending = true;
} }
} }
} catch (e){} } catch (e){/**/}
}, },
onLatestDataUpdated: () => { onLatestDataUpdated: () => {
try { try {
@ -948,15 +946,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.latestDataUpdatePending = true; this.latestDataUpdatePending = true;
} }
} }
} catch (e){} } catch (e){/**/}
}, },
onDataUpdateError: (subscription, e) => { onDataUpdateError: (_subscription, e) => {
this.handleWidgetException(e); this.handleWidgetException(e);
}, },
onLatestDataUpdateError: (subscription, e) => { onLatestDataUpdateError: (_subscription, e) => {
this.handleWidgetException(e); this.handleWidgetException(e);
}, },
onSubscriptionMessage: (subscription, message) => { onSubscriptionMessage: (_subscription, message) => {
if (this.displayWidgetInstance()) { if (this.displayWidgetInstance()) {
if (this.widgetInstanceInited) { if (this.widgetInstanceInited) {
this.displayMessage(message.severity, message.message); this.displayMessage(message.severity, message.message);
@ -965,7 +963,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} }
} }
}, },
onInitialPageDataChanged: (subscription, nextPageData) => { onInitialPageDataChanged: (_subscription, _nextPageData) => {
this.reInit(); this.reInit();
}, },
forceReInit: () => { forceReInit: () => {
@ -977,12 +975,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.detectChanges(); this.detectChanges();
} }
}, },
legendDataUpdated: (subscription, detectChanges) => { legendDataUpdated: (_subscription, detectChanges) => {
if (detectChanges) { if (detectChanges) {
this.detectChanges(); this.detectChanges();
} }
}, },
timeWindowUpdated: (subscription, timeWindowConfig) => { timeWindowUpdated: (_subscription, timeWindowConfig) => {
this.ngZone.run(() => { this.ngZone.run(() => {
this.widget.config.timewindow = timeWindowConfig; this.widget.config.timewindow = timeWindowConfig;
this.detectChanges(true); this.detectChanges(true);
@ -1017,8 +1015,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.defaultComponentsOptions(options); this.defaultComponentsOptions(options);
this.createSubscription(options).subscribe( this.createSubscription(options).subscribe({
(subscription) => { next: (subscription) => {
// backward compatibility // backward compatibility
this.widgetContext.datasources = subscription.datasources; this.widgetContext.datasources = subscription.datasources;
@ -1032,12 +1030,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
createSubscriptionSubject.complete(); createSubscriptionSubject.complete();
}); });
}, },
(err) => { error: (err) => {
this.ngZone.run(() => { this.ngZone.run(() => {
createSubscriptionSubject.error(err); createSubscriptionSubject.error(err);
}); });
} }
); });
} else if (this.widget.type === widgetType.rpc) { } else if (this.widget.type === widgetType.rpc) {
this.loadingData = false; this.loadingData = false;
options = { options = {
@ -1074,7 +1072,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.detectChanges(); this.detectChanges();
} }
}, },
onRpcErrorCleared: (subscription) => { onRpcErrorCleared: (_subscription) => {
if (this.dynamicWidgetComponent) { if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.rpcErrorText = null; this.dynamicWidgetComponent.rpcErrorText = null;
this.dynamicWidgetComponent.rpcRejection = null; this.dynamicWidgetComponent.rpcRejection = null;
@ -1085,20 +1083,20 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} }
} }
}; };
this.createSubscription(options).subscribe( this.createSubscription(options).subscribe({
(subscription) => { next: (subscription) => {
this.widgetContext.defaultSubscription = subscription; this.widgetContext.defaultSubscription = subscription;
this.ngZone.run(() => { this.ngZone.run(() => {
createSubscriptionSubject.next(); createSubscriptionSubject.next();
createSubscriptionSubject.complete(); createSubscriptionSubject.complete();
}); });
}, },
(err) => { error: (err) => {
this.ngZone.run(() => { this.ngZone.run(() => {
createSubscriptionSubject.error(err); createSubscriptionSubject.error(err);
}); });
} }
); });
this.detectChanges(); this.detectChanges();
} else if (this.widget.type === widgetType.static) { } else if (this.widget.type === widgetType.static) {
this.loadingData = false; this.loadingData = false;
@ -1159,7 +1157,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} }
const state = objToBase64URI([ stateObject ]); const state = objToBase64URI([ stateObject ]);
const isSinglePage = this.route.snapshot.data.singlePageMode; const isSinglePage = this.route.snapshot.data.singlePageMode;
let url; let url: string;
if (isSinglePage) { if (isSinglePage) {
url = `/dashboard/${targetDashboardId}?state=${state}`; url = `/dashboard/${targetDashboardId}?state=${state}`;
} else { } else {
@ -1168,7 +1166,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
if (descriptor.openNewBrowserTab) { if (descriptor.openNewBrowserTab) {
window.open(url, '_blank'); window.open(url, '_blank');
} else { } else {
this.router.navigateByUrl(url); this.router.navigateByUrl(url).then(() => {});
} }
break; break;
case WidgetActionType.openURL: case WidgetActionType.openURL:
@ -1467,7 +1465,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
popoverWidth = '25vw', popoverWidth = '25vw',
popoverHeight = '25vh', popoverHeight = '25vh',
popoverStyle: { [klass: string]: any } = {}) { popoverStyle: { [klass: string]: any } = {}) {
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; const trigger = ($event.target || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) { if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
@ -1560,7 +1558,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} }
private elementClick($event: Event) { private elementClick($event: Event) {
const elementClicked = ($event.target || $event.srcElement) as Element; const elementClicked = ($event.target) as Element;
const descriptors = this.getActionDescriptors('elementClick'); const descriptors = this.getActionDescriptors('elementClick');
if (descriptors.length) { if (descriptors.length) {
const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(',');

7
ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts

@ -97,7 +97,7 @@ export class JsLibraryTableConfigResolver {
entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), entity => checkBoxCell(entity.tenantId.id === NULL_UUID)),
); );
this.config.cellActionDescriptors = this.configureCellActions(getCurrentAuthUser(this.store)); this.config.cellActionDescriptors = this.configureCellActions();
this.config.groupActionDescriptors = [{ this.config.groupActionDescriptors = [{
name: this.translate.instant('action.delete'), name: this.translate.instant('action.delete'),
@ -137,6 +137,7 @@ export class JsLibraryTableConfigResolver {
resourceSubType: '' resourceSubType: ''
}; };
const authUser = getCurrentAuthUser(this.store); const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority); this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority);
return this.config; return this.config;
@ -309,7 +310,7 @@ export class JsLibraryTableConfigResolver {
} }
} }
private configureCellActions(authUser: AuthUser): Array<CellActionDescriptor<ResourceInfo>> { private configureCellActions(): Array<CellActionDescriptor<ResourceInfo>> {
const actions: Array<CellActionDescriptor<ResourceInfo>> = []; const actions: Array<CellActionDescriptor<ResourceInfo>> = [];
actions.push( actions.push(
{ {
@ -321,7 +322,7 @@ export class JsLibraryTableConfigResolver {
{ {
name: this.translate.instant('javascript.delete'), name: this.translate.instant('javascript.delete'),
icon: 'delete', icon: 'delete',
isEnabled: (resource) => this.isResourceEditable(resource, authUser.authority), isEnabled: (resource) => this.config.deleteEnabled(resource),
onAction: ($event, entity) => this.deleteResource($event, entity) onAction: ($event, entity) => this.deleteResource($event, entity)
}, },
); );

4
ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts

@ -47,10 +47,10 @@ export class EditorPanelComponent implements OnInit {
tinyMceOptions: Partial<EditorOptions> = { tinyMceOptions: Partial<EditorOptions> = {
base_url: '/assets/tinymce', base_url: '/assets/tinymce',
suffix: '.min', suffix: '.min',
plugins: ['lists'], plugins: ['link', 'table', 'image', 'imagetools', 'lists'],
menubar: 'edit insert view format', menubar: 'edit insert view format',
toolbar: ['fontfamily fontsize | bold italic underline strikethrough forecolor backcolor', toolbar: ['fontfamily fontsize | bold italic underline strikethrough forecolor backcolor',
'alignleft aligncenter alignright alignjustify | bullist'], 'alignleft aligncenter alignright alignjustify | bullist | link table image'],
toolbar_mode: 'sliding', toolbar_mode: 'sliding',
height: 400, height: 400,
autofocus: false, autofocus: false,

5
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -22,7 +22,7 @@
</button> </button>
</div> </div>
<form [formGroup]="ruleNodeFormGroup" class="mat-padding"> <form [formGroup]="ruleNodeFormGroup" class="mat-padding">
<section class="title-row flex align-center"> <section class="title-row align-center flex">
<mat-form-field class="mat-block flex-1"> <mat-form-field class="mat-block flex-1">
<mat-label translate>rulenode.name</mat-label> <mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required> <input matInput formControlName="name" required>
@ -38,8 +38,7 @@
<tb-entity-debug-settings-button <tb-entity-debug-settings-button
class="mr-2" class="mr-2"
formControlName="debugSettings" formControlName="debugSettings"
[entityLabel]="'debug-settings.rule-node' | translate" [entityType]="entityType.RULE_NODE"
[debugLimitsConfiguration]="ruleChainDebugPerTenantLimitsConfiguration"
/> />
<button mat-stroked-button <button mat-stroked-button
class="tb-rounded-btn flex-1" class="tb-rounded-btn flex-1"

13
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts

@ -26,10 +26,8 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; import { FcRuleNode } from '@shared/models/rule-node.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { RuleNodeConfigComponent } from './rule-node-config.component'; import { RuleNodeConfigComponent } from './rule-node-config.component';
@ -39,7 +37,6 @@ import { ComponentClusteringMode } from '@shared/models/component-descriptor.mod
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { ServiceType } from '@shared/models/queue.models'; import { ServiceType } from '@shared/models/queue.models';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
@Component({ @Component({
selector: 'tb-rule-node', selector: 'tb-rule-node',
@ -73,21 +70,17 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
@Output() @Output()
changeScript = new EventEmitter<void>(); changeScript = new EventEmitter<void>();
ruleNodeType = RuleNodeType;
entityType = EntityType; entityType = EntityType;
serviceType = ServiceType.TB_RULE_ENGINE; serviceType = ServiceType.TB_RULE_ENGINE;
ruleNodeFormGroup: UntypedFormGroup; ruleNodeFormGroup: UntypedFormGroup;
readonly ruleChainDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>, constructor(private fb: UntypedFormBuilder,
private fb: UntypedFormBuilder,
private router: Router) { private router: Router) {
super(store); super();
this.ruleNodeFormGroup = this.fb.group({}); this.ruleNodeFormGroup = this.fb.group({});
} }

7
ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts

@ -22,7 +22,7 @@ import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder } from '@angular/forms'; import { UntypedFormBuilder } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@Component({ @Component({
@ -44,6 +44,7 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router,
private authService: AuthService, private authService: AuthService,
private translate: TranslateService, private translate: TranslateService,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder) {
@ -71,7 +72,9 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD
} else { } else {
this.authService.resetPassword( this.authService.resetPassword(
this.resetToken, this.resetToken,
this.resetPassword.get('newPassword').value).subscribe(); this.resetPassword.get('newPassword').value).subscribe(
() => this.router.navigateByUrl('login')
);
} }
} }
} }

8
ui-ngx/src/app/shared/components/file-input.component.html

@ -51,9 +51,9 @@
</div> </div>
</ng-container> </ng-container>
<div class="tb-file-info-container"> <div class="tb-file-info-container">
<tb-error *ngIf="!fileName && required && requiredAsError" class="tb-file-name" error="{{ noFileText | translate }}"></tb-error> <tb-error *ngIf="!fileName && required && requiredAsError" class="font-medium" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" class="tb-file-name" translate>{{ noFileText }}</div> <div *ngIf="!fileName && !requiredAsError" class="tb-file-name" tbTruncateWithTooltip translate>{{ noFileText }}</div>
<div *ngIf="fileName" class="tb-file-name" [title]="fileName">{{ fileName }}</div> <div *ngIf="fileName" class="tb-file-name" tbTruncateWithTooltip>{{ fileName }}</div>
<div *ngIf="maxSizeByte && !disabled" class="tb-file-hint" translate [translateParams]="{ size: maxSizeByte | fileSize }">dashboard.maximum-upload-file-size</div> <div *ngIf="maxSizeByte && !disabled" class="tb-file-hint" tbTruncateWithTooltip translate [translateParams]="{ size: maxSizeByte | fileSize }">dashboard.maximum-upload-file-size</div>
</div> </div>
</div> </div>

4
ui-ngx/src/app/shared/components/file-input.component.scss

@ -131,15 +131,11 @@ $previewSize: 100px !default;
.tb-file-name { .tb-file-name {
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
font-weight: 500; font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
} }
.tb-file-hint { .tb-file-hint {
color: rgba(0, 0, 0, 0.38); color: rgba(0, 0, 0, 0.38);
font-weight: 400; font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
} }
} }

12
ui-ngx/src/app/shared/components/popover.component.ts

@ -375,7 +375,6 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
tbComponentInjector: Injector | null = null; tbComponentInjector: Injector | null = null;
tbComponentStyle: { [klass: string]: any } = {}; tbComponentStyle: { [klass: string]: any } = {};
tbOverlayClassName!: string; tbOverlayClassName!: string;
tbOverlayStyle: { [klass: string]: any } = {};
tbPopoverInnerStyle: { [klass: string]: any } = {}; tbPopoverInnerStyle: { [klass: string]: any } = {};
tbPopoverInnerContentStyle: { [klass: string]: any } = {}; tbPopoverInnerContentStyle: { [klass: string]: any } = {};
tbBackdrop = false; tbBackdrop = false;
@ -470,6 +469,16 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
return this.tbModal ? 'tb-popover-overlay-backdrop' : ''; return this.tbModal ? 'tb-popover-overlay-backdrop' : '';
} }
set tbOverlayStyle(value: { [klass: string]: any }) {
this._tbOverlayStyle = value;
this.cdr.markForCheck();
}
get tbOverlayStyle(): { [klass: string]: any } {
return this._tbOverlayStyle;
}
preferredPlacement: PopoverPlacement = 'top'; preferredPlacement: PopoverPlacement = 'top';
strictPosition = false; strictPosition = false;
origin!: CdkOverlayOrigin; origin!: CdkOverlayOrigin;
@ -485,6 +494,7 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
this.cdr.markForCheck(); this.cdr.markForCheck();
} }
}, {threshold: [0.5]}); }, {threshold: [0.5]});
private _tbOverlayStyle: { [klass: string]: any } = {};
constructor( constructor(
public cdr: ChangeDetectorRef, public cdr: ChangeDetectorRef,

4
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts

@ -554,7 +554,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; const trigger = ($event.target || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) { if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
@ -614,7 +614,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; const trigger = ($event.target || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) { if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {

2
ui-ngx/src/app/shared/components/time/timezone.component.ts

@ -151,7 +151,7 @@ export class TimezoneComponent implements ControlValueAccessor, OnInit {
if (this.disablePanel) { if (this.disablePanel) {
return; return;
} }
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; const trigger = ($event.target || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) { if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {

50
ui-ngx/src/app/shared/import-export/import-dialog.component.html

@ -28,32 +28,30 @@
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <div class="flex flex-1 flex-col">
<div class="flex flex-1 flex-col"> <tb-toggle-select *ngIf="enableImportFromContent"
<tb-toggle-select *ngIf="enableImportFromContent" class="flex-1"
class="flex-1" formControlName="importType">
formControlName="importType"> <tb-toggle-option value="file">{{ importFileLabel | translate }}</tb-toggle-option>
<tb-toggle-option value="file">{{ importFileLabel | translate }}</tb-toggle-option> <tb-toggle-option value="content">{{ importContentLabel | translate }}</tb-toggle-option>
<tb-toggle-option value="content">{{ importContentLabel | translate }}</tb-toggle-option> </tb-toggle-select>
</tb-toggle-select> <tb-file-input *ngIf="importFormGroup.get('importType').value === 'file'"
<tb-file-input *ngIf="importFormGroup.get('importType').value === 'file'" [contentConvertFunction]="loadDataFromJsonContent"
[contentConvertFunction]="loadDataFromJsonContent" [existingFileName]="currentFileName"
[existingFileName]="currentFileName" (fileNameChanged)="currentFileName = $event"
(fileNameChanged)="currentFileName = $event" formControlName="fileContent"
formControlName="fileContent" required
required label="{{importFileLabel | translate}}"
label="{{importFileLabel | translate}}" dropLabel="{{ 'import.drop-json-file-or' | translate }}"
dropLabel="{{ 'import.drop-json-file-or' | translate }}" accept=".json,application/json"
accept=".json,application/json" allowedExtensions="json">
allowedExtensions="json"> </tb-file-input>
</tb-file-input> <tb-json-object-edit *ngIf="importFormGroup.get('importType').value === 'content'"
<tb-json-object-edit *ngIf="importFormGroup.get('importType').value === 'content'" formControlName="jsonContent"
formControlName="jsonContent" jsonRequired
jsonRequired label="{{ importContentLabel | translate }}">
label="{{ importContentLabel | translate }}"> </tb-json-object-edit>
</tb-json-object-edit> </div>
</div>
</fieldset>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"

2
ui-ngx/src/app/shared/models/tenant.model.ts

@ -137,7 +137,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxSms: 0, maxSms: 0,
smsEnabled: true, smsEnabled: true,
maxCreatedAlarms: 0, maxCreatedAlarms: 0,
maxDebugModeDurationMinutes: 0, maxDebugModeDurationMinutes: 15,
tenantServerRestLimitsConfiguration: '', tenantServerRestLimitsConfiguration: '',
customerServerRestLimitsConfiguration: '', customerServerRestLimitsConfiguration: '',
maxWsSessionsPerTenant: 0, maxWsSessionsPerTenant: 0,

4
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -501,6 +501,7 @@ export enum ShapeFillImageType {
export interface ShapeFillImageSettings { export interface ShapeFillImageSettings {
type: ShapeFillImageType; type: ShapeFillImageType;
image?: string; image?: string;
preserveAspectRatio?: boolean;
opacity?: number; // (0-1) opacity?: number; // (0-1)
angle?: number; // (0-360) angle?: number; // (0-360)
scale?: number; // (0-...) scale?: number; // (0-...)
@ -550,6 +551,7 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial<
fillImage: { fillImage: {
type: ShapeFillImageType.image, type: ShapeFillImageType.image,
image: '/assets/widget-preview-empty.svg', image: '/assets/widget-preview-empty.svg',
preserveAspectRatio: true,
opacity: 1, opacity: 1,
angle: 0, angle: 0,
scale: 1 scale: 1
@ -600,6 +602,7 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial<C
fillImage: { fillImage: {
type: ShapeFillImageType.image, type: ShapeFillImageType.image,
image: '/assets/widget-preview-empty.svg', image: '/assets/widget-preview-empty.svg',
preserveAspectRatio: true,
opacity: 1, opacity: 1,
angle: 0, angle: 0,
scale: 1 scale: 1
@ -1186,6 +1189,7 @@ export interface MarkerIconInfo {
export interface ShapeFillImageInfo { export interface ShapeFillImageInfo {
url: string; url: string;
preserveAspectRatio?: boolean;
opacity?: number; opacity?: number;
angle?: number; angle?: number;
scale?: number; scale?: number;

2
ui-ngx/src/assets/dashboard/customer_user_home_page.json

@ -193,7 +193,7 @@
"padding": "16px", "padding": "16px",
"settings": { "settings": {
"useMarkdownTextFunction": false, "useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div class=\"flex flex-row gap-3\">\n <a mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-total md:!hidden\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>", "markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div class=\"flex flex-row gap-3\">\n <a mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\" [queryParams]=\"{active: false}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\" [queryParams]=\"{active: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-total md:!hidden\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n" "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n"
}, },

56
ui-ngx/src/assets/help/en_US/math/math-methods_fn.md

@ -0,0 +1,56 @@
## Built-in operators
The following operators can be used in expressions:
| Operator | Description | Example |
|----------|-------------------------------|-----------------|
| `+` | Addition or unary plus | `2 + 3` or `+3` |
| `-` | Subtraction or unary minus | `5 - 2` or `-3` |
| `*` | Multiplication | `4 * 2` |
| `/` | Division | `10 / 2` |
| `^` | Exponentiation | `2 ^ 3` |
| `%` | Modulo | `10 % 3` |
## Implicit multiplication
Expressions supports implicit multiplication, allowing you to omit the multiplication operator in certain cases. For example, the expression `2cos(yx)` is interpreted as `2*cos(y*x)`. Similarly, `3x` is interpreted as `3*x`, and `xy` as `x*y`. This feature makes writing expressions more concise and natural.
## Built-in constants
Available constants for calculations:
| Constant | Description | Value |
|-------------|-----------------------------|-------------------|
| `π` or `pi` | The mathematical constant π | 3.141592653589793 |
| `e` | Euler's number | 2.718281828459045 |
| `φ` | The golden ratio | 1.618033988749895 |
## Built-in mathematical functions
The following built-in mathematical functions can be used to perform various calculations:
| Function | Description | Example |
|---------------|---------------------------------------------------------------------|-------------------|
| `abs(x)` | Absolute value of `x`. | `abs(-7) = 7` |
| `acos(x)` | Arc cosine of `x`, result in radians. Requires `-1 ≤ x ≤ 1`. | `acos(1) = 0` |
| `asin(x)` | Arc sine of `x`, result in radians. Requires `-1 ≤ x ≤ 1`. | `asin(0) = 0` |
| `atan(x)` | Arc tangent of `x`, result in radians. | `atan(0) = 0` |
| `cbrt(x)` | Cube root of `x`. | `cbrt(8) = 2` |
| `ceil(x)` | Rounds `x` up to the nearest integer. | `ceil(3.1) = 4` |
| `cos(x)` | Cosine of `x`, where `x` is in radians. | `cos(0) = 1` |
| `cosh(x)` | Hyperbolic cosine of `x`. | `cosh(0) = 1` |
| `cot(x)` | Cotangent of `x` (1 / tan(`x`)), where `x` is in radians. | `cot(0.7854) ≈ 1` |
| `exp(x)` | Computes `e^x`. | `exp(0) = 1` |
| `expm1(x)` | Computes `e^x - 1` accurately for small `x`. | `expm1(0) = 0` |
| `floor(x)` | Rounds `x` down to the nearest integer. | `floor(3.9) = 3` |
| `ln(x)` | Natural logarithm (base *e*) of `x`. Requires `x > 0`. | `ln(1) = 0` |
| `log(x)` | Natural logarithm (base *e*) of `x`. Requires `x > 0`. | `log(1) = 0` |
| `lg(x)` | Natural logarithm (base 10) of `x`. Requires `x > 0`. | `lg(10) = 1` |
| `log10(x)` | Logarithm base 10 of `x`. Requires `x > 0`. | `log10(100) = 2` |
| `log2(x)` | Logarithm base 2 of `x`. Requires `x > 0`. | `log2(8) = 3` |
| `logab(a, b)` | Logarithm of `b` with base `a`. Requires `a > 0`, `b > 0`, `a ≠ 1`. | `logab(2, 8) = 3` |
| `log1p(x)` | Computes `ln(1 + x)` accurately for small `x`. Requires `x > -1`. | `log1p(0) = 0` |
| `pow(x, y)` | Raises `x` to the power of `y` (`x^y`). | `pow(2, 3) = 8` |
| `signum(x)` | Returns the sign of `x`: -1 if `x < 0`, 0 if `x = 0`, 1 if `x > 0`. | `signum(-5) = -1` |
| `sin(x)` | Sine of `x`, where `x` is in radians. | `sin(0) = 0` |
| `sinh(x)` | Hyperbolic sine of `x`. | `sinh(0) = 0` |
| `sqrt(x)` | Square root of `x`. Requires `x ≥ 0`. | `sqrt(4) = 2` |
| `tan(x)` | Tangent of `x`, where `x` is in radians. | `tan(0) = 0` |
| `tanh(x)` | Hyperbolic tangent of `x`. | `tanh(0) = 0` |

27
ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md

@ -20,6 +20,7 @@ Should return shape fill image data having the following structure:
```typescript ```typescript
{ {
url: string; url: string;
preserveAspectRatio?: boolean;
opacity?: number; opacity?: number;
angle?: number; angle?: number;
scale?: number; scale?: number;
@ -27,6 +28,7 @@ Should return shape fill image data having the following structure:
``` ```
- *url* - fill image url; - *url* - fill image url;
- *preserveAspectRatio* - optional property indicating whether to preserve image aspect ratio (`true` if not specified);
- *opacity* - optional image opacity, number value from 0 to 1; - *opacity* - optional image opacity, number value from 0 to 1;
- *angle* - optional image rotation angle, number value from 0 to 360; - *angle* - optional image rotation angle, number value from 0 to 360;
- *scale* - optional image scale, number value (1 - original size, smaller value - scale down, bigger value - scale up); - *scale* - optional image scale, number value (1 - original size, smaller value - scale down, bigger value - scale up);
@ -39,12 +41,33 @@ In case no data is returned, default fill image will be used.
<ul> <ul>
<li> <li>
TODO: Calculate image URL and rotation angle depending on <code>windSpeed</code> and <code>windDirection</code> telemetry values for a <code>weather station</code> device type.<br/>
Let's assume 3 images are defined in the Shape fill images section. Each image corresponds to a particular wind speed level: low (e.g., <5 m/s), medium (e.g., 5-15 m/s), and high (e.g., >15 m/s).
</li> </li>
</ul> </ul>
```javascript ```javascript
TODO: const type = data.Type;
if (type === 'weather station') {
const result = {
url: images[0],
opacity: 0.8,
angle: 0
};
const windSpeed = data.windSpeed;
const windDirection = data.windDirection;
if (typeof windSpeed !== 'undefined' && typeof windDirection !== 'undefined') {
if (windSpeed < 5) {
result.url = images[0];
} else if (windSpeed < 15) {
result.url = images[1];
} else {
result.url = images[2];
}
result.angle = windDirection;
}
return result;
}
{:copy-code} {:copy-code}
``` ```

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

@ -996,8 +996,6 @@
"all-messages": "All messages ({{time}})", "all-messages": "All messages ({{time}})",
"failures": "Failures", "failures": "Failures",
"entity": "entity", "entity": "entity",
"rule-node": "rule node",
"calculated-field": "calculated field",
"hint": { "hint": {
"main-limited": "No more than {{msg}} {{entity}} debug messages per {{time}} will be recorded.", "main-limited": "No more than {{msg}} {{entity}} debug messages per {{time}} will be recorded.",
"on-failure": "Log error messages only.", "on-failure": "Log error messages only.",
@ -1059,6 +1057,7 @@
"delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.", "delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.",
"delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 calculated field} other {# calculated fields} }?", "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 calculated field} other {# calculated fields} }?",
"delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.",
"test-with-this-message": "Test with this message",
"hint": { "hint": {
"arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.",
"arguments-empty": "Arguments should not be empty.", "arguments-empty": "Arguments should not be empty.",
@ -1069,7 +1068,7 @@
"argument-name-pattern": "Argument name is invalid.", "argument-name-pattern": "Argument name is invalid.",
"argument-name-duplicate": "Argument with such name already exists.", "argument-name-duplicate": "Argument with such name already exists.",
"argument-name-max-length": "Argument name should be less than 256 characters.", "argument-name-max-length": "Argument name should be less than 256 characters.",
"argument-name-ctx": "Argument name 'ctx' is reserved and cannot be used.", "argument-name-forbidden": "Argument name is reserved and cannot be used.",
"argument-type-required": "Argument type is required.", "argument-type-required": "Argument type is required.",
"max-args": "Maximum number of arguments reached.", "max-args": "Maximum number of arguments reached.",
"decimals-range": "Decimals by default should be a number between 0 and 15.", "decimals-range": "Decimals by default should be a number between 0 and 15.",
@ -1121,8 +1120,6 @@
"documentation": "Documentation", "documentation": "Documentation",
"time-left": "{{time}} left", "time-left": "{{time}} left",
"output": "Output", "output": "Output",
"test-function": "Test function",
"test-with-this-message": "{{test}} with this message",
"suffix": { "suffix": {
"s": "s", "s": "s",
"ms": "ms" "ms": "ms"
@ -8137,6 +8134,7 @@
"fill-image": "Fill image", "fill-image": "Fill image",
"fill-image-type-image": "Image", "fill-image-type-image": "Image",
"fill-image-type-function": "Function", "fill-image-type-function": "Function",
"preserve-aspect-ratio": "Preserve aspect ratio",
"opacity": "Opacity", "opacity": "Opacity",
"angle": "Rotation angle", "angle": "Rotation angle",
"scale": "Scale", "scale": "Scale",

198
ui-ngx/src/assets/locale/locale.constant-ja_JP.json

@ -20,45 +20,45 @@
"yes": "はい", "yes": "はい",
"no": "いいえ", "no": "いいえ",
"update": "更新", "update": "更新",
"remove": "削除する", "remove": "削除",
"search": "サーチ", "search": " 検索",
"clear-search": "検索をクリアする", "clear-search": "検索をクリア",
"assign": "割り当てます", "assign": "割り当て",
"unassign": "割り当て解除", "unassign": "割り当て解除",
"share": "シェア", "share": "シェア",
"make-private": "プライベートにする", "make-private": "プライベートにする",
"apply": "適用", "apply": "適用",
"apply-changes": "変更を適用する", "apply-changes": "変更を適用",
"edit-mode": "編集モード", "edit-mode": "編集モード",
"enter-edit-mode": "編集モードに入る", "enter-edit-mode": "編集モードに入る",
"decline-changes": "変更を拒否する", "decline-changes": "変更を拒否",
"close": "閉じる", "close": "閉じる",
"back": "バック", "back": "戻る",
"run": "走る", "run": "実行",
"sign-in": "サインイン!", "sign-in": "サインイン!",
"edit": "編集", "edit": "編集",
"view": "ビュー", "view": "ビュー",
"create": "作成する", "create": "作成",
"drag": "ドラッグ", "drag": "ドラッグ",
"refresh": "リフレッシュ", "refresh": "リフレッシュ",
"undo": "元に戻す", "undo": "元に戻す",
"copy": "コピー", "copy": "コピー",
"paste": "ペースト", "paste": "ペースト",
"copy-reference": "コピーリファレンス", "copy-reference": "参照コピー",
"paste-reference": "参照貼り付け", "paste-reference": "参照貼り付け",
"import": "インポート", "import": "インポート",
"export": "輸出する", "export": "エクスポート",
"share-via": "{{provider}}" "share-via": "{{provider}}"
}, },
"aggregation": { "aggregation": {
"aggregation": "集約", "aggregation": "集約",
"function": "データ集約機能", "function": "データ集約機能",
"limit": "最大値", "limit": "上限",
"group-interval": "グループ化の間隔", "group-interval": "グループ化の間隔",
"min": "分", "min": "最小",
"max": "最大", "max": "最大",
"avg": "平均", "avg": "平均",
"sum": "和", "sum": "総和",
"count": "カウント", "count": "カウント",
"none": "なし" "none": "なし"
}, },
@ -77,17 +77,17 @@
"smtp-host": "SMTPホスト", "smtp-host": "SMTPホスト",
"smtp-host-required": "SMTPホストが必要です。", "smtp-host-required": "SMTPホストが必要です。",
"smtp-port": "SMTPポート", "smtp-port": "SMTPポート",
"smtp-port-required": "smtpポートを指定する必要があります。", "smtp-port-required": "SMTPポートを指定してください。",
"smtp-port-invalid": "それは有効なsmtpポートのようには見えません。", "smtp-port-invalid": "有効なSMTPポートではありません。",
"timeout-msec": "タイムアウト(ミリ秒)", "timeout-msec": "タイムアウト(ミリ秒)",
"timeout-required": "タイムアウトが必要です。", "timeout-required": "タイムアウト設定が必要です。",
"timeout-invalid": "それは有効なタイムアウトのようには見えません。", "timeout-invalid": "有効なタイムアウト設定ではありません。",
"enable-tls": "TLSを有効にする", "enable-tls": "TLSを有効にする",
"tls-version": "TLSバージョン", "tls-version": "TLSバージョン",
"send-test-mail": "テストメールを送信する" "send-test-mail": "テストメールを送信する"
}, },
"alarm": { "alarm": {
"alarm": "警報", "alarm": "アラーム",
"alarms": "アラーム", "alarms": "アラーム",
"select-alarm": "アラームを選択", "select-alarm": "アラームを選択",
"no-alarms-matching": "'{{entity}}'発見されました。", "no-alarms-matching": "'{{entity}}'発見されました。",
@ -110,12 +110,12 @@
"created-time": "作成時刻", "created-time": "作成時刻",
"type": "タイプ", "type": "タイプ",
"severity": "重大度", "severity": "重大度",
"originator": "創始者", "originator": "発信元",
"originator-type": "発信者タイプ", "originator-type": "発信元タイプ",
"details": "詳細", "details": "詳細",
"status": "状態", "status": "状態",
"alarm-details": "アラームの詳細", "alarm-details": "アラームの詳細",
"start-time": "始まる時間", "start-time": "開始時間",
"end-time": "終了時間", "end-time": "終了時間",
"ack-time": "確認された時間", "ack-time": "確認された時間",
"clear-time": "クリアされた時間", "clear-time": "クリアされた時間",
@ -124,7 +124,7 @@
"severity-minor": "マイナー", "severity-minor": "マイナー",
"severity-warning": "警告", "severity-warning": "警告",
"severity-indeterminate": "不確定", "severity-indeterminate": "不確定",
"acknowledge": "認める", "acknowledge": "承認",
"clear": "クリア", "clear": "クリア",
"search": "アラームの検索", "search": "アラームの検索",
"selected-alarms": "{ count, plural, =1 {1 alarm} other {# alarms} }選択された", "selected-alarms": "{ count, plural, =1 {1 alarm} other {# alarms} }選択された",
@ -148,7 +148,7 @@
"filter-type-entity-name": "エンティティ名", "filter-type-entity-name": "エンティティ名",
"filter-type-state-entity": "ダッシュボード状態からのエンティティ", "filter-type-state-entity": "ダッシュボード状態からのエンティティ",
"filter-type-state-entity-description": "ダッシュボードの状態パラメータから取得されたエンティティ", "filter-type-state-entity-description": "ダッシュボードの状態パラメータから取得されたエンティティ",
"filter-type-asset-type": "資産の種類", "filter-type-asset-type": "アセットの種類",
"filter-type-asset-type-description": "'{{assetTypes}}'", "filter-type-asset-type-description": "'{{assetTypes}}'",
"filter-type-asset-type-and-name-description": "'{{assetTypes}}''{{prefix}}'", "filter-type-asset-type-and-name-description": "'{{assetTypes}}''{{prefix}}'",
"filter-type-device-type": "デバイスタイプ", "filter-type-device-type": "デバイスタイプ",
@ -156,7 +156,7 @@
"filter-type-device-type-and-name-description": "'{{deviceTypes}}''{{prefix}}'", "filter-type-device-type-and-name-description": "'{{deviceTypes}}''{{prefix}}'",
"filter-type-relations-query": "関係クエリ", "filter-type-relations-query": "関係クエリ",
"filter-type-relations-query-description": "{{entities}}{{relationType}}{{direction}}{{rootEntity}}", "filter-type-relations-query-description": "{{entities}}{{relationType}}{{direction}}{{rootEntity}}",
"filter-type-asset-search-query": "資産検索クエリ", "filter-type-asset-search-query": "アセット検索クエリ",
"filter-type-asset-search-query-description": "{{assetTypes}}{{relationType}}{{direction}}{{rootEntity}}", "filter-type-asset-search-query-description": "{{assetTypes}}{{relationType}}{{direction}}{{rootEntity}}",
"filter-type-device-search-query": "デバイス検索クエリ", "filter-type-device-search-query": "デバイス検索クエリ",
"filter-type-device-search-query-description": "{{deviceTypes}}{{relationType}}{{direction}}{{rootEntity}}", "filter-type-device-search-query-description": "{{deviceTypes}}{{relationType}}{{direction}}{{rootEntity}}",
@ -178,31 +178,31 @@
"any-relation": "どれか" "any-relation": "どれか"
}, },
"asset": { "asset": {
"asset": "資産", "asset": "アセット",
"assets": "資産", "assets": "アセット",
"management": "資産運用管理", "management": "アセット管理",
"view-assets": "アセットの表示", "view-assets": "アセットの表示",
"add": "アセットを追加", "add": "アセットを追加",
"assign-to-customer": "顧客に割り当てる", "assign-to-customer": "顧客に割り当てる",
"assign-asset-to-customer": "顧客に資産を割り当てる", "assign-asset-to-customer": "顧客にアセットを割り当てる",
"assign-asset-to-customer-text": "顧客に割り当てる資産を選択してください", "assign-asset-to-customer-text": "顧客に割り当てるアセットを選択してください",
"no-assets-text": "アセットが見つかりません", "no-assets-text": "アセットが見つかりません",
"assign-to-customer-text": "資産を割り当てる顧客を選択してください", "assign-to-customer-text": "アセットを割り当てる顧客を選択してください",
"public": "パブリック", "public": "公開",
"assignedToCustomer": "顧客に割り当てられた", "assignedToCustomer": "顧客に割り当てられた",
"make-public": "アセットを公開する", "make-public": "アセットを公開する",
"make-private": "アセットをプライベートにする", "make-private": "アセットをプライベートにする",
"unassign-from-customer": "顧客からの割り当て解除", "unassign-from-customer": "顧客からの割り当て解除",
"delete": "アセットを削除", "delete": "アセットを削除",
"asset-public": "資産は公開されています", "asset-public": "アセットは公開されています",
"asset-type": "資産の種類", "asset-type": "アセットの種類",
"asset-type-required": "資産の種類が必要です。", "asset-type-required": "アセットの種類が必要です。",
"select-asset-type": "アセットタイプを選択", "select-asset-type": "アセットタイプを選択",
"enter-asset-type": "アセットタイプを入力", "enter-asset-type": "アセットタイプを入力",
"any-asset": "すべてのアセット", "any-asset": "すべてのアセット",
"no-asset-types-matching": "'{{entitySubtype}}'発見されました。", "no-asset-types-matching": "'{{entitySubtype}}'発見されました。",
"asset-type-list-empty": "選択されたアセットタイプはありません。", "asset-type-list-empty": "選択されたアセットタイプはありません。",
"asset-types": "資産タイプ", "asset-types": "アセットタイプ",
"name": "名", "name": "名",
"name-required": "名前は必須です。", "name-required": "名前は必須です。",
"description": "説明", "description": "説明",
@ -211,7 +211,7 @@
"details": "詳細", "details": "詳細",
"events": "イベント", "events": "イベント",
"add-asset-text": "新しいアセットを追加する", "add-asset-text": "新しいアセットを追加する",
"asset-details": "資産の詳細", "asset-details": "アセットの詳細",
"assign-assets": "アセットの割り当て", "assign-assets": "アセットの割り当て",
"assign-assets-text": "{ count, plural, =1 {1 asset} other {# assets} }顧客に", "assign-assets-text": "{ count, plural, =1 {1 asset} other {# assets} }顧客に",
"delete-assets": "アセットを削除する", "delete-assets": "アセットを削除する",
@ -219,25 +219,25 @@
"unassign-assets-action-title": "{ count, plural, =1 {1 asset} other {# assets} }顧客から", "unassign-assets-action-title": "{ count, plural, =1 {1 asset} other {# assets} }顧客から",
"assign-new-asset": "新しいアセットを割り当てる", "assign-new-asset": "新しいアセットを割り当てる",
"delete-asset-title": "'{{assetName}}'?", "delete-asset-title": "'{{assetName}}'?",
"delete-asset-text": "確認後、資産と関連するすべてのデータが回復不能になることに注意してください。", "delete-asset-text": "確認後、アセットと関連するすべてのデータが回復不能になることに注意してください。",
"delete-assets-title": "{ count, plural, =1 {1 asset} other {# assets} }?", "delete-assets-title": "{ count, plural, =1 {1 asset} other {# assets} }?",
"delete-assets-action-title": "{ count, plural, =1 {1 asset} other {# assets} }", "delete-assets-action-title": "{ count, plural, =1 {1 asset} other {# assets} }",
"delete-assets-text": "確認後、選択したすべての資産が削除され、関連するすべてのデータは回復不能になりますので注意してください。", "delete-assets-text": "確認後、選択したすべてのアセットが削除され、関連するすべてのデータは回復不能になりますので注意してください。",
"make-public-asset-title": "'{{assetName}}'パブリック?", "make-public-asset-title": "'{{assetName}}'パブリック?",
"make-public-asset-text": "確認後、資産とそのすべてのデータは公開され、他の人がアクセスできるようになります。", "make-public-asset-text": "確認後、アセットとそのすべてのデータは公開され、他の人がアクセスできるようになります。",
"make-private-asset-title": "'{{assetName}}'プライベート?", "make-private-asset-title": "'{{assetName}}'プライベート?",
"make-private-asset-text": "確認後、資産とそのすべてのデータは非公開にされ、他の人がアクセスすることはできません。", "make-private-asset-text": "確認後、アセットとそのすべてのデータは非公開にされ、他の人がアクセスすることはできません。",
"unassign-asset-title": "'{{assetName}}'?", "unassign-asset-title": "'{{assetName}}'?",
"unassign-asset-text": "確認後、資産は割り当て解除され、顧客はアクセスできなくなります。", "unassign-asset-text": "確認後、アセットは割り当て解除され、顧客はアクセスできなくなります。",
"unassign-asset": "アセットの割り当てを解除する", "unassign-asset": "アセットの割り当てを解除する",
"unassign-assets-title": "{ count, plural, =1 {1 asset} other {# assets} }?", "unassign-assets-title": "{ count, plural, =1 {1 asset} other {# assets} }?",
"unassign-assets-text": "確認後、選択されたすべての資産が割り当て解除され、顧客がアクセスできなくなります。", "unassign-assets-text": "確認後、選択されたすべてのアセットが割り当て解除され、顧客がアクセスできなくなります。",
"copyId": "アセットIDをコピーする", "copyId": "アセットIDをコピーする",
"idCopiedMessage": "アセットIDがクリップボードにコピーされました", "idCopiedMessage": "アセットIDがクリップボードにコピーされました",
"select-asset": "アセットを選択", "select-asset": "アセットを選択",
"no-assets-matching": "'{{entity}}'発見されました。", "no-assets-matching": "'{{entity}}'発見されました。",
"asset-required": "資産が必要です", "asset-required": "アセットが必要です",
"name-starts-with": "アセット名はで始まります", "name-starts-with": "次で始まるアセット名",
"label": "ラベル" "label": "ラベル"
}, },
"attribute": { "attribute": {
@ -312,12 +312,12 @@
}, },
"contact": { "contact": {
"country": "国", "country": "国",
"city": "シティ", "city": "市区町村",
"state": "州/県", "state": "都道府県",
"postal-code": "郵便番号", "postal-code": "郵便番号",
"postal-code-invalid": "無効な郵便番号形式です。", "postal-code-invalid": "無効な郵便番号形式です。",
"address": "住所", "address": "住所",
"address2": "アドレス2", "address2": "住所2",
"phone": "電話", "phone": "電話",
"email": "Eメール", "email": "Eメール",
"no-address": "住所がありません" "no-address": "住所がありません"
@ -325,8 +325,8 @@
"common": { "common": {
"username": "ユーザー名", "username": "ユーザー名",
"password": "パスワード", "password": "パスワード",
"enter-username": "ユーザーネームを入力してください", "enter-username": "ユーザー名を入力してください",
"enter-password": "パスワードを入力する", "enter-password": "パスワードを入力してください",
"enter-search": "検索を入力", "enter-search": "検索を入力",
"created-time": "作成時刻" "created-time": "作成時刻"
}, },
@ -339,27 +339,27 @@
"customer": "顧客", "customer": "顧客",
"customers": "顧客", "customers": "顧客",
"management": "顧客管理", "management": "顧客管理",
"dashboard": "カスタマーダッシュボード", "dashboard": "顧客ダッシュボード",
"dashboards": "カスタマーダッシュボード", "dashboards": "顧客ダッシュボード",
"devices": "顧客デバイス", "devices": "顧客デバイス",
"assets": "顧客資産", "assets": "顧客アセット",
"public-dashboards": "パブリックダッシュボード", "public-dashboards": "パブリックダッシュボード",
"public-devices": "パブリックデバイス", "public-devices": "パブリックデバイス",
"public-assets": "公的資産", "public-assets": "パブリックアセット",
"add": "顧客を追加", "add": "顧客を追加",
"delete": "顧客を削除する", "delete": "顧客を削除",
"manage-customer-users": "顧客ユーザーを管理する", "manage-customer-users": "顧客を管理する",
"manage-customer-devices": "顧客のデバイスを管理する", "manage-customer-devices": "顧客のデバイスを管理する",
"manage-customer-dashboards": "顧客ダッシュボードの管理", "manage-customer-dashboards": "顧客ダッシュボードの管理",
"manage-public-devices": "パブリックデバイスを管理する", "manage-public-devices": "パブリックデバイスを管理する",
"manage-public-dashboards": "公開ダッシュボードの管理", "manage-public-dashboards": "パブリックダッシュボードの管理",
"manage-customer-assets": "顧客資産の管理", "manage-customer-assets": "顧客アセットの管理",
"manage-public-assets": "公的資産を管理する", "manage-public-assets": "パブリックアセットを管理する",
"add-customer-text": "新規顧客を追加", "add-customer-text": "新規顧客を追加",
"no-customers-text": "顧客が見つかりません", "no-customers-text": "顧客が見つかりません",
"customer-details": "お客様情報", "customer-details": "顧客情報",
"delete-customer-title": "'{{customerTitle}}'?", "delete-customer-title": "'{{customerTitle}}'?",
"delete-customer-text": "確認後、お客様および関連するすべてのデータが回復不能になるので注意してください。", "delete-customer-text": "確認後、顧客および関連するすべてのデータが回復不能になるので注意してください。",
"delete-customers-title": "{ count, plural, =1 {1 customer} other {# customers} }?", "delete-customers-title": "{ count, plural, =1 {1 customer} other {# customers} }?",
"delete-customers-action-title": "{ count, plural, =1 {1 customer} other {# customers} }", "delete-customers-action-title": "{ count, plural, =1 {1 customer} other {# customers} }",
"delete-customers-text": "確認後、選択したすべての顧客は削除され、関連するすべてのデータは回復不能になります。", "delete-customers-text": "確認後、選択したすべての顧客は削除され、関連するすべてのデータは回復不能になります。",
@ -382,10 +382,10 @@
"default-customer-required": "テナントレベルのダッシュボードをデバッグするには、デフォルトの顧客が必要です" "default-customer-required": "テナントレベルのダッシュボードをデバッグするには、デフォルトの顧客が必要です"
}, },
"datetime": { "datetime": {
"date-from": "デートから", "date-from": "開始日",
"time-from": "からの時間", "time-from": "開始時刻",
"date-to": "日付", "date-to": "終了日",
"time-to": "の時間" "time-to": "終了時刻"
}, },
"dashboard": { "dashboard": {
"dashboard": "ダッシュボード", "dashboard": "ダッシュボード",
@ -463,11 +463,11 @@
"max-columns-count-message": "最大1000の列カウントのみが許可されます。", "max-columns-count-message": "最大1000の列カウントのみが許可されます。",
"widgets-margins": "ウィジェット間のマージン", "widgets-margins": "ウィジェット間のマージン",
"horizontal-margin": "水平マージン", "horizontal-margin": "水平マージン",
"horizontal-margin-required": "水平余白値が必要です。", "horizontal-margin-required": "水平マージン設定が必要です。",
"min-horizontal-margin-message": "最小水平マージン値としては0だけが許容されます。", "min-horizontal-margin-message": "最小水平マージン値としては0だけが許容されます。",
"max-horizontal-margin-message": "最大水平マージン値は50だけです。", "max-horizontal-margin-message": "最大水平マージン値は50だけです。",
"vertical-margin": "垂直マージン", "vertical-margin": "垂直マージン",
"vertical-margin-required": "垂直マージン値が必要です。", "vertical-margin-required": "垂直マージン設定が必要です。",
"min-vertical-margin-message": "最小の垂直マージン値として0のみが許可されます。", "min-vertical-margin-message": "最小の垂直マージン値として0のみが許可されます。",
"max-vertical-margin-message": "最大垂直マージン値は50のみです。", "max-vertical-margin-message": "最大垂直マージン値は50のみです。",
"autofill-height": "自動レイアウトの高さ", "autofill-height": "自動レイアウトの高さ",
@ -483,8 +483,8 @@
"display-entities-selection": "エンティティの選択を表示する", "display-entities-selection": "エンティティの選択を表示する",
"display-dashboard-timewindow": "タイムウィンドウを表示する", "display-dashboard-timewindow": "タイムウィンドウを表示する",
"display-dashboard-export": "エクスポートの表示", "display-dashboard-export": "エクスポートの表示",
"import": "インポートダッシュボード", "import": "ダッシュボードをインポート",
"export": "エクスポートダッシュボード", "export": "ダッシュボードをエクスポート",
"export-failed-error": "{{error}}", "export-failed-error": "{{error}}",
"create-new-dashboard": "新しいダッシュボードを作成する", "create-new-dashboard": "新しいダッシュボードを作成する",
"dashboard-file": "ダッシュボードファイル", "dashboard-file": "ダッシュボードファイル",
@ -530,15 +530,15 @@
}, },
"datakey": { "datakey": {
"settings": "設定", "settings": "設定",
"advanced": "上級", "advanced": "カスタム",
"label": "ラベル", "label": "ラベル",
"color": "色", "color": "色",
"units": "値の隣に表示する特別なシンボル", "units": "単位",
"decimals": "浮動小数点の後の桁数", "decimals": "小数点以下の桁数",
"data-generation-func": "データ生成関数", "data-generation-func": "データ生成関数",
"use-data-post-processing-func": "データ後処理機能を使用する", "use-data-post-processing-func": "データ後処理機能を使用する",
"configuration": "データキー設定", "configuration": "データキー設定",
"timeseries": "タイムズ", "timeseries": "時系列",
"attributes": "属性", "attributes": "属性",
"alarm": "アラームフィールド", "alarm": "アラームフィールド",
"timeseries-required": "エンティティの時系列データが必要です。", "timeseries-required": "エンティティの時系列データが必要です。",
@ -703,8 +703,8 @@
"type-devices": "デバイス", "type-devices": "デバイス",
"list-of-devices": "{ count, plural, =1 {One device} other {List of # devices} }", "list-of-devices": "{ count, plural, =1 {One device} other {List of # devices} }",
"device-name-starts-with": "'{{prefix}}'", "device-name-starts-with": "'{{prefix}}'",
"type-asset": "資産", "type-asset": "アセット",
"type-assets": "資産", "type-assets": "アセット",
"list-of-assets": "{ count, plural, =1 {One asset} other {List of # assets} }", "list-of-assets": "{ count, plural, =1 {One asset} other {List of # assets} }",
"asset-name-starts-with": "'{{prefix}}'", "asset-name-starts-with": "'{{prefix}}'",
"type-rule": "ルール", "type-rule": "ルール",
@ -731,7 +731,7 @@
"type-dashboards": "ダッシュボード", "type-dashboards": "ダッシュボード",
"list-of-dashboards": "{ count, plural, =1 {One dashboard} other {List of # dashboards} }", "list-of-dashboards": "{ count, plural, =1 {One dashboard} other {List of # dashboards} }",
"dashboard-name-starts-with": "'{{prefix}}'", "dashboard-name-starts-with": "'{{prefix}}'",
"type-alarm": "警報", "type-alarm": "アラーム",
"type-alarms": "アラーム", "type-alarms": "アラーム",
"list-of-alarms": "{ count, plural, =1 {One alarms} other {List of # alarms} }", "list-of-alarms": "{ count, plural, =1 {One alarms} other {List of # alarms} }",
"alarm-name-starts-with": "'{{prefix}}'", "alarm-name-starts-with": "'{{prefix}}'",
@ -787,7 +787,7 @@
"type": "タイプ", "type": "タイプ",
"key": "キー", "key": "キー",
"value": "値", "value": "値",
"id": "イド", "id": "ID",
"extension-id": "内線番号", "extension-id": "内線番号",
"extension-type": "拡張タイプ", "extension-type": "拡張タイプ",
"transformer-json": "JSON *", "transformer-json": "JSON *",
@ -998,13 +998,13 @@
"select": "ターゲットレイアウトを選択" "select": "ターゲットレイアウトを選択"
}, },
"legend": { "legend": {
"position": "伝説の位置", "position": "凡例の位置",
"show-max": "最大値を表示", "show-max": "最大値を表示",
"show-min": "最小値を表示する", "show-min": "最小値を表示",
"show-avg": "平均値を表示", "show-avg": "平均値を表示",
"show-total": "合計値を表示", "show-total": "合計値を表示",
"settings": "凡例の設定", "settings": "凡例の設定",
"min": "分", "min": "最小",
"max": "最大", "max": "最大",
"avg": "平均", "avg": "平均",
"total": "合計" "total": "合計"
@ -1018,7 +1018,7 @@
"password-again": "パスワードをもう一度", "password-again": "パスワードをもう一度",
"sign-in": "サインインしてください", "sign-in": "サインインしてください",
"username": "ユーザー名(電子メール)", "username": "ユーザー名(電子メール)",
"remember-me": "私を覚えてますか", "remember-me": "ログイン情報を記憶",
"forgot-password": "パスワードをお忘れですか?", "forgot-password": "パスワードをお忘れですか?",
"password-reset": "パスワードのリセット", "password-reset": "パスワードのリセット",
"new-password": "新しいパスワード", "new-password": "新しいパスワード",
@ -1030,7 +1030,7 @@
}, },
"position": { "position": {
"top": "上", "top": "上",
"bottom": "ボトム", "bottom": "下",
"left": "左", "left": "左",
"right": "右" "right": "右"
}, },
@ -1213,11 +1213,11 @@
"minutes-interval": "{ minutes, plural, =1 {1 minute} other {# minutes} }", "minutes-interval": "{ minutes, plural, =1 {1 minute} other {# minutes} }",
"hours-interval": "{ hours, plural, =1 {1 hour} other {# hours} }", "hours-interval": "{ hours, plural, =1 {1 hour} other {# hours} }",
"days-interval": "{ days, plural, =1 {1 day} other {# days} }", "days-interval": "{ days, plural, =1 {1 day} other {# days} }",
"days": "日々", "days": "日",
"hours": "時間", "hours": "時",
"minutes": "分", "minutes": "分",
"seconds": "秒", "seconds": "秒",
"advanced": "上級" "advanced": "カスタム"
}, },
"timewindow": { "timewindow": {
"days": "{ days, plural, =1 { day } other {# days } }", "days": "{ days, plural, =1 { day } other {# days } }",
@ -1225,12 +1225,12 @@
"minutes": "{ minutes, plural, =0 { minute } =1 {1 minute } other {# minutes } }", "minutes": "{ minutes, plural, =0 { minute } =1 {1 minute } other {# minutes } }",
"seconds": "{ seconds, plural, =0 { second } =1 {1 second } other {# seconds } }", "seconds": "{ seconds, plural, =0 { second } =1 {1 second } other {# seconds } }",
"realtime": "リアルタイム", "realtime": "リアルタイム",
"history": "歴史", "history": "履歴",
"last-prefix": "最終", "last-prefix": "直近",
"period": "{{ startTime }}{{ endTime }}", "period": "{{ startTime }}{{ endTime }}",
"edit": "タイムウィンドウを編集", "edit": "タイムウィンドウを編集",
"date-range": "期間", "date-range": "期間",
"last": "最終", "last": "直近",
"time-period": "期間" "time-period": "期間"
}, },
"user": { "user": {
@ -1379,7 +1379,7 @@
"widget-config": { "widget-config": {
"data": "データ", "data": "データ",
"settings": "設定", "settings": "設定",
"advanced": "上級", "advanced": "カスタム",
"title": "タイトル", "title": "タイトル",
"general-settings": "一般設定", "general-settings": "一般設定",
"display-title": "タイトルを表示", "display-title": "タイトルを表示",
@ -1392,13 +1392,13 @@
"widget-style": "ウィジェットスタイル", "widget-style": "ウィジェットスタイル",
"title-style": "タイトルスタイル", "title-style": "タイトルスタイル",
"mobile-mode-settings": "モバイルモードの設定", "mobile-mode-settings": "モバイルモードの設定",
"order": "注文", "order": "順番",
"height": "高さ", "height": "高さ",
"units": "値の隣に表示する特別なシンボル", "units": "単位",
"decimals": "浮動小数点の後の桁数", "decimals": "小数点以下の桁数",
"timewindow": "タイムウィンドウ", "timewindow": "タイムウィンドウ",
"use-dashboard-timewindow": "ダッシュボードのタイムウィンドウを使用する", "use-dashboard-timewindow": "ダッシュボードのタイムウィンドウを使用する",
"display-legend": "伝説を表示", "display-legend": "凡例を表示",
"datasources": "データソース", "datasources": "データソース",
"maximum-datasources": "{ count, plural, =1 {1 datasource is allowed.} other {# datasources are allowed} }", "maximum-datasources": "{ count, plural, =1 {1 datasource is allowed.} other {# datasources are allowed} }",
"datasource-type": "タイプ", "datasource-type": "タイプ",
@ -1407,7 +1407,7 @@
"add-datasource": "データソースを追加", "add-datasource": "データソースを追加",
"target-device": "ターゲットデバイス", "target-device": "ターゲットデバイス",
"alarm-source": "アラームソース", "alarm-source": "アラームソース",
"actions": "行動", "actions": "アクション",
"action": "アクション", "action": "アクション",
"add-action": "アクションを追加", "add-action": "アクションを追加",
"search-actions": "検索アクション", "search-actions": "検索アクション",
@ -1425,8 +1425,8 @@
"delete-action-text": "'{{actionName}}'?" "delete-action-text": "'{{actionName}}'?"
}, },
"widget-type": { "widget-type": {
"import": "インポートウィジェットタイプ", "import": "ウィジェットタイプをインポート",
"export": "ウィジェットのタイプをエクスポートする", "export": "ウィジェットのタイプをエクスポート",
"export-failed-error": "{{error}}", "export-failed-error": "{{error}}",
"create-new-widget-type": "新しいウィジェットタイプを作成する", "create-new-widget-type": "新しいウィジェットタイプを作成する",
"widget-type-file": "ウィジェットタイプファイル", "widget-type-file": "ウィジェットタイプファイル",
@ -1456,9 +1456,9 @@
"Dec": "12月", "Dec": "12月",
"January": "1月", "January": "1月",
"February": "2月", "February": "2月",
"March": "行進", "March": "3月",
"April": "4月", "April": "4月",
"June": "六月", "June": "6月",
"July": "7月", "July": "7月",
"August": "8月", "August": "8月",
"September": "9月", "September": "9月",
@ -1476,7 +1476,7 @@
"Year": "年", "Year": "年",
"This Year": "今年", "This Year": "今年",
"Last Year": "昨年", "Last Year": "昨年",
"Date picker": "日付ピッカー", "Date picker": "日付選択",
"Hour": "時", "Hour": "時",
"Day": "日", "Day": "日",
"Week": "週間", "Week": "週間",

969
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

File diff suppressed because it is too large

9
ui-ngx/src/typings/leaflet-extend-tb.d.ts

@ -168,6 +168,9 @@ declare module 'leaflet' {
fillPattern?: Pattern | undefined; fillPattern?: Pattern | undefined;
} }
type aspectRatioAlign = "none" | "xMinYMin" | "xMidYMin" | "xMaxYMin" | "xMinYMid" | "xMidYMid" | "xMaxYMid" | "xMinYMax" | "xMidYMax" | "xMaxYMax";
type aspectRatioMeetOrSlice = "meet" | "slice";
interface PatternOptions { interface PatternOptions {
x?: number | undefined; x?: number | undefined;
y?: number | undefined; y?: number | undefined;
@ -176,8 +179,8 @@ declare module 'leaflet' {
patternUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined; patternUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined;
patternContentUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined; patternContentUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined;
patternTransform?: string | null | undefined; patternTransform?: string | null | undefined;
preserveAspectRatioAlign?: "none" | "xMinYMin" | "xMidYMin" | "xMaxYMin" | "xMinYMid" | "xMidYMid" | "xMaxYMid" | "xMinYMax" | "xMidYMax" | "xMaxYMax" | undefined; preserveAspectRatioAlign?: aspectRatioAlign | undefined;
preserveAspectRatioMeetOrSlice?: "meet" | "slice" | undefined; preserveAspectRatioMeetOrSlice?: aspectRatioMeetOrSlice | undefined;
viewBox?: [number, number, number, number] | undefined; viewBox?: [number, number, number, number] | undefined;
angle?: number | null | undefined; angle?: number | null | undefined;
className?: string | undefined; className?: string | undefined;
@ -222,6 +225,8 @@ declare module 'leaflet' {
imageUrl: string; imageUrl: string;
width: number; width: number;
height: number; height: number;
preserveAspectRatioAlign?: aspectRatioAlign | undefined;
preserveAspectRatioMeetOrSlice?: aspectRatioMeetOrSlice | undefined;
opacity?: number; opacity?: number;
angle?: number; angle?: number;
scale?: number; scale?: number;

Loading…
Cancel
Save