Browse Source

Merge with rc

pull/12366/head
Igor Kulikov 2 years ago
parent
commit
1a8116cb93
  1. 24
      application/src/main/data/json/system/widget_types/gateway_configuration.json
  2. 24
      application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json
  3. 23
      application/src/main/data/json/system/widget_types/gateway_connectors.json
  4. 23
      application/src/main/data/json/system/widget_types/gateway_custom_statistics.json
  5. 23
      application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json
  6. 30
      application/src/main/data/json/system/widget_types/gateway_general_configuration.json
  7. 23
      application/src/main/data/json/system/widget_types/gateway_logs.json
  8. 44
      application/src/main/data/json/system/widget_types/gateway_status.json
  9. 23
      application/src/main/data/json/system/widget_types/service_rpc.json
  10. 4432
      application/src/main/data/resources/dashboards/gateways_dashboard.json
  11. 8
      application/src/main/data/resources/js_modules/gateway-management-extension.js
  12. 2
      application/src/main/data/upgrade/basic/schema_update.sql
  13. 14
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  14. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  15. 88
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  17. 9
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java
  19. 31
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  20. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
  21. 14
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  22. 13
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  23. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  24. 4
      application/src/main/java/org/thingsboard/server/service/install/update/ResourcesUpdater.java
  25. 7
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  26. 2
      application/src/main/resources/thingsboard.yml
  27. 11
      application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java
  28. 75
      application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java
  29. 20
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  30. 56
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java
  31. 8
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java
  32. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  33. 3
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  34. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
  35. 8
      msa/tb/docker/upgrade-tb.sh
  36. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  37. 19
      ui-ngx/.browserslistrc
  38. 3
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  39. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts
  40. 14
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts
  41. 4
      ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts
  42. 3
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  43. 8
      ui-ngx/src/app/shared/components/js-func-module-row.component.html
  44. 12
      ui-ngx/src/app/shared/components/js-func-module-row.component.ts
  45. 1
      ui-ngx/src/app/shared/components/js-func-modules.component.ts
  46. 4
      ui-ngx/src/app/shared/models/entity-type.models.ts
  47. 6
      ui-ngx/src/app/shared/models/js-function.models.ts
  48. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

24
application/src/main/data/json/system/widget_types/gateway_configuration.json

File diff suppressed because one or more lines are too long

24
application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/gateway_connectors.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/gateway_custom_statistics.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json

File diff suppressed because one or more lines are too long

30
application/src/main/data/json/system/widget_types/gateway_general_configuration.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/gateway_logs.json

File diff suppressed because one or more lines are too long

44
application/src/main/data/json/system/widget_types/gateway_status.json

@ -0,0 +1,44 @@
{
"fqn": "gateway_widgets.gateway_status",
"name": "Gateway status",
"deprecated": false,
"image": "tb-image;/api/images/system/gateway_status_system_widget_image.png",
"description": "Indicator of Gateway connection status.",
"descriptor": {
"type": "latest",
"sizeX": 7.5,
"sizeY": 3,
"resources": [
{
"url": "tb-resource;/api/resource/js_module/system/gateway-management-extension.js",
"isModule": true
}
],
"templateHtml": "<tb-gateway-status [deviceId]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-status>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayStatus?.onDataUpdated();\n};",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-gateway-status",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"<div class='card'>HTML code here</div>\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}"
},
"tags": [
"router",
"bridge",
"hub",
"access point",
"relay",
"opc ua",
"opc-ua",
"modbus",
"bacnet",
"odbc",
"ftp",
"snmp",
"mqtt",
"xmpp",
"ocpp",
"ble",
"bluetooth"
]
}

23
application/src/main/data/json/system/widget_types/service_rpc.json

File diff suppressed because one or more lines are too long

4432
application/src/main/data/resources/dashboards/gateways_dashboard.json

File diff suppressed because it is too large

8
application/src/main/data/resources/js_modules/gateway-management-extension.js

File diff suppressed because one or more lines are too long

2
application/src/main/data/upgrade/basic/schema_update.sql

@ -23,7 +23,7 @@ UPDATE user_credentials c SET failed_login_attempts = (SELECT (additional_info::
WHERE failed_login_attempts IS NULL; WHERE failed_login_attempts IS NULL;
UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - 'failedLoginAttempts' - 'userCredentialsEnabled')::text
WHERE additional_info IS NOT NULL AND additional_info != 'null'; WHERE additional_info IS NOT NULL AND additional_info != 'null' AND jsonb_typeof(additional_info::jsonb) = 'object';
-- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START -- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START

14
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -35,6 +35,8 @@ import org.thingsboard.server.service.install.migrate.TsLatestMigrateService;
import org.thingsboard.server.service.install.update.CacheCleanupService; import org.thingsboard.server.service.install.update.CacheCleanupService;
import org.thingsboard.server.service.install.update.DataUpdateService; import org.thingsboard.server.service.install.update.DataUpdateService;
import static org.thingsboard.server.service.install.update.DefaultDataUpdateService.getEnv;
@Service @Service
@Profile("install") @Profile("install")
@Slf4j @Slf4j
@ -94,11 +96,11 @@ public class ThingsboardInstallService {
public void performInstall() { public void performInstall() {
try { try {
if (isUpgrade) { if (isUpgrade) {
log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion);
if ("cassandra-latest-to-postgres".equals(upgradeFromVersion)) { if ("cassandra-latest-to-postgres".equals(upgradeFromVersion)) {
log.info("Migrating ThingsBoard latest timeseries data from cassandra to SQL database ..."); log.info("Migrating ThingsBoard latest timeseries data from cassandra to SQL database ...");
latestMigrateService.migrate(); latestMigrateService.migrate();
} else if (upgradeFromVersion.equals("3.9.0-resources")) {
installScripts.updateResourcesUsage();
} else { } else {
// TODO DON'T FORGET to update SUPPORTED_VERSIONS_FROM in DefaultDatabaseSchemaSettingsService // TODO DON'T FORGET to update SUPPORTED_VERSIONS_FROM in DefaultDatabaseSchemaSettingsService
databaseSchemaVersionService.validateSchemaSettings(); databaseSchemaVersionService.validateSchemaSettings();
@ -118,6 +120,14 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseIndexes(); entityDatabaseSchemaService.createDatabaseIndexes();
// Runs upgrade scripts that are not possible in plain SQL. // Runs upgrade scripts that are not possible in plain SQL.
// TODO: cleanup update code after each release // TODO: cleanup update code after each release
if (!getEnv("SKIP_RESOURCES_USAGE_MIGRATION", false)) {
installScripts.setUpdateResourcesUsage(true);
} else {
log.info("Skipping resources usage migration. Run the upgrade with fromVersion as '3.9.0-resources' to migrate");
}
if (installScripts.isUpdateResourcesUsage()) {
installScripts.updateResourcesUsage();
}
dataUpdateService.updateData(); dataUpdateService.updateData();
log.info("Updating system data..."); log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes(); dataUpdateService.upgradeRuleNodes();

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -349,7 +349,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) { private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) {
EdgeGrpcSession session = sessions.get(edgeId); EdgeGrpcSession session = sessions.get(edgeId);
if (session != null) { if (session != null) {
if (!session.isSyncCompleted()) { if (session.isSyncInProgress()) {
clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId); clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId);
} else { } else {
boolean success = false; boolean success = false;
@ -368,7 +368,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
UUID requestId = request.getId(); UUID requestId = request.getId();
EdgeGrpcSession session = sessions.get(request.getEdgeId()); EdgeGrpcSession session = sessions.get(request.getEdgeId());
if (session != null && !session.isSyncCompleted()) { if (session != null && session.isSyncInProgress()) {
responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment"));
} else { } else {
log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId()); log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId());
@ -411,7 +411,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
newEventLock.lock(); newEventLock.lock();
try { try {
if (Boolean.TRUE.equals(sessionNewEvents.get(edgeId))) { if (Boolean.TRUE.equals(sessionNewEvents.get(edgeId))) {
log.trace("[{}][{}] Set session new events flag to false", tenantId, edgeId.getId()); log.trace("[{}][{}] set session new events flag to false", tenantId, edgeId.getId());
sessionNewEvents.put(edgeId, false); sessionNewEvents.put(edgeId, false);
session.processHighPriorityEvents(); session.processHighPriorityEvents();
processEdgeEventMigrationIfNeeded(session, edgeId); processEdgeEventMigrationIfNeeded(session, edgeId);
@ -420,6 +420,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Override @Override
public void onSuccess(Boolean newEventsAdded) { public void onSuccess(Boolean newEventsAdded) {
if (Boolean.TRUE.equals(newEventsAdded)) { if (Boolean.TRUE.equals(newEventsAdded)) {
log.trace("[{}][{}] new events added. set session new events flag to true", tenantId, edgeId.getId());
sessionNewEvents.put(edgeId, true); sessionNewEvents.put(edgeId, true);
} }
scheduleEdgeEventsCheck(session); scheduleEdgeEventsCheck(session);

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

@ -28,6 +28,7 @@ import org.springframework.data.util.Pair;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
@ -130,13 +131,12 @@ public abstract class EdgeGrpcSession implements Closeable {
private Long previousStartTs; private Long previousStartTs;
private Long newStartSeqId; private Long newStartSeqId;
private Long previousStartSeqId; private Long previousStartSeqId;
private Long seqIdEnd;
private StreamObserver<RequestMsg> inputStream; private StreamObserver<RequestMsg> inputStream;
private StreamObserver<ResponseMsg> outputStream; private StreamObserver<ResponseMsg> outputStream;
private volatile boolean connected; private volatile boolean connected;
private volatile boolean syncCompleted; private volatile boolean syncInProgress;
private EdgeVersion edgeVersion; private EdgeVersion edgeVersion;
private int maxInboundMessageSize; private int maxInboundMessageSize;
@ -191,7 +191,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
startSyncProcess(fullSync); startSyncProcess(fullSync);
} else { } else {
syncCompleted = true; syncInProgress = false;
} }
} }
if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) { if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) {
@ -246,10 +246,14 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
public void startSyncProcess(boolean fullSync) { public void startSyncProcess(boolean fullSync) {
log.info("[{}][{}][{}] Staring edge sync process", tenantId, edge.getId(), sessionId); if (!syncInProgress) {
syncCompleted = false; log.info("[{}][{}][{}] Staring edge sync process", tenantId, edge.getId(), sessionId);
interruptGeneralProcessingOnSync(); syncInProgress = true;
doSync(new EdgeSyncCursor(ctx, edge, fullSync)); interruptGeneralProcessingOnSync();
doSync(new EdgeSyncCursor(ctx, edge, fullSync));
} else {
log.info("[{}][{}][{}] Sync is already started, skipping starting it now", tenantId, edge.getId(), sessionId);
}
} }
private void doSync(EdgeSyncCursor cursor) { private void doSync(EdgeSyncCursor cursor) {
@ -292,6 +296,7 @@ public abstract class EdgeGrpcSession implements Closeable {
protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) { protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) {
try { try {
log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink);
processHighPriorityEvents(); processHighPriorityEvents();
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
@ -327,7 +332,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.trace("[{}] no event(s) found. Stop processing edge events", sessionId); log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink);
result.set(null); result.set(null);
} }
} catch (Exception e) { } catch (Exception e) {
@ -406,6 +411,8 @@ public abstract class EdgeGrpcSession implements Closeable {
|| sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) { || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", tenantId, edge.getId(), sessionId); log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", tenantId, edge.getId(), sessionId);
stopCurrentSendDownlinkMsgsTask(true); stopCurrentSendDownlinkMsgsTask(true);
} else {
log.trace("[{}][{}][{}] Previous send downlink future is not active", tenantId, edge.getId(), sessionId);
} }
} }
@ -519,12 +526,12 @@ public abstract class EdgeGrpcSession implements Closeable {
try { try {
if (msg.getSuccess()) { if (msg.getSuccess()) {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
log.debug("[{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg); log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg);
} else { } else {
log.error("[{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg.getErrorMsg()); log.error("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg());
} }
if (sessionState.getPendingMsgsMap().isEmpty()) { if (sessionState.getPendingMsgsMap().isEmpty()) {
log.debug("[{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getRoutingKey()); log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getId(), sessionId);
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
@ -534,7 +541,7 @@ public abstract class EdgeGrpcSession implements Closeable {
public void processHighPriorityEvents() { public void processHighPriorityEvents() {
try { try {
if (isConnected() && isSyncCompleted()) { if (isConnected() && !isSyncInProgress()) {
if (highPriorityQueue.isEmpty()) { if (highPriorityQueue.isEmpty()) {
return; return;
} }
@ -543,6 +550,7 @@ public abstract class EdgeGrpcSession implements Closeable {
while ((event = highPriorityQueue.poll()) != null) { while ((event = highPriorityQueue.poll()) != null) {
highPriorityEvents.add(event); highPriorityEvents.add(event);
} }
log.trace("[{}][{}] Sending high priority events {}", tenantId, sessionId, highPriorityEvents.size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents);
sendDownlinkMsgsPack(downlinkMsgsPack).get(); sendDownlinkMsgsPack(downlinkMsgsPack).get();
} }
@ -553,18 +561,18 @@ public abstract class EdgeGrpcSession implements Closeable {
public ListenableFuture<Boolean> processEdgeEvents() throws Exception { public ListenableFuture<Boolean> processEdgeEvents() throws Exception {
SettableFuture<Boolean> result = SettableFuture.create(); SettableFuture<Boolean> result = SettableFuture.create();
log.trace("[{}][{}] starting processing edge events", tenantId, sessionId); if (isConnected() && !isSyncInProgress()) {
if (isConnected() && isSyncCompleted()) {
Pair<Long, Long> startTsAndSeqId = getQueueStartTsAndSeqId().get(); Pair<Long, Long> startTsAndSeqId = getQueueStartTsAndSeqId().get();
previousStartTs = startTsAndSeqId.getFirst(); previousStartTs = startTsAndSeqId.getFirst();
previousStartSeqId = startTsAndSeqId.getSecond(); previousStartSeqId = startTsAndSeqId.getSecond();
GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher( GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher(
previousStartTs, previousStartTs,
previousStartSeqId, previousStartSeqId,
seqIdEnd,
false, false,
Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService()); ctx.getEdgeEventService());
log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}",
tenantId, sessionId, previousStartTs, previousStartSeqId);
Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) { public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
@ -574,18 +582,16 @@ public abstract class EdgeGrpcSession implements Closeable {
@Override @Override
public void onSuccess(@Nullable List<Long> list) { public void onSuccess(@Nullable List<Long> list) {
log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId); log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId);
boolean newEventsAvailable;
if (fetcher.isSeqIdNewCycleStarted()) { if (fetcher.isSeqIdNewCycleStarted()) {
seqIdEnd = fetcher.getSeqIdEnd(); newEventsAvailable = isNewEdgeEventsAvailable();
boolean newEventsAvailable = isNewEdgeEventsAvailable();
result.set(newEventsAvailable);
} else { } else {
seqIdEnd = null; newEventsAvailable = isSeqIdStartedNewCycle();
boolean newEventsAvailable = isSeqIdStartedNewCycle();
if (!newEventsAvailable) { if (!newEventsAvailable) {
newEventsAvailable = isNewEdgeEventsAvailable(); newEventsAvailable = isNewEdgeEventsAvailable();
} }
result.set(newEventsAvailable);
} }
result.set(newEventsAvailable);
} }
@Override @Override
@ -607,7 +613,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
if (!isSyncCompleted()) { if (isSyncInProgress()) {
log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, sessionId); log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, sessionId);
result.set(Boolean.TRUE); result.set(Boolean.TRUE);
} else { } else {
@ -672,9 +678,12 @@ public abstract class EdgeGrpcSession implements Closeable {
private boolean isSeqIdStartedNewCycle() { private boolean isSeqIdStartedNewCycle() {
try { try {
log.trace("[{}][{}][{}] Checking if seq id started new cycle", tenantId, edge.getId(), sessionId);
TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis());
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, previousStartSeqId == 0 ? null : previousStartSeqId - 1, pageLink); PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, previousStartSeqId == 0 ? null : previousStartSeqId - 1, pageLink);
return !edgeEvents.getData().isEmpty(); boolean result = !edgeEvents.getData().isEmpty();
log.trace("[{}][{}][{}] Result of check if seq id started new cycle, result = {}", tenantId, edge.getId(), sessionId, result);
return result;
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", tenantId, edge.getId(), sessionId, e); log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", tenantId, edge.getId(), sessionId, e);
} }
@ -683,9 +692,12 @@ public abstract class EdgeGrpcSession implements Closeable {
private boolean isNewEdgeEventsAvailable() { private boolean isNewEdgeEventsAvailable() {
try { try {
log.trace("[{}][{}][{}] Checking if new edge events available", tenantId, edge.getId(), sessionId);
TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis()); TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, newStartTs, System.currentTimeMillis());
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), newStartSeqId, null, pageLink); PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), newStartSeqId, null, pageLink);
return !edgeEvents.getData().isEmpty() || !highPriorityQueue.isEmpty(); boolean result = !edgeEvents.getData().isEmpty() || !highPriorityQueue.isEmpty();
log.trace("[{}][{}][{}] Result of check if new edge events available, result = {}", tenantId, edge.getId(), sessionId, result);
return result;
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", tenantId, edge.getId(), sessionId, e); log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", tenantId, edge.getId(), sessionId, e);
} }
@ -724,7 +736,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
private void markSyncCompletedSendEdgeEventUpdate() { private void markSyncCompletedSendEdgeEventUpdate() {
syncCompleted = true; syncInProgress = false;
ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId())); ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId()));
} }
@ -737,29 +749,27 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
} }
private void sendDownlinkMsg(ResponseMsg downlinkMsg) { private void sendDownlinkMsg(ResponseMsg responseMsg) {
if (downlinkMsg.getDownlinkMsg().getWidgetTypeUpdateMsgCount() > 0) {
log.trace("[{}][{}] Sending downlink widgetTypeUpdateMsg, downlinkMsgId = {}", tenantId, sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId());
} else {
log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, sessionId, downlinkMsg);
}
if (isConnected()) { if (isConnected()) {
String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000);
log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, sessionId, responseMsgStr);
downlinkMsgLock.lock(); downlinkMsgLock.lock();
String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr;
try { try {
outputStream.onNext(downlinkMsg); outputStream.onNext(responseMsg);
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsg, e); log.error("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e);
connected = false; connected = false;
sessionCloseListener.accept(edge, sessionId); sessionCloseListener.accept(edge, sessionId);
} finally { } finally {
downlinkMsgLock.unlock(); downlinkMsgLock.unlock();
} }
log.trace("[{}][{}] Response msg successfully sent. downlinkMsgId = {}", tenantId, sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId()); log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, sessionId, downlinkMsgStr);
} }
} }
protected DownlinkMsg convertEntityEventToDownlink(EdgeEvent edgeEvent) { private DownlinkMsg convertEntityEventToDownlink(EdgeEvent edgeEvent) {
log.trace("[{}] Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", edgeEvent.getTenantId(), edgeEvent, edgeEvent.getAction()); log.trace("[{}][{}] Executing convertEntityEventToDownlink, edgeEvent [{}], action [{}]", tenantId, sessionId, edgeEvent, edgeEvent.getAction());
return switch (edgeEvent.getType()) { return switch (edgeEvent.getType()) {
case EDGE -> ctx.getEdgeProcessor().convertEdgeEventToDownlink(edgeEvent); case EDGE -> ctx.getEdgeProcessor().convertEdgeEventToDownlink(edgeEvent);
case DEVICE -> ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent, edgeVersion); case DEVICE -> ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent, edgeVersion);
@ -789,7 +799,7 @@ public abstract class EdgeGrpcSession implements Closeable {
case OAUTH2_CLIENT -> ctx.getOAuth2EdgeProcessor().convertOAuth2ClientEventToDownlink(edgeEvent, edgeVersion); case OAUTH2_CLIENT -> ctx.getOAuth2EdgeProcessor().convertOAuth2ClientEventToDownlink(edgeEvent, edgeVersion);
case DOMAIN -> ctx.getOAuth2EdgeProcessor().convertOAuth2DomainEventToDownlink(edgeEvent, edgeVersion); case DOMAIN -> ctx.getOAuth2EdgeProcessor().convertOAuth2DomainEventToDownlink(edgeEvent, edgeVersion);
default -> { default -> {
log.warn("[{}] Unsupported edge event type [{}]", edgeEvent.getTenantId(), edgeEvent); log.warn("[{}][{}] Unsupported edge event type [{}]", tenantId, sessionId, edgeEvent);
yield null; yield null;
} }
}; };
@ -923,8 +933,8 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.error("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); log.error("[{}][{}] Can't process uplink msg [{}]", tenantId, sessionId, uplinkMsg, e);
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -73,7 +73,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) { private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) {
log.trace("[{}][{}] starting processing edge events", tenantId, sessionId); log.trace("[{}][{}] starting processing edge events", tenantId, sessionId);
if (isConnected() && isSyncCompleted() && !isHighPriorityProcessing) { if (isConnected() && !isSyncInProgress() && !isHighPriorityProcessing) {
List<EdgeEvent> edgeEvents = new ArrayList<>(); List<EdgeEvent> edgeEvents = new ArrayList<>();
for (TbProtoQueueMsg<ToEdgeEventNotificationMsg> msg : msgs) { for (TbProtoQueueMsg<ToEdgeEventNotificationMsg> msg : msgs) {
EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg());

9
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java

@ -33,8 +33,6 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
private final Long queueStartTs; private final Long queueStartTs;
private Long seqIdStart; private Long seqIdStart;
@Getter @Getter
private Long seqIdEnd;
@Getter
private boolean seqIdNewCycleStarted; private boolean seqIdNewCycleStarted;
private Long maxReadRecordsCount; private Long maxReadRecordsCount;
private final EdgeEventService edgeEventService; private final EdgeEventService edgeEventService;
@ -53,10 +51,11 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
@Override @Override
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
try { try {
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, seqIdEnd, (TimePageLink) pageLink); log.trace("[{}] Finding general edge events [{}], seqIdStart = {}, pageLink = {}",
tenantId, edge.getId(), seqIdStart, pageLink);
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, null, (TimePageLink) pageLink);
if (edgeEvents.getData().isEmpty()) { if (edgeEvents.getData().isEmpty()) {
this.seqIdEnd = Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount); edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount), (TimePageLink) pageLink);
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, seqIdEnd, (TimePageLink) pageLink);
if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) { if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) {
log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId()); log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId());
this.seqIdNewCycleStarted = true; this.seqIdNewCycleStarted = true;

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java

@ -73,7 +73,7 @@ public abstract class RelationEdgeProcessor extends BaseRelationProcessor implem
public ListenableFuture<Void> processRelationNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { public ListenableFuture<Void> processRelationNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
EntityRelation relation = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityRelation.class); EntityRelation relation = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityRelation.class);
if (relation == null || (relation.getFrom().getEntityType().equals(EntityType.EDGE) || relation.getTo().getEntityType().equals(EntityType.EDGE))) { if (relation == null) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB()); EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());

31
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java

@ -235,6 +235,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
if (relationsList != null && !relationsList.isEmpty()) { if (relationsList != null && !relationsList.isEmpty()) {
List<ListenableFuture<Void>> futures = new ArrayList<>(); List<ListenableFuture<Void>> futures = new ArrayList<>();
for (List<EntityRelation> entityRelations : relationsList) { for (List<EntityRelation> entityRelations : relationsList) {
if (entityRelations.isEmpty()) {
continue;
}
log.trace("[{}][{}][{}][{}] relation(s) are going to be pushed to edge.", tenantId, edge.getId(), entityId, entityRelations.size()); log.trace("[{}][{}][{}][{}] relation(s) are going to be pushed to edge.", tenantId, edge.getId(), entityId, entityRelations.size());
for (EntityRelation relation : entityRelations) { for (EntityRelation relation : entityRelations) {
try { try {
@ -255,19 +258,23 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
} }
} }
} }
Futures.addCallback(Futures.allAsList(futures), new FutureCallback<>() { if (futures.isEmpty()) {
@Override futureToSet.set(null);
public void onSuccess(@Nullable List<Void> voids) { } else {
futureToSet.set(null); Futures.addCallback(Futures.allAsList(futures), new FutureCallback<>() {
} @Override
public void onSuccess(@Nullable List<Void> voids) {
futureToSet.set(null);
}
@Override @Override
public void onFailure(Throwable throwable) { public void onFailure(Throwable throwable) {
String errMsg = String.format("[%s][%s] Exception during saving edge events [%s]!", tenantId, edge.getId(), relationRequestMsg); String errMsg = String.format("[%s][%s] Exception during saving edge events [%s]!", tenantId, edge.getId(), relationRequestMsg);
log.error(errMsg, throwable); log.error(errMsg, throwable);
futureToSet.setException(new RuntimeException(errMsg, throwable)); futureToSet.setException(new RuntimeException(errMsg, throwable));
} }
}, dbCallbackExecutorService); }, dbCallbackExecutorService);
}
} else { } else {
futureToSet.set(null); futureToSet.set(null);
} }

5
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -74,7 +75,7 @@ public class DashboardSyncService {
List<RepoFile> resources = listFiles("resources"); List<RepoFile> resources = listFiles("resources");
for (RepoFile resourceFile : resources) { for (RepoFile resourceFile : resources) {
byte[] data = getFileContent(resourceFile.path()); byte[] data = getFileContent(resourceFile.path());
resourceService.createOrUpdateSystemResource(ResourceType.JS_MODULE, resourceFile.name(), data); resourceService.createOrUpdateSystemResource(ResourceType.JS_MODULE, ResourceSubType.EXTENSION, resourceFile.name(), data);
} }
List<RepoFile> images = listFiles("images"); List<RepoFile> images = listFiles("images");
for (RepoFile imageFile : images) { for (RepoFile imageFile : images) {
@ -89,7 +90,7 @@ public class DashboardSyncService {
widgetsBundleService.updateSystemWidgets(widgetsBundles, widgetTypes); widgetsBundleService.updateSystemWidgets(widgetsBundles, widgetTypes);
RepoFile dashboardFile = listFiles("dashboards").get(0); RepoFile dashboardFile = listFiles("dashboards").get(0);
resourceService.createOrUpdateSystemResource(ResourceType.DASHBOARD, GATEWAYS_DASHBOARD_KEY, getFileContent(dashboardFile.path())); resourceService.createOrUpdateSystemResource(ResourceType.DASHBOARD, null, GATEWAYS_DASHBOARD_KEY, getFileContent(dashboardFile.path()));
log.info("Gateways dashboard sync completed"); log.info("Gateways dashboard sync completed");
} }

14
application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java

@ -17,6 +17,8 @@ package org.thingsboard.server.service.install;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@ -39,6 +41,9 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
private final BuildProperties buildProperties; private final BuildProperties buildProperties;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
@Value("${install.upgrade.from_version:}")
private String upgradeFromVersion;
private String packageSchemaVersion; private String packageSchemaVersion;
private String schemaVersionFromDb; private String schemaVersionFromDb;
@ -102,6 +107,15 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
@Override @Override
public String getDbSchemaVersion() { public String getDbSchemaVersion() {
if (schemaVersionFromDb == null) { if (schemaVersionFromDb == null) {
if (StringUtils.isNotBlank(upgradeFromVersion)) {
/*
* TODO - Remove after the release of 3.9.0:
* This a temporary workaround due to the issue that schema version in the
* tb_schema_settings was set as 3.6.4 during the install of 3.8.1.
* */
schemaVersionFromDb = upgradeFromVersion;
return schemaVersionFromDb;
}
Long version = getSchemaVersionFromDb(); Long version = getSchemaVersionFromDb();
if (version == null) { if (version == null) {
onSchemaSettingsError("Upgrade failed: the database schema version is missing."); onSchemaSettingsError("Upgrade failed: the database schema version is missing.");

13
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -123,6 +123,9 @@ public class InstallScripts {
@Getter @Setter @Getter @Setter
private boolean updateImages = false; private boolean updateImages = false;
@Getter @Setter
private boolean updateResourcesUsage = false;
@Autowired @Autowired
private ImageService imageService; private ImageService imageService;
@ -416,9 +419,9 @@ public class InstallScripts {
} }
Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR); Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR);
loadSystemResources(resourcesDir.resolve("images"), ResourceType.IMAGE); loadSystemResources(resourcesDir.resolve("images"), ResourceType.IMAGE, null);
loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE); loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION);
loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD); loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD, null);
} }
public void loadDashboards(TenantId tenantId, CustomerId customerId) { public void loadDashboards(TenantId tenantId, CustomerId customerId) {
@ -514,7 +517,7 @@ public class InstallScripts {
resourcesUpdater.updateWidgetsResources(); resourcesUpdater.updateWidgetsResources();
} }
private void loadSystemResources(Path dir, ResourceType resourceType) { private void loadSystemResources(Path dir, ResourceType resourceType, ResourceSubType resourceSubType) {
listDir(dir).forEach(resourceFile -> { listDir(dir).forEach(resourceFile -> {
String resourceKey = resourceFile.getFileName().toString(); String resourceKey = resourceFile.getFileName().toString();
try { try {
@ -522,7 +525,7 @@ public class InstallScripts {
if (resourceType == ResourceType.IMAGE) { if (resourceType == ResourceType.IMAGE) {
imageService.createOrUpdateSystemImage(resourceKey, data); imageService.createOrUpdateSystemImage(resourceKey, data);
} else { } else {
resourceService.createOrUpdateSystemResource(resourceType, resourceKey, data); resourceService.createOrUpdateSystemResource(resourceType, resourceSubType, resourceKey, data);
} }
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Unable to load system resource " + resourceFile, e); throw new RuntimeException("Unable to load system resource " + resourceFile, e);

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

@ -65,7 +65,6 @@ public class DefaultDataUpdateService implements DataUpdateService {
public void updateData() throws Exception { public void updateData() throws Exception {
log.info("Updating data ..."); log.info("Updating data ...");
//TODO: should be cleaned after each release //TODO: should be cleaned after each release
installScripts.updateResourcesUsage();
log.info("Data updated."); log.info("Data updated.");
} }

4
application/src/main/java/org/thingsboard/server/service/install/update/ResourcesUpdater.java

@ -113,7 +113,7 @@ public class ResourcesUpdater {
} }
executor.shutdown(); executor.shutdown();
if (!executor.awaitTermination(10, TimeUnit.MINUTES)) { if (!executor.awaitTermination(5, TimeUnit.HOURS)) {
throw new RuntimeException("Dashboards resources update timeout"); // just in case, should happen throw new RuntimeException("Dashboards resources update timeout"); // just in case, should happen
} }
log.info("Updated {} dashboards", updatedCount); log.info("Updated {} dashboards", updatedCount);
@ -142,7 +142,7 @@ public class ResourcesUpdater {
} }
executor.shutdown(); executor.shutdown();
if (!executor.awaitTermination(10, TimeUnit.MINUTES)) { if (!executor.awaitTermination(5, TimeUnit.HOURS)) {
throw new RuntimeException("Widgets resources update timeout"); throw new RuntimeException("Widgets resources update timeout");
} }
log.info("Updated {} widgets", updatedCount); log.info("Updated {} widgets", updatedCount);

7
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -122,6 +122,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
if (notificationTemplate == null) { if (notificationTemplate == null) {
throw new IllegalArgumentException("Template is missing"); throw new IllegalArgumentException("Template is missing");
} }
NotificationType notificationType = notificationTemplate.getNotificationType();
Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>(); Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>();
List<NotificationTarget> targets = new ArrayList<>(); List<NotificationTarget> targets = new ArrayList<>();
@ -143,13 +144,13 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
try { try {
channels.get(deliveryMethod).check(tenantId); channels.get(deliveryMethod).check(tenantId);
} catch (Exception e) { } catch (Exception e) {
if (ruleId == null) { if (ruleId == null && !notificationType.isSystem()) {
throw new IllegalArgumentException(e.getMessage()); throw new IllegalArgumentException(e.getMessage());
} else { } else {
return; // if originated by rule - just ignore delivery method return; // if originated by rule or notification type is system - just ignore delivery method
} }
} }
if (ruleId == null && !notificationTemplate.getNotificationType().isSystem()) { if (ruleId == null && !notificationType.isSystem()) {
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) { if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
throw new IllegalArgumentException("Recipients for " + deliveryMethod.getName() + " delivery method not chosen"); throw new IllegalArgumentException("Recipients for " + deliveryMethod.getName() + " delivery method not chosen");
} }

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

@ -600,7 +600,7 @@ cache:
maxSize: "${CACHE_SPECS_EDGE_SESSIONS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_EDGE_SESSIONS_MAX_SIZE:10000}" # 0 means the cache is disabled
relatedEdges: relatedEdges:
timeToLiveInMinutes: "${CACHE_SPECS_RELATED_EDGES_TTL:1440}" # Related Edges cache TTL timeToLiveInMinutes: "${CACHE_SPECS_RELATED_EDGES_TTL:1440}" # Related Edges cache TTL
maxSize: "${CACHE_SPECS_RELATED_EDGES_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_RELATED_EDGES_MAX_SIZE:0}" # 0 means the cache is disabled
repositorySettings: repositorySettings:
timeToLiveInMinutes: "${CACHE_SPECS_REPOSITORY_SETTINGS_TTL:1440}" # Repository settings cache TTL timeToLiveInMinutes: "${CACHE_SPECS_REPOSITORY_SETTINGS_TTL:1440}" # Repository settings cache TTL
maxSize: "${CACHE_SPECS_REPOSITORY_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_REPOSITORY_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled

11
application/src/test/java/org/thingsboard/server/cache/CaffeineCacheDefaultConfigurationTest.java

@ -24,6 +24,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.test.context.SpringBootContextLoader; import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -32,6 +33,10 @@ import static org.assertj.core.api.Assertions.assertThat;
@ContextConfiguration(classes = CaffeineCacheDefaultConfigurationTest.class, loader = SpringBootContextLoader.class) @ContextConfiguration(classes = CaffeineCacheDefaultConfigurationTest.class, loader = SpringBootContextLoader.class)
@ComponentScan({"org.thingsboard.server.cache"}) @ComponentScan({"org.thingsboard.server.cache"})
@EnableConfigurationProperties @EnableConfigurationProperties
@TestPropertySource(properties = {
"cache.specs.edgeSessions.timeToLiveInMinutes=1",
"cache.specs.relatedEdges.maxSize=1"
})
@Slf4j @Slf4j
public class CaffeineCacheDefaultConfigurationTest { public class CaffeineCacheDefaultConfigurationTest {
@ -46,11 +51,7 @@ public class CaffeineCacheDefaultConfigurationTest {
SoftAssertions softly = new SoftAssertions(); SoftAssertions softly = new SoftAssertions();
cacheSpecsMap.getSpecs().forEach((name, cacheSpecs) -> { cacheSpecsMap.getSpecs().forEach((name, cacheSpecs) -> {
softly.assertThat(name).as("cache name").isNotEmpty(); softly.assertThat(name).as("cache name").isNotEmpty();
if (name.equals("edgeSessions")) { softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0);
softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isEqualTo(0);
} else {
softly.assertThat(cacheSpecs.getTimeToLiveInMinutes()).as("cache %s time to live", name).isGreaterThan(0);
}
softly.assertThat(cacheSpecs.getMaxSize()).as("cache %s max size", name).isGreaterThan(0); softly.assertThat(cacheSpecs.getMaxSize()).as("cache %s max size", name).isGreaterThan(0);
}); });
softly.assertAll(); softly.assertAll();

75
application/src/test/java/org/thingsboard/server/edge/RelationEdgeTest.java

@ -21,8 +21,10 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -31,6 +33,8 @@ import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import java.util.UUID;
@DaoSqlTest @DaoSqlTest
public class RelationEdgeTest extends AbstractEdgeTest { public class RelationEdgeTest extends AbstractEdgeTest {
@ -58,13 +62,14 @@ public class RelationEdgeTest extends AbstractEdgeTest {
// delete relation // delete relation
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(1);
var deletedRelation = doDelete("/api/v2/relation?" +
"fromId=" + relation.getFrom().getId().toString() + String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&relationTypeGroup=%s&toId=%s&toType=%s",
"&fromType=" + relation.getFrom().getEntityType().name() + device.getId().toString(), EntityType.DEVICE.name(), "test",
"&relationType=" + relation.getType() + RelationTypeGroup.COMMON.name(), asset.getId().toString(), EntityType.ASSET.name()
"&relationTypeGroup=" + relation.getTypeGroup().name() + );
"&toId=" + relation.getTo().getId().toString() +
"&toType=" + relation.getTo().getEntityType().name(), EntityRelation.class); var deletedRelation = doDelete(deleteUrl, EntityRelation.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage(); latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof RelationUpdateMsg); Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
@ -94,13 +99,13 @@ public class RelationEdgeTest extends AbstractEdgeTest {
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
EntityRelation relation = doGet("/api/relation?" + String getUrl = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&relationTypeGroup=%s&toId=%s&toType=%s",
"&fromId=" + device2.getUuidId() + device2.getUuidId(), EntityType.DEVICE.name(), "test",
"&fromType=" + device2.getId().getEntityType().name() + RelationTypeGroup.COMMON.name(), device1.getUuidId(), EntityType.DEVICE.name()
"&relationType=" + "test" + );
"&relationTypeGroup=" + RelationTypeGroup.COMMON.name() +
"&toId=" + device1.getUuidId() + var relation = doGet(getUrl, EntityRelation.class);
"&toType=" + device1.getId().getEntityType().name(), EntityRelation.class);
Assert.assertNotNull(relation); Assert.assertNotNull(relation);
} }
@ -154,6 +159,47 @@ public class RelationEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType());
} }
@Test
public void testRelationFromEdgeToDevice() throws Exception {
// create relation
Device device = findDeviceByName("Edge Device 1");
EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB()));
EntityRelation relation = new EntityRelation();
relation.setType("test");
relation.setFrom(edgeId);
relation.setTo(device.getId());
relation.setTypeGroup(RelationTypeGroup.COMMON);
edgeImitator.expectMessageAmount(1);
relation = doPost("/api/v2/relation", relation, EntityRelation.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
RelationUpdateMsg relationUpdateMsg = (RelationUpdateMsg) latestMessage;
EntityRelation entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
Assert.assertNotNull(entityRelation);
Assert.assertEquals(relation, entityRelation);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, relationUpdateMsg.getMsgType());
// delete relation
edgeImitator.expectMessageAmount(1);
String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&relationTypeGroup=%s&toId=%s&toType=%s",
edgeId, EntityType.EDGE.name(), "test",
RelationTypeGroup.COMMON.name(), device.getId().toString(), EntityType.DEVICE.name()
);
var deletedRelation = doDelete(deleteUrl, EntityRelation.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof RelationUpdateMsg);
relationUpdateMsg = (RelationUpdateMsg) latestMessage;
entityRelation = JacksonUtil.fromString(relationUpdateMsg.getEntity(), EntityRelation.class, true);
Assert.assertNotNull(entityRelation);
Assert.assertEquals(deletedRelation, entityRelation);
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, relationUpdateMsg.getMsgType());
}
private EntityRelation buildEntityRelationForUplinkMsg(DeviceId deviceId1, DeviceId deviceId2) { private EntityRelation buildEntityRelationForUplinkMsg(DeviceId deviceId1, DeviceId deviceId2) {
EntityRelation relation = new EntityRelation(); EntityRelation relation = new EntityRelation();
relation.setType("test"); relation.setType("test");
@ -163,4 +209,5 @@ public class RelationEdgeTest extends AbstractEdgeTest {
relation.setAdditionalInfo(TextNode.valueOf("{}")); relation.setAdditionalInfo(TextNode.valueOf("{}"));
return relation; return relation;
} }
} }

20
application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java

@ -39,7 +39,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -50,8 +49,8 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
@Test @Test
public void testRuleChains() throws Exception { public void testRuleChains() throws Exception {
// create rule chain // create rule chain: 2 messages from create rule chain, 2 messages from load metadata
edgeImitator.expectMessageAmount(2); edgeImitator.expectMessageAmount(4);
RuleChain ruleChain = new RuleChain(); RuleChain ruleChain = new RuleChain();
ruleChain.setName("Edge Test Rule Chain"); ruleChain.setName("Edge Test Rule Chain");
ruleChain.setType(RuleChainType.EDGE); ruleChain.setType(RuleChainType.EDGE);
@ -60,15 +59,21 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
+ "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class);
createRuleChainMetadata(savedRuleChain); createRuleChainMetadata(savedRuleChain);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
Optional<RuleChainUpdateMsg> ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); List<RuleChainUpdateMsg> ruleChainUpdateMsgs = edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class);
Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent()); Assert.assertEquals(2, ruleChainUpdateMsgs.size());
RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get(); List<RuleChainMetadataUpdateMsg> ruleChainMetadataUpdateMsgs = edgeImitator.findAllMessagesByType(RuleChainMetadataUpdateMsg.class);
Assert.assertEquals(2, ruleChainMetadataUpdateMsgs.size());
RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainUpdateMsgs.get(0);
RuleChain ruleChainMsg = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true); RuleChain ruleChainMsg = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true);
Assert.assertNotNull(ruleChainMsg); Assert.assertNotNull(ruleChainMsg);
Assert.assertTrue(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()) || Assert.assertTrue(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()) ||
UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType())); UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(ruleChainUpdateMsg.getMsgType()));
Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId()); Assert.assertEquals(savedRuleChain.getId(), ruleChainMsg.getId());
Assert.assertEquals(savedRuleChain.getName(), ruleChainMsg.getName()); Assert.assertEquals(savedRuleChain.getName(), ruleChainMsg.getName());
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateMsgs.get(0);
RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
Assert.assertNotNull(ruleChainMetaData);
Assert.assertEquals(ruleChainMetaData.getRuleChainId(), savedRuleChain.getId());
testRuleChainMetadataRequestMsg(savedRuleChain.getId()); testRuleChainMetadataRequestMsg(savedRuleChain.getId());
@ -77,7 +82,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
doDelete("/api/edge/" + edge.getUuidId() doDelete("/api/edge/" + edge.getUuidId()
+ "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); Optional<RuleChainUpdateMsg> ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class);
Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent()); Assert.assertTrue(ruleChainUpdateMsgOpt.isPresent());
ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get(); ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType());
@ -262,4 +267,5 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
.andExpect(status().isOk()); .andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages(5)); Assert.assertTrue(edgeImitator.waitForMessages(5));
} }
} }

56
application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java

@ -1697,6 +1697,62 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
assertEquals(4.2d, actual); assertEquals(4.2d, actual);
} }
// Bitwise Operations
@Test
public void bitwiseOperationsBoolean_Test() throws ExecutionException, InterruptedException {
msgStr = "{}";
decoderStr = """
var x = true;
var y = false;
return {
"andResult": x & y,
"orResult": x | y,
"xorResult": x ^ y,
"leftShift": x << y,
"rightShift": x >> y,
"rightUnShift": x >>> y
}
""";
LinkedHashMap<String, Object> expected = new LinkedHashMap<>();
expected.put("andResult", 0);
expected.put("orResult", 1);
expected.put("xorResult", 1);
expected.put("leftShift", 1);
expected.put("rightShift", 1);
expected.put("rightUnShift", 1);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expected, actual);
}
@Test
public void bitwiseOperationsMix_Test() throws ExecutionException, InterruptedException {
msgStr = "{}";
decoderStr = """
var x = true;
var y = false;
var i = 10;
var b = -14;
var l = 9223372036854775807;
return {
"andResult": x & b,
"orResult": i | y,
"xorResult": i ^ l,
"leftShift": l << i,
"rightShift": l >> b,
"rightUnShift": i >>> x
}
""";
LinkedHashMap<String, Object> expected = new LinkedHashMap<>();
expected.put("andResult", 0);
expected.put("orResult", 10);
expected.put("xorResult", 9223372036854775797L);
expected.put("leftShift", -1024L);
expected.put("rightShift", 8191L);
expected.put("rightUnShift", 5);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expected, actual);
}
// base64 // base64
@Test @Test
public void base64_Test() throws ExecutionException, InterruptedException { public void base64_Test() throws ExecutionException, InterruptedException {

8
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java

@ -73,13 +73,13 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneObjectInstance_Result_CONTENT_Value_Count_Equal_Greater_3_After_Cancel_Count_2() throws Exception {
sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
String idVer_3_0 = objectInstanceIdVer_3; String idVer_3_0 = objectInstanceIdVer_3;
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0);
int cntUpdate = 3; int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
} }
@ -88,13 +88,13 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationO
* @throws Exception * @throws Exception
*/ */
@Test @Test
public void testObserveOneObject_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { public void testObserveOneObject_Result_CONTENT_Value_Count_Equal_Greater_3_After_Cancel_Count_2() throws Exception {
sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr());
String idVer_3_0 = objectInstanceIdVer_3; String idVer_3_0 = objectInstanceIdVer_3;
sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0); sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0);
int cntUpdate = 3; int cntUpdate = 3;
verify(defaultUplinkMsgHandlerTest, timeout(10000).times(cntUpdate)) verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null));
} }

3
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
@ -88,6 +89,6 @@ public interface ResourceService extends EntityDaoService {
Collection<TbResourceInfo> getUsedResources(TenantId tenantId, WidgetTypeDetails widgetTypeDetails); Collection<TbResourceInfo> getUsedResources(TenantId tenantId, WidgetTypeDetails widgetTypeDetails);
TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, byte[] data); TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data);
} }

3
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -597,7 +597,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
} }
@Override @Override
public TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, byte[] data) { public TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data) {
if (resourceType == ResourceType.DASHBOARD) { if (resourceType == ResourceType.DASHBOARD) {
Dashboard dashboard = JacksonUtil.fromBytes(data, Dashboard.class); Dashboard dashboard = JacksonUtil.fromBytes(data, Dashboard.class);
dashboard.setTenantId(TenantId.SYS_TENANT_ID); dashboard.setTenantId(TenantId.SYS_TENANT_ID);
@ -615,6 +615,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
resource = new TbResource(); resource = new TbResource();
resource.setTenantId(TenantId.SYS_TENANT_ID); resource.setTenantId(TenantId.SYS_TENANT_ID);
resource.setResourceType(resourceType); resource.setResourceType(resourceType);
resource.setResourceSubType(resourceSubType);
resource.setResourceKey(resourceKey); resource.setResourceKey(resourceKey);
resource.setFileName(resourceKey); resource.setFileName(resourceKey);
resource.setTitle(resourceKey); resource.setTitle(resourceKey);

4
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java

@ -134,7 +134,7 @@ public class JpaBaseEdgeEventDao extends JpaPartitionedAbstractDao<EdgeEventEnti
@Override @Override
public ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent) { public ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent) {
log.debug("Save edge event [{}] ", edgeEvent); log.debug("Saving EdgeEvent [{}] ", edgeEvent);
if (edgeEvent.getId() == null) { if (edgeEvent.getId() == null) {
UUID timeBased = Uuids.timeBased(); UUID timeBased = Uuids.timeBased();
edgeEvent.setId(new EdgeEventId(timeBased)); edgeEvent.setId(new EdgeEventId(timeBased));
@ -156,7 +156,7 @@ public class JpaBaseEdgeEventDao extends JpaPartitionedAbstractDao<EdgeEventEnti
} }
private ListenableFuture<Void> save(EdgeEventEntity entity) { private ListenableFuture<Void> save(EdgeEventEntity entity) {
log.debug("Save edge event [{}] ", entity); log.debug("Saving EdgeEventEntity [{}] ", entity);
if (entity.getTenantId() == null) { if (entity.getTenantId() == null) {
log.trace("Save system edge event with predefined id {}", systemTenantId); log.trace("Save system edge event with predefined id {}", systemTenantId);
entity.setTenantId(systemTenantId); entity.setTenantId(systemTenantId);

8
msa/tb/docker/upgrade-tb.sh

@ -20,23 +20,15 @@ start-db.sh
CONF_FOLDER="${pkg.installFolder}/conf" CONF_FOLDER="${pkg.installFolder}/conf"
jarfile=${pkg.installFolder}/bin/${pkg.name}.jar jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
configfile=${pkg.name}.conf configfile=${pkg.name}.conf
upgradeversion=${DATA_FOLDER}/.upgradeversion
source "${CONF_FOLDER}/${configfile}" source "${CONF_FOLDER}/${configfile}"
FROM_VERSION=`cat ${upgradeversion}`
echo "Starting ThingsBoard upgrade ..." echo "Starting ThingsBoard upgrade ..."
fromVersion="${FROM_VERSION// }"
java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \ java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
-Dspring.jpa.hibernate.ddl-auto=none \ -Dspring.jpa.hibernate.ddl-auto=none \
-Dinstall.upgrade=true \ -Dinstall.upgrade=true \
-Dinstall.upgrade.from_version=${fromVersion} \
-Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \
org.springframework.boot.loader.launch.PropertiesLauncher org.springframework.boot.loader.launch.PropertiesLauncher
echo "${pkg.upgradeVersion}" > ${upgradeversion}
stop-db.sh stop-db.sh

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

File diff suppressed because one or more lines are too long

19
ui-ngx/.browserslistrc

@ -0,0 +1,19 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.dev/reference/versions#browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
>0.5%
not dead
last 2 Chrome versions
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
last 2 Android major versions
Firefox ESR

3
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -52,6 +52,7 @@ import {
isUndefined, isUndefined,
isUndefinedOrNull, isUndefinedOrNull,
mergeDeep, mergeDeep,
mergeDeepIgnoreArray,
parseFunction parseFunction
} from '@core/utils'; } from '@core/utils';
import { BehaviorSubject, forkJoin, Observable, Observer, of, Subject } from 'rxjs'; import { BehaviorSubject, forkJoin, Observable, Observer, of, Subject } from 'rxjs';
@ -516,7 +517,7 @@ export class ScadaSymbolObject {
const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml'); const doc: XMLDocument = new DOMParser().parseFromString(this.svgContent, 'image/svg+xml');
this.metadata = parseScadaSymbolMetadataFromDom(doc); this.metadata = parseScadaSymbolMetadataFromDom(doc);
const defaults = defaultScadaSymbolObjectSettings(this.metadata); const defaults = defaultScadaSymbolObjectSettings(this.metadata);
this.settings = mergeDeep<ScadaSymbolObjectSettings>({} as ScadaSymbolObjectSettings, this.settings = mergeDeepIgnoreArray<ScadaSymbolObjectSettings>({} as ScadaSymbolObjectSettings,
defaults, this.inputSettings || {} as ScadaSymbolObjectSettings); defaults, this.inputSettings || {} as ScadaSymbolObjectSettings);
this.prepareMetadata(); this.prepareMetadata();
this.prepareSvgShape(doc); this.prepareSvgShape(doc);

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts

@ -44,7 +44,7 @@ import {
} from '@home/components/widget/lib/scada/scada-symbol.models'; } from '@home/components/widget/lib/scada/scada-symbol.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { mergeDeep } from '@core/utils'; import { isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils';
import { import {
ScadaSymbolBehaviorGroup, ScadaSymbolBehaviorGroup,
toBehaviorGroups toBehaviorGroups
@ -207,7 +207,7 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
private setupValue() { private setupValue() {
if (this.metadata) { if (this.metadata) {
const defaults = defaultScadaSymbolObjectSettings(this.metadata); const defaults = defaultScadaSymbolObjectSettings(this.metadata);
this.modelValue = mergeDeep<ScadaSymbolObjectSettings>(defaults, this.modelValue); this.modelValue = mergeDeepIgnoreArray<ScadaSymbolObjectSettings>(defaults, this.modelValue);
this.scadaSymbolObjectSettingsFormGroup.patchValue( this.scadaSymbolObjectSettingsFormGroup.patchValue(
this.modelValue, {emitEvent: false} this.modelValue, {emitEvent: false}
); );

14
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts

@ -20,15 +20,10 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { import { OAuth2Client, OAuth2ClientInfo, platformTypeTranslations } from '@shared/models/oauth2.models';
getProviderHelpLink,
OAuth2Client,
OAuth2ClientInfo,
platformTypeTranslations
} from '@shared/models/oauth2.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { OAuth2Service } from '@core/http/oauth2.service'; import { OAuth2Service } from '@core/http/oauth2.service';
import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component';
import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/client-table-header.component'; import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/client-table-header.component';
@ -48,10 +43,7 @@ export class ClientsTableConfigResolver {
this.config.entityType = EntityType.OAUTH2_CLIENT; this.config.entityType = EntityType.OAUTH2_CLIENT;
this.config.rowPointer = true; this.config.rowPointer = true;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.OAUTH2_CLIENT); this.config.entityTranslations = entityTypeTranslations.get(EntityType.OAUTH2_CLIENT);
this.config.entityResources = { this.config.entityResources = entityTypeResources.get(EntityType.OAUTH2_CLIENT);
helpLinkId: null,
helpLinkIdForEntity: (entity: OAuth2Client) => getProviderHelpLink(entity.additionalInfo.providerName)
};
this.config.entityComponent = ClientComponent; this.config.entityComponent = ClientComponent;
this.config.headerComponent = ClientTableHeaderComponent; this.config.headerComponent = ClientTableHeaderComponent;
this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'};

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

@ -63,7 +63,9 @@ export class EditorPanelComponent implements OnInit {
const uiContainer = document.querySelector('.tox.tox-tinymce-aux'); const uiContainer = document.querySelector('.tox.tox-tinymce-aux');
container.parentNode.appendChild(uiContainer); container.parentNode.appendChild(uiContainer);
}); });
} },
relative_urls: false,
urlconverter_callback: (url) => url
}; };
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder) {

3
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -95,7 +95,8 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
autofocus: false, autofocus: false,
branding: false, branding: false,
promotion: false, promotion: false,
relative_urls: false relative_urls: false,
urlconverter_callback: (url) => url
}; };
private propagateChange = null; private propagateChange = null;

8
ui-ngx/src/app/shared/components/js-func-module-row.component.html

@ -18,6 +18,14 @@
<div [formGroup]="moduleRowFormGroup" class="tb-form-table-row tb-js-func-module-row"> <div [formGroup]="moduleRowFormGroup" class="tb-form-table-row tb-js-func-module-row">
<mat-form-field class="tb-inline-field tb-alias-field" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-inline-field tb-alias-field" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="alias" placeholder="{{ 'widget-config.set' | translate }}"> <input required matInput formControlName="alias" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'js-func.invalid-module-alias-name' | translate"
*ngIf="moduleRowFormGroup.get('alias').invalid && moduleRowFormGroup.get('alias').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field> </mat-form-field>
<tb-resource-autocomplete class="tb-module-link-field" <tb-resource-autocomplete class="tb-module-link-field"
#resourceAutocomplete #resourceAutocomplete

12
ui-ngx/src/app/shared/components/js-func-module-row.component.ts

@ -39,7 +39,7 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
import { ResourceSubType } from '@shared/models/resource.models'; import { ResourceSubType } from '@shared/models/resource.models';
import { Observable, of } from 'rxjs'; import { Observable } from 'rxjs';
import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component'; import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { loadModuleMarkdownDescription, loadModuleMarkdownSourceCode } from '@shared/models/js-function.models'; import { loadModuleMarkdownDescription, loadModuleMarkdownSourceCode } from '@shared/models/js-function.models';
@ -103,7 +103,7 @@ export class JsFuncModuleRowComponent implements ControlValueAccessor, OnInit, V
ngOnInit() { ngOnInit() {
this.moduleRowFormGroup = this.fb.group({ this.moduleRowFormGroup = this.fb.group({
alias: [null, [this.moduleAliasValidator()]], alias: [null, [this.moduleAliasValidator(), Validators.pattern(/^[$_\p{ID_Start}][$\p{ID_Continue}]*$/u)]],
moduleLink: [null, [Validators.required]] moduleLink: [null, [Validators.required]]
}); });
this.moduleRowFormGroup.valueChanges.pipe( this.moduleRowFormGroup.valueChanges.pipe(
@ -133,7 +133,7 @@ export class JsFuncModuleRowComponent implements ControlValueAccessor, OnInit, V
public validate(_c: UntypedFormControl) { public validate(_c: UntypedFormControl) {
const aliasControl = this.moduleRowFormGroup.get('alias'); const aliasControl = this.moduleRowFormGroup.get('alias');
if (aliasControl.hasError('moduleAliasNotUnique')) { if (aliasControl.hasError('moduleAliasNotUnique') || aliasControl.hasError('pattern')) {
aliasControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); aliasControl.updateValueAndValidity({onlySelf: false, emitEvent: false});
} }
if (aliasControl.hasError('moduleAliasNotUnique')) { if (aliasControl.hasError('moduleAliasNotUnique')) {
@ -142,6 +142,12 @@ export class JsFuncModuleRowComponent implements ControlValueAccessor, OnInit, V
moduleAliasNotUnique: true moduleAliasNotUnique: true
}; };
} }
if (aliasControl.hasError('pattern')) {
this.moduleRowFormGroup.get('alias').markAsTouched();
return {
invalidVariableName: true
};
}
const module: JsFuncModuleRow = {...this.modelValue, ...this.moduleRowFormGroup.value}; const module: JsFuncModuleRow = {...this.modelValue, ...this.moduleRowFormGroup.value};
if (!moduleValid(module)) { if (!moduleValid(module)) {
return { return {

1
ui-ngx/src/app/shared/components/js-func-modules.component.ts

@ -119,6 +119,7 @@ export class JsFuncModulesComponent implements OnInit {
removeModule(index: number, emitEvent = true) { removeModule(index: number, emitEvent = true) {
(this.modulesFormGroup.get('modules') as UntypedFormArray).removeAt(index, {emitEvent}); (this.modulesFormGroup.get('modules') as UntypedFormArray).removeAt(index, {emitEvent});
this.modulesFormGroup.get('modules').markAsDirty({emitEvent});
} }
addModule() { addModule() {

4
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -16,6 +16,7 @@
import { TenantId } from './id/tenant-id'; import { TenantId } from './id/tenant-id';
import { BaseData, HasId } from '@shared/models/base-data'; import { BaseData, HasId } from '@shared/models/base-data';
import { getProviderHelpLink, OAuth2Client } from '@shared/models/oauth2.models';
export enum EntityType { export enum EntityType {
TENANT = 'TENANT', TENANT = 'TENANT',
@ -588,7 +589,8 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
[ [
EntityType.OAUTH2_CLIENT, EntityType.OAUTH2_CLIENT,
{ {
helpLinkId: 'oauth2Settings' helpLinkId: 'oauth2Settings',
helpLinkIdForEntity: (entity: OAuth2Client) => getProviderHelpLink(entity.additionalInfo.providerName)
} }
], ],
[ [

6
ui-ngx/src/app/shared/models/js-function.models.ts

@ -98,7 +98,7 @@ export const loadModuleMarkdownDescription = (http: HttpClient, translate: Trans
} else { } else {
propDescription += `<p class="!pl-4 !pr-4"><em>const</em> <strong>${propName}</strong>: <code>${type}</code>`; propDescription += `<p class="!pl-4 !pr-4"><em>const</em> <strong>${propName}</strong>: <code>${type}</code>`;
if (type !== 'object') { if (type !== 'object') {
propDescription += ` = ${prop}`; propDescription += ' = ' + (type === 'string' ? `"${handleHtmlSpecialChars(prop)}"` : `${prop}`);
} }
propDescription += '</p>'; propDescription += '</p>';
} }
@ -133,6 +133,10 @@ export const loadModuleMarkdownDescription = (http: HttpClient, translate: Trans
); );
} }
const handleHtmlSpecialChars = (text: string): string => {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
export const loadModuleMarkdownSourceCode = (http: HttpClient, translate: TranslateService, resource: ResourceInfo): Observable<string> => { export const loadModuleMarkdownSourceCode = (http: HttpClient, translate: TranslateService, resource: ResourceInfo): Observable<string> => {
let sourceCode = `<div class="flex flex-col !pl-4"><h6>${resource.title}</h6><small>${translate.instant('js-func.source-code')}</small></div>\n\n`; let sourceCode = `<div class="flex flex-col !pl-4"><h6>${resource.title}</h6><small>${translate.instant('js-func.source-code')}</small></div>\n\n`;
return loadFunctionModuleSource(http, resource.link).pipe( return loadFunctionModuleSource(http, resource.link).pipe(

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

@ -3377,6 +3377,7 @@
"no-modules": "No modules configured", "no-modules": "No modules configured",
"add-module": "Add module", "add-module": "Add module",
"module-alias": "Alias", "module-alias": "Alias",
"invalid-module-alias-name": "Invalid alias name",
"module-resource": "JS module resource", "module-resource": "JS module resource",
"not-unique-module-aliases-error": "Modules aliases must be unique!", "not-unique-module-aliases-error": "Modules aliases must be unique!",
"show-module-info": "Show module info", "show-module-info": "Show module info",

Loading…
Cancel
Save