Browse Source

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

pull/13040/head
IrynaMatveieva 1 year 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)",
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'.")
@PostMapping(value = "/noauth/resetPassword")
public JwtPair resetPassword(@Parameter(description = "Reset password request.")
public void resetPassword(@Parameter(description = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
String resetToken = resetPasswordRequest.getResetToken();
@ -263,8 +262,6 @@ public class AuthController extends BaseController {
}
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId()));
return tokenFactory.createTokenPair(securityUser);
} else {
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();
builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts));
} 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;
case ATTRIBUTES_UPDATED:
@ -522,7 +522,7 @@ public class EdgeMsgConstructorUtils {
builder.setPostAttributeScope(getScopeOfDefault(data));
builder.setAttributeTs(ts);
} 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;
case POST_ATTRIBUTES:
@ -533,7 +533,7 @@ public class EdgeMsgConstructorUtils {
builder.setPostAttributeScope(getScopeOfDefault(data));
builder.setAttributeTs(ts);
} 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;
case ATTRIBUTES_DELETED:
@ -546,7 +546,7 @@ public class EdgeMsgConstructorUtils {
attributeDeleteMsg.build();
builder.setAttributeDeleteMsg(attributeDeleteMsg);
} 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;
}

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

@ -203,7 +203,7 @@ public abstract class EdgeGrpcSession implements Closeable {
@Override
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();
}
@ -255,7 +255,7 @@ public abstract class EdgeGrpcSession implements Closeable {
private void doSync(EdgeSyncCursor cursor) {
if (cursor.hasNext()) {
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());
ListenableFuture<Pair<Long, Long>> future = startProcessingEdgeEvents(next);
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());
}
} 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) {
result.add(downlinkMsg);
@ -763,7 +763,7 @@ public abstract class EdgeGrpcSession implements Closeable {
try {
outputStream.onNext(responseMsg);
} 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;
sessionCloseListener.accept(edge, sessionId);
} finally {
@ -909,7 +909,7 @@ public abstract class EdgeGrpcSession implements Closeable {
}
} catch (Exception e) {
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())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
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 TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
}
Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));
Customer publicCustomer = ctx.getCustomerService().findPublicCustomer(edge.getTenantId());
if (publicCustomer != null) {
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));
}
if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) {
fetchers.add(new CustomerEdgeEventFetcher(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());
CustomerId customerId = new CustomerId(EntityIdFactory.getByEdgeEventTypeAndUuid(type, uuid).getId());
switch (actionType) {
case UPDATED:
List<ListenableFuture<Void>> futures = new ArrayList<>();
PageDataIterable<Edge> edges = new PageDataIterable<>(link -> edgeCtx.getEdgeService().findEdgesByTenantIdAndCustomerId(tenantId, customerId, link), 1024);
for (Edge edge : edges) {
futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, customerId, null));
case ADDED:
Customer customerById = edgeCtx.getCustomerService().findCustomerById(tenantId, customerId);
if (customerById != null && customerById.isPublic()) {
return findEdgesAndSaveEdgeEvents(link -> edgeCtx.getEdgeService().findEdgesByTenantId(tenantId, link),
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:
EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB()));
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
public EdgeEventType getEdgeEventType() {
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) {
var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId));
serviceIdOpt.ifPresentOrElse(
serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()),
() -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg)
);
if (edgesEnabled) {
var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId));
serviceIdOpt.ifPresentOrElse(
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) {

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.util.TbCoreComponent;
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.IdMsgPair;
@ -194,36 +195,42 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
protected void handleNotification(UUID id, TbProtoQueueMsg<ToEdgeNotificationMsg> msg, TbCallback callback) {
ToEdgeNotificationMsg toEdgeNotificationMsg = msg.getValue();
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()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeHighPriority());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess();
} else if (toEdgeNotificationMsg.hasEdgeEventUpdate()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeEventUpdate());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess();
} else if (toEdgeNotificationMsg.hasToEdgeSyncRequest()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getToEdgeSyncRequest());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess();
} else if (toEdgeNotificationMsg.hasFromEdgeSyncResponse()) {
EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getFromEdgeSyncResponse());
edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg);
callback.onSuccess();
} else if (toEdgeNotificationMsg.hasComponentLifecycle()) {
ComponentLifecycleMsg componentLifecycle = ProtoUtils.fromProto(toEdgeNotificationMsg.getComponentLifecycle());
TenantId tenantId = componentLifecycle.getTenantId();
EdgeId edgeId = new EdgeId(componentLifecycle.getEntityId().getId());
if (ComponentLifecycleEvent.DELETED.equals(componentLifecycle.getEvent())) {
edgeCtx.getEdgeRpcService().deleteEdge(tenantId, edgeId);
edgeRpcService.deleteEdge(tenantId, edgeId);
} else if (ComponentLifecycleEvent.UPDATED.equals(componentLifecycle.getEvent())) {
Edge edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edgeId);
edgeCtx.getEdgeRpcService().updateEdge(tenantId, edge);
edgeRpcService.updateEdge(tenantId, edge);
}
callback.onSuccess();
}
} catch (Exception e) {
log.error("Error processing edge notification message", e);
log.error("Error processing edge notification message {}", toEdgeNotificationMsg, e);
callback.onFailure(e);
}

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

@ -1,10 +1,10 @@
______ __ _ ____ __
/_ __/ / /_ (_) ____ ____ _ _____ / __ ) ____ ____ _ _____ ____/ /
/ / / __ \ / / / __ \ / __ `/ / ___/ / __ | / __ \ / __ `/ / ___/ / __ /
/ / / / / / / / / / / / / /_/ / (__ ) / /_/ / / /_/ // /_/ / / / / /_/ /
/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_____/ \____/ \__,_/ /_/ \__,_/
/____/
_____ _ _ ____ _
|_ _| |__ (_)_ __ __ _ ___| __ ) ___ __ _ _ __ __| |
| | | '_ \| | '_ \ / _` / __| _ \ / _ \ / _` | '__/ _` |
| | | | | | | | | | (_| \__ \ |_) | (_) | (_| | | | (_| |
|_| |_| |_|_|_| |_|\__, |___/____/ \___/ \__,_|_| \__,_|
|___/
===================================================
===========================================================
:: ${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}"
gateway:
# 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:
@ -1590,11 +1590,24 @@ queue:
# tb_rule_engine.sq:
# - key: max.poll.records
# 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:
# 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
# Example of specific consumer properties value per topic for edge event
value: "${TB_QUEUE_KAFKA_EDGE_EVENT_MAX_POLL_RECORDS:50}"
# 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_NOTIFICATIONS_MAX_POLL_RECORDS:10}"
tb_housekeeper:
# Consumer properties for Housekeeper tasks topic
- key: max.poll.records
@ -1765,9 +1778,13 @@ queue:
max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}"
# Maximum timeout for requests to EDQS
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:
# Enable/disable statistics for EDQS
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:
# Default topic name
topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}"
@ -1837,11 +1854,13 @@ queue:
# Interval in milliseconds to poll messages
poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}"
edge:
# Default topic name
# Topic name to notify edge service on entity updates, assignment, etc.
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}"
# 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}"
# Amount of partitions used by Edge services
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.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
@ -94,6 +95,9 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
private DeviceProfileId mqttDeviceProfileId;
private DeviceProfileId coapDeviceProfileId;
@Value("${device.connectivity.gateway.image_version:3.7-stable}")
private String gatewayImageVersion;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -298,7 +302,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"services:\n" +
" # ThingsBoard IoT Gateway Service Configuration\n" +
" tb-gateway:\n" +
" image: thingsboard/tb-gateway:latest\n" +
" image: thingsboard/tb-gateway:" + gatewayImageVersion + "\n" +
" container_name: tb-gateway\n" +
" restart: always\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()));
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 " +
"-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);
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 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);
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");
Mockito.doNothing().when(mailService).sendPasswordWasResetEmail(anyString(), anyString());
JsonNode tokenInfo = readResponse(
doPost("/api/noauth/resetPassword", resetPasswordRequest)
.andExpect(status().isOk()), JsonNode.class);
doPost("/api/noauth/resetPassword", resetPasswordRequest)
.andExpect(status().isOk());
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();

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);
// 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");
updateRootRuleChainMetadata();

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

@ -44,9 +44,8 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest {
@Test
public void testTenantProfiles() throws Exception {
loginSysAdmin();
// save current values into tmp to revert after test
TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile);
// updated edge tenant profile
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("Tenant Profile Edge Test", tenantProfileMsg.getName());
doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class);
loginTenantAdmin();
}
@Test
public void testIsolatedTenantProfile() throws Exception {
loginSysAdmin();
TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile);
// set tenant profile isolated and add 2 queues - main and isolated
edgeTenantProfile.setIsolatedTbRuleEngine(true);
@ -110,6 +110,10 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest {
Assert.assertNotNull(queue);
Assert.assertEquals(tenantId, queue.getTenantId());
}
loginSysAdmin();
doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class);
loginTenantAdmin();
}
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 findPublicCustomer(TenantId tenantId);
PageData<Customer> findCustomersByTenantId(TenantId tenantId, PageLink pageLink);
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)) {
return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
log.error("Error during deserialization message, [{}]", e.getMessage());
log.error("Error during deserialization", e);
return null;
}
}
@ -50,7 +50,7 @@ public class JavaSerDesUtil {
ois.writeObject(msq);
return boas.toByteArray();
} catch (IOException e) {
log.error("Error during serialization message, [{}]", e.getMessage());
log.error("Error during serialization", 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) {
return node != null ? node.toString() : "";
return node != null && !node.isNull() ? node.toString() : "";
}
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 additionalInfo;
@Override
public String getName() {
return super.getEmail();
}
public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId,
Long version, String firstName, String lastName, String email,
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
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.SneakyThrows;
import org.thingsboard.common.util.TbBytePool;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.edqs.util.TbBytePool;
import org.xerial.snappy.Snappy;
public class CompressedStringDataPoint extends AbstractDataPoint {
public static final int MIN_STR_SIZE_TO_COMPRESS = 512;
@Getter
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 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 {

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 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 {

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.query.EntityCountQuery;
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 java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Predicate;
@ -42,7 +41,7 @@ import java.util.function.Predicate;
public class DefaultEdqsRepository implements EdqsRepository {
private final static ConcurrentMap<TenantId, TenantRepo> repos = new ConcurrentHashMap<>();
private final Optional<EdqsStatsService> statsService;
private final EdqsStatsService statsService;
public TenantRepo get(TenantId tenantId) {
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.Entity;
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.query.QueryResult;
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.relation.EntityRelation;
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.AssetData;
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.processor.EntityQueryProcessor;
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.TbStringPool;
import java.util.ArrayList;
import java.util.Collections;
@ -65,7 +63,6 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
@ -96,9 +93,9 @@ public class TenantRepo {
private final Lock entityUpdateLock = new ReentrantLock();
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.edqsStatsService = edqsStatsService;
}
@ -144,9 +141,9 @@ public class TenantRepo {
RelationsRepo repo = relations.computeIfAbsent(entity.getTypeGroup(), tg -> new RelationsRepo());
EntityData<?> from = getOrCreate(entity.getFrom());
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) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.UPDATED));
edqsStatsService.reportAdded(ObjectType.RELATION);
}
} else if (RelationTypeGroup.DASHBOARD.equals(entity.getTypeGroup())) {
if (EntityRelation.CONTAINS_TYPE.equals(entity.getType()) && entity.getFrom().getEntityType() == EntityType.CUSTOMER) {
@ -166,7 +163,7 @@ public class TenantRepo {
if (relationsRepo != null) {
boolean removed = relationsRepo.remove(entityRelation.getFrom().getId(), entityRelation.getTo().getId(), entityRelation.getType());
if (removed) {
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.DELETED));
edqsStatsService.reportRemoved(ObjectType.RELATION);
}
}
} else if (RelationTypeGroup.DASHBOARD.equals(entityRelation.getTypeGroup())) {
@ -188,7 +185,6 @@ public class TenantRepo {
EntityType entityType = entity.getType();
EntityData entityData = getOrCreate(entityType, entityId);
processFields(fields);
EntityFields oldFields = entityData.getFields();
entityData.setFields(fields);
if (oldFields == null) {
@ -225,7 +221,7 @@ public class TenantRepo {
if (removed.getFields() != null) {
getEntitySet(entityType).remove(removed);
}
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED));
edqsStatsService.reportRemoved(entity.type());
UUID customerId = removed.getCustomerId();
if (customerId != null) {
@ -246,7 +242,7 @@ public class TenantRepo {
Integer keyId = KeyDictionary.get(attributeKv.getKey());
boolean added = entityData.putAttr(keyId, attributeKv.getScope(), attributeKv.getDataPoint());
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) {
boolean removed = entityData.removeAttr(KeyDictionary.get(attributeKv.getKey()), attributeKv.getScope());
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());
boolean added = entityData.putTs(keyId, latestTsKv.getDataPoint());
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) {
boolean removed = entityData.removeTs(KeyDictionary.get(latestTsKv.getKey()));
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) {
return entityMapByType.computeIfAbsent(entityType, et -> new ConcurrentHashMap<>());
}
@ -301,7 +291,7 @@ public class TenantRepo {
return getEntityMap(entityType).computeIfAbsent(entityId, id -> {
log.debug("[{}] Adding {} {}", tenantId, entityType, id);
EntityData<?> entityData = constructEntityData(entityType, entityId);
edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED));
edqsStatsService.reportAdded(ObjectType.fromEntityType(entityType));
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.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
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.module.SimpleModule;
import com.google.protobuf.ByteString;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.TbStringPool;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
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.xerial.snappy.Snappy;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -57,6 +64,9 @@ import java.util.UUID;
@Slf4j
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 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();
proto.setTs(ts);
switch (kvEntry.getDataType()) {
@ -134,7 +144,7 @@ public class EdqsConverter {
case DOUBLE -> proto.setDoubleV(kvEntry.getDoubleValue().get());
case STRING -> {
String strValue = kvEntry.getStrValue().get();
if (strValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) {
if (strValue.length() < stringCompressionLengthThreshold) {
proto.setStringV(strValue);
} else {
proto.setCompressedStringV(ByteString.copyFrom(compress(strValue)));
@ -142,7 +152,7 @@ public class EdqsConverter {
}
case JSON -> {
String jsonValue = kvEntry.getJsonValue().get();
if (jsonValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) {
if (jsonValue.length() < stringCompressionLengthThreshold) {
proto.setJsonV(jsonValue);
} else {
proto.setCompressedJsonV(ByteString.copyFrom(compress(jsonValue)));
@ -152,7 +162,7 @@ public class EdqsConverter {
return proto.build();
}
public static DataPoint fromDataPointProto(DataPointProto proto) {
public DataPoint fromDataPointProto(DataPointProto proto) {
long ts = proto.getTs();
if (proto.hasBoolV()) {
return new BoolDataPoint(ts, proto.getBoolV());
@ -188,14 +198,6 @@ public class EdqsConverter {
return edqsEntity;
}
public EdqsObject check(ObjectType type, Object object) {
if (object instanceof EdqsObject edqsObject) {
return edqsObject;
} else {
return toEntity(type.toEntityType(), object);
}
}
@SuppressWarnings("unchecked")
@SneakyThrows
public <T extends EdqsObject> byte[] serialize(ObjectType type, T value) {
@ -220,12 +222,18 @@ public class EdqsConverter {
@RequiredArgsConstructor
private static class JsonConverter<T> implements Converter<T> {
private static final SimpleModule module = new SimpleModule();
private static final ObjectMapper mapper = JsonMapper.builder()
.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
.visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
.visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE)
.build();
static {
module.addDeserializer(String.class, new InterningStringDeserializer());
mapper.registerModule(module);
}
private final Class<T> type;
@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}")
private String tbRuleEngineNotificationsTopic;
@Value("${queue.transport.notifications-topics:tb_transport.notifications}")
@Value("${queue.transport.notifications-topic:tb_transport.notifications}")
private String tbTransportNotificationsTopic;
@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
public class TbKafkaSettings {
private static final List<String> DYNAMIC_TOPICS = List.of("tb_edge_event.notifications");
@Value("${queue.kafka.bootstrap.servers}")
private String servers;
@ -163,18 +161,20 @@ public class TbKafkaSettings {
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.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) {
DYNAMIC_TOPICS.stream()
.filter(topic::startsWith)
.findFirst()
.ifPresent(prefix -> consumerPropertiesPerTopic.getOrDefault(prefix, Collections.emptyList())
.forEach(kv -> props.put(kv.getKey(), kv.getValue())));
List<TbProperty> properties = consumerPropertiesPerTopic.get(topic);
if (properties == null) {
for (Map.Entry<String, List<TbProperty>> entry : consumerPropertiesPerTopic.entrySet()) {
if (topic.startsWith(entry.getKey())) {
properties = entry.getValue();
break;
}
}
}
if (properties != null) {
properties.forEach(kv -> props.put(kv.getKey(), kv.getValue()));
}
}
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;
}
@Override
public TbQueueProducer<TbProtoQueueMsg<ToEdgeMsg>> getTbEdgeMsgProducer() {
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;
}
public void record(long timeMs) {
public void record(long timeMs) {
record(timeMs, TimeUnit.MILLISECONDS);
}
public void record(long timing, TimeUnit timeUnit) {
count++;
totalTime += timeMs;
timer.record(timeMs, TimeUnit.MILLISECONDS);
totalTime += timeUnit.toMillis(timing);
timer.record(timing, timeUnit);
}
public double getAvg() {
@ -47,7 +51,7 @@ public class StatsTimer {
return (double) totalTime / count;
}
public void reset() {
public void reset() {
count = 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
* limitations under the License.
*/
package org.thingsboard.server.edqs.util;
package org.thingsboard.common.util;
import com.google.common.hash.Hashing;
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
* limitations under the License.
*/
package org.thingsboard.server.edqs.util;
package org.thingsboard.common.util;
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.errors.LargeObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
@ -158,7 +157,7 @@ public class GitRepository {
return repository;
} catch (RepositoryNotFoundException e) {
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")) {
log.warn("Couldn't fetch {} due to {}, reinitializing", directory, e.getMessage());
} else {

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

@ -213,12 +213,11 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
@Override
public Customer findOrCreatePublicCustomer(TenantId tenantId) {
log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
Optional<Customer> publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
if (publicCustomerOpt.isPresent()) {
return publicCustomerOpt.get();
var publicCustomer = findPublicCustomer(tenantId);
if (publicCustomer != null) {
return publicCustomer;
}
var publicCustomer = new Customer();
publicCustomer = new Customer();
publicCustomer.setTenantId(tenantId);
publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
try {
@ -230,7 +229,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
return saveCustomer(publicCustomer, false);
} catch (DataValidationException e) {
if (CUSTOMER_UNIQUE_TITLE_EX_MSG.equals(e.getMessage())) {
publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
Optional<Customer> publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
if (publicCustomerOpt.isPresent()) {
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
public PageData<Customer> findCustomersByTenantId(TenantId tenantId, PageLink 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;
@Value("${device.connectivity.coaps.pem_cert_file:}")
private String coapsPemCertFile;
@Value("${device.connectivity.gateway.image_version:latest}")
@Value("${device.connectivity.gateway.image_version:3.7-stable}")
private String gatewayImageVersion;
@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.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasCustomerId;
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.RelationsQueryFilter;
import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.stats.EdqsStatsService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import java.util.ArrayList;
@ -89,6 +91,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
@Lazy
private EdqsApiService edqsApiService;
@Autowired
private EdqsStatsService edqsStatsService;
@Override
public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery 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);
validateEntityCountQuery(query);
TbStopWatch stopWatch = TbStopWatch.create();
Long result;
if (edqsApiService.isEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) {
EdqsRequest request = EdqsRequest.builder()
.entityCountQuery(query)
.build();
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
@ -113,27 +123,31 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id);
validateEntityDataQuery(query);
TbStopWatch stopWatch = TbStopWatch.create();
PageData<EntityData> result;
if (edqsApiService.isEnabled() && validForEdqs(query)) {
EdqsRequest request = EdqsRequest.builder()
.entityDataQuery(query)
.build();
EdqsResponse response = processEdqsRequest(tenantId, customerId, request);
return response.getEntityDataQueryResult();
}
if (!isValidForOptimization(query)) {
return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query);
}
// 1 step - find entity data by filter and sort columns
PageData<EntityData> entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query);
if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) {
return entityDataByQuery;
result = response.getEntityDataQueryResult();
} else {
if (!isValidForOptimization(query)) {
result = entityQueryDao.findEntityDataByQuery(tenantId, customerId, query);
} else {
// 1 step - find entity data by filter and sort columns
PageData<EntityData> entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query);
if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) {
result = entityDataByQuery;
} else {
// 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());
}
}
}
// 2 step - find entity data by entity ids from the 1st step
List<EntityData> result = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData());
return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext());
edqsStatsService.reportDataQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos());
return result;
}
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}"
# Maximum timeout for requests to EDQS
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:
# Enable/disable statistics for EDQS
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.springframework.beans.factory.annotation.Autowired;
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.Configuration;
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.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.stats.DummyEdqsStatsService;
import org.thingsboard.server.edqs.util.EdqsConverter;
import java.util.Collections;
@ -78,6 +80,8 @@ public abstract class AbstractEDQTest {
protected DefaultEdqsRepository repository;
@Autowired
protected EdqsConverter edqsConverter;
@MockBean
private DummyEdqsStatsService edqsStatsService;
protected final TenantId tenantId = TenantId.fromUUID(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;
maxArgumentsPerCF: number;
ruleChainDebugPerTenantLimitsConfiguration?: string;
calculatedFieldDebugPerTenantLimitsConfiguration?: string;
}
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> {
return this.http.post<LoginResponse>('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).pipe(
tap((loginResponse: LoginResponse) => {
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true);
}
));
public resetPassword(resetToken: string, password: string): Observable<void> {
return this.http.post<void>('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions());
}
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) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
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)) {
return;
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
@ -389,7 +389,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
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 { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { MINUTE } from '@shared/models/time/time.models';
import { Store } from '@ngrx/store';
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 { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -47,7 +46,8 @@ import {
getCalculatedFieldArgumentsHighlights,
} from '@shared/models/calculated-field.models';
import {
CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData,
CalculatedFieldDebugDialogComponent,
CalculatedFieldDebugDialogData,
CalculatedFieldDialogComponent,
CalculatedFieldDialogData,
CalculatedFieldScriptTestDialogComponent,
@ -60,9 +60,6 @@ import { DatePipe } from '@angular/common';
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;
additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'),
@ -189,9 +186,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.entityDebugSettingsService.openDebugStrategyPanel({
debugSettings,
debugConfig: {
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
maxDebugModeDuration: this.maxDebugModeDuration,
entityLabel: this.translate.instant('debug-settings.calculated-field'),
entityType: EntityType.CALCULATED_FIELD,
additionalActionConfig,
},
onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings)
@ -215,7 +210,6 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
value,
buttonTitle,
entityId: this.entityId,
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
tenantId: this.tenantId,
entityName: this.entityName,
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"
[active]="true"
[entityId]="data.value.id"
[functionTestButtonLabel]="'common.test-function' | translate"
(debugEventSelected)="onDebugEventSelected($event)"
/>
</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 {
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.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments)
}
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
formControlName="debugSettings"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityLabel]="'debug-settings.calculated-field' | translate"
[debugLimitsConfiguration]="data.debugLimitsConfiguration"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
@ -80,6 +79,12 @@
</div>
<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>
<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) {
<mat-error>
@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>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'common.test-function' | translate }}"
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="configFormGroup.get('arguments').invalid"
@ -123,7 +128,7 @@
type="button"
(click)="onTestScript()"
[disabled]="configFormGroup.get('arguments').invalid">
{{ 'common.test-function' | translate }}
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</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;
buttonTitle: string;
entityId: EntityId;
debugLimitsConfiguration: string;
tenantId: string;
entityName?: string;
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">
warning
</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
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-ctx' | translate"
[matTooltip]="'calculated-fields.hint.argument-name-forbidden' | translate"
class="tb-error">
warning
</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);
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({
entityType: [ArgumentEntityType.Current],
id: ['']
@ -254,10 +254,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
}
}
private notEqualCtxValidator(): ValidatorFn {
private forbiddenArgumentNameValidator(): ValidatorFn {
return (control: FormControl) => {
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>
</section>
<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>
</mat-drawer-content>
<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) {
this.onAddWidgetClosed();
this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe(
(widgetTypeInfo) => {
this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe({
next: (widgetTypeInfo) => {
const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
if (!config.title) {
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() {

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

@ -16,6 +16,7 @@
-->
<button mat-stroked-button
type="button"
class="tb-rounded-btn w-36 flex-1"
color="primary"
#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 { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { EntityType } from '@shared/models/entity-type.models';
@Component({
selector: 'tb-entity-debug-settings-button',
@ -58,9 +59,8 @@ import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entit
})
export class EntityDebugSettingsButtonComponent implements ControlValueAccessor {
@Input() debugLimitsConfiguration: string;
@Input() entityLabel: string;
@Input() additionalActionConfig: AdditionalDebugActionConfig;
@Input({required: true}) entityType: EntityType;
debugSettingsFormGroup = this.fb.group({
failuresEnabled: [false],
@ -123,8 +123,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor
debugSettings: this.debugSettingsFormGroup.value,
debugConfig: {
maxDebugModeDuration: this.maxDebugModeDuration,
debugLimitsConfiguration: this.debugLimitsConfiguration,
entityLabel: this.entityLabel,
entityType: this.entityType,
additionalActionConfig: this.additionalActionConfig,
},
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="tb-form-panel-title" translate>debug-settings.label</div>
@if (debugLimitsConfiguration) {
@if (maxMessagesCount) {
<div class="hint-container">
<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>
}

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 { CommonModule } from '@angular/common';
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 { of, shareReplay, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators';
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({
selector: 'tb-entity-debug-settings-panel',
@ -51,10 +53,9 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
@Input({ transform: booleanAttribute }) failuresEnabled = false;
@Input({ transform: booleanAttribute }) allEnabled = false;
@Input() entityLabel: string;
@Input() entityType: EntityType;
@Input() allEnabledUntil = 0;
@Input() maxDebugModeDuration: number;
@Input() debugLimitsConfiguration: string;
@Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
@Input() additionalActionConfig: AdditionalDebugActionConfig;
onFailuresControl = this.fb.control(false);
@ -63,6 +64,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
maxMessagesCount: string;
maxTimeFrameDuration: number;
initialAllEnabled: boolean;
entityLabel: string;
isDebugAllActive$ = this.debugAllControl.valueChanges.pipe(
startWith(this.debugAllControl.value),
@ -101,11 +103,13 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
}
ngOnInit(): void {
this.maxMessagesCount = this.debugLimitsConfiguration?.split(':')[0];
this.maxTimeFrameDuration = parseInt(this.debugLimitsConfiguration?.split(':')[1]) * SECOND;
const debugLimitsConfiguration = this.getByEntityTypeDebugLimit(this.entityType);
this.maxMessagesCount = debugLimitsConfiguration?.split(':')[0];
this.maxTimeFrameDuration = parseInt(debugLimitsConfiguration?.split(':')[1]) * SECOND;
this.onFailuresControl.patchValue(this.failuresEnabled);
this.debugAllControl.patchValue(this.allEnabled);
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 {
@ -135,4 +139,13 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
this.allEnabledUntil = 0;
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 { EntityType } from '@shared/models/entity-type.models';
export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => void> {
action: Action;
@ -24,10 +25,9 @@ export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => vo
export interface EntityDebugSettingPanelConfig {
debugSettings: EntityDebugSettings;
debugConfig: {
maxDebugModeDuration: number;
debugLimitsConfiguration: string;
entityLabel?: string;
maxDebugModeDuration?: number;
additionalActionConfig?: AdditionalDebugActionConfig;
entityType: EntityType;
}
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;
case DebugEventType.DEBUG_CALCULATED_FIELD:
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',
isEnabled: () => true,
onAction: (_, entity) => this.debugEventSelected.next(entity.body)
@ -571,7 +571,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
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>
<mat-form-field class="mat-block no-margin-top flex-1" subscriptSizing="dynamic">
<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>
rule-node-config.custom-expression-field-input-required
</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;
private propagateChange = (_val: any) => {};
private propagateChange: (value: any) => void;
private propagateChangePending = false;
constructor(private fb: UntypedFormBuilder,
private utils: UtilsService,
@ -161,7 +162,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.validate(null)) {
if (this.propagateChangePending) {
setTimeout(() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}, 0);
@ -227,7 +228,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
if (this.singleDatasource && !this.datasourcesFormArray.length) {
this.addDatasource(false);
}
if (changed) {
if (changed || this.validate(null)) {
setTimeout(() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}, 0);
@ -329,7 +330,12 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
if (this.datasourcesOptional) {
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[]>) {

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) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop',
@ -621,7 +621,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
@ -1189,7 +1189,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
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) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
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();
}
this.datePickerSelect.close();
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
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) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
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;
width: number;
height: number;
opacity?: number;
angle?: number;
scale?: number;
preserveAspectRatio: boolean;
opacity: number;
angle: number;
scale: number;
};
fillStripe?: {
weight: number;
@ -122,23 +123,35 @@ abstract class ShapePatternProcessor<S = any> {
fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor});
pattern.addElement(fillRect);
} else if (patternInfo.type === ShapeFillType.image) {
pattern = new L.TB.Pattern({
const patternOptions: L.TB.PatternOptions = {
width: 1,
height: 1,
patternUnits: 'objectBoundingBox',
patternContentUnits: 'objectBoundingBox',
preserveAspectRatioAlign: 'xMidYMid',
preserveAspectRatioMeetOrSlice: 'slice',
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,
width: patternInfo.fillImage.width,
height: patternInfo.fillImage.height,
opacity: patternInfo.fillImage.opacity,
angle: patternInfo.fillImage.angle,
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);
} else if (patternInfo.type === ShapeFillType.stripe) {
const stripeInfo = patternInfo.fillStripe;
@ -221,6 +234,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor<ShapeFillImageSet
if (!currentImage?.url) {
currentImage = {
url: this.settings.image,
preserveAspectRatio: this.settings.preserveAspectRatio,
opacity: this.settings.opacity,
angle: this.settings.angle,
scale: this.settings.scale
@ -231,6 +245,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor<ShapeFillImageSet
private loadPatternInfoFromImage(image: ShapeFillImageInfo): Observable<ShapePatternInfo> {
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 imagePipe = this.dataLayer.getCtx().$injector.get(ImagePipe);
return loadImageWithAspect(imagePipe, imageUrl).pipe(
@ -241,6 +256,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor<ShapeFillImageSet
image: res.url,
width: res.width,
height: res.height,
preserveAspectRatio,
opacity,
angle: image?.angle,
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('width', `${options.width}`);
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[] = [];
if (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) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
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(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
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) {
@ -131,7 +151,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
}
applyColorSettings() {
const colorSettings = this.colorSettingsFormGroup.value;
const colorSettings: ColorSettings = this.colorSettingsFormGroup.getRawValue();
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() {
const colorSettings: DataLayerColorSettings = this.colorSettingsFormGroup.value;
const colorSettings: DataLayerColorSettings = this.colorSettingsFormGroup.getRawValue();
this.colorSettingsApplied.emit(colorSettings);
}
@ -122,8 +122,15 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen
const type: DataLayerColorType = this.colorSettingsFormGroup.get('type').value;
if (type === DataLayerColorType.range) {
this.colorSettingsFormGroup.get('rangeKey').enable({emitEvent: false});
this.colorSettingsFormGroup.get('range').enable({emitEvent: false});
} else {
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']"
[globalVariables]="functionScopeVariables"
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-multiple-gallery-image-input label="{{ 'widgets.maps.data-layer.marker.marker-images' | translate }}"
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-form-panel no-padding no-border">
<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 translate>widgets.maps.data-layer.shape.opacity</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
@ -58,7 +63,8 @@
[functionArgs]="['data', 'images', 'dsData']"
[globalVariables]="functionScopeVariables"
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-multiple-gallery-image-input label="{{ 'widgets.maps.data-layer.shape.fill-images' | translate }}"
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, []],
image: [this.shapeFillImageSettings?.image, [Validators.required]],
preserveAspectRatio: [this.shapeFillImageSettings?.preserveAspectRatio, []],
opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]],
angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]],
scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]],
@ -83,6 +84,7 @@ export class ShapeFillImageSettingsPanelComponent implements OnInit {
const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value;
if (type === ShapeFillImageType.image) {
this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('angle').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});
} else {
this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false});
this.shapeFillImageSettingsFormGroup.get('angle').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() {
const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value;
const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings);
const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings, 136, 118);
this.stripePreviewStyle = {
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 spaceWeight = isDefinedAndNotNull(settings?.spaceWeight) ? settings.spaceWeight : 9;
const angle = isDefinedAndNotNull(settings?.angle) ? settings.angle : 45;
const height = weight + spaceWeight;
const color = settings?.color?.color || '#8f8f8f';
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">
<rect x="0" y="0" width="48" height="48" fill="url(#stripePattern)" fill-opacity="1"></rect>
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="${previewWidth}" height="${previewHeight}" fill="url(#stripePattern)" fill-opacity="1"></rect>
<defs>
<pattern id="stripePattern" x="0" y="0" width="8" height="${height}" patternUnits="userSpaceOnUse"
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();
}
if (this.sources.length) {
const target = $event.target || $event.srcElement || $event.currentTarget;
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
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) {
return widgetInfo;
} 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"
[defaultIconColor]="widgetSettings.get('color').value"
[actionSources]="modelValue.actionSources"
[additionalWidgetActionTypes]="modelValue.typeParameters.additionalWidgetActionTypes"
[additionalWidgetActionTypes]="modelValue.typeParameters?.additionalWidgetActionTypes"
formControlName="actions">
</tb-manage-widget-actions>
</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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { UtilsService } from '@core/services/utils.service';
import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
import {
@ -205,7 +204,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
@Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>,
@Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>,
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
private widgetService: WidgetService,
private resources: ResourcesService,
private timeService: TimeService,
private deviceService: DeviceService,
@ -346,17 +344,17 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.subscriptionContext.widgetUtils = this.widgetContext.utils;
this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService);
this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe(
(widgetInfo) => {
this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe({
next: (widgetInfo) => {
this.widgetInfo = widgetInfo;
this.loadFromWidgetInfo();
},
(errorData) => {
error: (errorData) => {
this.widgetInfo = errorData.widgetInfo;
this.errorMessages = errorData.errorMessages;
this.loadFromWidgetInfo();
}
);
});
const noDataDisplayMessage = this.widget.config.noDataDisplayMessage;
if (isNotEmptyStr(noDataDisplayMessage)) {
@ -521,15 +519,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.widgetTypeInstance.onDestroy = () => {};
}
this.initialize().subscribe(
() => {
this.initialize().subscribe({
next: () => {
this.onInit();
},
(err) => {
error: () => {
this.widgetContext.inited = true;
// console.log(err);
}
);
});
}
private detectChanges(detectContainerChanges = false) {
@ -681,8 +679,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
private reInitImpl() {
this.onDestroy();
if (!this.typeParameters.useCustomDatasources) {
this.createDefaultSubscription().subscribe(
() => {
this.createDefaultSubscription().subscribe({
next: () => {
if (this.destroyed) {
this.onDestroy();
} else {
@ -692,7 +690,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.onInit();
}
},
() => {
error: () => {
if (this.destroyed) {
this.onDestroy();
} else {
@ -701,7 +699,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.onInit();
}
}
);
});
} else {
this.widgetContext.reset();
this.subscriptionInited = true;
@ -751,8 +749,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
}
));
if (!this.typeParameters.useCustomDatasources) {
this.createDefaultSubscription().subscribe(
() => {
this.createDefaultSubscription().subscribe({
next: () => {
this.subscriptionInited = true;
try {
this.configureDynamicWidgetComponent();
@ -762,11 +760,11 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
initSubject.error(err);
}
},
(err) => {
error: (err) => {
this.subscriptionInited = true;
initSubject.error(err);
}
);
});
} else {
this.loadingData = false;
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);
this.widgetErrorData = this.utils.processWidgetException(e);
this.detectChanges();
@ -866,8 +864,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>();
options.dashboardTimewindow = this.widgetContext.dashboardTimewindow;
const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options);
subscription.init$.subscribe(
() => {
subscription.init$.subscribe({
next: () => {
this.widgetContext.subscriptions[subscription.id] = subscription;
if (subscribe) {
subscription.subscribe();
@ -875,10 +873,10 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
(err) => {
error: (err) => {
createSubscriptionSubject.error(err);
}
);
});
return createSubscriptionSubject.asObservable();
}
@ -900,15 +898,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
} else {
options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo);
}
this.createSubscription(options, subscribe).subscribe(
(subscription) => {
this.createSubscription(options, subscribe).subscribe({
next: (subscription) => {
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
(err) => {
error: (err) => {
createSubscriptionSubject.error(err);
}
);
});
return createSubscriptionSubject.asObservable();
}
@ -937,7 +935,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.dataUpdatePending = true;
}
}
} catch (e){}
} catch (e){/**/}
},
onLatestDataUpdated: () => {
try {
@ -948,15 +946,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.latestDataUpdatePending = true;
}
}
} catch (e){}
} catch (e){/**/}
},
onDataUpdateError: (subscription, e) => {
onDataUpdateError: (_subscription, e) => {
this.handleWidgetException(e);
},
onLatestDataUpdateError: (subscription, e) => {
onLatestDataUpdateError: (_subscription, e) => {
this.handleWidgetException(e);
},
onSubscriptionMessage: (subscription, message) => {
onSubscriptionMessage: (_subscription, message) => {
if (this.displayWidgetInstance()) {
if (this.widgetInstanceInited) {
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();
},
forceReInit: () => {
@ -977,12 +975,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.detectChanges();
}
},
legendDataUpdated: (subscription, detectChanges) => {
legendDataUpdated: (_subscription, detectChanges) => {
if (detectChanges) {
this.detectChanges();
}
},
timeWindowUpdated: (subscription, timeWindowConfig) => {
timeWindowUpdated: (_subscription, timeWindowConfig) => {
this.ngZone.run(() => {
this.widget.config.timewindow = timeWindowConfig;
this.detectChanges(true);
@ -1017,8 +1015,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.defaultComponentsOptions(options);
this.createSubscription(options).subscribe(
(subscription) => {
this.createSubscription(options).subscribe({
next: (subscription) => {
// backward compatibility
this.widgetContext.datasources = subscription.datasources;
@ -1032,12 +1030,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
createSubscriptionSubject.complete();
});
},
(err) => {
error: (err) => {
this.ngZone.run(() => {
createSubscriptionSubject.error(err);
});
}
);
});
} else if (this.widget.type === widgetType.rpc) {
this.loadingData = false;
options = {
@ -1074,7 +1072,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.detectChanges();
}
},
onRpcErrorCleared: (subscription) => {
onRpcErrorCleared: (_subscription) => {
if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.rpcErrorText = null;
this.dynamicWidgetComponent.rpcRejection = null;
@ -1085,20 +1083,20 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
}
}
};
this.createSubscription(options).subscribe(
(subscription) => {
this.createSubscription(options).subscribe({
next: (subscription) => {
this.widgetContext.defaultSubscription = subscription;
this.ngZone.run(() => {
createSubscriptionSubject.next();
createSubscriptionSubject.complete();
});
},
(err) => {
error: (err) => {
this.ngZone.run(() => {
createSubscriptionSubject.error(err);
});
}
);
});
this.detectChanges();
} else if (this.widget.type === widgetType.static) {
this.loadingData = false;
@ -1159,7 +1157,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
}
const state = objToBase64URI([ stateObject ]);
const isSinglePage = this.route.snapshot.data.singlePageMode;
let url;
let url: string;
if (isSinglePage) {
url = `/dashboard/${targetDashboardId}?state=${state}`;
} else {
@ -1168,7 +1166,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
if (descriptor.openNewBrowserTab) {
window.open(url, '_blank');
} else {
this.router.navigateByUrl(url);
this.router.navigateByUrl(url).then(() => {});
}
break;
case WidgetActionType.openURL:
@ -1467,7 +1465,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
popoverWidth = '25vw',
popoverHeight = '25vh',
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)) {
this.popoverService.hidePopover(trigger);
} else {
@ -1560,7 +1558,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
}
private elementClick($event: Event) {
const elementClicked = ($event.target || $event.srcElement) as Element;
const elementClicked = ($event.target) as Element;
const descriptors = this.getActionDescriptors('elementClick');
if (descriptors.length) {
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)),
);
this.config.cellActionDescriptors = this.configureCellActions(getCurrentAuthUser(this.store));
this.config.cellActionDescriptors = this.configureCellActions();
this.config.groupActionDescriptors = [{
name: this.translate.instant('action.delete'),
@ -137,6 +137,7 @@ export class JsLibraryTableConfigResolver {
resourceSubType: ''
};
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.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority);
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>> = [];
actions.push(
{
@ -321,7 +322,7 @@ export class JsLibraryTableConfigResolver {
{
name: this.translate.instant('javascript.delete'),
icon: 'delete',
isEnabled: (resource) => this.isResourceEditable(resource, authUser.authority),
isEnabled: (resource) => this.config.deleteEnabled(resource),
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> = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['lists'],
plugins: ['link', 'table', 'image', 'imagetools', 'lists'],
menubar: 'edit insert view format',
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',
height: 400,
autofocus: false,

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

@ -22,7 +22,7 @@
</button>
</div>
<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-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required>
@ -38,8 +38,7 @@
<tb-entity-debug-settings-button
class="mr-2"
formControlName="debugSettings"
[entityLabel]="'debug-settings.rule-node' | translate"
[debugLimitsConfiguration]="ruleChainDebugPerTenantLimitsConfiguration"
[entityType]="entityType.RULE_NODE"
/>
<button mat-stroked-button
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
} from '@angular/core';
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 { 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 { Subject } from 'rxjs';
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 { ServiceType } from '@shared/models/queue.models';
import { takeUntil } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
@Component({
selector: 'tb-rule-node',
@ -73,21 +70,17 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
@Output()
changeScript = new EventEmitter<void>();
ruleNodeType = RuleNodeType;
entityType = EntityType;
serviceType = ServiceType.TB_RULE_ENGINE;
ruleNodeFormGroup: UntypedFormGroup;
readonly ruleChainDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
constructor(private fb: UntypedFormBuilder,
private router: Router) {
super(store);
super();
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 { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
@Component({
@ -44,6 +44,7 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private router: Router,
private authService: AuthService,
private translate: TranslateService,
public fb: UntypedFormBuilder) {
@ -71,7 +72,9 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD
} else {
this.authService.resetPassword(
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>
</ng-container>
<div class="tb-file-info-container">
<tb-error *ngIf="!fileName && required && requiredAsError" class="tb-file-name" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" class="tb-file-name" translate>{{ noFileText }}</div>
<div *ngIf="fileName" class="tb-file-name" [title]="fileName">{{ fileName }}</div>
<div *ngIf="maxSizeByte && !disabled" class="tb-file-hint" translate [translateParams]="{ size: maxSizeByte | fileSize }">dashboard.maximum-upload-file-size</div>
<tb-error *ngIf="!fileName && required && requiredAsError" class="font-medium" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" class="tb-file-name" tbTruncateWithTooltip translate>{{ noFileText }}</div>
<div *ngIf="fileName" class="tb-file-name" tbTruncateWithTooltip>{{ fileName }}</div>
<div *ngIf="maxSizeByte && !disabled" class="tb-file-hint" tbTruncateWithTooltip translate [translateParams]="{ size: maxSizeByte | fileSize }">dashboard.maximum-upload-file-size</div>
</div>
</div>

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

@ -131,15 +131,11 @@ $previewSize: 100px !default;
.tb-file-name {
color: rgba(0, 0, 0, 0.54);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
.tb-file-hint {
color: rgba(0, 0, 0, 0.38);
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;
tbComponentStyle: { [klass: string]: any } = {};
tbOverlayClassName!: string;
tbOverlayStyle: { [klass: string]: any } = {};
tbPopoverInnerStyle: { [klass: string]: any } = {};
tbPopoverInnerContentStyle: { [klass: string]: any } = {};
tbBackdrop = false;
@ -470,6 +469,16 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
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';
strictPosition = false;
origin!: CdkOverlayOrigin;
@ -485,6 +494,7 @@ export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
this.cdr.markForCheck();
}
}, {threshold: [0.5]});
private _tbOverlayStyle: { [klass: string]: any } = {};
constructor(
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) {
$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)) {
this.popoverService.hidePopover(trigger);
} else {
@ -614,7 +614,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
if ($event) {
$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)) {
this.popoverService.hidePopover(trigger);
} 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) {
return;
}
const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element;
const trigger = ($event.target || $event.currentTarget) as Element;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} 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>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<tb-toggle-select *ngIf="enableImportFromContent"
class="flex-1"
formControlName="importType">
<tb-toggle-option value="file">{{ importFileLabel | translate }}</tb-toggle-option>
<tb-toggle-option value="content">{{ importContentLabel | translate }}</tb-toggle-option>
</tb-toggle-select>
<tb-file-input *ngIf="importFormGroup.get('importType').value === 'file'"
[contentConvertFunction]="loadDataFromJsonContent"
[existingFileName]="currentFileName"
(fileNameChanged)="currentFileName = $event"
formControlName="fileContent"
required
label="{{importFileLabel | translate}}"
dropLabel="{{ 'import.drop-json-file-or' | translate }}"
accept=".json,application/json"
allowedExtensions="json">
</tb-file-input>
<tb-json-object-edit *ngIf="importFormGroup.get('importType').value === 'content'"
formControlName="jsonContent"
jsonRequired
label="{{ importContentLabel | translate }}">
</tb-json-object-edit>
</div>
</fieldset>
<div class="flex flex-1 flex-col">
<tb-toggle-select *ngIf="enableImportFromContent"
class="flex-1"
formControlName="importType">
<tb-toggle-option value="file">{{ importFileLabel | translate }}</tb-toggle-option>
<tb-toggle-option value="content">{{ importContentLabel | translate }}</tb-toggle-option>
</tb-toggle-select>
<tb-file-input *ngIf="importFormGroup.get('importType').value === 'file'"
[contentConvertFunction]="loadDataFromJsonContent"
[existingFileName]="currentFileName"
(fileNameChanged)="currentFileName = $event"
formControlName="fileContent"
required
label="{{importFileLabel | translate}}"
dropLabel="{{ 'import.drop-json-file-or' | translate }}"
accept=".json,application/json"
allowedExtensions="json">
</tb-file-input>
<tb-json-object-edit *ngIf="importFormGroup.get('importType').value === 'content'"
formControlName="jsonContent"
jsonRequired
label="{{ importContentLabel | translate }}">
</tb-json-object-edit>
</div>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<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,
smsEnabled: true,
maxCreatedAlarms: 0,
maxDebugModeDurationMinutes: 0,
maxDebugModeDurationMinutes: 15,
tenantServerRestLimitsConfiguration: '',
customerServerRestLimitsConfiguration: '',
maxWsSessionsPerTenant: 0,

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

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

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

@ -193,7 +193,7 @@
"padding": "16px",
"settings": {
"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,
"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
{
url: string;
preserveAspectRatio?: boolean;
opacity?: number;
angle?: number;
scale?: number;
@ -27,6 +28,7 @@ Should return shape fill image data having the following structure:
```
- *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;
- *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);
@ -39,12 +41,33 @@ In case no data is returned, default fill image will be used.
<ul>
<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>
</ul>
```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}
```

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

@ -996,8 +996,6 @@
"all-messages": "All messages ({{time}})",
"failures": "Failures",
"entity": "entity",
"rule-node": "rule node",
"calculated-field": "calculated field",
"hint": {
"main-limited": "No more than {{msg}} {{entity}} debug messages per {{time}} will be recorded.",
"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-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.",
"test-with-this-message": "Test with this message",
"hint": {
"arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.",
"arguments-empty": "Arguments should not be empty.",
@ -1069,7 +1068,7 @@
"argument-name-pattern": "Argument name is invalid.",
"argument-name-duplicate": "Argument with such name already exists.",
"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.",
"max-args": "Maximum number of arguments reached.",
"decimals-range": "Decimals by default should be a number between 0 and 15.",
@ -1121,8 +1120,6 @@
"documentation": "Documentation",
"time-left": "{{time}} left",
"output": "Output",
"test-function": "Test function",
"test-with-this-message": "{{test}} with this message",
"suffix": {
"s": "s",
"ms": "ms"
@ -8137,6 +8134,7 @@
"fill-image": "Fill image",
"fill-image-type-image": "Image",
"fill-image-type-function": "Function",
"preserve-aspect-ratio": "Preserve aspect ratio",
"opacity": "Opacity",
"angle": "Rotation angle",
"scale": "Scale",

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

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

Loading…
Cancel
Save